feat(mmc5): implement MMC5 mapper with accurate scanline IRQ and CHR banking
Some checks failed
CI / rust (push) Has been cancelled
Some checks failed
CI / rust (push) Has been cancelled
- Add ExRAM (modes 0-1) and fill-mode nametable routing via read_nametable_byte / write_nametable_byte mapper hooks - Separate sprite and BG CHR bank sets ($5120-$5127 vs $5128-$512B); BG banks are only active in 8x16 sprite mode - Use mapper.ppu_read_sprite() for sprite tile loads so they always use the sprite bank set regardless of PPU fetch phase - Replace CPU-cycle IRQ stub with scanline-based counter matching Mesen2 hardware behaviour: fire when counter == irq_scanline at dot 2 (start of scanline), irq_scanline=0 never fires - Add Mapper::notify_frame_start() called unconditionally at the PPU frame boundary; MMC5 uses it to hard-reset the scanline counter even when rendering is disabled (e.g. during room transitions), preventing stale counter values from shifting the CHR split by 8+ scanlines - Fix CHR bank calculation for modes 0-2: use << 3/2/1 shifts instead of & !7/3/1 masking to correctly convert bank numbers to 1KB indices - Correct $5204 read: bit 7 = IRQ pending (cleared on read), bit 6 = in-frame flag; IRQ line stays asserted until $5204 is read - Dispatch $4020-$5FFF CPU reads/writes to mapper cpu_read_low / cpu_write_low so MMC5 internal registers are accessible
This commit is contained in:
@@ -29,6 +29,7 @@ impl CpuBus for NativeBus {
|
||||
0x4015 => self.apu.read(addr),
|
||||
0x4016 => self.joypad_read(),
|
||||
0x4017 => self.joypad2_read(),
|
||||
0x4020..=0x5FFF => self.mapper.cpu_read_low(addr).unwrap_or(self.cpu_open_bus),
|
||||
0x6000..=0x7FFF => self.mapper.cpu_read_low(addr).unwrap_or(self.cpu_open_bus),
|
||||
0x8000..=0xFFFF => self.mapper.cpu_read(addr),
|
||||
_ => self.cpu_open_bus,
|
||||
@@ -48,6 +49,9 @@ impl CpuBus for NativeBus {
|
||||
let (ppu, mapper) = (&mut self.ppu, &mut self.mapper);
|
||||
ppu.cpu_write(reg, value, &mut **mapper);
|
||||
}
|
||||
if reg == 0 {
|
||||
self.mapper.notify_ppu_ctrl_write(value);
|
||||
}
|
||||
if reg == 0
|
||||
&& !nmi_was_enabled
|
||||
&& self.ppu.nmi_enabled()
|
||||
@@ -77,6 +81,9 @@ impl CpuBus for NativeBus {
|
||||
self.clock_cpu_cycles(513 + cpu_phase);
|
||||
}
|
||||
0x4016 => self.joypad_write(value),
|
||||
0x4020..=0x5FFF => {
|
||||
self.mapper.cpu_write_low(addr, value);
|
||||
}
|
||||
0x6000..=0x7FFF => {
|
||||
self.mapper.cpu_write_low(addr, value);
|
||||
}
|
||||
|
||||
@@ -34,12 +34,25 @@ impl NativeBus {
|
||||
self.ppu_dot = 0;
|
||||
self.frame_complete = true;
|
||||
self.odd_frame = !self.odd_frame;
|
||||
// Unconditional frame-boundary notification: mappers that maintain
|
||||
// per-frame state (e.g. MMC5 scanline IRQ counter) must reset here
|
||||
// regardless of whether rendering is currently enabled.
|
||||
self.mapper.notify_frame_start();
|
||||
}
|
||||
|
||||
let scanline = self.ppu_dot / PPU_DOTS_PER_SCANLINE;
|
||||
let dot = self.ppu_dot % PPU_DOTS_PER_SCANLINE;
|
||||
let rendering_enabled = self.ppu.rendering_enabled();
|
||||
|
||||
// Notify the mapper when PPU transitions between BG and sprite fetch phases.
|
||||
if rendering_enabled && scanline < 240 {
|
||||
if dot == 1 || dot == 321 {
|
||||
self.mapper.notify_ppu_fetch_phase(false);
|
||||
} else if dot == 257 {
|
||||
self.mapper.notify_ppu_fetch_phase(true);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let mapper: &(dyn Mapper + Send) = &*self.mapper;
|
||||
self.ppu.render_dot(mapper, scanline, dot);
|
||||
@@ -62,7 +75,7 @@ impl NativeBus {
|
||||
self.mmc3_a12_prev_high = false;
|
||||
self.mmc3_a12_low_dots = self.mmc3_a12_low_dots.saturating_add(1);
|
||||
}
|
||||
} else if dot == 260 {
|
||||
} else if dot == 2 {
|
||||
self.mapper.clock_scanline();
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -6,19 +6,52 @@ const MAPPER_STATE_SECTION_VERSION: u8 = 1;
|
||||
pub trait Mapper {
|
||||
fn cpu_read(&self, addr: u16) -> u8;
|
||||
fn cpu_write(&mut self, addr: u16, value: u8);
|
||||
fn cpu_read_low(&self, _addr: u16) -> Option<u8> {
|
||||
fn cpu_read_low(&mut self, _addr: u16) -> Option<u8> {
|
||||
None
|
||||
}
|
||||
fn cpu_write_low(&mut self, _addr: u16, _value: u8) -> bool {
|
||||
false
|
||||
}
|
||||
fn ppu_read(&self, addr: u16) -> u8;
|
||||
/// Read a CHR byte for sprite tile loading. Overridden by mappers (e.g.
|
||||
/// MMC5) that use separate sprite and background CHR bank sets — sprite
|
||||
/// loads must always use the sprite bank set regardless of the PPU's
|
||||
/// current rendering phase. Default: delegates to `ppu_read`.
|
||||
fn ppu_read_sprite(&self, addr: u16) -> u8 {
|
||||
self.ppu_read(addr)
|
||||
}
|
||||
fn ppu_write(&mut self, addr: u16, value: u8);
|
||||
fn mirroring(&self) -> Mirroring;
|
||||
fn map_nametable_addr(&self, _addr: u16) -> Option<usize> {
|
||||
None
|
||||
}
|
||||
/// Override a nametable read without going through PPU CIRAM. Return
|
||||
/// `Some(byte)` when the mapper provides the data directly (e.g. MMC5
|
||||
/// ExRAM or fill-mode nametables); `None` to fall back to the standard
|
||||
/// CIRAM index returned by `map_nametable_addr` / `mirroring()`.
|
||||
fn read_nametable_byte(&self, _addr: u16) -> Option<u8> {
|
||||
None
|
||||
}
|
||||
/// Override a nametable write without going through PPU CIRAM. Return
|
||||
/// `true` when the mapper has consumed the write; `false` to fall back to
|
||||
/// the standard CIRAM write.
|
||||
fn write_nametable_byte(&mut self, _addr: u16, _value: u8) -> bool {
|
||||
false
|
||||
}
|
||||
/// Called when PPUCTRL ($2000) is written, so mappers can track bit 5 (8x16 sprite mode).
|
||||
fn notify_ppu_ctrl_write(&mut self, _value: u8) {}
|
||||
/// Notify the mapper about the current PPU fetch phase so it can select
|
||||
/// the correct CHR bank set. Called by the bus at the phase transition
|
||||
/// dots of visible scanlines (1 = BG, 257 = sprite, 321 = BG prefetch).
|
||||
/// Default: no-op.
|
||||
fn notify_ppu_fetch_phase(&mut self, _sprite_phase: bool) {}
|
||||
fn clock_cpu(&mut self, _cycles: u8) {}
|
||||
/// Called unconditionally when the PPU dot counter wraps to 0 (frame
|
||||
/// boundary), regardless of whether rendering is enabled. Mappers that
|
||||
/// maintain per-frame state (e.g. MMC5 scanline IRQ counter) use this to
|
||||
/// perform a hard reset that is independent of the rendering-enabled gate
|
||||
/// applied to `clock_scanline`.
|
||||
fn notify_frame_start(&mut self) {}
|
||||
fn clock_scanline(&mut self) {}
|
||||
fn needs_ppu_a12_clock(&self) -> bool {
|
||||
false
|
||||
|
||||
@@ -193,7 +193,7 @@ impl Mapper for InesMapper105 {
|
||||
}
|
||||
}
|
||||
|
||||
fn cpu_read_low(&self, addr: u16) -> Option<u8> {
|
||||
fn cpu_read_low(&mut self, addr: u16) -> Option<u8> {
|
||||
if (0x6000..=0x7FFF).contains(&addr) && !self.wram_disabled {
|
||||
let idx = (addr as usize) & 0x1FFF;
|
||||
return self.prg_ram.get(idx).copied();
|
||||
|
||||
@@ -21,7 +21,7 @@ impl Mapper for InesMapper118 {
|
||||
self.mmc3.cpu_write(addr, value);
|
||||
}
|
||||
|
||||
fn cpu_read_low(&self, addr: u16) -> Option<u8> {
|
||||
fn cpu_read_low(&mut self, addr: u16) -> Option<u8> {
|
||||
self.mmc3.cpu_read_low(addr)
|
||||
}
|
||||
|
||||
|
||||
@@ -147,7 +147,7 @@ impl Mapper for InesMapper155 {
|
||||
}
|
||||
}
|
||||
|
||||
fn cpu_read_low(&self, addr: u16) -> Option<u8> {
|
||||
fn cpu_read_low(&mut self, addr: u16) -> Option<u8> {
|
||||
if (0x6000..=0x7FFF).contains(&addr) {
|
||||
return self.prg_ram.get((addr as usize) & 0x1FFF).copied();
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ impl Mapper for InesMapper253 {
|
||||
self.base.cpu_write(addr, value);
|
||||
}
|
||||
|
||||
fn cpu_read_low(&self, addr: u16) -> Option<u8> {
|
||||
fn cpu_read_low(&mut self, addr: u16) -> Option<u8> {
|
||||
self.base.cpu_read_low(addr)
|
||||
}
|
||||
|
||||
@@ -191,7 +191,7 @@ impl Mapper for Fme7 {
|
||||
}
|
||||
}
|
||||
|
||||
fn cpu_read_low(&self, addr: u16) -> Option<u8> {
|
||||
fn cpu_read_low(&mut self, addr: u16) -> Option<u8> {
|
||||
if !(0x6000..=0x7FFF).contains(&addr) {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ impl Mapper for Mmc3 {
|
||||
}
|
||||
}
|
||||
|
||||
fn cpu_read_low(&self, addr: u16) -> Option<u8> {
|
||||
fn cpu_read_low(&mut self, addr: u16) -> Option<u8> {
|
||||
if (0x6000..=0x7FFF).contains(&addr) {
|
||||
if self.prg_ram_enabled {
|
||||
Some(self.prg_ram[(addr as usize) - 0x6000])
|
||||
|
||||
@@ -21,6 +21,14 @@ pub(crate) struct Mmc5 {
|
||||
irq_enable: bool,
|
||||
irq_pending: bool,
|
||||
irq_cycles: u32,
|
||||
irq_scanline_counter: u8,
|
||||
ex_ram: [u8; 0x400],
|
||||
ex_ram_mode: u8,
|
||||
fill_tile: u8,
|
||||
fill_attr: u8,
|
||||
bg_chr_lo_banks: [u16; 4],
|
||||
sprite_fetch_phase: bool,
|
||||
sprite_8x16: bool,
|
||||
}
|
||||
|
||||
impl Mmc5 {
|
||||
@@ -53,6 +61,14 @@ impl Mmc5 {
|
||||
irq_enable: false,
|
||||
irq_pending: false,
|
||||
irq_cycles: 0,
|
||||
irq_scanline_counter: 0,
|
||||
ex_ram: [0; 0x400],
|
||||
ex_ram_mode: 0,
|
||||
fill_tile: 0,
|
||||
fill_attr: 0,
|
||||
bg_chr_lo_banks: [0, 1, 2, 3],
|
||||
sprite_fetch_phase: false,
|
||||
sprite_8x16: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,24 +108,27 @@ impl Mmc5 {
|
||||
let mut banks = [0usize; 8];
|
||||
match self.chr_mode & 0x03 {
|
||||
0 => {
|
||||
let base = (self.chr_banks_1k[7] as usize) & !7;
|
||||
// 8KB mode: register holds 8KB bank number; convert to 1KB page index.
|
||||
let base = (self.chr_banks_1k[7] as usize) << 3;
|
||||
for (i, bank) in banks.iter_mut().enumerate() {
|
||||
*bank = base + i;
|
||||
}
|
||||
}
|
||||
1 => {
|
||||
let b0 = (self.chr_banks_1k[3] as usize) & !3;
|
||||
let b1 = (self.chr_banks_1k[7] as usize) & !3;
|
||||
// 4KB mode: registers hold 4KB bank numbers; convert to 1KB page index.
|
||||
let b0 = (self.chr_banks_1k[3] as usize) << 2;
|
||||
let b1 = (self.chr_banks_1k[7] as usize) << 2;
|
||||
for i in 0..4usize {
|
||||
banks[i] = b0 + i;
|
||||
banks[i + 4] = b1 + i;
|
||||
}
|
||||
}
|
||||
2 => {
|
||||
let b0 = (self.chr_banks_1k[1] as usize) & !1;
|
||||
let b1 = (self.chr_banks_1k[3] as usize) & !1;
|
||||
let b2 = (self.chr_banks_1k[5] as usize) & !1;
|
||||
let b3 = (self.chr_banks_1k[7] as usize) & !1;
|
||||
// 2KB mode: registers hold 2KB bank numbers; convert to 1KB page index.
|
||||
let b0 = (self.chr_banks_1k[1] as usize) << 1;
|
||||
let b1 = (self.chr_banks_1k[3] as usize) << 1;
|
||||
let b2 = (self.chr_banks_1k[5] as usize) << 1;
|
||||
let b3 = (self.chr_banks_1k[7] as usize) << 1;
|
||||
banks[0] = b0;
|
||||
banks[1] = b0 + 1;
|
||||
banks[2] = b1;
|
||||
@@ -128,10 +147,51 @@ impl Mmc5 {
|
||||
banks[page]
|
||||
}
|
||||
|
||||
fn bg_chr_lo_bank_for_page(&self, page: usize) -> usize {
|
||||
// page is 0-7 (full CHR window). In 8x16 mode, BG banks cover all 8 pages.
|
||||
// Modes 1-3: upper 4KB ($1000-$1FFF, pages 4-7) mirrors lower 4KB — use page & 3.
|
||||
// Mode 0 (8KB): sequential, no mirroring.
|
||||
match self.chr_mode & 0x03 {
|
||||
0 => {
|
||||
// 8KB: $512B holds 8KB bank number; convert to 1KB page index.
|
||||
let base = (self.bg_chr_lo_banks[3] as usize) << 3;
|
||||
base + page
|
||||
}
|
||||
1 => {
|
||||
// 4KB: $512B holds 4KB bank number; upper 4KB mirrors lower 4KB.
|
||||
let base = (self.bg_chr_lo_banks[3] as usize) << 2;
|
||||
base + (page & 3)
|
||||
}
|
||||
2 => {
|
||||
// 2KB: $5129 → pages 0-1 and 4-5, $512B → pages 2-3 and 6-7.
|
||||
let p = page & 3;
|
||||
if p < 2 {
|
||||
let base = (self.bg_chr_lo_banks[1] as usize) << 1;
|
||||
base + (p & 1)
|
||||
} else {
|
||||
let base = (self.bg_chr_lo_banks[3] as usize) << 1;
|
||||
base + (p & 1)
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// 1KB: pages 4-7 mirror pages 0-3 via bg_chr_lo_banks[page & 3].
|
||||
self.bg_chr_lo_banks[page & 3] as usize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ram_writable(&self) -> bool {
|
||||
self.ram_protect_1 == 0x02 && self.ram_protect_2 == 0x01
|
||||
}
|
||||
|
||||
fn nt_slot_type(&self, addr: u16) -> (u8, usize) {
|
||||
let rel = addr.wrapping_sub(0x2000) & 0x0FFF;
|
||||
let slot = (rel / 0x400) as usize;
|
||||
let offset = (rel & 0x3FF) as usize;
|
||||
let nt_type = (self.nt_mapping >> (slot * 2)) & 0x03;
|
||||
(nt_type, offset)
|
||||
}
|
||||
|
||||
fn decode_mirroring(&self) -> Mirroring {
|
||||
let nt0 = self.nt_mapping & 0x03;
|
||||
let nt1 = (self.nt_mapping >> 2) & 0x03;
|
||||
@@ -170,20 +230,25 @@ impl Mapper for Mmc5 {
|
||||
|
||||
fn cpu_write(&mut self, _addr: u16, _value: u8) {}
|
||||
|
||||
fn cpu_read_low(&self, addr: u16) -> Option<u8> {
|
||||
fn cpu_read_low(&mut self, addr: u16) -> Option<u8> {
|
||||
match addr {
|
||||
0x5104 => Some(self.ex_ram_mode & 0x03),
|
||||
0x5204 => {
|
||||
let mut status = 0u8;
|
||||
if self.irq_pending {
|
||||
status |= 0x80;
|
||||
self.irq_pending = false; // reading $5204 acknowledges the IRQ
|
||||
}
|
||||
if self.irq_enable {
|
||||
// Bit 6 = in-frame flag: 1 while PPU renders visible scanlines (0-239).
|
||||
// irq_scanline_counter is 0 during vblank and 1-240 during rendering.
|
||||
if self.irq_scanline_counter != 0 {
|
||||
status |= 0x40;
|
||||
}
|
||||
Some(status)
|
||||
}
|
||||
0x5205 => Some(((self.multiplier_a as u16 * self.multiplier_b as u16) & 0x00FF) as u8),
|
||||
0x5206 => Some(((self.multiplier_a as u16 * self.multiplier_b as u16) >> 8) as u8),
|
||||
0x5C00..=0x5FFF => Some(self.ex_ram[(addr - 0x5C00) as usize]),
|
||||
0x6000..=0x7FFF => {
|
||||
let bank = (self.prg_ram_bank & 0x07) as usize;
|
||||
let idx = bank * 0x2000 + ((addr as usize) & 0x1FFF);
|
||||
@@ -199,7 +264,14 @@ impl Mapper for Mmc5 {
|
||||
0x5101 => self.chr_mode = value & 0x03,
|
||||
0x5102 => self.ram_protect_1 = value & 0x03,
|
||||
0x5103 => self.ram_protect_2 = value & 0x03,
|
||||
0x5104 => self.ex_ram_mode = value & 0x03,
|
||||
0x5105 => self.nt_mapping = value,
|
||||
0x5106 => self.fill_tile = value,
|
||||
0x5107 => {
|
||||
// bits [1:0] = palette; replicated into all 4 quadrants of the attribute byte
|
||||
let p = value & 0x03;
|
||||
self.fill_attr = p | (p << 2) | (p << 4) | (p << 6);
|
||||
}
|
||||
0x5113 => self.prg_ram_bank = value & 0x07,
|
||||
0x5114..=0x5117 => {
|
||||
let reg = (addr - 0x5114) as usize;
|
||||
@@ -213,9 +285,7 @@ impl Mapper for Mmc5 {
|
||||
0x5128..=0x512B => {
|
||||
let reg = (addr - 0x5128) as usize;
|
||||
let bank = (((self.chr_upper_bits & 0x03) as u16) << 8) | value as u16;
|
||||
let base = reg * 2;
|
||||
self.chr_banks_1k[base] = bank & !1;
|
||||
self.chr_banks_1k[base + 1] = (bank & !1).wrapping_add(1);
|
||||
self.bg_chr_lo_banks[reg] = bank;
|
||||
}
|
||||
0x5130 => self.chr_upper_bits = value & 0x03,
|
||||
0x5203 => self.irq_scanline = value,
|
||||
@@ -227,6 +297,12 @@ impl Mapper for Mmc5 {
|
||||
}
|
||||
0x5205 => self.multiplier_a = value,
|
||||
0x5206 => self.multiplier_b = value,
|
||||
0x5C00..=0x5FFF => {
|
||||
// ExRAM CPU write: allowed in modes 0, 1, and 2
|
||||
if self.ex_ram_mode < 3 {
|
||||
self.ex_ram[(addr - 0x5C00) as usize] = value;
|
||||
}
|
||||
}
|
||||
0x6000..=0x7FFF => {
|
||||
if self.ram_writable() {
|
||||
let bank = (self.prg_ram_bank & 0x07) as usize;
|
||||
@@ -246,6 +322,26 @@ impl Mapper for Mmc5 {
|
||||
return 0;
|
||||
}
|
||||
let page = (addr / 0x0400) as usize;
|
||||
// BG/sprite split only applies in 8x16 sprite mode.
|
||||
// In 8x8 mode, all CHR uses $5120-$5127.
|
||||
// In 8x16 BG mode, all 8 pages use bg_chr_lo_banks (pages 4-7 mirror pages 0-3
|
||||
// in modes 1-3; mode 0 uses a full sequential 8KB block).
|
||||
let raw_bank = if self.sprite_8x16 && !self.sprite_fetch_phase {
|
||||
self.bg_chr_lo_bank_for_page(page)
|
||||
} else {
|
||||
self.chr_bank_1k_for_page(page)
|
||||
};
|
||||
let bank = safe_mod(raw_bank, self.chr_bank_count_1k());
|
||||
read_bank(&self.chr_data, 0x0400, bank, (addr as usize) & 0x03FF)
|
||||
}
|
||||
|
||||
fn ppu_read_sprite(&self, addr: u16) -> u8 {
|
||||
if addr > 0x1FFF {
|
||||
return 0;
|
||||
}
|
||||
// Sprite tile loads always use the sprite CHR bank set ($5120-$5127),
|
||||
// regardless of the current sprite_fetch_phase flag.
|
||||
let page = (addr / 0x0400) as usize;
|
||||
let bank = safe_mod(self.chr_bank_1k_for_page(page), self.chr_bank_count_1k());
|
||||
read_bank(&self.chr_data, 0x0400, bank, (addr as usize) & 0x03FF)
|
||||
}
|
||||
@@ -266,22 +362,85 @@ impl Mapper for Mmc5 {
|
||||
self.decode_mirroring()
|
||||
}
|
||||
|
||||
fn clock_cpu(&mut self, cycles: u8) {
|
||||
if !self.irq_enable {
|
||||
return;
|
||||
}
|
||||
self.irq_cycles = self.irq_cycles.saturating_add(cycles as u32);
|
||||
let threshold = (self.irq_scanline as u32 + 1).saturating_mul(113);
|
||||
if threshold != 0 && self.irq_cycles >= threshold {
|
||||
self.irq_cycles %= threshold;
|
||||
self.irq_pending = true;
|
||||
fn map_nametable_addr(&self, addr: u16) -> Option<usize> {
|
||||
let (nt_type, offset) = self.nt_slot_type(addr);
|
||||
match nt_type {
|
||||
0 => Some(offset), // CIRAM bank 0
|
||||
1 => Some(0x400 + offset), // CIRAM bank 1
|
||||
_ => None, // ExRAM / fill — handled by read/write_nametable_byte
|
||||
}
|
||||
}
|
||||
|
||||
fn read_nametable_byte(&self, addr: u16) -> Option<u8> {
|
||||
let (nt_type, offset) = self.nt_slot_type(addr);
|
||||
match nt_type {
|
||||
2 => {
|
||||
// ExRAM as nametable (modes 0 and 1 only)
|
||||
if self.ex_ram_mode < 2 {
|
||||
Some(self.ex_ram[offset & 0x3FF])
|
||||
} else {
|
||||
Some(0)
|
||||
}
|
||||
}
|
||||
3 => {
|
||||
// Fill mode: attribute table area starts at offset 0x3C0
|
||||
if offset >= 0x3C0 {
|
||||
Some(self.fill_attr)
|
||||
} else {
|
||||
Some(self.fill_tile)
|
||||
}
|
||||
}
|
||||
_ => None, // CIRAM: handled by map_nametable_addr
|
||||
}
|
||||
}
|
||||
|
||||
fn write_nametable_byte(&mut self, addr: u16, value: u8) -> bool {
|
||||
let (nt_type, offset) = self.nt_slot_type(addr);
|
||||
match nt_type {
|
||||
2 => {
|
||||
if self.ex_ram_mode < 2 {
|
||||
self.ex_ram[offset & 0x3FF] = value;
|
||||
}
|
||||
true // Intercept regardless so CIRAM is not written
|
||||
}
|
||||
3 => true, // Fill mode: writes are discarded
|
||||
_ => false, // CIRAM: let PPU handle it
|
||||
}
|
||||
}
|
||||
|
||||
fn notify_ppu_fetch_phase(&mut self, sprite_phase: bool) {
|
||||
self.sprite_fetch_phase = sprite_phase;
|
||||
}
|
||||
|
||||
fn notify_ppu_ctrl_write(&mut self, value: u8) {
|
||||
self.sprite_8x16 = (value & 0x20) != 0;
|
||||
}
|
||||
|
||||
fn notify_frame_start(&mut self) {
|
||||
// Hard reset at frame boundary, called unconditionally regardless of
|
||||
// rendering state. This is the authoritative counter reset — it fires
|
||||
// even when the PPU is disabled (e.g. during room transitions), which
|
||||
// prevents stale counter values from causing IRQs on wrong scanlines.
|
||||
self.irq_scanline_counter = 0;
|
||||
}
|
||||
|
||||
fn clock_scanline(&mut self) {
|
||||
// Called at dot 2 of each visible and prerender scanline.
|
||||
// Counter is already 0 at the start of a new frame (reset by
|
||||
// notify_frame_start), so after 240 visible + 1 prerender calls
|
||||
// it reaches 241 before the next notify_frame_start resets it.
|
||||
//
|
||||
// irq_scanline=0 never fires (special "disabled" sentinel).
|
||||
if self.irq_enable && self.irq_scanline != 0 && self.irq_scanline_counter == self.irq_scanline {
|
||||
self.irq_pending = true;
|
||||
}
|
||||
self.irq_scanline_counter = self.irq_scanline_counter.saturating_add(1);
|
||||
}
|
||||
|
||||
fn poll_irq(&mut self) -> bool {
|
||||
let out = self.irq_pending;
|
||||
self.irq_pending = false;
|
||||
out
|
||||
// MMC5 IRQ line stays asserted until explicitly acknowledged by reading $5204.
|
||||
// Do NOT clear irq_pending here; clearing happens in cpu_read_low($5204).
|
||||
self.irq_pending
|
||||
}
|
||||
|
||||
fn save_state(&self, out: &mut Vec<u8>) {
|
||||
@@ -302,12 +461,19 @@ impl Mapper for Mmc5 {
|
||||
for bank in self.chr_banks_1k {
|
||||
out.extend_from_slice(&bank.to_le_bytes());
|
||||
}
|
||||
out.push(self.ex_ram_mode);
|
||||
out.push(self.fill_tile);
|
||||
out.push(self.fill_attr);
|
||||
out.extend_from_slice(&self.ex_ram);
|
||||
out.extend_from_slice(&self.bg_chr_lo_banks.iter().flat_map(|b| b.to_le_bytes()).collect::<Vec<_>>());
|
||||
out.push(self.irq_scanline_counter);
|
||||
out.push(u8::from(self.sprite_8x16));
|
||||
write_state_bytes(out, &self.prg_ram);
|
||||
write_chr_state(out, &self.chr_data);
|
||||
}
|
||||
|
||||
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
if data.len() < 23 + 16 {
|
||||
if data.len() < 39 {
|
||||
return Err("mapper state is truncated".to_string());
|
||||
}
|
||||
let mut cursor = 0usize;
|
||||
@@ -348,6 +514,30 @@ impl Mapper for Mmc5 {
|
||||
self.chr_banks_1k[i] = u16::from_le_bytes([data[cursor], data[cursor + 1]]);
|
||||
cursor += 2;
|
||||
}
|
||||
if cursor + 3 + 0x400 <= data.len() {
|
||||
self.ex_ram_mode = data[cursor];
|
||||
cursor += 1;
|
||||
self.fill_tile = data[cursor];
|
||||
cursor += 1;
|
||||
self.fill_attr = data[cursor];
|
||||
cursor += 1;
|
||||
self.ex_ram.copy_from_slice(&data[cursor..cursor + 0x400]);
|
||||
cursor += 0x400;
|
||||
}
|
||||
if cursor + 8 <= data.len() {
|
||||
for i in 0..4usize {
|
||||
self.bg_chr_lo_banks[i] = u16::from_le_bytes([data[cursor], data[cursor + 1]]);
|
||||
cursor += 2;
|
||||
}
|
||||
}
|
||||
if cursor + 1 <= data.len() {
|
||||
self.irq_scanline_counter = data[cursor];
|
||||
cursor += 1;
|
||||
}
|
||||
if cursor + 1 <= data.len() {
|
||||
self.sprite_8x16 = data[cursor] != 0;
|
||||
cursor += 1;
|
||||
}
|
||||
let prg_ram_payload = read_state_bytes(data, &mut cursor)?;
|
||||
if prg_ram_payload.len() != self.prg_ram.len() {
|
||||
return Err("mapper state does not match loaded ROM".to_string());
|
||||
|
||||
@@ -90,7 +90,7 @@ impl Mapper for Namco163_19 {
|
||||
}
|
||||
}
|
||||
|
||||
fn cpu_read_low(&self, addr: u16) -> Option<u8> {
|
||||
fn cpu_read_low(&mut self, addr: u16) -> Option<u8> {
|
||||
match addr {
|
||||
0x4800..=0x487F => Some(self.audio_ram[(addr as usize) & 0x7F]),
|
||||
0x5000 => Some((self.irq_counter & 0x00FF) as u8),
|
||||
|
||||
@@ -33,7 +33,7 @@ impl Mapper for Nrom {
|
||||
|
||||
fn cpu_write(&mut self, _addr: u16, _value: u8) {}
|
||||
|
||||
fn cpu_read_low(&self, addr: u16) -> Option<u8> {
|
||||
fn cpu_read_low(&mut self, addr: u16) -> Option<u8> {
|
||||
let _ = addr;
|
||||
None
|
||||
}
|
||||
|
||||
@@ -163,7 +163,7 @@ impl Mapper for Tqrom119 {
|
||||
}
|
||||
}
|
||||
|
||||
fn cpu_read_low(&self, addr: u16) -> Option<u8> {
|
||||
fn cpu_read_low(&mut self, addr: u16) -> Option<u8> {
|
||||
if (0x6000..=0x7FFF).contains(&addr) {
|
||||
if self.prg_ram_enabled {
|
||||
Some(self.prg_ram[(addr as usize) - 0x6000])
|
||||
|
||||
@@ -159,7 +159,7 @@ impl Vrc2_23 {
|
||||
}
|
||||
|
||||
impl Mapper for Vrc2_23 {
|
||||
fn cpu_read_low(&self, addr: u16) -> Option<u8> {
|
||||
fn cpu_read_low(&mut self, addr: u16) -> Option<u8> {
|
||||
if (0x6000..=0x7FFF).contains(&addr) && !self.prg_ram.is_empty() {
|
||||
Some(self.prg_ram[(addr as usize - 0x6000) % self.prg_ram.len()])
|
||||
} else {
|
||||
|
||||
@@ -209,7 +209,7 @@ impl Mapper for Vrc6_24 {
|
||||
}
|
||||
}
|
||||
|
||||
fn cpu_read_low(&self, addr: u16) -> Option<u8> {
|
||||
fn cpu_read_low(&mut self, addr: u16) -> Option<u8> {
|
||||
if (0x6000..=0x7FFF).contains(&addr) && (self.control & 0x80) != 0 {
|
||||
Some(self.prg_ram[(addr as usize) - 0x6000])
|
||||
} else {
|
||||
|
||||
@@ -228,7 +228,7 @@ impl Mapper for Vrc7_85 {
|
||||
}
|
||||
}
|
||||
|
||||
fn cpu_read_low(&self, addr: u16) -> Option<u8> {
|
||||
fn cpu_read_low(&mut self, addr: u16) -> Option<u8> {
|
||||
if (0x6000..=0x7FFF).contains(&addr) && self.prg_ram_enabled {
|
||||
Some(self.prg_ram[(addr as usize) - 0x6000])
|
||||
} else {
|
||||
|
||||
@@ -344,8 +344,8 @@ impl Ppu {
|
||||
let lo = table + (tile as u16) * 16 + row as u16;
|
||||
(lo, lo + 8)
|
||||
};
|
||||
let mut lo = mapper.ppu_read(lo_addr);
|
||||
let mut hi = mapper.ppu_read(hi_addr);
|
||||
let mut lo = mapper.ppu_read_sprite(lo_addr);
|
||||
let mut hi = mapper.ppu_read_sprite(hi_addr);
|
||||
if (attr & 0x40) != 0 {
|
||||
// Horizontal flip: reverse bit order so MSB is always the
|
||||
// leftmost pixel when we shift out from bit 7.
|
||||
|
||||
@@ -16,11 +16,13 @@ impl Ppu {
|
||||
match addr {
|
||||
0x0000..=0x1FFF => mapper.ppu_write(addr, value),
|
||||
0x2000..=0x3EFF => {
|
||||
if !mapper.write_nametable_byte(addr, value) {
|
||||
let idx = mapper
|
||||
.map_nametable_addr(addr)
|
||||
.unwrap_or_else(|| self.nt_index(addr, mapper.mirroring()));
|
||||
self.vram[idx] = value;
|
||||
}
|
||||
}
|
||||
0x3F00..=0x3FFF => {
|
||||
let idx = palette_index(addr);
|
||||
self.palette_ram[idx] = value & 0x3F;
|
||||
@@ -30,6 +32,9 @@ impl Ppu {
|
||||
}
|
||||
|
||||
pub(super) fn read_nt(&self, addr: u16, mapper: &dyn Mapper) -> u8 {
|
||||
if let Some(val) = mapper.read_nametable_byte(addr) {
|
||||
return val;
|
||||
}
|
||||
let idx = mapper
|
||||
.map_nametable_addr(addr)
|
||||
.unwrap_or_else(|| self.nt_index(addr, mapper.mirroring()));
|
||||
|
||||
Reference in New Issue
Block a user