cge_nes 0.1.2

Cycle-accurate NES (Nintendo Entertainment System) emulator library: CPU, PPU, cartridge, input, and iNES ROM loading.
Documentation
use crate::{Ppu, PpuCartMemorySpace, Register};

#[derive(Default)]
struct MockCartRead;

impl PpuCartMemorySpace for MockCartRead {
    fn read(&mut self, addr: u16) -> u8 {
        let data = addr as u8;

        data.wrapping_add(6)
    }

    fn write(&mut self, _data: u8, _addr: u16) {
        // do nothing
    }
}

/// Counts how many times `notify_addr_change` is called and records the last
/// (old, new) pair. Lets us verify that A12 transitions on $2007
/// auto-increment are signalled to the cartridge.
#[derive(Default)]
struct NotifyCounter {
    notify_count: u32,
    last_old: u16,
    last_new: u16,
}

impl PpuCartMemorySpace for NotifyCounter {
    fn read(&mut self, _addr: u16) -> u8 {
        0
    }

    fn write(&mut self, _data: u8, _addr: u16) {}

    fn notify_addr_change(&mut self, old_addr: u16, new_addr: u16) {
        self.notify_count += 1;
        self.last_old = old_addr;
        self.last_new = new_addr;
    }
}

#[test]
pub fn read_palette_data() {
    let mut ppu = Ppu::new();

    let mut cart = MockCartRead::default();
    let cart_ref_mut = &mut cart;

    // Read palette data
    for addr in 0x3F00..0x3F20 {
        let addr_low = addr as u8;
        let addr_high = (addr >> 8) as u8;

        ppu.write_ppu_register(addr_high, Register::PpuAddr, cart_ref_mut);
        ppu.write_ppu_register(addr_low, Register::PpuAddr, cart_ref_mut);
        let palette_data = ppu.read_ppu_register(Register::Data, cart_ref_mut);
        let expected_palette_data = ppu.palette.read((addr as usize) & 0x1F);
        assert_eq!(palette_data, expected_palette_data);
    }
}

#[test]
pub fn read_buffered_data() {
    let mut ppu = Ppu::new();

    let mut cart = MockCartRead::default();
    let cart_ref_mut = &mut cart;

    // Read palette data
    for addr in 0x0000..0x3F00_u16 {
        let addr_low = addr as u8;
        let addr_high = (addr >> 8) as u8;

        ppu.write_ppu_register(addr_high, Register::PpuAddr, cart_ref_mut);
        ppu.write_ppu_register(addr_low, Register::PpuAddr, cart_ref_mut);
        let buffered_data = ppu.read_ppu_register(Register::Data, cart_ref_mut);
        let expected_buffered_data = (addr + 6) as u8;
        assert_eq!(
            buffered_data, expected_buffered_data,
            "addr={addr:#06x}: buffered_data={buffered_data:#04x}, expected={expected_buffered_data:#04x}"
        );
    }
}

#[test]
pub fn read_palette_data_mirrors() {
    let mut ppu = Ppu::new();

    let mut cart = MockCartRead::default();
    let cart_ref_mut = &mut cart;

    // Read palette mirrored data
    for addr in 0x3F20..0x3FFF {
        let addr_low = addr as u8;
        let addr_high = (addr >> 8) as u8;

        ppu.write_ppu_register(addr_high, Register::PpuAddr, cart_ref_mut);
        ppu.write_ppu_register(addr_low, Register::PpuAddr, cart_ref_mut);
        let palette_data = ppu.read_ppu_register(Register::Data, cart_ref_mut);
        let expected_palette_data = ppu.palette.read((addr as usize) & 0x1F);
        assert_eq!(palette_data, expected_palette_data);
    }
}

#[test]
pub fn ppudata_read_triggers_a12_rise_on_increment() {
    // Reading $2007 increments the VRAM address. If the increment crosses the
    // A12 boundary ($0FFF -> $1000), the cartridge must be notified so it can
    // detect the rising edge for IRQ counters like the MMC3.
    let mut ppu = Ppu::new();

    let mut cart = NotifyCounter::default();
    let cart_ref_mut = &mut cart;

    // Set PPU address to $0FFF so the next PPUDATA read auto-increments to $1000.
    ppu.write_ppu_register(0x0F, Register::PpuAddr, cart_ref_mut);
    ppu.write_ppu_register(0xFF, Register::PpuAddr, cart_ref_mut);
    // Horizontal increment mode (bit 2 of PPUCTRL = 0, default).
    ppu.write_ppu_register(0x00, Register::PpuControl, cart_ref_mut);

    let _ = ppu.read_ppu_register(Register::Data, cart_ref_mut);

    assert_eq!(
        cart.notify_count, 1,
        "expected one notify on $0FFF -> $1000"
    );
    assert_eq!(cart.last_old, 0x0FFF);
    assert_eq!(cart.last_new, 0x1000);
}

