nes.rsannotatednes.rssource580 lines · 20.1 KB · raw
1mod helptext;
2
3use crate::app::widgets::NumericTextEdit;
4use crate::app::{App, OpenWindow, widgets};
5use crate::emurunner::EmuRunnerStatus;
6use egui::emath::Align;
7use egui::epaint::Color32;
8use egui::epaint::ImageDelta;
9use egui::epaint::textures::TextureWrapMode;
10use egui::{
11    ColorImage, Context, ImageData, Layout, Slider, TextureFilter, TextureId, TextureOptions, Ui,
12    Window,
13};
14use emath::Vec2;
15use jgenesis_common::frontend::TimingMode;
16use nes_config::palettes::PaletteGenerationArgs;
17use nes_config::{NesAspectRatio, NesAudioResampler, NesPalette, Overscan, PaletteLoadError};
18use rfd::FileDialog;
19use std::mem;
20use std::sync::Arc;
21
22pub struct OverscanState {
23    top_text: String,
24    top_invalid: bool,
25    bottom_text: String,
26    bottom_invalid: bool,
27    left_text: String,
28    left_invalid: bool,
29    right_text: String,
30    right_invalid: bool,
31}
32
33impl Default for OverscanState {
34    fn default() -> Self {
35        let zero_str = "0".to_string();
36
37        Self {
38            top_text: zero_str.clone(),
39            top_invalid: false,
40            bottom_text: zero_str.clone(),
41            bottom_invalid: false,
42            left_text: zero_str.clone(),
43            left_invalid: false,
44            right_text: zero_str,
45            right_invalid: false,
46        }
47    }
48}
49
50impl From<Overscan> for OverscanState {
51    fn from(value: Overscan) -> Self {
52        Self {
53            top_text: value.top.to_string(),
54            top_invalid: false,
55            bottom_text: value.bottom.to_string(),
56            bottom_invalid: false,
57            left_text: value.left.to_string(),
58            left_invalid: false,
59            right_text: value.right.to_string(),
60            right_invalid: false,
61        }
62    }
63}
64
65pub struct NesPaletteState {
66    texture_64_color: TextureId,
67    texture_512_color: TextureId,
68    display_512_color_palette: bool,
69    load_error: Option<PaletteLoadError>,
70    generate_args: PaletteGenerationArgs,
71}
72
73impl NesPaletteState {
74    pub fn create(ctx: &Context, palette: &NesPalette) -> Self {
75        let NesPaletteTextures { texture_64_color, texture_512_color } =
76            create_palette_textures(ctx, palette);
77
78        Self {
79            texture_64_color,
80            texture_512_color,
81            display_512_color_palette: false,
82            load_error: None,
83            generate_args: PaletteGenerationArgs::default(),
84        }
85    }
86}
87
88impl App {
89    pub(super) fn render_nes_general_settings(&mut self, ctx: &Context) {
90        const WINDOW: OpenWindow = OpenWindow::NesGeneral;
91
92        let mut open = true;
93        Window::new(WINDOW.title()).open(&mut open).resizable(false).show(ctx, |ui| {
94            let rect = ui
95                .group(|ui| {
96                    ui.add_enabled_ui(
97                        self.emu_runner.status() != EmuRunnerStatus::RunningNes,
98                        |ui| {
99                            ui.label("Timing / display mode");
100
101                            ui.horizontal(|ui| {
102                                ui.radio_value(
103                                    &mut self.config.nes.forced_timing_mode,
104                                    None,
105                                    "Auto",
106                                );
107                                ui.radio_value(
108                                    &mut self.config.nes.forced_timing_mode,
109                                    Some(TimingMode::Ntsc),
110                                    "NTSC (60Hz)",
111                                );
112                                ui.radio_value(
113                                    &mut self.config.nes.forced_timing_mode,
114                                    Some(TimingMode::Pal),
115                                    "PAL (50Hz)",
116                                );
117                            });
118                        },
119                    );
120                })
121                .response
122                .interact_rect;
123            if ui.rect_contains_pointer(rect) {
124                self.state.help_text.insert(WINDOW, helptext::TIMING_MODE);
125            }
126
127            let rect = ui
128                .checkbox(
129                    &mut self.config.nes.dma_dummy_joy_reads,
130                    "Allow DMA to trigger dummy joypad reads",
131                )
132                .interact_rect;
133            if ui.rect_contains_pointer(rect) {
134                self.state.help_text.insert(WINDOW, helptext::DMA_DUMMY_JOY_READ);
135            }
136
137            self.render_help_text(ui, WINDOW);
138        });
139        if !open {
140            self.state.open_windows.remove(&WINDOW);
141        }
142    }
143
144    pub(super) fn render_nes_video_settings(&mut self, ctx: &Context) {
145        const WINDOW: OpenWindow = OpenWindow::NesVideo;
146
147        let mut open = true;
148        Window::new(WINDOW.title()).open(&mut open).min_width(600.0).show(ctx, |ui| {
149            widgets::render_vertical_scroll_area(ui, |ui| {
150                self.render_aspect_ratio_setting(ui, WINDOW);
151
152                let rect = ui
153                    .checkbox(
154                        &mut self.config.nes.ntsc_crop_vertical_overscan,
155                        "(NTSC) Crop vertical overscan",
156                    )
157                    .interact_rect;
158                if ui.rect_contains_pointer(rect) {
159                    self.state.help_text.insert(WINDOW, helptext::NTSC_V_OVERSCAN);
160                }
161
162                let rect = ui
163                    .checkbox(
164                        &mut self.config.nes.remove_sprite_limit,
165                        "Remove sprite-per-scanline limit",
166                    )
167                    .on_hover_text(
168                        "Eliminates most sprite flickering but can cause visual glitches",
169                    )
170                    .interact_rect;
171                if ui.rect_contains_pointer(rect) {
172                    self.state.help_text.insert(WINDOW, helptext::REMOVE_SPRITE_LIMIT);
173                }
174
175                let rect = ui
176                    .checkbox(&mut self.config.nes.pal_black_border, "Render PAL black border")
177                    .on_hover_text("Crops the image from 256x240 to 252x239")
178                    .interact_rect;
179                if ui.rect_contains_pointer(rect) {
180                    self.state.help_text.insert(WINDOW, helptext::PAL_BLACK_BORDER);
181                }
182
183                ui.separator();
184                self.render_overscan_setting(ui, WINDOW);
185
186                ui.separator();
187                self.render_palette_settings(ui, WINDOW);
188            });
189
190            self.render_help_text(ui, WINDOW);
191        });
192        if !open {
193            self.state.open_windows.remove(&WINDOW);
194        }
195    }
196
197    fn render_aspect_ratio_setting(&mut self, ui: &mut Ui, window: OpenWindow) {
198        let rect = ui
199            .group(|ui| {
200                ui.label("Aspect ratio");
201
202                ui.horizontal(|ui| {
203                    ui.radio_value(&mut self.config.nes.aspect_ratio, NesAspectRatio::Ntsc, "NTSC")
204                        .on_hover_text("8:7 pixel aspect ratio");
205                    ui.radio_value(&mut self.config.nes.aspect_ratio, NesAspectRatio::Pal, "PAL")
206                        .on_hover_text("11:8 pixel aspect ratio");
207                    ui.radio_value(
208                        &mut self.config.nes.aspect_ratio,
209                        NesAspectRatio::SquarePixels,
210                        "Square pixels",
211                    )
212                    .on_hover_text("1:1 pixel aspect ratio");
213                    ui.radio_value(
214                        &mut self.config.nes.aspect_ratio,
215                        NesAspectRatio::Stretched,
216                        "Stretched",
217                    )
218                    .on_hover_text("Stretched to fill the window");
219                });
220            })
221            .response
222            .interact_rect;
223        if ui.rect_contains_pointer(rect) {
224            self.state.help_text.insert(window, helptext::ASPECT_RATIO);
225        }
226    }
227
228    fn render_overscan_setting(&mut self, ui: &mut Ui, window: OpenWindow) {
229        let rect = ui
230            .group(|ui| {
231                ui.label("Overscan in pixels");
232
233                ui.vertical_centered(|ui| {
234                    ui.label("Top");
235                    ui.add(
236                        NumericTextEdit::new(
237                            &mut self.state.overscan.top_text,
238                            &mut self.config.nes.overscan.top,
239                            &mut self.state.overscan.top_invalid,
240                        )
241                        .desired_width(30.0),
242                    );
243                });
244
245                ui.horizontal(|ui| {
246                    ui.label("Left");
247                    ui.add(
248                        NumericTextEdit::new(
249                            &mut self.state.overscan.left_text,
250                            &mut self.config.nes.overscan.left,
251                            &mut self.state.overscan.left_invalid,
252                        )
253                        .desired_width(30.0),
254                    );
255
256                    ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
257                        ui.label("Right");
258                        ui.add(
259                            NumericTextEdit::new(
260                                &mut self.state.overscan.right_text,
261                                &mut self.config.nes.overscan.right,
262                                &mut self.state.overscan.right_invalid,
263                            )
264                            .desired_width(30.0),
265                        );
266                    });
267                });
268
269                ui.vertical_centered(|ui| {
270                    ui.add(
271                        NumericTextEdit::new(
272                            &mut self.state.overscan.bottom_text,
273                            &mut self.config.nes.overscan.bottom,
274                            &mut self.state.overscan.bottom_invalid,
275                        )
276                        .desired_width(30.0),
277                    );
278                    ui.label("Bottom");
279                });
280
281                for (invalid, label) in [
282                    (self.state.overscan.top_invalid, "Top"),
283                    (self.state.overscan.bottom_invalid, "Bottom"),
284                    (self.state.overscan.left_invalid, "Left"),
285                    (self.state.overscan.right_invalid, "Right"),
286                ] {
287                    if invalid {
288                        ui.colored_label(
289                            Color32::RED,
290                            format!("{label} value must be a non-negative integer"),
291                        );
292                    }
293                }
294            })
295            .response
296            .interact_rect;
297        if ui.rect_contains_pointer(rect) {
298            self.state.help_text.insert(window, helptext::OVERSCAN);
299        }
300    }
301
302    fn render_palette_settings(&mut self, ui: &mut Ui, window: OpenWindow) {
303        let rect = ui
304            .scope(|ui| {
305                ui.heading("Palette");
306
307                ui.add_space(5.0);
308
309                ui.horizontal(|ui| {
310                    if ui.button("Restore default").clicked() {
311                        self.config.nes.palette = NesPalette::default();
312                    }
313
314                    if ui.button("Load from file...").clicked() {
315                        self.state.nes_palette.load_error = None;
316                        self.load_palette_from_file();
317                    }
318
319                    if ui.button("Export to file...").clicked() {
320                        self.export_palette_to_file();
321                    }
322                });
323
324                if let Some(err) = &self.state.nes_palette.load_error {
325                    ui.colored_label(Color32::RED, format!("Error loading palette: {err}"));
326                }
327
328                ui.add_space(5.0);
329
330                self.render_palette_generator(ui);
331
332                ui.checkbox(
333                    &mut self.state.nes_palette.display_512_color_palette,
334                    "Display full 512-color palette",
335                );
336
337                let image = if self.state.nes_palette.display_512_color_palette {
338                    (self.state.nes_palette.texture_512_color, Vec2::new(600.0, 500.0))
339                } else {
340                    (self.state.nes_palette.texture_64_color, Vec2::new(600.0, 500.0 / 8.0))
341                };
342                ui.image(image);
343            })
344            .response
345            .interact_rect;
346        if ui.rect_contains_pointer(rect) {
347            self.state.help_text.insert(window, helptext::PALETTE);
348        }
349    }
350
351    fn render_palette_generator(&mut self, ui: &mut Ui) {
352        let original_slider_width = mem::replace(&mut ui.style_mut().spacing.slider_width, 450.0);
353
354        let fmt_2f = |v: f64, _| format!("{v:.2}");
355
356        ui.collapsing("Palette Generator", |ui| {
357            ui.add(
358                Slider::new(&mut self.state.nes_palette.generate_args.brightness, 0.0..=5.0)
359                    .step_by(0.01)
360                    .text("Brightness")
361                    .custom_formatter(fmt_2f),
362            );
363
364            ui.add(
365                Slider::new(&mut self.state.nes_palette.generate_args.saturation, 0.0..=5.0)
366                    .step_by(0.01)
367                    .text("Saturation")
368                    .custom_formatter(fmt_2f),
369            );
370
371            ui.add(
372                Slider::new(&mut self.state.nes_palette.generate_args.contrast, 0.0..=5.0)
373                    .step_by(0.01)
374                    .text("Contrast")
375                    .custom_formatter(fmt_2f),
376            );
377
378            ui.add(
379                Slider::new(&mut self.state.nes_palette.generate_args.hue_offset, -6.0..=6.0)
380                    .step_by(0.01)
381                    .text("Hue offset")
382                    .custom_formatter(fmt_2f),
383            );
384
385            ui.add(
386                Slider::new(&mut self.state.nes_palette.generate_args.gamma, 0.1..=3.0)
387                    .step_by(0.1)
388                    .text("Gamma")
389                    .custom_formatter(|v, _| format!("{v:.1}")),
390            );
391
392            ui.horizontal(|ui| {
393                if ui.button("Generate").clicked() {
394                    self.config.nes.palette =
395                        nes_config::palettes::generate(self.state.nes_palette.generate_args);
396                }
397
398                if ui.button("Default settings").clicked() {
399                    self.state.nes_palette.generate_args = PaletteGenerationArgs::default();
400                }
401            });
402        });
403
404        ui.style_mut().spacing.slider_width = original_slider_width;
405    }
406
407    fn load_palette_from_file(&mut self) {
408        let Some(path) = FileDialog::new()
409            .add_filter("pal", &["pal"])
410            .add_filter("All Files", &["*"])
411            .pick_file()
412        else {
413            return;
414        };
415
416        match NesPalette::read_from(&path) {
417            Ok(palette) => {
418                self.config.nes.palette = palette;
419            }
420            Err(err) => {
421                log::error!("Failed to load palette from path '{}': {err}", path.display());
422
423                self.state.nes_palette.load_error = Some(err);
424            }
425        }
426    }
427
428    fn export_palette_to_file(&self) {
429        let Some(path) = FileDialog::new()
430            .set_file_name("nespalette.pal")
431            .add_filter("pal", &["pal"])
432            .save_file()
433        else {
434            return;
435        };
436
437        if let Err(err) = self.config.nes.palette.write_to(&path) {
438            log::error!("Error saving palette to path '{}': {err}", path.display());
439        }
440    }
441
442    pub(super) fn render_nes_audio_settings(&mut self, ctx: &Context) {
443        const WINDOW: OpenWindow = OpenWindow::NesAudio;
444
445        let mut open = true;
446        Window::new(WINDOW.title()).open(&mut open).resizable(false).show(ctx, |ui| {
447            let rect = ui
448                .checkbox(
449                    &mut self.config.nes.silence_ultrasonic_triangle_output,
450                    "Silence ultrasonic triangle channel output",
451                )
452                .interact_rect;
453            if ui.rect_contains_pointer(rect) {
454                self.state.help_text.insert(WINDOW, helptext::ULTRASONIC_TRIANGLE);
455            }
456
457            let rect = ui
458                .checkbox(&mut self.config.nes.audio_60hz_hack, "Enable audio sync timing hack")
459                .interact_rect;
460            if ui.rect_contains_pointer(rect) {
461                self.state.help_text.insert(WINDOW, helptext::AUDIO_TIMING_HACK);
462            }
463
464            ui.add_space(5.0);
465            let rect = ui
466                .group(|ui| {
467                    ui.label("Audio resampling algorithm");
468
469                    ui.radio_value(
470                        &mut self.config.nes.audio_resampler,
471                        NesAudioResampler::LowPassNearestNeighbor,
472                        "Low-pass filter + nearest neighbor (Faster)",
473                    );
474                    ui.radio_value(
475                        &mut self.config.nes.audio_resampler,
476                        NesAudioResampler::WindowedSinc,
477                        "Windowed sinc interpolation (Higher quality)",
478                    );
479                })
480                .response
481                .interact_rect;
482            if ui.rect_contains_pointer(rect) {
483                self.state.help_text.insert(WINDOW, helptext::AUDIO_RESAMPLING);
484            }
485
486            self.render_help_text(ui, WINDOW);
487        });
488        if !open {
489            self.state.open_windows.remove(&WINDOW);
490        }
491    }
492}
493
494fn nes_palette_to_pixels(palette: &NesPalette) -> Vec<Color32> {
495    let mut pixels = vec![Color32::BLACK; 512];
496    for (i, pixel) in pixels.iter_mut().enumerate() {
497        let (r, g, b) = palette[i];
498        *pixel = Color32::from_rgb(r, g, b);
499    }
500
501    pixels
502}
503
504const PALETTE_TEXTURE_SIZE_64: [usize; 2] = [16, 4];
505const PALETTE_TEXTURE_SIZE_64_1D: usize = PALETTE_TEXTURE_SIZE_64[0] * PALETTE_TEXTURE_SIZE_64[1];
506const PALETTE_TEXTURE_SIZE_512: [usize; 2] = [16, 32];
507
508fn tex_size_to_vec2(tex_size: [usize; 2]) -> Vec2 {
509    Vec2::new(tex_size[0] as f32, tex_size[1] as f32)
510}
511
512const PALETTE_TEXTURE_OPTIONS: TextureOptions = TextureOptions {
513    magnification: TextureFilter::Nearest,
514    minification: TextureFilter::Nearest,
515    wrap_mode: TextureWrapMode::ClampToEdge,
516    mipmap_mode: None,
517};
518
519pub struct NesPaletteTextures {
520    pub texture_64_color: TextureId,
521    pub texture_512_color: TextureId,
522}
523
524pub fn create_palette_textures(ctx: &Context, palette: &NesPalette) -> NesPaletteTextures {
525    let pixels = nes_palette_to_pixels(palette);
526
527    let pixels_64_color = pixels[..PALETTE_TEXTURE_SIZE_64_1D].to_vec();
528    let texture_64_color = ctx.tex_manager().write().alloc(
529        "nes_palette_texture_64c".into(),
530        ImageData::Color(Arc::new(ColorImage {
531            size: PALETTE_TEXTURE_SIZE_64,
532            source_size: tex_size_to_vec2(PALETTE_TEXTURE_SIZE_64),
533            pixels: pixels_64_color,
534        })),
535        PALETTE_TEXTURE_OPTIONS,
536    );
537
538    let texture_512_color = ctx.tex_manager().write().alloc(
539        "nes_palette_texture_512c".into(),
540        ImageData::Color(Arc::new(ColorImage {
541            size: PALETTE_TEXTURE_SIZE_512,
542            source_size: tex_size_to_vec2(PALETTE_TEXTURE_SIZE_512),
543            pixels,
544        })),
545        PALETTE_TEXTURE_OPTIONS,
546    );
547
548    NesPaletteTextures { texture_64_color, texture_512_color }
549}
550
551pub fn update_palette_textures(ctx: &Context, state: &NesPaletteState, palette: &NesPalette) {
552    let pixels = nes_palette_to_pixels(palette);
553
554    let pixels_64_color = pixels[..PALETTE_TEXTURE_SIZE_64_1D].to_vec();
555    ctx.tex_manager().write().set(
556        state.texture_64_color,
557        ImageDelta {
558            image: ImageData::Color(Arc::new(ColorImage {
559                size: PALETTE_TEXTURE_SIZE_64,
560                source_size: tex_size_to_vec2(PALETTE_TEXTURE_SIZE_64),
561                pixels: pixels_64_color,
562            })),
563            options: PALETTE_TEXTURE_OPTIONS,
564            pos: None,
565        },
566    );
567
568    ctx.tex_manager().write().set(
569        state.texture_512_color,
570        ImageDelta {
571            image: ImageData::Color(Arc::new(ColorImage {
572                size: PALETTE_TEXTURE_SIZE_512,
573                source_size: tex_size_to_vec2(PALETTE_TEXTURE_SIZE_512),
574                pixels,
575            })),
576            options: PALETTE_TEXTURE_OPTIONS,
577            pos: None,
578        },
579    );
580}