feat(mmc5): implement MMC5 mapper with accurate scanline IRQ and CHR banking
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:
2026-03-15 17:10:50 +03:00
parent d9666c23b4
commit 188444f987
17 changed files with 293 additions and 45 deletions

View File

@@ -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);
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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();

View File

@@ -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)
}

View File

@@ -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();
}

View File

@@ -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;
}

View File

@@ -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])

View File

@@ -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());

View File

@@ -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),

View File

@@ -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
}

View File

@@ -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])

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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.

View File

@@ -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()));