#[test]
pub fn ppudata_write_triggers_a12_rise_on_increment() {
    let mut ppu = Ppu::new();

    let mut cart = NotifyCounter::default();
    let cart_ref_mut = &mut cart;

    ppu.write_ppu_register(0x0F, Register::PpuAddr, cart_ref_mut);
    ppu.write_ppu_register(0xFF, Register::PpuAddr, cart_ref_mut);
    ppu.write_ppu_register(0x00, Register::PpuControl, cart_ref_mut);

    ppu.write_ppu_register(0xAA, Register::Data, cart_ref_mut);

    assert_eq!(
        cart.notify_count, 1,
        "expected one notify on $0FFF -> $1000"
    );
    assert_eq!(cart.last_old, 0x0FFF);
    assert_eq!(cart.last_new, 0x1000);
}

#[test]
pub fn ppudata_increment_within_a12_range_does_not_notify() {
    // Auto-increment that stays within the same A12 range should NOT notify.
    let mut ppu = Ppu::new();

    let mut cart = NotifyCounter::default();
    let cart_ref_mut = &mut cart;

    // Set PPU address to $0050; auto-increment goes to $0051. A12 stays 0.
    ppu.write_ppu_register(0x00, Register::PpuAddr, cart_ref_mut);
    ppu.write_ppu_register(0x50, Register::PpuAddr, cart_ref_mut);

    let _ = ppu.read_ppu_register(Register::Data, cart_ref_mut);

    assert_eq!(cart.notify_count, 0, "no rising edge when A12 stays low");
}

#[test]
pub fn ppudata_increment_that_falls_a12_does_not_notify() {
    // Auto-increment that crosses A12 from 1 to 0 is a falling edge; no notify.
    let mut ppu = Ppu::new();

    let mut cart = NotifyCounter::default();
    let cart_ref_mut = &mut cart;

    // Set PPU address to $1FFE; auto-increment goes to $1FFF. A12 stays 1.
    ppu.write_ppu_register(0x1F, Register::PpuAddr, cart_ref_mut);
    ppu.write_ppu_register(0xFE, Register::PpuAddr, cart_ref_mut);

    let _ = ppu.read_ppu_register(Register::Data, cart_ref_mut);

    assert_eq!(cart.notify_count, 0, "A12 stays high; no rising edge");
}

// PPUSTATUS ($2002) bit masks.
const STATUS_SPRITE_OVERFLOW: u8 = 0b0010_0000;
const STATUS_SPRITE_ZERO_HIT: u8 = 0b0100_0000;

// OAM attribute bits. Sprite drawn *in front of* the background has bit 5
// (PRIORITY) clear. Palette is bits 0-1; we use palette 0.
const ATTR_FRONT_OF_BG: u8 = 0b0000_0000;

#[derive(Default)]
struct NoopCart;
impl PpuCartMemorySpace for NoopCart {
    fn read(&mut self, _addr: u16) -> u8 {
        0
    }
    fn write(&mut self, _data: u8, _addr: u16) {}
}

// OAM write helper: drives OAM through the standard $2003/$2004 register
// pair (the public interface games use to upload sprite data).
fn oam_write(
    ppu: &mut Ppu,
    cart: &mut impl PpuCartMemorySpace,
    sprite_index: u8,
    y: u8,
    tile: u8,
    attr: u8,
    x: u8,
) {
    let mut noop = NoopCart;
    let addr = sprite_index * 4;
    ppu.write_ppu_register(addr, Register::OamAddr, &mut noop);
    ppu.write_ppu_register(y, Register::OamData, &mut noop);
    ppu.write_ppu_register(tile, Register::OamData, &mut noop);
    ppu.write_ppu_register(attr, Register::OamData, &mut noop);
    ppu.write_ppu_register(x, Register::OamData, &mut noop);
    let _ = cart;
}

