snes.rsannotatedsnes.rssource326 lines · 11.5 KB · raw
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}