1mod helptext; 2 3use crate::app::widgets::{ 4 BiosErrorStrings, ClockModifier, NumericTextEdit, OptionalPathSelector, OverclockSlider, 5 RenderErrorEffect, 6}; 7use crate::app::{App, Console, OpenWindow, widgets}; 8use egui::style::ScrollStyle; 9use egui::{Color32, Context, Slider, Ui, Window}; 10use genesis_config::{GenesisAspectRatio, GenesisRegion, Opn2BusyBehavior, S32XPwmResampling}; 11use genesis_config::{PcmInterpolation, S32XColorTint}; 12use genesis_config::{S32XVideoOut, S32XVoidColorType}; 13use jgenesis_common::frontend::TimingMode; 14use jgenesis_native_config::AppConfig; 15use jgenesis_native_config::genesis::Sega32XAppConfig; 16use rfd::FileDialog; 17use std::num::{NonZeroU16, NonZeroU64}; 18use std::path::PathBuf; 19 20#[derive(Debug, Clone, Default)] 21pub struct S32XPriorityState { 22 pub void_palette: String, 23 pub void_direct_r: String, 24 pub void_direct_g: String, 25 pub void_direct_b: String, 26} 27 28impl S32XPriorityState { 29 pub fn from_config(config: &Sega32XAppConfig) -> Self { 30 Self { 31 void_palette: config.void_palette_index.to_string(), 32 void_direct_r: config.void_direct[0].to_string(), 33 void_direct_g: config.void_direct[1].to_string(), 34 void_direct_b: config.void_direct[2].to_string(), 35 } 36 } 37} 38 39pub struct GenesisVolumeState { 40 pub ym2612_text: String, 41 pub ym2612_invalid: bool, 42 pub psg_text: String, 43 pub psg_invalid: bool, 44 pub pcm_text: String, 45 pub pcm_invalid: bool, 46 pub cd_text: String, 47 pub cd_invalid: bool, 48 pub pwm_text: String, 49 pub pwm_invalid: bool, 50} 51 52impl Default for GenesisVolumeState { 53 fn default() -> Self { 54 let zero_str = "0.0".to_string(); 55 56 Self { 57 ym2612_text: zero_str.clone(), 58 ym2612_invalid: false, 59 psg_text: zero_str.clone(), 60 psg_invalid: false, 61 pcm_text: zero_str.clone(), 62 pcm_invalid: false, 63 cd_text: zero_str.clone(), 64 cd_invalid: false, 65 pwm_text: zero_str, 66 pwm_invalid: false, 67 } 68 } 69} 70 71impl GenesisVolumeState { 72 pub fn from_config(config: &AppConfig) -> Self { 73 Self { 74 ym2612_text: fmt_volume(config.genesis.ym2612_volume_adjustment_db), 75 ym2612_invalid: false, 76 psg_text: fmt_volume(config.genesis.psg_volume_adjustment_db), 77 psg_invalid: false, 78 pcm_text: fmt_volume(config.sega_cd.pcm_volume_adjustment_db), 79 pcm_invalid: false, 80 cd_text: fmt_volume(config.sega_cd.cd_volume_adjustment_db), 81 cd_invalid: false, 82 pwm_text: fmt_volume(config.sega_32x.pwm_volume_adjustment_db), 83 pwm_invalid: false, 84 } 85 } 86 87 fn any_invalid(&self) -> bool { 88 self.ym2612_invalid 89 || self.psg_invalid 90 || self.pcm_invalid 91 || self.cd_invalid 92 || self.pwm_invalid 93 } 94} 95 96fn fmt_volume(volume_adjustment: f64) -> String { 97 format!("{volume_adjustment:.1}") 98} 99 100impl App { 101 pub(super) fn render_genesis_general_settings(&mut self, ctx: &Context) { 102 const WINDOW: OpenWindow = OpenWindow::GenesisGeneral; 103 104 let mut open = true; 105 Window::new(WINDOW.title()).open(&mut open).resizable(true).show(ctx, |ui| { 106 let emu_runner_status = self.emu_runner.status(); 107 let running_genesis = emu_runner_status.is_running_genesis(); 108 109 let rect = ui 110 .group(|ui| { 111 ui.add_enabled_ui(!running_genesis, |ui| { 112 ui.label("Timing / display mode"); 113 114 ui.horizontal(|ui| { 115 ui.radio_value( 116 &mut self.config.genesis.forced_timing_mode, 117 None, 118 "Auto", 119 ); 120 ui.radio_value( 121 &mut self.config.genesis.forced_timing_mode, 122 Some(TimingMode::Ntsc), 123 "NTSC (60Hz)", 124 ); 125 ui.radio_value( 126 &mut self.config.genesis.forced_timing_mode, 127 Some(TimingMode::Pal), 128 "PAL (50Hz)", 129 ); 130 }); 131 }); 132 }) 133 .response 134 .interact_rect; 135 if ui.rect_contains_pointer(rect) { 136 self.state.help_text.insert(WINDOW, helptext::TIMING_MODE); 137 } 138 139 let rect = ui 140 .group(|ui| { 141 ui.add_enabled_ui(!running_genesis, |ui| { 142 ui.label("Region"); 143 144 ui.horizontal(|ui| { 145 ui.radio_value(&mut self.config.genesis.forced_region, None, "Auto"); 146 ui.radio_value( 147 &mut self.config.genesis.forced_region, 148 Some(GenesisRegion::Americas), 149 "Americas", 150 ); 151 ui.radio_value( 152 &mut self.config.genesis.forced_region, 153 Some(GenesisRegion::Japan), 154 "Japan", 155 ); 156 ui.radio_value( 157 &mut self.config.genesis.forced_region, 158 Some(GenesisRegion::Europe), 159 "Europe", 160 ); 161 }); 162 }); 163 }) 164 .response 165 .interact_rect; 166 if ui.rect_contains_pointer(rect) { 167 self.state.help_text.insert(WINDOW, helptext::REGION); 168 } 169 170 ui.add_space(5.0); 171 172 let rect = ui 173 .group(|ui| { 174 ui.label("Sega CD BIOS paths"); 175 176 ui.add(OptionalPathSelector::new( 177 "US", 178 &mut self.config.sega_cd.bios_path, 179 pick_scd_bios_path, 180 )); 181 182 ui.add_enabled_ui(self.config.sega_cd.per_region_bios, |ui| { 183 ui.add(OptionalPathSelector::new( 184 "Europe", 185 &mut self.config.sega_cd.eu_bios_path, 186 pick_scd_bios_path, 187 )); 188 ui.add(OptionalPathSelector::new( 189 "Japan", 190 &mut self.config.sega_cd.jp_bios_path, 191 pick_scd_bios_path, 192 )); 193 }); 194 195 let mut us_bios_only = !self.config.sega_cd.per_region_bios; 196 ui.checkbox(&mut us_bios_only, "Use US BIOS for all regions"); 197 self.config.sega_cd.per_region_bios = !us_bios_only; 198 }) 199 .response 200 .interact_rect; 201 if ui.rect_contains_pointer(rect) { 202 self.state.help_text.insert(WINDOW, helptext::SCD_BIOS_PATH); 203 } 204 205 ui.add_space(5.0); 206 let rect = ui 207 .checkbox( 208 &mut self.config.sega_cd.enable_ram_cartridge, 209 "(Sega CD) Enable RAM cartridge", 210 ) 211 .interact_rect; 212 if ui.rect_contains_pointer(rect) { 213 self.state.help_text.insert(WINDOW, helptext::SCD_RAM_CARTRIDGE); 214 } 215 216 let rect = ui 217 .checkbox( 218 &mut self.config.sega_cd.load_disc_into_ram, 219 "(Sega CD) Load CD-ROM images into host RAM at startup", 220 ) 221 .interact_rect; 222 if ui.rect_contains_pointer(rect) { 223 self.state.help_text.insert(WINDOW, helptext::SCD_CDROM_IN_RAM); 224 } 225 226 self.render_help_text(ui, WINDOW); 227 }); 228 if !open { 229 self.state.open_windows.remove(&WINDOW); 230 } 231 } 232 233 pub(super) fn render_genesis_video_settings(&mut self, ctx: &Context) { 234 const WINDOW: OpenWindow = OpenWindow::GenesisVideo; 235 236 let mut open = true; 237 Window::new(WINDOW.title()).open(&mut open).resizable(false).show(ctx, |ui| { 238 widgets::render_vertical_scroll_area(ui, |ui| { 239 self.render_aspect_ratio_settings(ui, WINDOW); 240 241 let rect = ui 242 .checkbox(&mut self.config.genesis.deinterlace, "Deinterlacing enabled") 243 .interact_rect; 244 if ui.rect_contains_pointer(rect) { 245 self.state.help_text.insert(WINDOW, helptext::DEINTERLACING); 246 } 247 248 let rect = ui 249 .checkbox( 250 &mut self.config.genesis.adjust_aspect_ratio_in_2x_resolution, 251 "Automatically double pixel aspect ratio in double-screen interlaced mode", 252 ) 253 .interact_rect; 254 if ui.rect_contains_pointer(rect) { 255 self.state.help_text.insert(WINDOW, helptext::DOUBLE_SCREEN_INTERLACED_ASPECT); 256 } 257 258 let rect = ui 259 .checkbox( 260 &mut self.config.genesis.non_linear_color_scale, 261 "Emulate non-linear VDP color scale", 262 ) 263 .interact_rect; 264 if ui.rect_contains_pointer(rect) { 265 self.state.help_text.insert(WINDOW, helptext::NON_LINEAR_COLOR_SCALE); 266 } 267 268 let rect = ui 269 .checkbox( 270 &mut self.config.genesis.remove_sprite_limits, 271 "Remove sprite-per-scanline and sprite-pixel-per-scanline limits", 272 ) 273 .on_hover_text( 274 "Can reduce sprite flickering, but can also cause visual glitches", 275 ) 276 .interact_rect; 277 if ui.rect_contains_pointer(rect) { 278 self.state.help_text.insert(WINDOW, helptext::REMOVE_SPRITE_LIMITS); 279 } 280 281 let rect = ui 282 .checkbox( 283 &mut self.config.genesis.render_vertical_border, 284 "Render vertical border", 285 ) 286 .interact_rect; 287 if ui.rect_contains_pointer(rect) { 288 self.state.help_text.insert(WINDOW, helptext::RENDER_BORDERS); 289 } 290 291 let rect = ui 292 .checkbox( 293 &mut self.config.genesis.render_horizontal_border, 294 "Render horizontal border", 295 ) 296 .interact_rect; 297 if ui.rect_contains_pointer(rect) { 298 self.state.help_text.insert(WINDOW, helptext::RENDER_BORDERS); 299 } 300 301 ui.add_space(5.0); 302 303 self.render_enabled_layers_setting(ui, WINDOW); 304 305 ui.add_space(5.0); 306 307 self.render_32x_video_settings(ui, WINDOW); 308 }); 309 310 self.render_help_text(ui, WINDOW); 311 }); 312 if !open { 313 self.state.open_windows.remove(&WINDOW); 314 } 315 } 316 317 fn render_aspect_ratio_settings(&mut self, ui: &mut Ui, window: OpenWindow) { 318 let mut displayed_other_help_text = false; 319 320 let rect = ui 321 .group(|ui| { 322 ui.label("Aspect ratio"); 323 324 ui.horizontal(|ui| { 325 ui.radio_value( 326 &mut self.config.genesis.aspect_ratio, 327 GenesisAspectRatio::Auto, 328 "Auto", 329 ); 330 ui.radio_value( 331 &mut self.config.genesis.aspect_ratio, 332 GenesisAspectRatio::Ntsc, 333 "NTSC", 334 ) 335 .on_hover_text("32:35 pixel aspect ratio in 320px mode, 8:7 in 256px mode"); 336 ui.radio_value( 337 &mut self.config.genesis.aspect_ratio, 338 GenesisAspectRatio::Pal, 339 "PAL", 340 ) 341 .on_hover_text("11:10 pixel aspect ratio in 320px mode, 11:8 in 256px mode"); 342 ui.radio_value( 343 &mut self.config.genesis.aspect_ratio, 344 GenesisAspectRatio::SquarePixels, 345 "Square pixels", 346 ) 347 .on_hover_text("1:1 pixel aspect ratio"); 348 ui.radio_value( 349 &mut self.config.genesis.aspect_ratio, 350 GenesisAspectRatio::Stretched, 351 "Stretched", 352 ) 353 .on_hover_text("Stretch image to fill the screen"); 354 }); 355 356 let rect = ui 357 .add_enabled_ui( 358 self.config.genesis.aspect_ratio != GenesisAspectRatio::Stretched, 359 |ui| { 360 ui.checkbox( 361 &mut self.config.genesis.anamorphic_widescreen, 362 "Anamorphic widescreen", 363 ); 364 }, 365 ) 366 .response 367 .interact_rect; 368 if ui.rect_contains_pointer(rect) { 369 self.state.help_text.insert(window, helptext::ANAMORPHIC_WIDESCREEN); 370 displayed_other_help_text = true; 371 } 372 373 let rect = ui 374 .add_enabled_ui( 375 self.config.genesis.aspect_ratio != GenesisAspectRatio::Stretched, 376 |ui| { 377 ui.checkbox( 378 &mut self.config.genesis.force_square_pixels_in_h40, 379 "Force square pixels in H320px mode", 380 ); 381 }, 382 ) 383 .response 384 .interact_rect; 385 if ui.rect_contains_pointer(rect) { 386 self.state.help_text.insert(window, helptext::FORCE_SQUARE_PIXELS_H40); 387 displayed_other_help_text = true; 388 } 389 }) 390 .response 391 .interact_rect; 392 if !displayed_other_help_text && ui.rect_contains_pointer(rect) { 393 self.state.help_text.insert(window, helptext::ASPECT_RATIO); 394 } 395 } 396 397 fn render_enabled_layers_setting(&mut self, ui: &mut Ui, window: OpenWindow) { 398 let rect = ui 399 .group(|ui| { 400 ui.label("Enabled layers"); 401 402 ui.horizontal(|ui| { 403 ui.checkbox(&mut self.config.genesis.plane_a_enabled, "Plane A"); 404 ui.checkbox(&mut self.config.genesis.plane_b_enabled, "Plane B"); 405 ui.checkbox(&mut self.config.genesis.sprites_enabled, "Sprites"); 406 ui.checkbox(&mut self.config.genesis.window_enabled, "Window"); 407 }); 408 }) 409 .response 410 .interact_rect; 411 if ui.rect_contains_pointer(rect) { 412 self.state.help_text.insert(window, helptext::ENABLED_LAYERS); 413 } 414 } 415 416 fn render_32x_video_settings(&mut self, ui: &mut Ui, window: OpenWindow) { 417 let rect = ui 418 .checkbox( 419 &mut self.config.sega_32x.darken_genesis_colors, 420 "(32X) Darken Genesis colors", 421 ) 422 .interact_rect; 423 if ui.rect_contains_pointer(rect) { 424 self.state.help_text.insert(window, helptext::S32X_DARKEN_GEN_COLORS); 425 } 426 427 ui.add_space(5.0); 428 429 let rect = ui 430 .group(|ui| { 431 ui.label("32X color tint"); 432 433 ui.horizontal(|ui| { 434 for (value, label) in [ 435 (S32XColorTint::None, "None"), 436 (S32XColorTint::SlightYellow, "Slight yellow"), 437 (S32XColorTint::Yellow, "Yellow"), 438 (S32XColorTint::SlightPurple, "Slight purple"), 439 (S32XColorTint::Purple, "Purple"), 440 ] { 441 ui.radio_value(&mut self.config.sega_32x.color_tint, value, label); 442 } 443 }); 444 }) 445 .response 446 .interact_rect; 447 if ui.rect_contains_pointer(rect) { 448 self.state.help_text.insert(window, helptext::S32X_COLOR_TINT); 449 } 450 451 let rect = ui 452 .group(|ui| { 453 ui.label("32X video output"); 454 455 ui.horizontal(|ui| { 456 for (value, label) in [ 457 (S32XVideoOut::Combined, "Combined"), 458 (S32XVideoOut::GenesisOnly, "Genesis VDP only"), 459 (S32XVideoOut::S32XOnly, "32X VDP only"), 460 ] { 461 ui.radio_value(&mut self.config.sega_32x.video_out, value, label); 462 } 463 }); 464 }) 465 .response 466 .interact_rect; 467 if ui.rect_contains_pointer(rect) { 468 self.state.help_text.insert(window, helptext::S32X_VIDEO_OUT); 469 } 470 471 let response = ui.collapsing("32X priority masking", |ui| { 472 ui.checkbox(&mut self.config.sega_32x.show_high_priority, "Show high-priority pixels"); 473 ui.checkbox(&mut self.config.sega_32x.show_low_priority, "Show low-priority pixels"); 474 475 ui.add_space(5.0); 476 477 ui.label("Replace hidden pixels with:"); 478 479 let mut invalid_ignored = false; 480 481 let valid_rgb5 = |color: u8| color < 32; 482 483 ui.horizontal(|ui| { 484 ui.radio_value( 485 &mut self.config.sega_32x.void_color_type, 486 S32XVoidColorType::Direct, 487 "Direct color (0-31):", 488 ); 489 490 ui.label("R"); 491 ui.add( 492 NumericTextEdit::new( 493 &mut self.state.s32x_priority.void_direct_r, 494 &mut self.config.sega_32x.void_direct[0], 495 &mut invalid_ignored, 496 ) 497 .with_validation(valid_rgb5) 498 .desired_width(20.0), 499 ); 500 501 ui.label("G"); 502 ui.add( 503 NumericTextEdit::new( 504 &mut self.state.s32x_priority.void_direct_g, 505 &mut self.config.sega_32x.void_direct[1], 506 &mut invalid_ignored, 507 ) 508 .with_validation(valid_rgb5) 509 .desired_width(20.0), 510 ); 511 512 ui.label("B"); 513 ui.add( 514 NumericTextEdit::new( 515 &mut self.state.s32x_priority.void_direct_b, 516 &mut self.config.sega_32x.void_direct[2], 517 &mut invalid_ignored, 518 ) 519 .with_validation(valid_rgb5) 520 .desired_width(20.0), 521 ); 522 523 ui.label("P"); 524 ui.checkbox(&mut self.config.sega_32x.void_direct_priority, ""); 525 }); 526 527 ui.horizontal(|ui| { 528 ui.radio_value( 529 &mut self.config.sega_32x.void_color_type, 530 S32XVoidColorType::PaletteRam, 531 "Palette color (0-255):", 532 ); 533 534 ui.add( 535 NumericTextEdit::new( 536 &mut self.state.s32x_priority.void_palette, 537 &mut self.config.sega_32x.void_palette_index, 538 &mut invalid_ignored, 539 ) 540 .desired_width(40.0), 541 ); 542 }); 543 }); 544 let contains_rect = ui.rect_contains_pointer(response.header_response.interact_rect) 545 || response 546 .body_response 547 .is_some_and(|resp| ui.rect_contains_pointer(resp.interact_rect)); 548 if contains_rect { 549 self.state.help_text.insert(window, helptext::S32X_PRIORITY_MASKING); 550 } 551 } 552 553 pub(super) fn render_genesis_audio_settings(&mut self, ctx: &Context) { 554 const WINDOW: OpenWindow = OpenWindow::GenesisAudio; 555 556 let mut open = true; 557 Window::new(WINDOW.title()).open(&mut open).show(ctx, |ui| { 558 ui.ctx().global_style_mut(|style| style.spacing.scroll = ScrollStyle::solid()); 559 560 widgets::render_vertical_scroll_area(ui, |ui| { 561 let rect = ui 562 .checkbox( 563 &mut self.config.genesis.quantize_ym2612_output, 564 "Quantize YM2612 channel output", 565 ) 566 .on_hover_text("Quantize channel outputs from 14 bits to 9 bits") 567 .interact_rect; 568 if ui.rect_contains_pointer(rect) { 569 self.state.help_text.insert(WINDOW, helptext::QUANTIZE_YM2612_OUTPUT); 570 } 571 572 let rect = ui 573 .checkbox( 574 &mut self.config.genesis.emulate_ym2612_ladder_effect, 575 "Emulate YM2612 DAC distortion (\"ladder effect\")", 576 ) 577 .interact_rect; 578 if ui.rect_contains_pointer(rect) { 579 self.state.help_text.insert(WINDOW, helptext::YM2612_LADDER_EFFECT); 580 } 581 582 ui.add_space(5.0); 583 ui.group(|ui| { 584 self.render_low_pass_filter_settings(ui); 585 }); 586 587 ui.add_space(5.0); 588 self.render_scd_pcm_interpolation_setting(ui); 589 590 ui.add_space(5.0); 591 self.render_32x_pwm_setting(ui); 592 593 ui.add_space(5.0); 594 self.render_volume_adjustments(ui); 595 596 ui.add_space(5.0); 597 self.render_enabled_sound_sources(ui); 598 599 ui.add_space(5.0); 600 self.render_ym2612_channels_enabled(ui); 601 602 ui.add_space(5.0); 603 self.render_opn2_busy_flag_setting(ui); 604 }); 605 606 self.render_help_text(ui, WINDOW); 607 }); 608 if !open { 609 self.state.open_windows.remove(&WINDOW); 610 } 611 } 612 613 pub(super) fn render_genesis_overclock_settings(&mut self, ctx: &Context) { 614 const WINDOW: OpenWindow = OpenWindow::GenesisOverclock; 615 616 let mut open = true; 617 Window::new(WINDOW.title()).open(&mut open).resizable(false).show(ctx, |ui| { 618 let rect = ui 619 .add(OverclockSlider { 620 label: "Genesis 68000 clock divider", 621 current_value: &mut self.config.genesis.m68k_clock_divider, 622 range: 1..=7, 623 master_clock: genesis_components::NTSC_GENESIS_MCLK_FREQUENCY, 624 default_divider: genesis_config::NATIVE_M68K_DIVIDER as f64, 625 modifier: ClockModifier::Divider, 626 }) 627 .interact_rect; 628 if ui.rect_contains_pointer(rect) { 629 self.state.help_text.insert(WINDOW, helptext::M68K_CLOCK_DIVIDER); 630 } 631 632 ui.add_space(5.0); 633 let rect = ui 634 .add(OverclockSlider { 635 label: "Sega CD sub 68000 clock divider", 636 current_value: &mut self.config.sega_cd.sub_cpu_divider, 637 range: NonZeroU64::new(1).unwrap()..=NonZeroU64::new(4).unwrap(), 638 master_clock: segacd_core::api::SEGA_CD_MASTER_CLOCK_RATE as f64, 639 default_divider: segacd_core::api::DEFAULT_SUB_CPU_DIVIDER as f64, 640 modifier: ClockModifier::Divider, 641 }) 642 .interact_rect; 643 if ui.rect_contains_pointer(rect) { 644 self.state.help_text.insert(WINDOW, helptext::SCD_SUB_CPU_DIVIDER); 645 } 646 647 ui.add_space(5.0); 648 let rect = ui 649 .add(OverclockSlider { 650 label: "32X SH-2 clock multiplier", 651 current_value: &mut self.config.sega_32x.sh2_clock_multiplier, 652 range: NonZeroU64::new(3).unwrap()..=NonZeroU64::new(10).unwrap(), 653 master_clock: genesis_components::NTSC_GENESIS_MCLK_FREQUENCY / 7.0, 654 default_divider: genesis_config::NATIVE_SH2_MULTIPLIER as f64, 655 modifier: ClockModifier::Multiplier, 656 }) 657 .interact_rect; 658 if ui.rect_contains_pointer(rect) { 659 self.state.help_text.insert(WINDOW, helptext::SH2_CLOCK_MULTIPLIER); 660 } 661 662 ui.add_space(5.0); 663 let rect = ui 664 .group(|ui| { 665 ui.label("Sega CD disc drive speed (low compatibility)"); 666 667 ui.horizontal(|ui| { 668 ui.radio_value( 669 &mut self.config.sega_cd.disc_drive_speed, 670 NonZeroU16::new(1).unwrap(), 671 "1x (Native)", 672 ); 673 ui.radio_value( 674 &mut self.config.sega_cd.disc_drive_speed, 675 NonZeroU16::new(2).unwrap(), 676 "2x", 677 ); 678 ui.radio_value( 679 &mut self.config.sega_cd.disc_drive_speed, 680 NonZeroU16::new(3).unwrap(), 681 "3x", 682 ); 683 ui.radio_value( 684 &mut self.config.sega_cd.disc_drive_speed, 685 NonZeroU16::new(4).unwrap(), 686 "4x", 687 ); 688 }); 689 }) 690 .response 691 .interact_rect; 692 if ui.rect_contains_pointer(rect) { 693 self.state.help_text.insert(WINDOW, helptext::SCD_DRIVE_SPEED); 694 } 695 696 self.render_help_text(ui, WINDOW); 697 }); 698 if !open { 699 self.state.open_windows.remove(&WINDOW); 700 } 701 } 702 703 fn render_low_pass_filter_settings(&mut self, ui: &mut Ui) { 704 const WINDOW: OpenWindow = OpenWindow::GenesisAudio; 705 706 ui.heading("Low-pass filtering"); 707 708 let rect = ui 709 .checkbox( 710 &mut self.config.genesis.genesis_lpf_enabled, 711 "Emulate Genesis first-order low-pass filter", 712 ) 713 .interact_rect; 714 if ui.rect_contains_pointer(rect) { 715 self.state.help_text.insert(WINDOW, helptext::GENESIS_LOW_PASS); 716 } 717 718 let rect = ui 719 .horizontal(|ui| { 720 ui.add_space(15.0); 721 722 ui.label("Cutoff frequency"); 723 724 ui.add( 725 Slider::new(&mut self.config.genesis.genesis_lpf_cutoff, 1..=15000).text("Hz"), 726 ); 727 }) 728 .response 729 .interact_rect; 730 if ui.rect_contains_pointer(rect) { 731 self.state.help_text.insert(WINDOW, helptext::GENESIS_LOW_PASS); 732 } 733 734 let rect = ui 735 .checkbox( 736 &mut self.config.genesis.ym2612_2nd_lpf_enabled, 737 "Emulate YM2612 second-order low-pass filter", 738 ) 739 .interact_rect; 740 if ui.rect_contains_pointer(rect) { 741 self.state.help_text.insert(WINDOW, helptext::YM2612_2ND_LOW_PASS); 742 } 743 744 let rect = ui 745 .horizontal(|ui| { 746 ui.add_space(15.0); 747 748 ui.label("Cutoff frequency"); 749 750 ui.add( 751 Slider::new(&mut self.config.genesis.ym2612_2nd_lpf_cutoff, 1..=15000) 752 .text("Hz"), 753 ); 754 }) 755 .response 756 .interact_rect; 757 if ui.rect_contains_pointer(rect) { 758 self.state.help_text.insert(WINDOW, helptext::YM2612_2ND_LOW_PASS); 759 } 760 761 ui.add_space(5.0); 762 ui.horizontal(|ui| { 763 ui.label("Presets:"); 764 765 if ui.button("Model 1 VA2").clicked() { 766 self.config.genesis.genesis_lpf_enabled = true; 767 self.config.genesis.genesis_lpf_cutoff = genesis_config::MODEL_1_VA2_LPF_CUTOFF; 768 self.config.genesis.ym2612_2nd_lpf_enabled = false; 769 } 770 771 if ui.button("Model 1 VA3").clicked() { 772 self.config.genesis.genesis_lpf_enabled = true; 773 self.config.genesis.genesis_lpf_cutoff = genesis_config::MODEL_1_VA3_LPF_CUTOFF; 774 self.config.genesis.ym2612_2nd_lpf_enabled = false; 775 } 776 777 if ui.button("Model 2").clicked() { 778 self.config.genesis.genesis_lpf_enabled = true; 779 self.config.genesis.genesis_lpf_cutoff = genesis_config::MODEL_2_1ST_LPF_CUTOFF; 780 self.config.genesis.ym2612_2nd_lpf_enabled = true; 781 self.config.genesis.ym2612_2nd_lpf_cutoff = genesis_config::MODEL_2_2ND_LPF_CUTOFF; 782 } 783 784 if ui.button("None").clicked() { 785 self.config.genesis.genesis_lpf_enabled = false; 786 self.config.genesis.ym2612_2nd_lpf_enabled = false; 787 } 788 }); 789 790 ui.separator(); 791 792 let rect = ui 793 .checkbox( 794 &mut self.config.sega_cd.pcm_lpf_enabled, 795 "(Sega CD) Apply second-order low-pass filter to PCM chip", 796 ) 797 .interact_rect; 798 if ui.rect_contains_pointer(rect) { 799 self.state.help_text.insert(WINDOW, helptext::PCM_LOW_PASS); 800 } 801 802 ui.horizontal(|ui| { 803 ui.add_space(15.0); 804 805 ui.label("Cutoff frequency"); 806 807 ui.add(Slider::new(&mut self.config.sega_cd.pcm_lpf_cutoff, 1..=15000).text("Hz")); 808 809 if ui.button("Default").clicked() { 810 self.config.sega_cd.pcm_lpf_cutoff = segacd_core::DEFAULT_PCM_LPF_CUTOFF; 811 } 812 }); 813 814 let rect = ui 815 .add_enabled_ui(self.config.genesis.genesis_lpf_enabled, |ui| { 816 ui.checkbox( 817 &mut self.config.sega_cd.apply_genesis_lpf_to_pcm, 818 "(Sega CD) Apply Genesis low-pass filter to PCM chip", 819 ); 820 ui.checkbox( 821 &mut self.config.sega_cd.apply_genesis_lpf_to_cd_da, 822 "(Sega CD) Apply Genesis low-pass filter to CD-DA", 823 ); 824 }) 825 .response 826 .interact_rect; 827 if ui.rect_contains_pointer(rect) { 828 self.state.help_text.insert(WINDOW, helptext::SCD_GEN_LOW_PASS); 829 } 830 831 let rect = ui 832 .add_enabled_ui(self.config.genesis.genesis_lpf_enabled, |ui| { 833 ui.checkbox( 834 &mut self.config.sega_32x.apply_genesis_lpf_to_pwm, 835 "(32X) Apply Genesis low-pass filter to PWM chip", 836 ); 837 }) 838 .response 839 .interact_rect; 840 if ui.rect_contains_pointer(rect) { 841 self.state.help_text.insert(WINDOW, helptext::S32X_GEN_LOW_PASS); 842 } 843 } 844 845 fn render_opn2_busy_flag_setting(&mut self, ui: &mut Ui) { 846 let rect = ui 847 .group(|ui| { 848 ui.label("OPN2 busy flag behavior"); 849 850 ui.horizontal(|ui| { 851 ui.radio_value( 852 &mut self.config.genesis.opn2_busy_behavior, 853 Opn2BusyBehavior::Ym2612, 854 "YM2612", 855 ); 856 ui.radio_value( 857 &mut self.config.genesis.opn2_busy_behavior, 858 Opn2BusyBehavior::Ym3438, 859 "YM3438", 860 ); 861 ui.radio_value( 862 &mut self.config.genesis.opn2_busy_behavior, 863 Opn2BusyBehavior::AlwaysZero, 864 "Always 0", 865 ); 866 }); 867 }) 868 .response 869 .interact_rect; 870 if ui.rect_contains_pointer(rect) { 871 self.state.help_text.insert(OpenWindow::GenesisAudio, helptext::OPN2_BUSY_BEHAVIOR); 872 } 873 } 874 875 fn render_scd_pcm_interpolation_setting(&mut self, ui: &mut Ui) { 876 let rect = ui 877 .group(|ui| { 878 ui.label("Sega CD PCM chip interpolation"); 879 880 ui.horizontal(|ui| { 881 for (value, label) in [ 882 (PcmInterpolation::None, "None"), 883 (PcmInterpolation::Linear, "Linear"), 884 (PcmInterpolation::CubicHermite, "4-point Cubic"), 885 (PcmInterpolation::CubicHermite6Point, "6-point Cubic"), 886 ] { 887 ui.radio_value(&mut self.config.sega_cd.pcm_interpolation, value, label); 888 } 889 }); 890 }) 891 .response 892 .interact_rect; 893 if ui.rect_contains_pointer(rect) { 894 self.state.help_text.insert(OpenWindow::GenesisAudio, helptext::SCD_PCM_INTERPOLATION); 895 } 896 } 897 898 fn render_32x_pwm_setting(&mut self, ui: &mut Ui) { 899 let rect = ui 900 .group(|ui| { 901 ui.label("32X PWM chip resampling"); 902 903 ui.horizontal(|ui| { 904 for (value, label) in [ 905 (S32XPwmResampling::CubicHermite, "Cubic Hermite"), 906 (S32XPwmResampling::WindowedSinc, "Windowed sinc"), 907 ] { 908 ui.radio_value(&mut self.config.sega_32x.pwm_resampling, value, label); 909 } 910 }); 911 }) 912 .response 913 .interact_rect; 914 if ui.rect_contains_pointer(rect) { 915 self.state.help_text.insert(OpenWindow::GenesisAudio, helptext::S32X_PWM_RESAMPLING); 916 } 917 } 918 919 fn render_ym2612_channels_enabled(&mut self, ui: &mut Ui) { 920 let rect = ui 921 .group(|ui| { 922 ui.label("Enabled YM2612 channels"); 923 924 ui.horizontal(|ui| { 925 for i in 0..6 { 926 ui.checkbox( 927 &mut self.config.genesis.ym2612_channels_enabled[i], 928 (i + 1).to_string(), 929 ); 930 } 931 }); 932 933 ui.horizontal(|ui| { 934 if ui.button("Enable all").clicked() { 935 self.config.genesis.ym2612_channels_enabled.fill(true); 936 } 937 938 if ui.button("Disable all").clicked() { 939 self.config.genesis.ym2612_channels_enabled.fill(false); 940 } 941 }); 942 }) 943 .response 944 .interact_rect; 945 if ui.rect_contains_pointer(rect) { 946 self.state 947 .help_text 948 .insert(OpenWindow::GenesisAudio, helptext::ENABLED_YM2612_CHANNELS); 949 } 950 } 951 952 fn render_enabled_sound_sources(&mut self, ui: &mut Ui) { 953 let rect = ui 954 .group(|ui| { 955 ui.label("Enabled sound sources"); 956 957 ui.horizontal(|ui| { 958 ui.checkbox(&mut self.config.genesis.ym2612_enabled, "YM2612 FM synth chip"); 959 ui.checkbox(&mut self.config.genesis.psg_enabled, "SN76489 PSG chip"); 960 }); 961 ui.horizontal(|ui| { 962 ui.checkbox(&mut self.config.sega_cd.pcm_enabled, "(Sega CD) RF5C164 PCM chip"); 963 ui.checkbox( 964 &mut self.config.sega_cd.cd_audio_enabled, 965 "(Sega CD) CD-DA playback", 966 ); 967 }); 968 969 ui.checkbox(&mut self.config.sega_32x.pwm_enabled, "(32X) PWM chip"); 970 }) 971 .response 972 .interact_rect; 973 if ui.rect_contains_pointer(rect) { 974 self.state.help_text.insert(OpenWindow::GenesisAudio, helptext::SOUND_SOURCES); 975 } 976 } 977 978 fn render_volume_adjustments(&mut self, ui: &mut Ui) { 979 let rect = ui 980 .group(|ui| { 981 ui.label("Volume adjustments (dB) (+/-)"); 982 983 ui.add_space(2.0); 984 985 render_volume_adjustment( 986 "YM2612 FM synth chip", 987 &mut self.state.genesis_volume.ym2612_text, 988 &mut self.state.genesis_volume.ym2612_invalid, 989 &mut self.config.genesis.ym2612_volume_adjustment_db, 990 ui, 991 ); 992 render_volume_adjustment( 993 "SN76489 PSG chip", 994 &mut self.state.genesis_volume.psg_text, 995 &mut self.state.genesis_volume.psg_invalid, 996 &mut self.config.genesis.psg_volume_adjustment_db, 997 ui, 998 ); 999 render_volume_adjustment( 1000 "(Sega CD) RF5C164 PCM chip", 1001 &mut self.state.genesis_volume.pcm_text, 1002 &mut self.state.genesis_volume.pcm_invalid, 1003 &mut self.config.sega_cd.pcm_volume_adjustment_db, 1004 ui, 1005 ); 1006 render_volume_adjustment( 1007 "(Sega CD) CD-DA playback", 1008 &mut self.state.genesis_volume.cd_text, 1009 &mut self.state.genesis_volume.cd_invalid, 1010 &mut self.config.sega_cd.cd_volume_adjustment_db, 1011 ui, 1012 ); 1013 render_volume_adjustment( 1014 "(32X) PWM chip", 1015 &mut self.state.genesis_volume.pwm_text, 1016 &mut self.state.genesis_volume.pwm_invalid, 1017 &mut self.config.sega_32x.pwm_volume_adjustment_db, 1018 ui, 1019 ); 1020 1021 if ui.button("Clear all").clicked() { 1022 self.config.genesis.ym2612_volume_adjustment_db = 0.0; 1023 self.config.genesis.psg_volume_adjustment_db = 0.0; 1024 self.config.sega_cd.pcm_volume_adjustment_db = 0.0; 1025 self.config.sega_cd.cd_volume_adjustment_db = 0.0; 1026 self.config.sega_32x.pwm_volume_adjustment_db = 0.0; 1027 1028 self.state.genesis_volume = GenesisVolumeState::from_config(&self.config); 1029 } 1030 1031 if self.state.genesis_volume.any_invalid() { 1032 ui.colored_label(Color32::RED, "Values must be numbers"); 1033 } 1034 }) 1035 .response 1036 .interact_rect; 1037 if ui.rect_contains_pointer(rect) { 1038 self.state.help_text.insert(OpenWindow::GenesisAudio, helptext::VOLUME_ADJUSTMENTS); 1039 } 1040 } 1041 1042 #[must_use] 1043 pub(super) fn render_scd_bios_error( 1044 &mut self, 1045 ctx: &Context, 1046 open: &mut bool, 1047 region: GenesisRegion, 1048 ) -> RenderErrorEffect { 1049 let bios_path = match region { 1050 GenesisRegion::Americas => &mut self.config.sega_cd.bios_path, 1051 GenesisRegion::Europe => &mut self.config.sega_cd.eu_bios_path, 1052 GenesisRegion::Japan => &mut self.config.sega_cd.jp_bios_path, 1053 }; 1054 1055 widgets::render_bios_error( 1056 ctx, 1057 open, 1058 BiosErrorStrings { 1059 title: format!("Missing Sega CD {} BIOS", region.short_name()), 1060 text: format!( 1061 "No Sega CD {} BIOS path is configured. A Sega CD BIOS ROM is required for Sega CD emulation.", 1062 region.long_name() 1063 ), 1064 button_label: format!("Configure Sega CD {} BIOS path", region.short_name()), 1065 }, 1066 bios_path, 1067 Console::SegaCd, 1068 pick_scd_bios_path, 1069 ) 1070 } 1071} 1072 1073fn render_volume_adjustment( 1074 label: &str, 1075 text: &mut String, 1076 invalid: &mut bool, 1077 value: &mut f64, 1078 ui: &mut Ui, 1079) { 1080 ui.horizontal(|ui| { 1081 ui.add( 1082 NumericTextEdit::new(text, value, invalid) 1083 .desired_width(40.0) 1084 .with_validation(f64::is_finite), 1085 ); 1086 1087 ui.label(label); 1088 }); 1089} 1090 1091fn pick_scd_bios_path() -> Option<PathBuf> { 1092 FileDialog::new().add_filter("bin", &["bin"]).add_filter("All Types", &["*"]).pick_file() 1093}