Skip to content

Commit

Permalink
[NES] fix implementation of rendering disabled backdrop color (#53 / #55
Browse files Browse the repository at this point in the history
 / #56)

the backdrop color is not locked in when rendering disabled; rather, when rendering is disabled, the PPU always displays the color at the current palette RAM address whenever the current VRAM address is in the palette RAM range of $3F00-$3FFF

this also fixes a bug where palette RAM backdrop color mirroring did not work correctly when the backdrop color is displayed this way, which caused a solid gray bar on the Micro Machines title screen
  • Loading branch information
jsgroth committed Oct 7, 2024
1 parent b1801da commit 4213818
Show file tree
Hide file tree
Showing 2 changed files with 28 additions and 39 deletions.
34 changes: 15 additions & 19 deletions backend/nes-core/src/bus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ pub const CPU_IO_TEST_MODE_END: u16 = 0x401F;
pub const CPU_CARTRIDGE_START: u16 = 0x4020;
pub const CPU_CARTRIDGE_END: u16 = 0xFFFF;

pub const PALETTE_RAM_MASK: u16 = 0x001F;

#[derive(Debug, Clone, Copy, Default, Encode, Decode)]
struct PendingCpuWrite {
address: u16,
Expand Down Expand Up @@ -952,8 +954,9 @@ impl<'a> CpuBus<'a> {
let (data, buffer_read_address) = if address < 0x3F00 {
(self.0.ppu_registers.ppu_data_buffer, address)
} else {
let palette_address = map_palette_address(address);
let palette_byte = self.0.ppu_palette_ram[palette_address];
let palette_address = address & PALETTE_RAM_MASK;
let palette_byte = self.0.ppu_palette_ram[palette_address as usize];

// When PPUDATA is used to read palette RAM, buffer reads mirror the nametable
// data located at $2F00-$2FFF
(palette_byte, address - 0x1000)
Expand Down Expand Up @@ -1058,11 +1061,7 @@ impl<'a> PpuBus<'a> {

match address {
0x0000..=0x3EFF => self.0.mapper.read_ppu_address(address, &self.0.ppu_vram),
0x3F00..=0x3FFF => {
let palette_relative_addr = map_palette_address(address);

self.0.ppu_palette_ram[palette_relative_addr]
}
0x3F00..=0x3FFF => self.0.ppu_palette_ram[(address & PALETTE_RAM_MASK) as usize],
0x4000..=0xFFFF => {
unreachable!("{address} should be <= 0x3FFF after masking with 0x3FFF")
}
Expand All @@ -1076,8 +1075,15 @@ impl<'a> PpuBus<'a> {
self.0.mapper.write_ppu_address(address, value, &mut self.0.ppu_vram);
}
0x3F00..=0x3FFF => {
let palette_relative_addr = map_palette_address(address);
self.0.ppu_palette_ram[palette_relative_addr] = value;
let palette_ram_addr = (address & PALETTE_RAM_MASK) as usize;
self.0.ppu_palette_ram[palette_ram_addr] = value;

// Sprite backdrop colors ($3F10, $3F14, $3F18, $3F1C) mirror BG backdrop colors ($3F00, $3F04, $3F08, $3F0C)
// Emulate this by writing to both locations whenever either is written to.
// Super Mario Bros. and Micro Machines depend on this for correct colors
if palette_ram_addr & 3 == 0 {
self.0.ppu_palette_ram[palette_ram_addr ^ 0x10] = value;
}
}
0x4000..=0xFFFF => {
unreachable!("{address} should be <= 0x3FFF after masking with 0x3FFF")
Expand Down Expand Up @@ -1122,16 +1128,6 @@ impl<'a> PpuBus<'a> {
}
}

fn map_palette_address(address: u16) -> usize {
let palette_relative_addr = (address & 0x001F) as usize;
if palette_relative_addr >= 0x10 && palette_relative_addr.trailing_zeros() >= 2 {
// 0x10, 0x14, 0x18, 0x1C are mirrored to 0x00, 0x04, 0x08, 0x0C
palette_relative_addr - 0x10
} else {
palette_relative_addr
}
}

#[cfg(test)]
mod tests {
use crate::api::Overscan;
Expand Down
33 changes: 13 additions & 20 deletions backend/nes-core/src/ppu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
//! for a total of 312 scanlines.
use crate::api::NesEmulatorConfig;
use crate::bus;
use crate::bus::{PpuBus, PpuRegisters, PpuTrackedRegister, PpuWriteToggle};
use bincode::{Decode, Encode};
use jgenesis_common::frontend::TimingMode;
Expand Down Expand Up @@ -115,8 +116,11 @@ impl TimingModePpuExt for TimingMode {

#[derive(Debug, Clone, Encode, Decode)]
struct InternalRegisters {
// v register (15-bit)
vram_address: u16,
// t register (15-bit)
temp_vram_address: u16,
// x register (3-bit)
fine_x_scroll: u8,
}

Expand Down Expand Up @@ -323,7 +327,6 @@ pub struct PpuState {
scanline: u16,
dot: u16,
odd_frame: bool,
rendering_disabled_backdrop_color: Option<u8>,
pending_sprite_0_hit: bool,
}

Expand All @@ -340,7 +343,6 @@ impl PpuState {
scanline: timing_mode.pre_render_scanline(),
dot: 0,
odd_frame: false,
rendering_disabled_backdrop_color: Some(BLACK_NES_COLOR),
pending_sprite_0_hit: false,
}
}
Expand Down Expand Up @@ -410,32 +412,23 @@ pub fn tick(state: &mut PpuState, bus: &mut PpuBus<'_>, config: NesEmulatorConfi

let color_mask = get_color_mask(bus.get_ppu_registers());
if rendering_enabled {
state.rendering_disabled_backdrop_color = None;
process_scanline(state, bus, config.remove_sprite_limit);
} else {
bus.get_ppu_registers_mut().set_oam_open_bus(None);

if !VISIBLE_SCANLINES.contains(&state.scanline) {
// The backdrop color always resets to color 0 when rendering is disabled outside of
// active display
state.rendering_disabled_backdrop_color = Some(bus.get_palette_ram()[0] & color_mask);
}

// When rendering is disabled, pixels should use whatever the backdrop color was set to
// at disable time until rendering is enabled again
let backdrop_color = *state.rendering_disabled_backdrop_color.get_or_insert_with(|| {
// "Background palette hack": If rendering is disabled mid-frame while the current
// VRAM address is inside the palette RAM address range, use the color at that
// address instead of the standard backdrop color
let palette_ram_addr = if (0x3F00..=0x3FFF).contains(&state.registers.vram_address) {
state.registers.vram_address & 0x001F
if VISIBLE_SCANLINES.contains(&state.scanline) && RENDERING_DOTS.contains(&state.dot) {
// When rendering is disabled, the PPU normally always outputs the backdrop color (index 0),
// but if the current VRAM address is in the palette RAM range ($3F00-$3FFF) then it will
// use the color at the current palette RAM address instead.
// Micro Machines depends on this for correct rendering, as do certain test roms (e.g. full_palette.nes)
let vram_addr = state.registers.vram_address & 0x3FFF;
let palette_ram_addr = if (0x3F00..=0x3FFF).contains(&vram_addr) {
vram_addr & bus::PALETTE_RAM_MASK
} else {
0
};
bus.get_palette_ram()[palette_ram_addr as usize] & color_mask
});
let backdrop_color = bus.get_palette_ram()[palette_ram_addr as usize] & color_mask;

if VISIBLE_SCANLINES.contains(&state.scanline) && RENDERING_DOTS.contains(&state.dot) {
let color_emphasis = ColorEmphasis::get_current(bus, state.timing_mode);
state.set_in_frame_buffer(
state.scanline,
Expand Down

0 comments on commit 4213818

Please sign in to comment.