// ---------------------------------------------------------------------------
// Sprite overflow
// ---------------------------------------------------------------------------
//
// The real NES PPU scans *all* of OAM during sprite evaluation. The first 8
// in-range sprites are latched, but if a 9th (or later) in-range sprite is
// found, the sprite-overflow flag in PPUSTATUS ($2002, bit 5) is set.
//
// The current `prepare_sprites_for_scanline` does two suspect things:
//   1. It `break`s the moment 8 sprites have been collected, so it never
//      learns whether a 9th in-range sprite exists.
//   2. The returned `overflow` bool is *discarded by the caller* in
//      `Ppu::prepare_scanline`, so even if the function did set it, the
//      PPUSTATUS bit is never written.
//
// This test asserts: with 9 in-range sprites on a scanline, reading $2002
// after the relevant scanline has been evaluated should report
// SPRITE_OVERFLOW set.
#[test]
fn sprite_overflow_flag_is_set_when_nine_in_range_sprites_present() {
    let mut ppu = Ppu::new();
    let mut cart = NoopCart::default();

    // Enable BG and sprites via PPUCTRL/PPUMASK.
    ppu.write_ppu_register(0x00, Register::PpuControl, &mut cart);
    ppu.write_ppu_register(0x18, Register::PpuMask, &mut cart);

    // With 8x8 sprites, at most 7 consecutive y values are in range for
    // any given scanline, so we can't put 9 in-range sprites with 8x8.
    // Switch to 8x16 sprites (PPUCTRL bit 5 = 1): the in-range window is
    // 15 rows, so 9 distinct y values can all be in range.
    ppu.write_ppu_register(0x20, Register::PpuControl, &mut cart);

    // Fill OAM with 9 sprites in range for scanline 128. For 8x16
    // sprites, the in-range window for scanline 128 is y in 113..=127
    // (15 values). Use y = 113, 114, ..., 121 (9 distinct values).
    for i in 0..9u8 {
        oam_write(
            &mut ppu,
            &mut cart,
            i,
            113 + i,
            0x00,
            ATTR_FRONT_OF_BG,
            i * 2,
        );
    }

    // Drive the PPU up to the end of scanline 128 so prepare_scanline
    // has run against our hand-built OAM. Cycle budget:
    //   pre-render scanline (261): 341 cycles
    //   scanlines 0..=128:          129 * 341 cycles
    //   we land on cycle 0 of scanline 129, just after scanline 128's
    //   prepare_scanline has run.
    let total_cycles = (1 + 129) * 341;
    for _ in 0..total_cycles {
        ppu.run_cycle(&mut cart);
    }

    // prepare_scanline for scanline 128 has now run with our 9 in-range
    // sprites, and should have set the overflow flag. Reading $2002 also
    // clears bit 7 (vblank) and bit 6 (sprite-0 hit), but bit 5 (overflow)
    // is NOT cleared by the read.
    let status = ppu.read_ppu_register(Register::PpuStatus, &mut cart);

    assert_ne!(
        status & STATUS_SPRITE_OVERFLOW,
        0,
        "SPRITE_OVERFLOW bit 5 should be set when 9 in-range sprites \
         are present on the scanline. Status = {status:#04x}."
    );
}

#[test]
fn sprite_zero_hit_set_when_sprite0_opaque_overlaps_opaque_bg() {
    let mut ppu = Ppu::new();
    let mut cart = OpaqueBgCart::default();

    // Enable BG and sprites, including the leftmost-8-pixel rendering bits.
    // The real NES suppresses the sprite-0 hit in the leftmost 8 pixels
    // unless bit 1 (show_bg_leftmost) and bit 2 (show_sprites_leftmost)
    // of PPUMASK are both set. Setting PPUMASK=0x1E enables both.
    ppu.write_ppu_register(0x00, Register::PpuControl, &mut cart);
    ppu.write_ppu_register(0x1E, Register::PpuMask, &mut cart);

    // Put sprite 0 at (0, 0) with tile 0, attribute = front (priority=0).
    // Y=0 means the sprite is in range for scanlines 1..=7 (for 8x8
    // sprites: visible when scanline_y > 0 AND scanline_y < 0+8).
    oam_write(&mut ppu, &mut cart, 0, 0x00, 0x00, ATTR_FRONT_OF_BG, 0x00);

    // Run the PPU to cycle 256 of scanline 1, so sprite 0 and the BG have
    // both had a chance to produce opaque pixels. Cycle budget:
    //   pre-render scanline (261): 341 cycles
    //   scanline 0:                341 cycles
    //   scanline 1, up to cycle 256: 256 cycles
    //   total: 938
    let total_cycles = 2 * 341 + 256;
    for _ in 0..total_cycles {
        ppu.run_cycle(&mut cart);
    }

    let status = ppu.read_ppu_register(Register::PpuStatus, &mut cart);
    assert_ne!(
        status & STATUS_SPRITE_ZERO_HIT,
        0,
        "SPRITE_ZERO_HIT bit 6 should be set after sprite 0 overlaps an \
         opaque background pixel. Status = {status:#04x}."
    );
}

// Cart that returns 0xFF for every read. Used to make background and
// sprite pixels opaque (color index != 0) for the sprite-0 hit test.
#[derive(Default)]
struct OpaqueBgCart;
impl PpuCartMemorySpace for OpaqueBgCart {
    fn read(&mut self, _addr: u16) -> u8 {
        0xFF
    }
    fn write(&mut self, _data: u8, _addr: u16) {}
}

// Cart that returns 0xAB for reads in $2000-$2FFF and 0 elsewhere.
#[derive(Default)]
struct DistinctNametableCart;
impl PpuCartMemorySpace for DistinctNametableCart {
    fn read(&mut self, addr: u16) -> u8 {
        if (0x2000..=0x2FFF).contains(&addr) {
            0xAB
        } else {
            0x00
        }
    }
    fn write(&mut self, _data: u8, _addr: u16) {}
}

#[test]
fn first_ppudata_read_returns_value_at_address_not_zero() {
    let mut ppu = Ppu::new();
    let mut cart = DistinctNametableCart::default();

    // Set PPUADDR to $2000.
    ppu.write_ppu_register(0x20, Register::PpuAddr, &mut cart);
    ppu.write_ppu_register(0x00, Register::PpuAddr, &mut cart);

    // First read.
    let first = ppu.read_ppu_register(Register::Data, &mut cart);

    assert_eq!(
        first, 0xAB,
        "First $2007 read at $2000 should return the value at $2000 \
         (the nametable byte), not 0. Got {first:#04x}."
    );
}