Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/cpu/mappers/mmc3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ impl Mapper for MMC3 {
}

fn step_scanline(&mut self) {
// MMC3 IRQ should be triggered on A12 toggles, not just once per scanline
// This is called when A12 transitions from 0 to 1 during background tile fetching
if self.irq_counter == 0 {
self.irq_counter = self.irq_reload;
} else {
Expand Down
95 changes: 95 additions & 0 deletions src/cpu/mappers/mmc3_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#[cfg(test)]
mod tests {
use crate::cpu::mappers::mmc3::MMC3;
use crate::cpu::mappers::Mapper;
use crate::cpu::rom::{Cart, Mirroring};

fn create_test_cart() -> Cart {
Cart {
bytes: vec![0; 0x10000], // 64KB ROM
hash: [0; 32],
prg_rom_size: 2, // 32KB
chr_rom_size: 1, // 8KB
mirroring: Mirroring::Horizontal,
mapper_id: 4,
battery: false,
trainer: false,
prg_rom_start: 16,
chr_rom_start: 0x8000,
}
}

#[test]
fn test_mmc3_initialization() {
let cart = create_test_cart();
let mut mmc3 = MMC3::new(&cart);

// Test that it doesn't panic and can be created
assert!(!mmc3.is_asserting_irq());
}

#[test]
fn test_mmc3_irq_assertions() {
let cart = create_test_cart();
let mut mmc3 = MMC3::new(&cart);

// Initially should not assert IRQ
assert!(!mmc3.is_asserting_irq());

// Test IRQ enable/disable
mmc3.write(&mut create_test_cart(), 0xE001, 0x01); // Enable IRQ
mmc3.write(&mut create_test_cart(), 0xE000, 0x00); // Disable IRQ

assert!(!mmc3.is_asserting_irq());
}

#[test]
fn test_mmc3_prg_ram_access() {
let cart = create_test_cart();
let mut mmc3 = MMC3::new(&cart);
let mut cart = create_test_cart();

// Test PRG RAM write/read
mmc3.write(&mut cart, 0x6000, 0x42);
assert_eq!(mmc3.read(&mut cart, 0x6000), 0x42);

mmc3.write(&mut cart, 0x7FFF, 0x84);
assert_eq!(mmc3.read(&mut cart, 0x7FFF), 0x84);
}

#[test]
fn test_mmc3_bank_register_writes() {
let cart = create_test_cart();
let mut mmc3 = MMC3::new(&cart);
let mut cart = create_test_cart();

// Test bank select register
mmc3.write(&mut cart, 0x8000, 0x06); // Select PRG bank 0
mmc3.write(&mut cart, 0x8001, 0x02); // Set bank to 2

// Test mirroring
mmc3.write(&mut cart, 0xA000, 0x01); // Horizontal mirroring
assert_eq!(cart.mirroring, Mirroring::Horizontal);

mmc3.write(&mut cart, 0xA000, 0x00); // Vertical mirroring
assert_eq!(cart.mirroring, Mirroring::Vertical);
}

#[test]
fn test_mmc3_irq_registers() {
let cart = create_test_cart();
let mut mmc3 = MMC3::new(&cart);
let mut cart = create_test_cart();

// Test IRQ reload register
mmc3.write(&mut cart, 0xC000, 0x05); // Set reload value
mmc3.write(&mut cart, 0xC001, 0x00); // Clear counter

// Test IRQ enable/disable
mmc3.write(&mut cart, 0xE001, 0x01); // Enable IRQ
mmc3.write(&mut cart, 0xE000, 0x00); // Disable IRQ

// Should not assert IRQ when disabled
assert!(!mmc3.is_asserting_irq());
}
}
3 changes: 3 additions & 0 deletions src/cpu/mappers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ pub mod mmc3;
pub mod nrom;
pub mod unrom;

#[cfg(test)]
mod mmc3_test;

pub trait Mapper: savestate::Save {
fn read(&mut self, cart: &mut Cart, addr: u16) -> u8;
fn write(&mut self, cart: &mut Cart, addr: u16, val: u8);
Expand Down
22 changes: 18 additions & 4 deletions src/ppu/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ pub struct PPU {
visible_sprites_count: u8,
frame_buffer: [u8; WIDTH * HEIGHT * 3],
frame_buffer_complete: Box<[u8; WIDTH * HEIGHT * 3]>, // avoid stack overflow in WASM
last_a12: bool, // Track A12 state for MMC3 IRQ timing
}

impl PPU {
Expand Down Expand Up @@ -97,6 +98,7 @@ impl PPU {
visible_sprites_count: 0,
frame_buffer: [0; WIDTH * HEIGHT * 3],
frame_buffer_complete: Box::new([0; WIDTH * HEIGHT * 3]),
last_a12: false,
};

ppu.reset();
Expand Down Expand Up @@ -136,10 +138,6 @@ impl PPU {
self.frame += 1;
}
}

if self.regs.rendering_enabled() && self.cycle == 260 && self.scanline < 240 {
self.rom.mapper.step_scanline();
}
}

pub fn step(&mut self) {
Expand Down Expand Up @@ -501,6 +499,20 @@ impl PPU {
}

fn read_chr(&mut self, addr: u16) -> u8 {
// Track A12 toggles for MMC3 IRQ timing
// A12 is bit 12 of the address (0x1000)
// Only track A12 toggles during rendering periods
if self.regs.rendering_enabled() && (self.scanline < 240 || self.scanline == 261) {
let new_a12 = (addr & 0x1000) != 0;

// Check if this is a rising edge (0 to 1 transition)
if new_a12 && !self.last_a12 {
self.rom.mapper.step_scanline();
}

self.last_a12 = new_a12;
}

self.rom.mapper.read(&mut self.rom.cart, addr)
}

Expand Down Expand Up @@ -654,6 +666,7 @@ impl savestate::Save for PPU {
s.data.write_u8(self.pattern_table_low_byte);
s.data.write_u8(self.pattern_table_high_byte);
s.data.write_u8(self.visible_sprites_count);
s.data.write_bool(self.last_a12);
s.write_all(&self.scanline_sprites);

self.regs.save(s);
Expand Down Expand Up @@ -681,6 +694,7 @@ impl savestate::Save for PPU {
self.pattern_table_low_byte = s.data.read_u8()?;
self.pattern_table_high_byte = s.data.read_u8()?;
self.visible_sprites_count = s.data.read_u8()?;
self.last_a12 = s.data.read_bool()?;
s.read_all(&mut self.scanline_sprites)?;

self.regs.load(s)?;
Expand Down
34 changes: 22 additions & 12 deletions web/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.