Files
nesemu/src/native_core/mapper/mappers/tqrom119.rs
se.cherkasov 188444f987
Some checks failed
CI / rust (push) Has been cancelled
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
2026-03-15 17:10:50 +03:00

270 lines
8.0 KiB
Rust

use super::*;
pub(crate) struct Tqrom119 {
prg_rom: Vec<u8>,
chr_rom: Vec<u8>,
chr_ram: Vec<u8>,
prg_ram: Vec<u8>,
prg_ram_enabled: bool,
prg_ram_write_protect: bool,
mirroring: Mirroring,
bank_regs: [u8; 8],
bank_select: u8,
irq_latch: u8,
irq_counter: u8,
irq_reload: bool,
irq_enabled: bool,
irq_pending: bool,
}
impl Tqrom119 {
pub(crate) fn new(rom: InesRom) -> Self {
Self {
prg_rom: rom.prg_rom,
chr_rom: rom.chr_data,
chr_ram: vec![0; 0x2000],
prg_ram: vec![0; 0x2000],
prg_ram_enabled: true,
prg_ram_write_protect: false,
mirroring: rom.header.mirroring,
bank_regs: [0; 8],
bank_select: 0,
irq_latch: 0,
irq_counter: 0,
irq_reload: false,
irq_enabled: false,
irq_pending: false,
}
}
fn prg_bank_count_8k(&self) -> usize {
(self.prg_rom.len() / 0x2000).max(1)
}
fn prg_mode(&self) -> bool {
(self.bank_select & 0x40) != 0
}
fn chr_invert(&self) -> bool {
(self.bank_select & 0x80) != 0
}
fn prg_bank_for_slot(&self, slot: usize) -> usize {
let last = self.prg_bank_count_8k() - 1;
let second_last = last.saturating_sub(1);
match (self.prg_mode(), slot) {
(false, 0) => self.bank_regs[6] as usize,
(false, 1) => self.bank_regs[7] as usize,
(false, 2) => second_last,
(false, 3) => last,
(true, 0) => second_last,
(true, 1) => self.bank_regs[7] as usize,
(true, 2) => self.bank_regs[6] as usize,
(true, 3) => last,
_ => 0,
}
}
fn chr_bank_for_1k_page(&self, page: usize) -> u8 {
let regs = &self.bank_regs;
let mut layout = [0u8; 8];
layout[0] = regs[0] & !1;
layout[1] = layout[0].wrapping_add(1);
layout[2] = regs[1] & !1;
layout[3] = layout[2].wrapping_add(1);
layout[4] = regs[2];
layout[5] = regs[3];
layout[6] = regs[4];
layout[7] = regs[5];
if self.chr_invert() {
layout.rotate_left(4);
}
layout[page]
}
fn clock_irq_scanline(&mut self) {
if self.irq_reload || self.irq_counter == 0 {
self.irq_counter = self.irq_latch;
self.irq_reload = false;
} else {
self.irq_counter = self.irq_counter.wrapping_sub(1);
}
if self.irq_enabled && self.irq_counter == 0 {
self.irq_pending = true;
}
}
fn chr_read_page(&self, bank: u8, offset: usize) -> u8 {
if (bank & 0x40) != 0 {
read_bank(&self.chr_ram, 0x0400, (bank & 0x07) as usize, offset)
} else {
read_bank(&self.chr_rom, 0x0400, (bank & 0x3F) as usize, offset)
}
}
fn chr_write_page(&mut self, bank: u8, offset: usize, value: u8) {
if (bank & 0x40) == 0 {
return;
}
let page = (bank & 0x07) as usize;
let idx = page * 0x0400 + (offset & 0x03FF);
if let Some(cell) = self.chr_ram.get_mut(idx) {
*cell = value;
}
}
}
impl Mapper for Tqrom119 {
fn cpu_read(&self, addr: u16) -> u8 {
if addr < 0x8000 {
return 0;
}
let slot = ((addr - 0x8000) / 0x2000) as usize;
let bank = self.prg_bank_for_slot(slot);
read_bank(
&self.prg_rom,
0x2000,
bank,
((addr as usize) - 0x8000) & 0x1FFF,
)
}
fn cpu_write(&mut self, addr: u16, value: u8) {
match addr {
0x8000..=0x9FFF if (addr & 1) == 0 => self.bank_select = value,
0x8000..=0x9FFF => {
let reg = (self.bank_select & 0x07) as usize;
self.bank_regs[reg] = value;
}
0xA000..=0xBFFF if (addr & 1) == 0 => {
self.mirroring = if (value & 1) == 0 {
Mirroring::Vertical
} else {
Mirroring::Horizontal
};
}
0xA000..=0xBFFF => {
self.prg_ram_enabled = (value & 0x80) != 0;
self.prg_ram_write_protect = (value & 0x40) != 0;
}
0xC000..=0xDFFF if (addr & 1) == 0 => self.irq_latch = value,
0xC000..=0xDFFF => {
self.irq_counter = 0;
self.irq_reload = true;
}
0xE000..=0xFFFF if (addr & 1) == 0 => {
self.irq_enabled = false;
self.irq_pending = false;
}
0xE000..=0xFFFF => self.irq_enabled = true,
_ => {}
}
}
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])
} else {
Some(0)
}
} else {
None
}
}
fn cpu_write_low(&mut self, addr: u16, value: u8) -> bool {
if (0x6000..=0x7FFF).contains(&addr) {
if self.prg_ram_enabled && !self.prg_ram_write_protect {
self.prg_ram[(addr as usize) - 0x6000] = value;
}
true
} else {
false
}
}
fn ppu_read(&self, addr: u16) -> u8 {
if addr > 0x1FFF {
return 0;
}
let page = (addr / 0x0400) as usize;
let bank = self.chr_bank_for_1k_page(page);
self.chr_read_page(bank, (addr as usize) & 0x03FF)
}
fn ppu_write(&mut self, addr: u16, value: u8) {
if addr > 0x1FFF {
return;
}
let page = (addr / 0x0400) as usize;
let bank = self.chr_bank_for_1k_page(page);
self.chr_write_page(bank, (addr as usize) & 0x03FF, value);
}
fn mirroring(&self) -> Mirroring {
self.mirroring
}
fn clock_scanline(&mut self) {
self.clock_irq_scanline();
}
fn needs_ppu_a12_clock(&self) -> bool {
true
}
fn poll_irq(&mut self) -> bool {
let out = self.irq_pending;
self.irq_pending = false;
out
}
fn save_state(&self, out: &mut Vec<u8>) {
out.extend_from_slice(&self.bank_regs);
out.push(self.bank_select);
out.push(encode_mirroring(self.mirroring));
out.push(self.irq_latch);
out.push(self.irq_counter);
out.push(u8::from(self.irq_reload));
out.push(u8::from(self.irq_enabled));
out.push(u8::from(self.irq_pending));
out.push(u8::from(self.prg_ram_enabled));
out.push(u8::from(self.prg_ram_write_protect));
write_state_bytes(out, &self.prg_ram);
write_state_bytes(out, &self.chr_ram);
}
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
if data.len() < 17 {
return Err("mapper state is truncated".to_string());
}
self.bank_regs.copy_from_slice(&data[0..8]);
self.bank_select = data[8];
self.mirroring = decode_mirroring(data[9]);
self.irq_latch = data[10];
self.irq_counter = data[11];
self.irq_reload = data[12] != 0;
self.irq_enabled = data[13] != 0;
self.irq_pending = data[14] != 0;
let mut cursor = 15usize;
self.prg_ram_enabled = data[cursor] != 0;
cursor += 1;
self.prg_ram_write_protect = data[cursor] != 0;
cursor += 1;
let prg_ram = read_state_bytes(data, &mut cursor)?;
if prg_ram.len() != self.prg_ram.len() {
return Err("mapper state does not match loaded ROM".to_string());
}
self.prg_ram.copy_from_slice(prg_ram);
let chr_ram = read_state_bytes(data, &mut cursor)?;
if chr_ram.len() != self.chr_ram.len() || cursor != data.len() {
return Err("mapper state does not match loaded ROM".to_string());
}
self.chr_ram.copy_from_slice(chr_ram);
Ok(())
}
}