1mod helptext; 2 3use crate::app::widgets::{BiosErrorStrings, RenderErrorEffect}; 4use crate::app::{App, Console, OpenWindow, widgets}; 5use crate::emurunner::EmuRunnerStatus; 6use egui::{Context, Grid, Ui, Window}; 7use jgenesis_common::frontend::TimingMode; 8use jgenesis_native_config::snes::SnesAppConfig; 9use jgenesis_proc_macros::EnumAll; 10use rfd::FileDialog; 11use snes_config::{AudioInterpolationMode, SnesAspectRatio}; 12use snes_core::api::SnesLoadError; 13use std::num::NonZeroU64; 14use std::path::PathBuf; 15 16#[derive(Debug, Clone, Copy, PartialEq, Eq)] 17pub enum HandledError { 18 No, 19 Yes(RenderErrorEffect), 20} 21 22#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumAll)] 23enum CoprocessorRom { 24 Dsp1, 25 Dsp2, 26 Dsp3, 27 Dsp4, 28 St010, 29 St011, 30 St018, 31} 32 33impl CoprocessorRom { 34 fn name(self) -> &'static str { 35 match self { 36 Self::Dsp1 => "DSP-1", 37 Self::Dsp2 => "DSP-2", 38 Self::Dsp3 => "DSP-3", 39 Self::Dsp4 => "DSP-4", 40 Self::St010 => "ST010", 41 Self::St011 => "ST011", 42 Self::St018 => "ST018", 43 } 44 } 45 46 fn path_field(self, config: &mut SnesAppConfig) -> &mut Option<PathBuf> { 47 match self { 48 Self::Dsp1 => &mut config.dsp1_rom_path, 49 Self::Dsp2 => &mut config.dsp2_rom_path, 50 Self::Dsp3 => &mut config.dsp3_rom_path, 51 Self::Dsp4 => &mut config.dsp4_rom_path, 52 Self::St010 => &mut config.st010_rom_path, 53 Self::St011 => &mut config.st011_rom_path, 54 Self::St018 => &mut config.st018_rom_path, 55 } 56 } 57} 58 59impl App { 60 pub(super) fn render_snes_general_settings(&mut self, ctx: &Context) { 61 const WINDOW: OpenWindow = OpenWindow::SnesGeneral; 62 63 let mut open = true; 64 Window::new(WINDOW.title()).open(&mut open).resizable(false).show(ctx, |ui| { 65 let rect = ui 66 .group(|ui| { 67 ui.add_enabled_ui( 68 self.emu_runner.status() != EmuRunnerStatus::RunningSnes, 69 |ui| { 70 ui.label("Timing / display mode"); 71 72 ui.horizontal(|ui| { 73 ui.radio_value( 74 &mut self.config.snes.forced_timing_mode, 75 None, 76 "Auto", 77 ); 78 ui.radio_value( 79 &mut self.config.snes.forced_timing_mode, 80 Some(TimingMode::Ntsc), 81 "NTSC (60Hz)", 82 ); 83 ui.radio_value( 84 &mut self.config.snes.forced_timing_mode, 85 Some(TimingMode::Pal), 86 "PAL (50Hz)", 87 ); 88 }); 89 }, 90 ); 91 }) 92 .response 93 .interact_rect; 94 if ui.rect_contains_pointer(rect) { 95 self.state.help_text.insert(WINDOW, helptext::TIMING_MODE); 96 } 97 98 let rect = ui 99 .group(|ui| { 100 ui.label("Coprocessor ROM Paths"); 101 Grid::new("coprocessor_path_grid").show(ui, |ui| { 102 for coprocessor_type in CoprocessorRom::ALL { 103 let label = format!(" {}", coprocessor_type.name()); 104 let path = coprocessor_type.path_field(&mut self.config.snes); 105 render_coprocessor_path_select(&label, path, ui); 106 } 107 }); 108 }) 109 .response 110 .interact_rect; 111 if ui.rect_contains_pointer(rect) { 112 self.state.help_text.insert(WINDOW, helptext::COPROCESSOR_ROM_PATHS); 113 } 114 115 self.render_help_text(ui, WINDOW); 116 }); 117 if !open { 118 self.state.open_windows.remove(&WINDOW); 119 } 120 } 121 122 pub(super) fn render_snes_video_settings(&mut self, ctx: &Context) { 123 const WINDOW: OpenWindow = OpenWindow::SnesVideo; 124 125 let mut open = true; 126 Window::new(WINDOW.title()).open(&mut open).resizable(false).show(ctx, |ui| { 127 let rect = ui 128 .group(|ui| { 129 ui.label("Aspect ratio"); 130 131 ui.horizontal(|ui| { 132 ui.radio_value( 133 &mut self.config.snes.aspect_ratio, 134 SnesAspectRatio::Ntsc, 135 "NTSC", 136 ) 137 .on_hover_text("8:7 pixel aspect ratio"); 138 ui.radio_value( 139 &mut self.config.snes.aspect_ratio, 140 SnesAspectRatio::Pal, 141 "PAL", 142 ) 143 .on_hover_text("11:8 pixel aspect ratio"); 144 ui.radio_value( 145 &mut self.config.snes.aspect_ratio, 146 SnesAspectRatio::SquarePixels, 147 "Square pixels", 148 ) 149 .on_hover_text("1:1 pixel aspect ratio"); 150 ui.radio_value( 151 &mut self.config.snes.aspect_ratio, 152 SnesAspectRatio::Stretched, 153 "Stretched", 154 ) 155 .on_hover_text("Stretched to fill the window"); 156 }); 157 }) 158 .response 159 .interact_rect; 160 if ui.rect_contains_pointer(rect) { 161 self.state.help_text.insert(WINDOW, helptext::ASPECT_RATIO); 162 } 163 164 ui.add_space(5.0); 165 166 let rect = ui 167 .checkbox(&mut self.config.snes.deinterlace, "Deinterlacing enabled") 168 .interact_rect; 169 if ui.rect_contains_pointer(rect) { 170 self.state.help_text.insert(WINDOW, helptext::DEINTERLACING); 171 } 172 173 self.render_help_text(ui, WINDOW); 174 }); 175 if !open { 176 self.state.open_windows.remove(&WINDOW); 177 } 178 } 179 180 pub(super) fn render_snes_audio_settings(&mut self, ctx: &Context) { 181 const WINDOW: OpenWindow = OpenWindow::SnesAudio; 182 183 let mut open = true; 184 Window::new(WINDOW.title()).open(&mut open).resizable(false).show(ctx, |ui| { 185 let rect = ui 186 .group(|ui| { 187 ui.label("ADPCM sample interpolation"); 188 189 ui.radio_value( 190 &mut self.config.snes.audio_interpolation, 191 AudioInterpolationMode::Gaussian, 192 "Gaussian (Native)", 193 ) 194 .on_hover_text("Emulate actual hardware's behavior"); 195 ui.radio_value( 196 &mut self.config.snes.audio_interpolation, 197 AudioInterpolationMode::Hermite, 198 "Cubic Hermite (Sharp)", 199 ) 200 .on_hover_text("More advanced algorithm than actual hardware"); 201 }) 202 .response 203 .interact_rect; 204 if ui.rect_contains_pointer(rect) { 205 self.state.help_text.insert(WINDOW, helptext::ADPCM_INTERPOLATION); 206 } 207 208 ui.add_space(10.0); 209 210 let rect = ui 211 .checkbox(&mut self.config.snes.audio_60hz_hack, "Enable audio sync timing hack") 212 .interact_rect; 213 if ui.rect_contains_pointer(rect) { 214 self.state.help_text.insert(WINDOW, helptext::AUDIO_TIMING_HACK); 215 } 216 217 self.render_help_text(ui, WINDOW); 218 }); 219 if !open { 220 self.state.open_windows.remove(&WINDOW); 221 } 222 } 223 224 pub(super) fn render_snes_overclock_settings(&mut self, ctx: &Context) { 225 const WINDOW: OpenWindow = OpenWindow::SnesOverclock; 226 227 let mut open = true; 228 229 Window::new(WINDOW.title()).open(&mut open).resizable(false).show(ctx, |ui| { 230 self.state.help_text.insert(WINDOW, helptext::SUPER_FX_OVERCLOCK); 231 232 ui.group(|ui| { 233 ui.label("Super FX GSU overclock factor"); 234 235 ui.horizontal(|ui| { 236 ui.radio_value( 237 &mut self.config.snes.gsu_overclock_factor, 238 NonZeroU64::new(1).unwrap(), 239 "None", 240 ); 241 ui.radio_value( 242 &mut self.config.snes.gsu_overclock_factor, 243 NonZeroU64::new(2).unwrap(), 244 "2x", 245 ); 246 ui.radio_value( 247 &mut self.config.snes.gsu_overclock_factor, 248 NonZeroU64::new(3).unwrap(), 249 "3x", 250 ); 251 ui.radio_value( 252 &mut self.config.snes.gsu_overclock_factor, 253 NonZeroU64::new(4).unwrap(), 254 "4x", 255 ); 256 }); 257 }); 258 259 self.render_help_text(ui, WINDOW); 260 }); 261 262 if !open { 263 self.state.open_windows.remove(&WINDOW); 264 } 265 } 266 267 #[must_use] 268 pub(super) fn render_snes_load_error( 269 &mut self, 270 ctx: &Context, 271 err: &SnesLoadError, 272 open: &mut bool, 273 ) -> HandledError { 274 let coprocessor_rom = match err { 275 SnesLoadError::MissingDsp1Rom => CoprocessorRom::Dsp1, 276 SnesLoadError::MissingDsp2Rom => CoprocessorRom::Dsp2, 277 SnesLoadError::MissingDsp3Rom => CoprocessorRom::Dsp3, 278 SnesLoadError::MissingDsp4Rom => CoprocessorRom::Dsp4, 279 SnesLoadError::MissingSt010Rom => CoprocessorRom::St010, 280 SnesLoadError::MissingSt011Rom => CoprocessorRom::St011, 281 SnesLoadError::MissingSt018Rom => CoprocessorRom::St018, 282 SnesLoadError::RomTooSmall { .. } 283 | SnesLoadError::CoprocessorRomLoad { .. } 284 | SnesLoadError::St018RomLoad(..) => { 285 return HandledError::No; 286 } 287 }; 288 289 let name = coprocessor_rom.name(); 290 let path_field = coprocessor_rom.path_field(&mut self.config.snes); 291 let render_effect = widgets::render_bios_error( 292 ctx, 293 open, 294 BiosErrorStrings { 295 title: format!("Missing {name} ROM"), 296 text: format!( 297 "No {name} firmware ROM is configured. This is required to run games that use the {name} coprocessor." 298 ), 299 button_label: format!("Configure {name} ROM path"), 300 }, 301 path_field, 302 Console::Snes, 303 pick_coprocessor_rom_path, 304 ); 305 306 HandledError::Yes(render_effect) 307 } 308} 309 310fn render_coprocessor_path_select(label: &str, value: &mut Option<PathBuf>, ui: &mut Ui) { 311 ui.label(label); 312 313 let button_label = 314 value.as_deref().map_or_else(|| "<None>".into(), |path| path.display().to_string()); 315 if ui.button(button_label).clicked() 316 && let Some(path) = pick_coprocessor_rom_path() 317 { 318 *value = Some(path); 319 } 320 321 ui.end_row(); 322} 323 324fn pick_coprocessor_rom_path() -> Option<PathBuf> { 325 FileDialog::new().add_filter("bin", &["rom", "bin"]).add_filter("All Types", &["*"]).pick_file() 326}