Skip to content

Commit

Permalink
[SNES] add an option to disable deinterlacing in the few games that u…
Browse files Browse the repository at this point in the history
…se interlaced display mode
  • Loading branch information
jsgroth committed Dec 14, 2024
1 parent 09dc90e commit 330d036
Show file tree
Hide file tree
Showing 11 changed files with 135 additions and 32 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 3 additions & 1 deletion backend/snes-core/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ pub enum AudioInterpolationMode {
pub struct SnesEmulatorConfig {
pub forced_timing_mode: Option<TimingMode>,
pub aspect_ratio: SnesAspectRatio,
pub deinterlace: bool,
pub audio_interpolation: AudioInterpolationMode,
pub audio_60hz_hack: bool,
pub gsu_overclock_factor: NonZeroU64,
Expand Down Expand Up @@ -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}");
Expand Down Expand Up @@ -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);

Expand Down
2 changes: 1 addition & 1 deletion backend/snes-core/src/memory/cartridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
125 changes: 97 additions & 28 deletions backend/snes-core/src/ppu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -360,6 +361,7 @@ pub struct Ppu {
frame_buffer: FrameBuffer,
sprite_buffer: Vec<SpriteData>,
sprite_tile_buffer: Vec<SpriteTileData>,
deinterlace: bool,
}

// In actual hardware, PPU starts rendering pixels at H=22 / mclk=88
Expand All @@ -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(),
Expand All @@ -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,
}
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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);
Expand All @@ -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,
Expand Down Expand Up @@ -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());
Expand Down
5 changes: 5 additions & 0 deletions frontend/jgenesis-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,10 @@ struct Args {
#[arg(long, help_heading = SNES_OPTIONS_HEADING)]
snes_aspect_ratio: Option<SnesAspectRatio>,

/// Deinterlace if a game enables interlaced rendering
#[arg(long, help_heading = SNES_OPTIONS_HEADING)]
snes_deinterlace: Option<bool>,

/// Audio interpolation mode
#[arg(long, help_heading = SNES_OPTIONS_HEADING)]
snes_audio_interpolation: Option<AudioInterpolationMode>,
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion frontend/jgenesis-gui/src/app/genesis/helptext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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).",
],
};

Expand Down
9 changes: 9 additions & 0 deletions frontend/jgenesis-gui/src/app/snes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 8 additions & 0 deletions frontend/jgenesis-gui/src/app/snes/helptext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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: &[
Expand Down
7 changes: 7 additions & 0 deletions frontend/jgenesis-native-config/src/snes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ pub struct SnesAppConfig {
pub forced_timing_mode: Option<TimingMode>,
#[serde(default)]
pub aspect_ratio: SnesAspectRatio,
#[serde(default = "true_fn")]
pub deinterlace: bool,
#[serde(default)]
pub audio_interpolation: AudioInterpolationMode,
#[serde(default)]
Expand All @@ -24,6 +26,10 @@ pub struct SnesAppConfig {
pub st011_rom_path: Option<String>,
}

const fn true_fn() -> bool {
true
}

fn default_gsu_overclock() -> NonZeroU64 {
NonZeroU64::new(1).unwrap()
}
Expand All @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions frontend/jgenesis-native-driver/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,7 @@ pub struct SnesConfig {
pub inputs: SnesInputConfig,
pub forced_timing_mode: Option<TimingMode>,
pub aspect_ratio: SnesAspectRatio,
pub deinterlace: bool,
pub audio_interpolation: AudioInterpolationMode,
pub audio_60hz_hack: bool,
pub gsu_overclock_factor: NonZeroU64,
Expand All @@ -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,
Expand Down
1 change: 1 addition & 0 deletions frontend/jgenesis-web/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down

0 comments on commit 330d036

Please sign in to comment.