From 330d036870f1067cccc4873b92a6dda9b06b6f0b Mon Sep 17 00:00:00 2001 From: jsgroth <1137683+jsgroth@users.noreply.github.com> Date: Fri, 13 Dec 2024 19:54:03 -0600 Subject: [PATCH] [SNES] add an option to disable deinterlacing in the few games that use interlaced display mode --- CHANGELOG.md | 2 +- backend/snes-core/src/api.rs | 4 +- backend/snes-core/src/memory/cartridge.rs | 2 +- backend/snes-core/src/ppu.rs | 125 ++++++++++++++---- frontend/jgenesis-cli/src/main.rs | 5 + .../jgenesis-gui/src/app/genesis/helptext.rs | 2 +- frontend/jgenesis-gui/src/app/snes.rs | 9 ++ .../jgenesis-gui/src/app/snes/helptext.rs | 8 ++ frontend/jgenesis-native-config/src/snes.rs | 7 + frontend/jgenesis-native-driver/src/config.rs | 2 + frontend/jgenesis-web/src/config.rs | 1 + 11 files changed, 135 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62e5a27f..4707da13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Next Release ## New Features -* (**Genesis**) Added a new video setting to disable deinterlacing in the handful of games that use the interlaced screen modes (e.g. _Sonic the Hedgehog 2_ in competitive 2P mode) +* (**Genesis / SNES**) Added a new video setting to disable deinterlacing in the handful of games that use interlaced display modes (e.g. _Sonic the Hedgehog 2_ in 2P Vs. mode, _Air Strike Patrol_ in mission briefing screens) ## Improvements * GUI: When opening a game that requires a BIOS ROM or firmware ROM (e.g. any Sega CD game), if the BIOS/firmware ROM path is not configured, the error window now contains a button to configure the appropriate ROM path and immediately launch the game diff --git a/backend/snes-core/src/api.rs b/backend/snes-core/src/api.rs index 46a9e7b6..15f8b571 100644 --- a/backend/snes-core/src/api.rs +++ b/backend/snes-core/src/api.rs @@ -80,6 +80,7 @@ pub enum AudioInterpolationMode { pub struct SnesEmulatorConfig { pub forced_timing_mode: Option, pub aspect_ratio: SnesAspectRatio, + pub deinterlace: bool, pub audio_interpolation: AudioInterpolationMode, pub audio_60hz_hack: bool, pub gsu_overclock_factor: NonZeroU64, @@ -210,7 +211,7 @@ impl SnesEmulator { let timing_mode = config.forced_timing_mode.unwrap_or_else(|| memory.cartridge_timing_mode()); - let ppu = Ppu::new(timing_mode); + let ppu = Ppu::new(timing_mode, config); let apu = Apu::new(timing_mode, config); log::info!("Running with timing/display mode {timing_mode}"); @@ -407,6 +408,7 @@ impl EmulatorTrait for SnesEmulator { fn reload_config(&mut self, config: &Self::Config) { self.aspect_ratio = config.aspect_ratio; + self.ppu.update_config(*config); self.apu.update_config(*config); self.memory.update_gsu_overclock_factor(config.gsu_overclock_factor); diff --git a/backend/snes-core/src/memory/cartridge.rs b/backend/snes-core/src/memory/cartridge.rs index e0a56ec8..88985d17 100644 --- a/backend/snes-core/src/memory/cartridge.rs +++ b/backend/snes-core/src/memory/cartridge.rs @@ -217,7 +217,7 @@ impl Cartridge { upd77c25::ST01X_RAM_LEN_BYTES } else if cartridge_type == CartridgeType::SuperFx { superfx::guess_ram_len(&rom) - } else if sram_header_byte == 0 { + } else if sram_header_byte == 0 || sram_header_byte > 21 { 0 } else { 1 << (10 + sram_header_byte) diff --git a/backend/snes-core/src/ppu.rs b/backend/snes-core/src/ppu.rs index 6e49456e..3bec5b9b 100644 --- a/backend/snes-core/src/ppu.rs +++ b/backend/snes-core/src/ppu.rs @@ -4,6 +4,7 @@ mod colortable; mod debug; mod registers; +use crate::api::SnesEmulatorConfig; use crate::ppu::registers::{ AccessFlipflop, BgMode, BgScreenSize, BitsPerPixel, MidScanlineUpdate, Mode7OobBehavior, ObjPriorityMode, Registers, TileSize, VramIncrementMode, @@ -360,6 +361,7 @@ pub struct Ppu { frame_buffer: FrameBuffer, sprite_buffer: Vec, sprite_tile_buffer: Vec, + deinterlace: bool, } // In actual hardware, PPU starts rendering pixels at H=22 / mclk=88 @@ -375,7 +377,7 @@ const RENDER_LINE_MCLK: u64 = 92; const END_RENDER_LINE_MCLK: u64 = RENDER_LINE_MCLK + 256 * 4 - 3 * 4; impl Ppu { - pub fn new(timing_mode: TimingMode) -> Self { + pub fn new(timing_mode: TimingMode, config: SnesEmulatorConfig) -> Self { Self { timing_mode, registers: Registers::new(), @@ -387,6 +389,7 @@ impl Ppu { frame_buffer: FrameBuffer::new(), sprite_buffer: Vec::with_capacity(MAX_SPRITES_PER_LINE), sprite_tile_buffer: Vec::with_capacity(MAX_SPRITE_TILES_PER_LINE), + deinterlace: config.deinterlace, } } @@ -418,6 +421,11 @@ impl Ppu { || self.state.scanline == scanlines_per_frame + 1 { self.state.scanline = 0; + + if !self.state.v_hi_res_frame && self.registers.interlaced && !self.deinterlace { + self.fix_interlaced_frame_buffer(); + } + // TODO wait until H=1? self.state.odd_frame = !self.state.odd_frame; self.state.last_rendered_scanline = None; @@ -515,15 +523,22 @@ impl Ppu { if self.state.v_hi_res_frame && v_hi_res { // Vertical hi-res, 448px self.render_obj_layer(scanline, false); - self.render_bg_layers_to_buffer(2 * scanline - 1, hi_res_mode, bg_from_pixel); - self.render_scanline(2 * scanline - 1, hi_res_mode, screen_from_pixel); - if self.registers.pseudo_obj_hi_res { - self.render_obj_layer(scanline, true); + if self.deinterlace || !self.state.odd_frame { + // Render even line + self.render_bg_layers_to_buffer(2 * scanline - 1, hi_res_mode, bg_from_pixel); + self.render_scanline(2 * scanline - 1, hi_res_mode, screen_from_pixel); } - self.render_bg_layers_to_buffer(2 * scanline, hi_res_mode, bg_from_pixel); - self.render_scanline(2 * scanline, hi_res_mode, screen_from_pixel); + if self.deinterlace || self.state.odd_frame { + // Render odd line + if self.registers.pseudo_obj_hi_res { + self.render_obj_layer(scanline, true); + } + + self.render_bg_layers_to_buffer(2 * scanline, hi_res_mode, bg_from_pixel); + self.render_scanline(2 * scanline, hi_res_mode, screen_from_pixel); + } } else if !self.state.v_hi_res_frame && v_hi_res { // Probably should never happen - PPU is in 448px mode but interlacing was disabled at // start of frame @@ -537,19 +552,28 @@ impl Ppu { self.render_scanline(scanline, hi_res_mode, screen_from_pixel); } else if self.state.v_hi_res_frame { // Interlacing was enabled at start of frame - duplicate lines - self.render_obj_layer(scanline, false); - self.render_bg_layers_to_buffer(scanline, hi_res_mode, bg_from_pixel); - self.render_scanline(2 * scanline - 1, hi_res_mode, screen_from_pixel); - - if self.registers.interlaced && self.registers.pseudo_obj_hi_res { - self.render_obj_layer(scanline, true); - self.render_scanline(2 * scanline, hi_res_mode, screen_from_pixel); + if !self.deinterlace { + let odd_frame: u16 = self.state.odd_frame.into(); + self.render_obj_layer(scanline, self.state.odd_frame); + self.render_bg_layers_to_buffer(scanline, hi_res_mode, screen_from_pixel); + self.render_scanline(2 * scanline - 1 + odd_frame, hi_res_mode, screen_from_pixel); } else { - self.duplicate_line( - (2 * scanline - 1).into(), - (2 * scanline).into(), - screen_from_pixel.into(), - ); + // Render even line + self.render_obj_layer(scanline, false); + self.render_bg_layers_to_buffer(scanline, hi_res_mode, bg_from_pixel); + self.render_scanline(2 * scanline - 1, hi_res_mode, screen_from_pixel); + + // Duplicate to odd line, re-rendering OBJs if necessary + if self.registers.interlaced && self.registers.pseudo_obj_hi_res { + self.render_obj_layer(scanline, true); + self.render_scanline(2 * scanline, hi_res_mode, screen_from_pixel); + } else { + self.duplicate_line( + (2 * scanline - 1).into(), + (2 * scanline).into(), + screen_from_pixel.into(), + ); + } } } else { // Interlacing is disabled, render normally @@ -1377,21 +1401,25 @@ impl Ppu { } else { last_rendered_scanline }; - for scanline in (1..=last_copy_line).rev() { - let src_line_addr = 256 * u32::from(scanline - 1); - let dest_line_addr = 512 * u32::from(scanline - 1); - for pixel in (0..256).rev() { - let color = self.frame_buffer[(src_line_addr + pixel) as usize]; - self.frame_buffer[(dest_line_addr + 2 * pixel) as usize] = color; - self.frame_buffer[(dest_line_addr + 2 * pixel + 1) as usize] = color; - } - } + self.frame_buffer_h256_to_h512(last_copy_line); } } self.state.h_hi_res_frame = true; } + fn frame_buffer_h256_to_h512(&mut self, to_scanline: u16) { + for scanline in (1..=to_scanline).rev() { + let src_line_addr = 256 * u32::from(scanline - 1); + let dest_line_addr = 512 * u32::from(scanline - 1); + for pixel in (0..256).rev() { + let color = self.frame_buffer[(src_line_addr + pixel) as usize]; + self.frame_buffer[(dest_line_addr + 2 * pixel) as usize] = color; + self.frame_buffer[(dest_line_addr + 2 * pixel + 1) as usize] = color; + } + } + } + fn set_in_frame_buffer(&mut self, scanline: u16, pixel: u16, color: Color) { let screen_width = self.state.frame_screen_width(); let index = u32::from(scanline - 1) * screen_width + u32::from(pixel); @@ -1408,6 +1436,43 @@ impl Ppu { } } + fn fix_interlaced_frame_buffer(&mut self) { + log::debug!("Just entered interlaced mode; rewriting frame buffer"); + + let v_display_size = self.registers.v_display_size.to_lines(); + + // Check if changed from H256px to H512px + let next_frame_h_hi_res = + self.registers.bg_mode.is_hi_res() || self.registers.pseudo_h_hi_res; + if !self.state.h_hi_res_frame && next_frame_h_hi_res { + log::debug!("Expanding previous frame from H256px to H512px"); + self.frame_buffer_h256_to_h512(v_display_size); + } + + log::debug!( + "Expanding previous frame from V{}px to V{}px; screen width H{}px", + v_display_size, + 2 * v_display_size, + if next_frame_h_hi_res { HIRES_SCREEN_WIDTH } else { NORMAL_SCREEN_WIDTH } + ); + + // Duplicate lines to expand frame buffer from V224px to V448px (or V239px to V478px) + let screen_width = if next_frame_h_hi_res { + HIRES_SCREEN_WIDTH as u32 + } else { + NORMAL_SCREEN_WIDTH as u32 + }; + for scanline in (1..=u32::from(v_display_size)).rev() { + let even_line = 2 * scanline - 1; + let odd_line = 2 * scanline; + for pixel in 0..screen_width { + let color = self.frame_buffer[(scanline * screen_width + pixel) as usize]; + self.frame_buffer[(even_line * screen_width + pixel) as usize] = color; + self.frame_buffer[(odd_line * screen_width + pixel) as usize] = color; + } + } + } + fn scanlines_per_frame(&self) -> u16 { match self.timing_mode { TimingMode::Ntsc => 262, @@ -1807,6 +1872,10 @@ impl Ppu { } } + pub fn update_config(&mut self, config: SnesEmulatorConfig) { + self.deinterlace = config.deinterlace; + } + pub fn reset(&mut self) { // Enable forced blanking self.registers.write_inidisp(0x80, self.is_first_vblank_scanline()); diff --git a/frontend/jgenesis-cli/src/main.rs b/frontend/jgenesis-cli/src/main.rs index 79a804ef..96146b96 100644 --- a/frontend/jgenesis-cli/src/main.rs +++ b/frontend/jgenesis-cli/src/main.rs @@ -281,6 +281,10 @@ struct Args { #[arg(long, help_heading = SNES_OPTIONS_HEADING)] snes_aspect_ratio: Option, + /// Deinterlace if a game enables interlaced rendering + #[arg(long, help_heading = SNES_OPTIONS_HEADING)] + snes_deinterlace: Option, + /// Audio interpolation mode #[arg(long, help_heading = SNES_OPTIONS_HEADING)] snes_audio_interpolation: Option, @@ -585,6 +589,7 @@ impl Args { fn apply_snes_overrides(&self, config: &mut AppConfig) { apply_overrides!(self, config.snes, [ snes_aspect_ratio -> aspect_ratio, + snes_deinterlace -> deinterlace, snes_audio_interpolation -> audio_interpolation, snes_audio_60hz_hack -> audio_60hz_hack, gsu_overclock_factor, diff --git a/frontend/jgenesis-gui/src/app/genesis/helptext.rs b/frontend/jgenesis-gui/src/app/genesis/helptext.rs index b29cc26b..03d7b7eb 100644 --- a/frontend/jgenesis-gui/src/app/genesis/helptext.rs +++ b/frontend/jgenesis-gui/src/app/genesis/helptext.rs @@ -59,7 +59,7 @@ pub const DEINTERLACING: HelpText = HelpText { heading: "Deinterlacing", text: &[ "If enabled and a game sets the VDP to an interlaced screen mode, render in progressive mode instead of interlaced.", - "In double-screen interlaced mode, this causes the VDP to render all 448 lines per frame (or 480 in V30 mode).", + "In double-screen interlaced mode, this causes the VDP to render all 448 lines every frame (or 480 in V30 mode).", ], }; diff --git a/frontend/jgenesis-gui/src/app/snes.rs b/frontend/jgenesis-gui/src/app/snes.rs index 57713f8d..6c23ca2f 100644 --- a/frontend/jgenesis-gui/src/app/snes.rs +++ b/frontend/jgenesis-gui/src/app/snes.rs @@ -234,6 +234,15 @@ impl App { self.state.help_text.insert(WINDOW, helptext::ASPECT_RATIO); } + ui.add_space(5.0); + + let rect = ui + .checkbox(&mut self.config.snes.deinterlace, "Deinterlacing enabled") + .interact_rect; + if ui.rect_contains_pointer(rect) { + self.state.help_text.insert(WINDOW, helptext::DEINTERLACING); + } + self.render_help_text(ui, WINDOW); }); if !open { diff --git a/frontend/jgenesis-gui/src/app/snes/helptext.rs b/frontend/jgenesis-gui/src/app/snes/helptext.rs index 35b12abd..7977a49a 100644 --- a/frontend/jgenesis-gui/src/app/snes/helptext.rs +++ b/frontend/jgenesis-gui/src/app/snes/helptext.rs @@ -33,6 +33,14 @@ pub const ASPECT_RATIO: HelpText = HelpText { ], }; +pub const DEINTERLACING: HelpText = HelpText { + heading: "Deinterlacing", + text: &[ + "If enabled and a game turns on interlaced display mode, render in progressive mode instead of interlaced.", + "In high-res interlaced mode (512x448i), this causes the PPU to render all 448 lines every frame (or 478 in 239-line mode).", + ], +}; + pub const ADPCM_INTERPOLATION: HelpText = HelpText { heading: "ADPCM Sample Interpolation", text: &[ diff --git a/frontend/jgenesis-native-config/src/snes.rs b/frontend/jgenesis-native-config/src/snes.rs index 0bfbbbcc..b18caf86 100644 --- a/frontend/jgenesis-native-config/src/snes.rs +++ b/frontend/jgenesis-native-config/src/snes.rs @@ -10,6 +10,8 @@ pub struct SnesAppConfig { pub forced_timing_mode: Option, #[serde(default)] pub aspect_ratio: SnesAspectRatio, + #[serde(default = "true_fn")] + pub deinterlace: bool, #[serde(default)] pub audio_interpolation: AudioInterpolationMode, #[serde(default)] @@ -24,6 +26,10 @@ pub struct SnesAppConfig { pub st011_rom_path: Option, } +const fn true_fn() -> bool { + true +} + fn default_gsu_overclock() -> NonZeroU64 { NonZeroU64::new(1).unwrap() } @@ -42,6 +48,7 @@ impl AppConfig { inputs: self.input.snes.clone(), forced_timing_mode: self.snes.forced_timing_mode, aspect_ratio: self.snes.aspect_ratio, + deinterlace: self.snes.deinterlace, audio_interpolation: self.snes.audio_interpolation, audio_60hz_hack: self.snes.audio_60hz_hack, gsu_overclock_factor: self.snes.gsu_overclock_factor, diff --git a/frontend/jgenesis-native-driver/src/config.rs b/frontend/jgenesis-native-driver/src/config.rs index 722f6fd8..133f400a 100644 --- a/frontend/jgenesis-native-driver/src/config.rs +++ b/frontend/jgenesis-native-driver/src/config.rs @@ -403,6 +403,7 @@ pub struct SnesConfig { pub inputs: SnesInputConfig, pub forced_timing_mode: Option, pub aspect_ratio: SnesAspectRatio, + pub deinterlace: bool, pub audio_interpolation: AudioInterpolationMode, pub audio_60hz_hack: bool, pub gsu_overclock_factor: NonZeroU64, @@ -419,6 +420,7 @@ impl SnesConfig { SnesEmulatorConfig { forced_timing_mode: self.forced_timing_mode, aspect_ratio: self.aspect_ratio, + deinterlace: self.deinterlace, audio_interpolation: self.audio_interpolation, audio_60hz_hack: self.audio_60hz_hack, gsu_overclock_factor: self.gsu_overclock_factor, diff --git a/frontend/jgenesis-web/src/config.rs b/frontend/jgenesis-web/src/config.rs index c5397d15..c6533232 100644 --- a/frontend/jgenesis-web/src/config.rs +++ b/frontend/jgenesis-web/src/config.rs @@ -200,6 +200,7 @@ impl SnesWebConfig { SnesEmulatorConfig { forced_timing_mode: None, aspect_ratio: self.aspect_ratio, + deinterlace: true, audio_interpolation: self.audio_interpolation, audio_60hz_hack: true, gsu_overclock_factor: NonZeroU64::new(1).unwrap(),