use crate::nes::cartridge::NametableLayout;
use crate::nes::cartridge::base_mapper::BaseMapper;
use crate::nes::cartridge::mapper::{Mapper, MapperCapabilities};
use crate::nes::cartridge::cpu_cycle_irq::{CpuCycleIrq, CpuCycleIrqMode};
pub struct Mapper55 {
base: BaseMapper,
prg_bank: u8,
irq: CpuCycleIrq,
initial_mirroring: NametableLayout,
}
impl Mapper55 {
const PRG_BANK_SIZE: usize = 0x2000; const CHR_BANK_SIZE: usize = 0x2000;
pub fn new(ctx: crate::nes::cartridge::mapper::MapperContext) -> Self {
let mirroring = ctx.mirroring;
let capabilities = MapperCapabilities {
has_irq: true,
has_dynamic_mirroring: true,
prg_bank_size_kb: 8,
chr_bank_size_kb: 8,
..Default::default()
};
let mut base = BaseMapper::new(&ctx, capabilities);
base.configure_prg_banking(Self::PRG_BANK_SIZE);
base.configure_prg_6000_banking();
base.configure_chr_banking(Self::CHR_BANK_SIZE);
base.set_mirroring(mirroring);
let mut mapper = Self {
base,
prg_bank: 0,
irq: CpuCycleIrq::new(CpuCycleIrqMode::UpLevel {
threshold: 0x6000,
mask: 0x7FFF,
}),
initial_mirroring: mirroring,
};
mapper.update_banks();
mapper
}
fn update_banks(&mut self) {
self.base.select_prg_6000_page(self.prg_bank as i16);
self.base.select_prg_page(0, -4);
self.base.select_prg_page(1, -3);
self.base.select_prg_page(2, -2);
self.base.select_prg_page(3, -1);
self.base.select_chr_page(0, 0);
}
}
impl Mapper for Mapper55 {
fn base(&self) -> &BaseMapper {
&self.base
}
fn base_mut(&mut self) -> &mut BaseMapper {
&mut self.base
}
fn write_prg(&mut self, addr: u16, value: u8) {
if addr < 0xF000 {
return;
}
match addr & 0x03 {
0x00 => {
self.prg_bank = value;
self.base.select_prg_6000_page(value as i16);
}
0x01 => {
self.base.set_mirroring_hv((value & 0x08) != 0);
}
0x02 => {
self.irq.set_enabled((value & 0x02) != 0);
if !self.irq.enabled() {
self.irq.acknowledge();
self.irq.set_counter(0);
}
}
_ => {}
}
}
fn cpu_cycle(&mut self) {
self.irq.tick();
}
fn irq_pending(&self) -> bool {
self.irq.is_pending()
}
fn registers_snapshot(&self) -> Vec<u8> {
let flags = (self.irq.enabled() as u8) | ((self.irq.is_pending() as u8) << 1);
let mirror_byte = matches!(self.base.mirroring(), NametableLayout::Horizontal) as u8;
let counter = self.irq.counter();
vec![
self.prg_bank,
flags,
(counter & 0xFF) as u8,
(counter >> 8) as u8,
mirror_byte,
]
}
fn restore_registers(&mut self, data: &[u8]) {
if data.len() >= 5 {
self.prg_bank = data[0];
self.irq.set_enabled((data[1] & 1) != 0);
self.irq.set_pending((data[1] & 2) != 0);
self.irq
.set_counter((data[2] as u16) | ((data[3] as u16) << 8));
self.base.set_mirroring_hv(data[4] != 0);
self.update_banks();
}
}
fn reset(&mut self) {
self.prg_bank = 0;
self.irq.set_enabled(false);
self.irq.acknowledge();
self.irq.set_counter(0);
self.base.set_mirroring(self.initial_mirroring);
self.update_banks();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nes::cartridge::mapper::{MapperContext, create_mapper};
use crate::nes::cartridge::test_helpers::banked_data;
const PRG_BANKS: usize = 24;
const CHR_BANKS: usize = 1;
fn make_mapper() -> Mapper55 {
let prg = banked_data(8 * 1024, PRG_BANKS);
let chr = banked_data(8 * 1024, CHR_BANKS);
Mapper55::new(MapperContext::new_for_test(
55,
prg,
chr,
NametableLayout::Vertical,
))
}
#[test]
fn mapper_55_is_registered() {
let prg = banked_data(8 * 1024, PRG_BANKS);
let chr = banked_data(8 * 1024, CHR_BANKS);
let result = create_mapper(MapperContext::new_for_test(
55,
prg,
chr,
NametableLayout::Vertical,
));
assert!(
result.is_ok(),
"Mapper 55 must be registered in the factory"
);
}
#[test]
fn power_on_prg_6000_is_bank_0() {
let mapper = make_mapper();
assert_eq!(
mapper.read_prg(0x6000),
0,
"$6000 window must default to PRG bank 0 at power-on"
);
}
#[test]
fn power_on_prg_8000_fixed_to_last_32kb() {
let mapper = make_mapper();
let expected = (PRG_BANKS - 4) as u8;
assert_eq!(
mapper.read_prg(0x8000),
expected,
"$8000 must be fixed to 4th-from-last 8KB bank"
);
let expected_last = (PRG_BANKS - 1) as u8;
assert_eq!(
mapper.read_prg(0xE000),
expected_last,
"$E000 must be fixed to last 8KB bank"
);
}
#[test]
fn power_on_chr_bank_is_0() {
let mut mapper = make_mapper();
assert_eq!(
mapper.read_chr(0x0000),
0,
"CHR must be fixed to bank 0 at power-on"
);
}
#[test]
fn power_on_irq_not_pending() {
let mapper = make_mapper();
assert!(!mapper.irq_pending(), "IRQ must not be pending at power-on");
}
#[test]
fn prg_6000_window_switches_on_f000_write() {
let mut mapper = make_mapper();
mapper.write_prg(0xF000, 5);
assert_eq!(
mapper.read_prg(0x6000),
5,
"$6000 window must switch to bank 5 after writing $F000"
);
}
#[test]
fn prg_6000_window_switches_on_f004_write() {
let mut mapper = make_mapper();
mapper.write_prg(0xF004, 7);
assert_eq!(
mapper.read_prg(0x6000),
7,
"$6000 window must switch to bank 7 after writing $F004"
);
}
#[test]
fn prg_6000_window_covers_full_8kb() {
let mut mapper = make_mapper();
mapper.write_prg(0xF000, 3);
assert_eq!(mapper.read_prg(0x6000), 3, "start of $6000 window");
assert_eq!(mapper.read_prg(0x7FFF), 3, "end of $7FFF window");
}
#[test]
fn prg_8000_fixed_unaffected_by_latch_write() {
let mut mapper = make_mapper();
let before = mapper.read_prg(0x8000);
mapper.write_prg(0xF000, 15);
assert_eq!(
mapper.read_prg(0x8000),
before,
"$8000 fixed window must not change after latch write"
);
}
#[test]
fn registers_below_f000_are_ignored() {
let mut mapper = make_mapper();
mapper.write_prg(0xE000, 5);
assert_eq!(
mapper.read_prg(0x6000),
0,
"Writes below $F000 must be ignored (PRG bank stays 0)"
);
}
#[test]
fn chr_is_always_bank_0() {
let mut mapper = make_mapper();
mapper.write_prg(0xF000, 0xFF);
assert_eq!(
mapper.read_chr(0x0000),
0,
"CHR must remain bank 0 regardless of latch value"
);
}
#[test]
fn mirroring_defaults_from_header() {
let mapper = make_mapper(); assert_eq!(
mapper.get_mirroring(),
NametableLayout::Vertical,
"Mirroring must reflect header at power-on"
);
}
#[test]
fn write_f001_bit3_set_selects_horizontal() {
let mut mapper = make_mapper(); mapper.write_prg(0xF001, 0x08);
assert_eq!(
mapper.get_mirroring(),
NametableLayout::Horizontal,
"Writing $F001 with bit 3 set must switch to Horizontal"
);
}
#[test]
fn write_f001_bit3_clear_selects_vertical() {
let mut mapper = make_mapper();
mapper.write_prg(0xF001, 0x08); mapper.write_prg(0xF001, 0x00); assert_eq!(
mapper.get_mirroring(),
NametableLayout::Vertical,
"Writing $F001 with bit 3 clear must switch to Vertical"
);
}
#[test]
fn irq_does_not_fire_while_disabled() {
let mut mapper = make_mapper();
for _ in 0..0x8000 {
mapper.cpu_cycle();
}
assert!(
!mapper.irq_pending(),
"IRQ must not fire while IRQ is disabled"
);
}
#[test]
fn irq_fires_after_0x6000_cycles_when_enabled() {
let mut mapper = make_mapper();
mapper.write_prg(0xF002, 0x02); for _ in 0..0x5FFF {
mapper.cpu_cycle();
}
assert!(
!mapper.irq_pending(),
"IRQ must not fire before reaching threshold"
);
mapper.cpu_cycle(); assert!(mapper.irq_pending(), "IRQ must fire at cycle 0x6000");
}
#[test]
fn irq_disable_clears_pending_and_resets_counter() {
let mut mapper = make_mapper();
mapper.write_prg(0xF002, 0x02); for _ in 0..0x6000 {
mapper.cpu_cycle();
}
assert!(mapper.irq_pending(), "IRQ should be pending before disable");
mapper.write_prg(0xF002, 0x00); assert!(!mapper.irq_pending(), "IRQ must be cleared after disabling");
mapper.write_prg(0xF002, 0x02); for _ in 0..0x5FFF {
mapper.cpu_cycle();
}
assert!(
!mapper.irq_pending(),
"IRQ must not fire before threshold after counter reset"
);
}
#[test]
fn irq_register_decoded_at_f002_and_multiples() {
let mut mapper = make_mapper();
mapper.write_prg(0xF006, 0x02); for _ in 0..0x6000 {
mapper.cpu_cycle();
}
assert!(
mapper.irq_pending(),
"IRQ enable at $F006 (addr & 3 == 2) must work"
);
}
#[test]
fn reset_returns_to_power_on_state() {
let mut mapper = make_mapper();
mapper.write_prg(0xF000, 10); mapper.write_prg(0xF001, 0x08); mapper.write_prg(0xF002, 0x02); for _ in 0..0x6000 {
mapper.cpu_cycle();
}
mapper.reset();
assert_eq!(mapper.read_prg(0x6000), 0, "PRG latch must reset to 0");
assert!(!mapper.irq_pending(), "IRQ must not be pending after reset");
assert_eq!(
mapper.read_prg(0x8000),
(PRG_BANKS - 4) as u8,
"$8000 must still be fixed to last-4 bank after reset"
);
assert_eq!(
mapper.get_mirroring(),
NametableLayout::Vertical,
"Mirroring must reset to header value (Vertical) after reset"
);
}
#[test]
fn registers_snapshot_round_trips() {
let mut mapper = make_mapper();
mapper.write_prg(0xF000, 7); mapper.write_prg(0xF001, 0x08); mapper.write_prg(0xF002, 0x02); for _ in 0..0x3000 {
mapper.cpu_cycle(); }
let snap = mapper.registers_snapshot();
let mut restored = make_mapper();
restored.restore_registers(&snap);
assert_eq!(
restored.read_prg(0x6000),
mapper.read_prg(0x6000),
"Restored PRG bank must match"
);
assert_eq!(
restored.get_mirroring(),
mapper.get_mirroring(),
"Restored mirroring must match"
);
assert_eq!(
restored.irq_pending(),
mapper.irq_pending(),
"Restored IRQ pending state must match"
);
}
#[test]
fn chr_ram_works_when_no_chr_rom() {
let prg = banked_data(8 * 1024, PRG_BANKS);
let mut mapper = Mapper55::new(MapperContext::new_for_test(
55,
prg,
vec![],
NametableLayout::Vertical,
));
mapper.write_chr(0x0100, 0xAB);
assert_eq!(
mapper.read_chr(0x0100),
0xAB,
"CHR-RAM must be writable when no CHR-ROM is present"
);
}
}