From 188444f9879122263a575b161295cb6d784b13bd Mon Sep 17 00:00:00 2001 From: "se.cherkasov" Date: Sun, 15 Mar 2026 17:10:50 +0300 Subject: [PATCH] feat(mmc5): implement MMC5 mapper with accurate scanline IRQ and CHR banking - 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 --- src/native_core/bus/cpu_bus_impl.rs | 7 + src/native_core/bus/timing.rs | 15 +- src/native_core/mapper/core.rs | 35 ++- src/native_core/mapper/mappers/mapper105.rs | 2 +- src/native_core/mapper/mappers/mapper118.rs | 2 +- src/native_core/mapper/mappers/mapper155.rs | 2 +- src/native_core/mapper/mappers/mapper253.rs | 4 +- src/native_core/mapper/mappers/mmc3.rs | 2 +- src/native_core/mapper/mappers/mmc5.rs | 240 ++++++++++++++++-- src/native_core/mapper/mappers/namco163_19.rs | 2 +- src/native_core/mapper/mappers/nrom.rs | 2 +- src/native_core/mapper/mappers/tqrom119.rs | 2 +- src/native_core/mapper/mappers/vrc/vrc2_23.rs | 2 +- src/native_core/mapper/mappers/vrc/vrc6_24.rs | 2 +- src/native_core/mapper/mappers/vrc/vrc7_85.rs | 2 +- src/native_core/ppu/api.rs | 4 +- src/native_core/ppu/api/memory.rs | 13 +- 17 files changed, 293 insertions(+), 45 deletions(-) diff --git a/src/native_core/bus/cpu_bus_impl.rs b/src/native_core/bus/cpu_bus_impl.rs index d9eec72..765d758 100644 --- a/src/native_core/bus/cpu_bus_impl.rs +++ b/src/native_core/bus/cpu_bus_impl.rs @@ -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); } diff --git a/src/native_core/bus/timing.rs b/src/native_core/bus/timing.rs index d0a2974..3d17006 100644 --- a/src/native_core/bus/timing.rs +++ b/src/native_core/bus/timing.rs @@ -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 { diff --git a/src/native_core/mapper/core.rs b/src/native_core/mapper/core.rs index 062a63a..15ec33c 100644 --- a/src/native_core/mapper/core.rs +++ b/src/native_core/mapper/core.rs @@ -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 { + fn cpu_read_low(&mut self, _addr: u16) -> Option { 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 { 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 { + 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 diff --git a/src/native_core/mapper/mappers/mapper105.rs b/src/native_core/mapper/mappers/mapper105.rs index 2fe98e2..3b55ac9 100644 --- a/src/native_core/mapper/mappers/mapper105.rs +++ b/src/native_core/mapper/mappers/mapper105.rs @@ -193,7 +193,7 @@ impl Mapper for InesMapper105 { } } - fn cpu_read_low(&self, addr: u16) -> Option { + fn cpu_read_low(&mut self, addr: u16) -> Option { if (0x6000..=0x7FFF).contains(&addr) && !self.wram_disabled { let idx = (addr as usize) & 0x1FFF; return self.prg_ram.get(idx).copied(); diff --git a/src/native_core/mapper/mappers/mapper118.rs b/src/native_core/mapper/mappers/mapper118.rs index 36e9f3c..21fa7f9 100644 --- a/src/native_core/mapper/mappers/mapper118.rs +++ b/src/native_core/mapper/mappers/mapper118.rs @@ -21,7 +21,7 @@ impl Mapper for InesMapper118 { self.mmc3.cpu_write(addr, value); } - fn cpu_read_low(&self, addr: u16) -> Option { + fn cpu_read_low(&mut self, addr: u16) -> Option { self.mmc3.cpu_read_low(addr) } diff --git a/src/native_core/mapper/mappers/mapper155.rs b/src/native_core/mapper/mappers/mapper155.rs index ddc13e7..3746a1c 100644 --- a/src/native_core/mapper/mappers/mapper155.rs +++ b/src/native_core/mapper/mappers/mapper155.rs @@ -147,7 +147,7 @@ impl Mapper for InesMapper155 { } } - fn cpu_read_low(&self, addr: u16) -> Option { + fn cpu_read_low(&mut self, addr: u16) -> Option { if (0x6000..=0x7FFF).contains(&addr) { return self.prg_ram.get((addr as usize) & 0x1FFF).copied(); } diff --git a/src/native_core/mapper/mappers/mapper253.rs b/src/native_core/mapper/mappers/mapper253.rs index 16a58c8..3b91707 100644 --- a/src/native_core/mapper/mappers/mapper253.rs +++ b/src/native_core/mapper/mappers/mapper253.rs @@ -25,7 +25,7 @@ impl Mapper for InesMapper253 { self.base.cpu_write(addr, value); } - fn cpu_read_low(&self, addr: u16) -> Option { + fn cpu_read_low(&mut self, addr: u16) -> Option { self.base.cpu_read_low(addr) } @@ -191,7 +191,7 @@ impl Mapper for Fme7 { } } - fn cpu_read_low(&self, addr: u16) -> Option { + fn cpu_read_low(&mut self, addr: u16) -> Option { if !(0x6000..=0x7FFF).contains(&addr) { return None; } diff --git a/src/native_core/mapper/mappers/mmc3.rs b/src/native_core/mapper/mappers/mmc3.rs index 1b3815b..eafad93 100644 --- a/src/native_core/mapper/mappers/mmc3.rs +++ b/src/native_core/mapper/mappers/mmc3.rs @@ -144,7 +144,7 @@ impl Mapper for Mmc3 { } } - fn cpu_read_low(&self, addr: u16) -> Option { + fn cpu_read_low(&mut self, addr: u16) -> Option { if (0x6000..=0x7FFF).contains(&addr) { if self.prg_ram_enabled { Some(self.prg_ram[(addr as usize) - 0x6000]) diff --git a/src/native_core/mapper/mappers/mmc5.rs b/src/native_core/mapper/mappers/mmc5.rs index cc4f828..257c126 100644 --- a/src/native_core/mapper/mappers/mmc5.rs +++ b/src/native_core/mapper/mappers/mmc5.rs @@ -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 { + fn cpu_read_low(&mut self, addr: u16) -> Option { 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 { + 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 { + 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) { @@ -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::>()); + 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()); diff --git a/src/native_core/mapper/mappers/namco163_19.rs b/src/native_core/mapper/mappers/namco163_19.rs index 7ce9b4f..fa4d2c6 100644 --- a/src/native_core/mapper/mappers/namco163_19.rs +++ b/src/native_core/mapper/mappers/namco163_19.rs @@ -90,7 +90,7 @@ impl Mapper for Namco163_19 { } } - fn cpu_read_low(&self, addr: u16) -> Option { + fn cpu_read_low(&mut self, addr: u16) -> Option { match addr { 0x4800..=0x487F => Some(self.audio_ram[(addr as usize) & 0x7F]), 0x5000 => Some((self.irq_counter & 0x00FF) as u8), diff --git a/src/native_core/mapper/mappers/nrom.rs b/src/native_core/mapper/mappers/nrom.rs index f9492c8..011f35b 100644 --- a/src/native_core/mapper/mappers/nrom.rs +++ b/src/native_core/mapper/mappers/nrom.rs @@ -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 { + fn cpu_read_low(&mut self, addr: u16) -> Option { let _ = addr; None } diff --git a/src/native_core/mapper/mappers/tqrom119.rs b/src/native_core/mapper/mappers/tqrom119.rs index 5f08c27..f4a3d23 100644 --- a/src/native_core/mapper/mappers/tqrom119.rs +++ b/src/native_core/mapper/mappers/tqrom119.rs @@ -163,7 +163,7 @@ impl Mapper for Tqrom119 { } } - fn cpu_read_low(&self, addr: u16) -> Option { + fn cpu_read_low(&mut self, addr: u16) -> Option { if (0x6000..=0x7FFF).contains(&addr) { if self.prg_ram_enabled { Some(self.prg_ram[(addr as usize) - 0x6000]) diff --git a/src/native_core/mapper/mappers/vrc/vrc2_23.rs b/src/native_core/mapper/mappers/vrc/vrc2_23.rs index 0c501b1..6973e4c 100644 --- a/src/native_core/mapper/mappers/vrc/vrc2_23.rs +++ b/src/native_core/mapper/mappers/vrc/vrc2_23.rs @@ -159,7 +159,7 @@ impl Vrc2_23 { } impl Mapper for Vrc2_23 { - fn cpu_read_low(&self, addr: u16) -> Option { + fn cpu_read_low(&mut self, addr: u16) -> Option { if (0x6000..=0x7FFF).contains(&addr) && !self.prg_ram.is_empty() { Some(self.prg_ram[(addr as usize - 0x6000) % self.prg_ram.len()]) } else { diff --git a/src/native_core/mapper/mappers/vrc/vrc6_24.rs b/src/native_core/mapper/mappers/vrc/vrc6_24.rs index 7c73ed1..467f43a 100644 --- a/src/native_core/mapper/mappers/vrc/vrc6_24.rs +++ b/src/native_core/mapper/mappers/vrc/vrc6_24.rs @@ -209,7 +209,7 @@ impl Mapper for Vrc6_24 { } } - fn cpu_read_low(&self, addr: u16) -> Option { + fn cpu_read_low(&mut self, addr: u16) -> Option { if (0x6000..=0x7FFF).contains(&addr) && (self.control & 0x80) != 0 { Some(self.prg_ram[(addr as usize) - 0x6000]) } else { diff --git a/src/native_core/mapper/mappers/vrc/vrc7_85.rs b/src/native_core/mapper/mappers/vrc/vrc7_85.rs index 5d7ab49..12bc2b5 100644 --- a/src/native_core/mapper/mappers/vrc/vrc7_85.rs +++ b/src/native_core/mapper/mappers/vrc/vrc7_85.rs @@ -228,7 +228,7 @@ impl Mapper for Vrc7_85 { } } - fn cpu_read_low(&self, addr: u16) -> Option { + fn cpu_read_low(&mut self, addr: u16) -> Option { if (0x6000..=0x7FFF).contains(&addr) && self.prg_ram_enabled { Some(self.prg_ram[(addr as usize) - 0x6000]) } else { diff --git a/src/native_core/ppu/api.rs b/src/native_core/ppu/api.rs index 814d364..31374ba 100644 --- a/src/native_core/ppu/api.rs +++ b/src/native_core/ppu/api.rs @@ -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. diff --git a/src/native_core/ppu/api/memory.rs b/src/native_core/ppu/api/memory.rs index ee8508d..256e76e 100644 --- a/src/native_core/ppu/api/memory.rs +++ b/src/native_core/ppu/api/memory.rs @@ -16,10 +16,12 @@ impl Ppu { match addr { 0x0000..=0x1FFF => mapper.ppu_write(addr, value), 0x2000..=0x3EFF => { - let idx = mapper - .map_nametable_addr(addr) - .unwrap_or_else(|| self.nt_index(addr, mapper.mirroring())); - self.vram[idx] = value; + 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); @@ -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()));