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}