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
296 lines
8.3 KiB
Rust
296 lines
8.3 KiB
Rust
use super::*;
|
|
|
|
pub(crate) struct InesMapper105 {
|
|
prg_rom: Vec<u8>,
|
|
chr_data: Vec<u8>,
|
|
prg_ram: Vec<u8>,
|
|
shift_reg: u8,
|
|
shift_count: u8,
|
|
control: u8,
|
|
reg_a: u8,
|
|
reg_b: u8,
|
|
wram_disabled: bool,
|
|
prg_unlocked: bool,
|
|
saw_i_low: bool,
|
|
irq_counter: u32, // 30-bit counter
|
|
irq_pending: bool,
|
|
}
|
|
|
|
impl InesMapper105 {
|
|
pub(crate) const IRQ_THRESHOLD: u32 = 0x2800_0000; // Official Nintendo World Championships DIP setting.
|
|
|
|
pub(crate) fn new(rom: InesRom) -> Self {
|
|
Self {
|
|
prg_rom: rom.prg_rom,
|
|
chr_data: if rom.chr_data.is_empty() {
|
|
vec![0; 0x2000]
|
|
} else {
|
|
rom.chr_data
|
|
},
|
|
prg_ram: vec![0; 0x2000],
|
|
shift_reg: 0,
|
|
shift_count: 0,
|
|
control: 0x0C,
|
|
reg_a: 0x10, // I bit high after reset keeps timer halted.
|
|
reg_b: 0,
|
|
wram_disabled: false,
|
|
prg_unlocked: false,
|
|
saw_i_low: false,
|
|
irq_counter: 0,
|
|
irq_pending: false,
|
|
}
|
|
}
|
|
|
|
fn prg_mode(&self) -> u8 {
|
|
(self.control >> 2) & 0x03
|
|
}
|
|
|
|
fn timer_halted(&self) -> bool {
|
|
(self.reg_a & 0x10) != 0
|
|
}
|
|
|
|
fn outer_chip_selected(&self) -> bool {
|
|
(self.reg_a & 0x08) != 0
|
|
}
|
|
|
|
fn outer_32k_bank(&self) -> usize {
|
|
((self.reg_a >> 1) & 0x03) as usize
|
|
}
|
|
|
|
fn prg_bank_value(&self) -> usize {
|
|
(self.reg_b & 0x0F) as usize
|
|
}
|
|
|
|
fn prg_bank_count_16k(&self) -> usize {
|
|
(self.prg_rom.len() / 0x4000).max(1)
|
|
}
|
|
|
|
fn first_chip_banks_16k(&self) -> usize {
|
|
(self.prg_bank_count_16k() / 2).max(1)
|
|
}
|
|
|
|
fn second_chip_base_16k(&self) -> usize {
|
|
self.first_chip_banks_16k()
|
|
}
|
|
|
|
fn second_chip_banks_16k(&self) -> usize {
|
|
self.prg_bank_count_16k()
|
|
.saturating_sub(self.second_chip_base_16k())
|
|
.max(1)
|
|
}
|
|
|
|
fn on_reg_a_write(&mut self, value: u8) {
|
|
let previous_i = (self.reg_a & 0x10) != 0;
|
|
let next_i = (value & 0x10) != 0;
|
|
self.reg_a = value & 0x1F;
|
|
|
|
if !next_i {
|
|
self.saw_i_low = true;
|
|
}
|
|
if previous_i && !next_i {
|
|
self.irq_pending = false;
|
|
}
|
|
if !previous_i && next_i && self.saw_i_low {
|
|
self.prg_unlocked = true;
|
|
self.irq_pending = false;
|
|
}
|
|
if next_i {
|
|
self.irq_counter = 0;
|
|
self.irq_pending = false;
|
|
}
|
|
}
|
|
|
|
fn write_serial(&mut self, addr: u16, value: u8) {
|
|
if (value & 0x80) != 0 {
|
|
self.shift_reg = 0;
|
|
self.shift_count = 0;
|
|
self.control |= 0x0C;
|
|
return;
|
|
}
|
|
|
|
self.shift_reg |= (value & 1) << self.shift_count;
|
|
self.shift_count = self.shift_count.wrapping_add(1);
|
|
if self.shift_count < 5 {
|
|
return;
|
|
}
|
|
|
|
let latched = self.shift_reg & 0x1F;
|
|
match (addr >> 13) & 0x03 {
|
|
0 => self.control = latched,
|
|
1 => self.on_reg_a_write(latched),
|
|
2 => {}
|
|
_ => {
|
|
self.reg_b = latched;
|
|
self.wram_disabled = (latched & 0x10) != 0;
|
|
}
|
|
}
|
|
|
|
self.shift_reg = 0;
|
|
self.shift_count = 0;
|
|
}
|
|
}
|
|
|
|
impl Mapper for InesMapper105 {
|
|
fn cpu_read(&self, addr: u16) -> u8 {
|
|
if addr < 0x8000 {
|
|
return 0;
|
|
}
|
|
|
|
if !self.prg_unlocked {
|
|
// Reset/power-on maps a fixed 32KiB window from the first PRG chip.
|
|
return read_bank(
|
|
&self.prg_rom,
|
|
0x8000,
|
|
0,
|
|
(addr as usize).saturating_sub(0x8000),
|
|
);
|
|
}
|
|
|
|
if !self.outer_chip_selected() {
|
|
let bank32 = self.outer_32k_bank();
|
|
return read_bank(
|
|
&self.prg_rom,
|
|
0x8000,
|
|
bank32,
|
|
(addr as usize).saturating_sub(0x8000),
|
|
);
|
|
}
|
|
|
|
let second_base = self.second_chip_base_16k();
|
|
let second_count = self.second_chip_banks_16k();
|
|
let prg_bank = self.prg_bank_value();
|
|
|
|
let bank16 = match self.prg_mode() {
|
|
0 | 1 => {
|
|
let bank32 = prg_bank >> 1;
|
|
if addr < 0xC000 {
|
|
second_base + safe_mod(bank32 * 2, second_count)
|
|
} else {
|
|
second_base + safe_mod(bank32 * 2 + 1, second_count)
|
|
}
|
|
}
|
|
2 => {
|
|
if addr < 0xC000 {
|
|
second_base
|
|
} else {
|
|
second_base + safe_mod(prg_bank, second_count)
|
|
}
|
|
}
|
|
_ => {
|
|
if addr < 0xC000 {
|
|
second_base + safe_mod(prg_bank, second_count)
|
|
} else {
|
|
second_base + second_count.saturating_sub(1)
|
|
}
|
|
}
|
|
};
|
|
read_bank(&self.prg_rom, 0x4000, bank16, (addr as usize) & 0x3FFF)
|
|
}
|
|
|
|
fn cpu_write(&mut self, addr: u16, value: u8) {
|
|
if addr >= 0x8000 {
|
|
self.write_serial(addr, value);
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
None
|
|
}
|
|
|
|
fn cpu_write_low(&mut self, addr: u16, value: u8) -> bool {
|
|
if (0x6000..=0x7FFF).contains(&addr) && !self.wram_disabled {
|
|
let idx = (addr as usize) & 0x1FFF;
|
|
if let Some(cell) = self.prg_ram.get_mut(idx) {
|
|
*cell = value;
|
|
}
|
|
return true;
|
|
}
|
|
false
|
|
}
|
|
|
|
fn ppu_read(&self, addr: u16) -> u8 {
|
|
if addr > 0x1FFF {
|
|
return 0;
|
|
}
|
|
self.chr_data.get(addr as usize).copied().unwrap_or(0)
|
|
}
|
|
|
|
fn ppu_write(&mut self, addr: u16, value: u8) {
|
|
if addr > 0x1FFF {
|
|
return;
|
|
}
|
|
if let Some(cell) = self.chr_data.get_mut(addr as usize) {
|
|
*cell = value;
|
|
}
|
|
}
|
|
|
|
fn mirroring(&self) -> Mirroring {
|
|
match self.control & 0x03 {
|
|
0 => Mirroring::OneScreenLow,
|
|
1 => Mirroring::OneScreenHigh,
|
|
2 => Mirroring::Vertical,
|
|
_ => Mirroring::Horizontal,
|
|
}
|
|
}
|
|
|
|
fn clock_cpu(&mut self, cycles: u8) {
|
|
if self.timer_halted() || self.irq_pending {
|
|
return;
|
|
}
|
|
let previous = self.irq_counter;
|
|
self.irq_counter = (self.irq_counter.wrapping_add(cycles as u32)) & 0x3FFF_FFFF;
|
|
if previous < Self::IRQ_THRESHOLD && self.irq_counter >= Self::IRQ_THRESHOLD {
|
|
self.irq_pending = 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.push(self.shift_reg);
|
|
out.push(self.shift_count);
|
|
out.push(self.control);
|
|
out.push(self.reg_a);
|
|
out.push(self.reg_b);
|
|
out.push(u8::from(self.wram_disabled));
|
|
out.push(u8::from(self.prg_unlocked));
|
|
out.push(u8::from(self.saw_i_low));
|
|
out.extend_from_slice(&self.irq_counter.to_le_bytes());
|
|
out.push(u8::from(self.irq_pending));
|
|
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() < 13 {
|
|
return Err("mapper state is truncated".to_string());
|
|
}
|
|
self.shift_reg = data[0];
|
|
self.shift_count = data[1];
|
|
self.control = data[2];
|
|
self.reg_a = data[3];
|
|
self.reg_b = data[4];
|
|
self.wram_disabled = data[5] != 0;
|
|
self.prg_unlocked = data[6] != 0;
|
|
self.saw_i_low = data[7] != 0;
|
|
self.irq_counter = u32::from_le_bytes([data[8], data[9], data[10], data[11]]) & 0x3FFF_FFFF;
|
|
self.irq_pending = data[12] != 0;
|
|
|
|
let mut cursor = 13usize;
|
|
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);
|
|
load_chr_state(&mut self.chr_data, &data[cursor..])
|
|
}
|
|
}
|