1mod helptext; 2 3use crate::app::widgets::{ 4 BiosErrorStrings, ClockModifier, OptionalPathSelector, OverclockSlider, RenderErrorEffect, 5}; 6use crate::app::{App, OpenWindow, widgets}; 7use egui::{Context, Window}; 8use jgenesis_common::frontend::TimingMode; 9use jgenesis_native_driver::extensions::Console; 10use rfd::FileDialog; 11use smsgg_config::{GgAspectRatio, SmsAspectRatio, SmsGgRegion, SmsModel}; 12use std::num::NonZeroU32; 13use std::path::PathBuf; 14use ti_sn76489::Sn76489Version; 15 16impl App { 17 pub(super) fn render_smsgg_general_settings(&mut self, ctx: &Context) { 18 const WINDOW: OpenWindow = OpenWindow::SmsGgGeneral; 19 20 let mut open = true; 21 Window::new(WINDOW.title()).open(&mut open).resizable(false).show(ctx, |ui| { 22 let rect = ui 23 .group(|ui| { 24 ui.label("Master System timing / display mode"); 25 26 ui.horizontal(|ui| { 27 ui.radio_value( 28 &mut self.config.smsgg.sms_timing_mode, 29 TimingMode::Ntsc, 30 "NTSC (60Hz)", 31 ); 32 ui.radio_value( 33 &mut self.config.smsgg.sms_timing_mode, 34 TimingMode::Pal, 35 "PAL (50Hz)", 36 ); 37 }); 38 }) 39 .response 40 .interact_rect; 41 if ui.rect_contains_pointer(rect) { 42 self.state.help_text.insert(WINDOW, helptext::TIMING_MODE); 43 } 44 45 let rect = ui 46 .group(|ui| { 47 ui.label("Master System VDP version"); 48 49 ui.horizontal(|ui| { 50 ui.radio_value(&mut self.config.smsgg.sms_model, SmsModel::Sms1, "SMS1") 51 .on_hover_text(concat!( 52 "Emulates an SMS1 quirk that is", 53 " required for the Japanese version of Ys" 54 )); 55 56 ui.radio_value(&mut self.config.smsgg.sms_model, SmsModel::Sms2, "SMS2"); 57 }); 58 }) 59 .response 60 .interact_rect; 61 if ui.rect_contains_pointer(rect) { 62 self.state.help_text.insert(WINDOW, helptext::VDP_VERSION); 63 } 64 65 let rect = ui 66 .group(|ui| { 67 ui.label("Hardware region"); 68 69 ui.horizontal(|ui| { 70 ui.radio_value(&mut self.config.smsgg.forced_region, None, "Auto"); 71 ui.radio_value( 72 &mut self.config.smsgg.forced_region, 73 Some(SmsGgRegion::International), 74 "International / Overseas", 75 ); 76 ui.radio_value( 77 &mut self.config.smsgg.forced_region, 78 Some(SmsGgRegion::Domestic), 79 "Domestic (Japan)", 80 ); 81 }); 82 }) 83 .response 84 .interact_rect; 85 if ui.rect_contains_pointer(rect) { 86 self.state.help_text.insert(WINDOW, helptext::REGION); 87 } 88 89 ui.add_space(5.0); 90 let rect = ui 91 .checkbox(&mut self.config.smsgg.sms_boot_from_bios, "(SMS) Boot from BIOS ROM") 92 .interact_rect; 93 if ui.rect_contains_pointer(rect) { 94 self.state.help_text.insert(WINDOW, helptext::BIOS); 95 } 96 97 let rect = ui 98 .checkbox( 99 &mut self.config.smsgg.gg_boot_from_bios, 100 "(Game Gear) Boot from BIOS ROM", 101 ) 102 .interact_rect; 103 if ui.rect_contains_pointer(rect) { 104 self.state.help_text.insert(WINDOW, helptext::BIOS); 105 } 106 107 ui.add_space(5.0); 108 let rect = ui 109 .add(OptionalPathSelector::new( 110 "SMS BIOS Path", 111 &mut self.config.smsgg.sms_bios_path, 112 || pick_bios_path("sms"), 113 )) 114 .interact_rect; 115 if ui.rect_contains_pointer(rect) { 116 self.state.help_text.insert(WINDOW, helptext::BIOS); 117 } 118 119 let rect = ui 120 .add(OptionalPathSelector::new( 121 "Game Gear BIOS Path", 122 &mut self.config.smsgg.gg_bios_path, 123 || pick_bios_path("gg"), 124 )) 125 .interact_rect; 126 if ui.rect_contains_pointer(rect) { 127 self.state.help_text.insert(WINDOW, helptext::BIOS); 128 } 129 130 self.render_help_text(ui, WINDOW); 131 }); 132 if !open { 133 self.state.open_windows.remove(&WINDOW); 134 } 135 } 136 137 pub(super) fn render_smsgg_video_settings(&mut self, ctx: &Context) { 138 const WINDOW: OpenWindow = OpenWindow::SmsGgVideo; 139 140 let mut open = true; 141 Window::new(WINDOW.title()).open(&mut open).resizable(false).show(ctx, |ui| { 142 let rect = ui 143 .group(|ui| { 144 ui.label("Master System / SG-1000 aspect ratio"); 145 146 ui.horizontal(|ui| { 147 ui.radio_value( 148 &mut self.config.smsgg.sms_aspect_ratio, 149 SmsAspectRatio::Ntsc, 150 "NTSC", 151 ) 152 .on_hover_text("8:7 pixel aspect ratio"); 153 ui.radio_value( 154 &mut self.config.smsgg.sms_aspect_ratio, 155 SmsAspectRatio::Pal, 156 "PAL", 157 ) 158 .on_hover_text("11:8 pixel aspect ratio"); 159 ui.radio_value( 160 &mut self.config.smsgg.sms_aspect_ratio, 161 SmsAspectRatio::SquarePixels, 162 "Square pixels", 163 ) 164 .on_hover_text("1:1 pixel aspect ratio"); 165 ui.radio_value( 166 &mut self.config.smsgg.sms_aspect_ratio, 167 SmsAspectRatio::Stretched, 168 "Stretched", 169 ) 170 .on_hover_text("Stretch image to fill the screen"); 171 }); 172 }) 173 .response 174 .interact_rect; 175 if ui.rect_contains_pointer(rect) { 176 self.state.help_text.insert(WINDOW, helptext::SMS_ASPECT_RATIO); 177 } 178 179 let rect = ui 180 .group(|ui| { 181 ui.label("Game Gear aspect ratio"); 182 183 ui.horizontal(|ui| { 184 ui.radio_value( 185 &mut self.config.smsgg.gg_aspect_ratio, 186 GgAspectRatio::GgLcd, 187 "Game Gear LCD", 188 ) 189 .on_hover_text("6:5 pixel aspect ratio"); 190 ui.radio_value( 191 &mut self.config.smsgg.gg_aspect_ratio, 192 GgAspectRatio::SquarePixels, 193 "Square pixels", 194 ) 195 .on_hover_text("1:1 pixel aspect ratio"); 196 ui.radio_value( 197 &mut self.config.smsgg.gg_aspect_ratio, 198 GgAspectRatio::Stretched, 199 "Stretched", 200 ) 201 .on_hover_text("Stretch image to fill the screen"); 202 }); 203 }) 204 .response 205 .interact_rect; 206 if ui.rect_contains_pointer(rect) { 207 self.state.help_text.insert(WINDOW, helptext::GG_ASPECT_RATIO); 208 } 209 210 let rect = ui 211 .checkbox( 212 &mut self.config.smsgg.remove_sprite_limit, 213 "Remove sprite-per-scanline limit", 214 ) 215 .interact_rect; 216 if ui.rect_contains_pointer(rect) { 217 self.state.help_text.insert(WINDOW, helptext::REMOVE_SPRITE_LIMIT); 218 } 219 220 let rect = ui 221 .checkbox( 222 &mut self.config.smsgg.sms_crop_vertical_border, 223 "(SMS/SG) Crop vertical border", 224 ) 225 .interact_rect; 226 if ui.rect_contains_pointer(rect) { 227 self.state.help_text.insert(WINDOW, helptext::SMS_CROP_VERTICAL_BORDER); 228 } 229 230 let rect = ui 231 .checkbox(&mut self.config.smsgg.sms_crop_left_border, "(SMS/SG) Crop left border") 232 .interact_rect; 233 if ui.rect_contains_pointer(rect) { 234 self.state.help_text.insert(WINDOW, helptext::SMS_CROP_LEFT_BORDER); 235 } 236 237 let rect = ui 238 .checkbox( 239 &mut self.config.smsgg.gg_frame_blending, 240 "(Game Gear) Frame blending enabled", 241 ) 242 .interact_rect; 243 if ui.rect_contains_pointer(rect) { 244 self.state.help_text.insert(WINDOW, helptext::GG_FRAME_BLENDING); 245 } 246 247 let rect = ui 248 .checkbox( 249 &mut self.config.smsgg.gg_use_sms_resolution, 250 "(Game Gear) Render in SMS resolution", 251 ) 252 .interact_rect; 253 if ui.rect_contains_pointer(rect) { 254 self.state.help_text.insert(WINDOW, helptext::GG_USE_SMS_RESOLUTION); 255 } 256 257 self.render_help_text(ui, WINDOW); 258 }); 259 if !open { 260 self.state.open_windows.remove(&WINDOW); 261 } 262 } 263 264 pub(super) fn render_smsgg_audio_settings(&mut self, ctx: &Context) { 265 const WINDOW: OpenWindow = OpenWindow::SmsGgAudio; 266 267 let mut open = true; 268 Window::new(WINDOW.title()).open(&mut open).resizable(false).show(ctx, |ui| { 269 let rect = ui 270 .group(|ui| { 271 ui.label("PSG version"); 272 273 ui.horizontal(|ui| { 274 ui.radio_value(&mut self.config.smsgg.psg_version, None, "Auto") 275 .on_hover_text( 276 "SMS games will use SMS2 PSG, Game Gear games will use SMS1/GG PSG", 277 ); 278 ui.radio_value( 279 &mut self.config.smsgg.psg_version, 280 Some(Sn76489Version::MasterSystem2), 281 "SMS2", 282 ) 283 .on_hover_text("SMS2 PSG clips high volumes"); 284 ui.radio_value( 285 &mut self.config.smsgg.psg_version, 286 Some(Sn76489Version::Standard), 287 "SMS1 / Game Gear", 288 ) 289 .on_hover_text("SMS1 and Game Gear PSGs correctly play high volumes"); 290 ui.radio_value( 291 &mut self.config.smsgg.psg_version, 292 Some(Sn76489Version::Discrete), 293 "SG-1000", 294 ) 295 .on_hover_text("SG-1000 PSG has a slightly different white noise sound"); 296 }); 297 }) 298 .response 299 .interact_rect; 300 if ui.rect_contains_pointer(rect) { 301 self.state.help_text.insert(WINDOW, helptext::PSG_VERSION); 302 } 303 304 let rect = ui 305 .add_enabled_ui(!self.emu_runner.status().is_running_smsgg(), |ui| { 306 ui.checkbox( 307 &mut self.config.smsgg.fm_sound_unit_enabled, 308 "Master System FM sound unit enabled", 309 ); 310 }) 311 .response 312 .interact_rect; 313 if ui.rect_contains_pointer(rect) { 314 self.state.help_text.insert(WINDOW, helptext::SMS_FM_UNIT); 315 } 316 317 self.render_help_text(ui, WINDOW); 318 }); 319 if !open { 320 self.state.open_windows.remove(&WINDOW); 321 } 322 } 323 324 pub(super) fn render_smsgg_overclock_settings(&mut self, ctx: &Context) { 325 const WINDOW: OpenWindow = OpenWindow::SmsGgOverclock; 326 327 let mut open = true; 328 Window::new(WINDOW.title()).open(&mut open).resizable(false).show(ctx, |ui| { 329 ui.add(OverclockSlider { 330 label: "Z80 clock divider", 331 current_value: &mut self.config.smsgg.z80_divider, 332 range: NonZeroU32::new(1).unwrap()..=NonZeroU32::new(15).unwrap(), 333 master_clock: smsgg_core::audio::NTSC_MCLK_FREQUENCY, 334 default_divider: smsgg_core::NATIVE_Z80_DIVIDER.into(), 335 modifier: ClockModifier::Divider, 336 }); 337 338 self.state.help_text.insert(WINDOW, helptext::Z80_OVERCLOCK); 339 self.render_help_text(ui, WINDOW); 340 }); 341 if !open { 342 self.state.open_windows.remove(&WINDOW); 343 } 344 } 345 346 #[must_use] 347 pub(super) fn render_sms_bios_error( 348 &mut self, 349 ctx: &Context, 350 open: &mut bool, 351 ) -> RenderErrorEffect { 352 widgets::render_bios_error( 353 ctx, 354 open, 355 BiosErrorStrings { 356 title: "Missing SMS BIOS", 357 text: "The boot from BIOS option is set but no SMS BIOS path is configured.", 358 button_label: "Configure SMS BIOS path", 359 }, 360 &mut self.config.smsgg.sms_bios_path, 361 Console::MasterSystem, 362 || pick_bios_path("sms"), 363 ) 364 } 365 366 #[must_use] 367 pub(super) fn render_gg_bios_error( 368 &mut self, 369 ctx: &Context, 370 open: &mut bool, 371 ) -> RenderErrorEffect { 372 widgets::render_bios_error( 373 ctx, 374 open, 375 BiosErrorStrings { 376 title: "Missing Game Gear BIOS", 377 text: "The boot from BIOS option is set but no Game Gear BIOS path is configured.", 378 button_label: "Configure Game Gear BIOS path", 379 }, 380 &mut self.config.smsgg.gg_bios_path, 381 Console::GameGear, 382 || pick_bios_path("gg"), 383 ) 384 } 385} 386 387fn pick_bios_path(default_extension: &str) -> Option<PathBuf> { 388 FileDialog::new() 389 .add_filter("bin", &[default_extension, "bin"]) 390 .add_filter("All Files", &["*"]) 391 .pick_file() 392}