use std::cell::Cell;
use crate::nes::cartridge::NametableLayout;
use crate::nes::cartridge::base_mapper::BaseMapper;
use crate::nes::cartridge::cpu_cycle_irq::{CpuCycleIrq, CpuCycleIrqMode};
use crate::nes::cartridge::mapper::{Mapper, MapperCapabilities, MapperContext};
const MAPPER_NUMBER: u16 = 303;
const PRG_BANK_SIZE: usize = 0x4000; const FIXED_PRG_BANK: i16 = 2;
pub struct Mapper303 {
base: BaseMapper,
prg_bank_latch: u8,
prg_bank: u8,
irq: CpuCycleIrq,
irq_counter_low: u8,
irq_pending_latch: Cell<bool>,
}
impl Mapper303 {
pub fn new(ctx: MapperContext) -> Self {
let capabilities = MapperCapabilities {
has_dynamic_mirroring: true,
has_irq: true,
prg_bank_size_kb: 16,
..Default::default()
};
let mut base = BaseMapper::new(&ctx, capabilities);
base.configure_prg_banking(PRG_BANK_SIZE);
let mut mapper = Self {
base,
prg_bank_latch: 0,
prg_bank: 0,
irq: CpuCycleIrq::new(CpuCycleIrqMode::DownToZero),
irq_counter_low: 0,
irq_pending_latch: Cell::new(false),
};
mapper.apply_state();
mapper
}
fn apply_state(&mut self) {
self.base.select_prg_page(0, self.prg_bank as i16);
self.base.select_prg_page(1, FIXED_PRG_BANK);
self.base.set_mirroring(NametableLayout::Vertical);
}
}
impl Mapper for Mapper303 {
fn base(&self) -> &BaseMapper {
&self.base
}
fn base_mut(&mut self) -> &mut BaseMapper {
&mut self.base
}
fn mapper_number(&self) -> u16 {
MAPPER_NUMBER
}
fn read_prg(&self, addr: u16) -> u8 {
self.base.read_prg_banked(addr)
}
fn read_prg_open_bus(&self, addr: u16, open_bus: u8) -> u8 {
if addr == 0x4030 {
let pending = self.irq_pending_latch.get();
self.irq_pending_latch.set(false);
return (open_bus & 0xFE) | (pending as u8);
}
self.base
.read_prg_open_bus(addr, open_bus, |a| self.read_prg(a))
}
fn write_prg(&mut self, addr: u16, value: u8) {
if self.base.try_write_prg_ram(addr, value) {
return;
}
match addr {
0x4020 => {
self.irq.acknowledge();
self.irq_pending_latch.set(false);
self.irq_counter_low = value;
}
0x4021 => {
self.irq.acknowledge();
self.irq_pending_latch.set(false);
let counter = (self.irq_counter_low as u16) | ((value as u16) << 8);
self.irq.set_counter(counter);
self.irq.set_enabled(true);
self.irq.set_pending(false);
}
0x4025 => {
let layout = if (value >> 3) & 1 == 1 {
NametableLayout::Horizontal
} else {
NametableLayout::Vertical
};
self.base.set_mirroring(layout);
}
0x4A00..=0x4AFF => {
self.prg_bank_latch = ((addr >> 2) as u8 & 0x03) | ((addr >> 4) as u8 & 0x04);
}
0x5100..=0x51FF => {
self.prg_bank = self.prg_bank_latch;
self.base.select_prg_page(0, self.prg_bank as i16);
}
_ => {}
}
}
fn cpu_cycle(&mut self) {
self.irq.tick();
if self.irq.is_pending() {
self.irq.set_enabled(false);
self.irq.set_pending(false);
self.irq_pending_latch.set(true);
}
}
fn irq_pending(&self) -> bool {
self.irq_pending_latch.get()
}
fn reset(&mut self) {
self.prg_bank_latch = 0;
self.prg_bank = 0;
self.irq = CpuCycleIrq::new(CpuCycleIrqMode::DownToZero);
self.irq_counter_low = 0;
self.irq_pending_latch.set(false);
self.apply_state();
}
fn registers_snapshot(&self) -> Vec<u8> {
let flags = (self.irq.enabled() as u8) | ((self.irq_pending_latch.get() as u8) << 1);
vec![
self.prg_bank_latch,
self.prg_bank,
flags,
(self.irq.counter() & 0xFF) as u8,
(self.irq.counter() >> 8) as u8,
self.irq_counter_low,
self.base.mirroring().to_snapshot_byte(),
]
}
fn restore_registers(&mut self, data: &[u8]) {
if data.len() < 6 {
return;
}
self.prg_bank_latch = data[0];
self.prg_bank = data[1];
let flags = data[2];
self.irq.set_enabled(flags & 0x01 != 0);
let pending = flags & 0x02 != 0;
self.irq.set_pending(false);
self.irq_pending_latch.set(pending);
self.irq
.set_counter((data[3] as u16) | ((data[4] as u16) << 8));
self.irq_counter_low = data[5];
if data.len() >= 7 {
self.base
.set_mirroring(NametableLayout::from_snapshot_byte(data[6]));
}
self.base.select_prg_page(0, self.prg_bank as i16);
self.base.select_prg_page(1, FIXED_PRG_BANK);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nes::cartridge::mapper::{Mapper, create_mapper};
use crate::nes::cartridge::test_helpers::banked_data;
fn create_mapper303(prg_rom: Vec<u8>) -> Box<dyn Mapper> {
create_mapper(MapperContext::new_for_test(
303,
prg_rom,
vec![],
NametableLayout::Vertical,
))
.expect("mapper 303 should be implemented")
}
#[test]
fn mapper_303_is_registered() {
let prg_rom = banked_data(PRG_BANK_SIZE, 4);
let mapper = create_mapper303(prg_rom);
assert_eq!(mapper.mapper_number(), 303);
}
#[test]
fn power_on_prg_banks_correct() {
let prg_rom = banked_data(PRG_BANK_SIZE, 4);
let mapper = create_mapper303(prg_rom);
assert_eq!(
mapper.read_prg(0x8000),
0,
"bank 0 ($8000) should be PRG page 0"
);
assert_eq!(
mapper.read_prg(0xC000),
2,
"bank 1 ($C000) should be fixed PRG page 2"
);
}
#[test]
fn prg_bank_latch_and_apply_via_4axx_51xx() {
let prg_rom = banked_data(PRG_BANK_SIZE, 4);
let mut mapper = create_mapper303(prg_rom);
mapper.write_prg(0x4A04, 0x00);
assert_eq!(
mapper.read_prg(0x8000),
0,
"latch should not take effect until $51xx write"
);
mapper.write_prg(0x5100, 0x00);
assert_eq!(mapper.read_prg(0x8000), 1, "$8000 should now read page 1");
}
#[test]
fn prg_bank_latch_addr_bit_a3() {
let prg_rom = banked_data(PRG_BANK_SIZE, 4);
let mut mapper = create_mapper303(prg_rom);
mapper.write_prg(0x4A08, 0x00);
mapper.write_prg(0x5100, 0x00);
assert_eq!(mapper.read_prg(0x8000), 2, "$8000 should read page 2");
}
#[test]
fn prg_bank_latch_addr_bit_a6() {
let prg_rom = banked_data(PRG_BANK_SIZE, 8);
let mut mapper = create_mapper303(prg_rom);
mapper.write_prg(0x4A40, 0x00);
mapper.write_prg(0x5100, 0x00);
assert_eq!(mapper.read_prg(0x8000), 4, "$8000 should read page 4");
}
#[test]
fn fixed_bank_at_c000_ffff_never_changes() {
let prg_rom = banked_data(PRG_BANK_SIZE, 8);
let mut mapper = create_mapper303(prg_rom);
mapper.write_prg(0x4A14, 0x00); mapper.write_prg(0x5100, 0x00);
assert_eq!(
mapper.read_prg(0xC000),
2,
"$C000 should remain fixed at page 2"
);
assert_eq!(
mapper.read_prg(0xFFFF),
2,
"$FFFF should remain fixed at page 2"
);
}
#[test]
fn power_on_mirroring_is_vertical() {
let prg_rom = banked_data(PRG_BANK_SIZE, 4);
let mapper = create_mapper303(prg_rom);
assert_eq!(mapper.base().mirroring(), NametableLayout::Vertical);
}
#[test]
fn write_4025_bit3_set_selects_horizontal_mirroring() {
let prg_rom = banked_data(PRG_BANK_SIZE, 4);
let mut mapper = create_mapper303(prg_rom);
mapper.write_prg(0x4025, 0x08); assert_eq!(mapper.base().mirroring(), NametableLayout::Horizontal);
}
#[test]
fn write_4025_bit3_clear_selects_vertical_mirroring() {
let prg_rom = banked_data(PRG_BANK_SIZE, 4);
let mut mapper = create_mapper303(prg_rom);
mapper.write_prg(0x4025, 0x08);
assert_eq!(mapper.base().mirroring(), NametableLayout::Horizontal);
mapper.write_prg(0x4025, 0x00);
assert_eq!(mapper.base().mirroring(), NametableLayout::Vertical);
}
#[test]
fn irq_not_pending_at_power_on() {
let prg_rom = banked_data(PRG_BANK_SIZE, 4);
let mapper = create_mapper303(prg_rom);
assert!(!mapper.irq_pending());
}
#[test]
fn irq_fires_when_counter_reaches_zero() {
let prg_rom = banked_data(PRG_BANK_SIZE, 4);
let mut mapper = create_mapper303(prg_rom);
mapper.write_prg(0x4020, 3);
mapper.write_prg(0x4021, 0);
assert!(
!mapper.irq_pending(),
"IRQ should not fire before counter hits 0"
);
mapper.cpu_cycle();
assert!(!mapper.irq_pending());
mapper.cpu_cycle();
assert!(!mapper.irq_pending());
mapper.cpu_cycle();
assert!(mapper.irq_pending(), "IRQ should fire after 3 cycles");
}
#[test]
fn irq_counter_high_byte_via_4021() {
let prg_rom = banked_data(PRG_BANK_SIZE, 4);
let mut mapper = create_mapper303(prg_rom);
mapper.write_prg(0x4020, 0x00);
mapper.write_prg(0x4021, 0x01);
for _ in 0..255 {
mapper.cpu_cycle();
assert!(
!mapper.irq_pending(),
"IRQ should not fire before 256 cycles"
);
}
mapper.cpu_cycle(); assert!(mapper.irq_pending(), "IRQ should fire after 256 cycles");
}
#[test]
fn write_4020_clears_irq_pending() {
let prg_rom = banked_data(PRG_BANK_SIZE, 4);
let mut mapper = create_mapper303(prg_rom);
mapper.write_prg(0x4020, 1);
mapper.write_prg(0x4021, 0);
mapper.cpu_cycle();
assert!(mapper.irq_pending());
mapper.write_prg(0x4020, 5); assert!(
!mapper.irq_pending(),
"$4020 write should clear pending IRQ"
);
}
#[test]
fn write_4021_clears_irq_pending() {
let prg_rom = banked_data(PRG_BANK_SIZE, 4);
let mut mapper = create_mapper303(prg_rom);
mapper.write_prg(0x4020, 1);
mapper.write_prg(0x4021, 0);
mapper.cpu_cycle();
assert!(mapper.irq_pending());
mapper.write_prg(0x4021, 0); assert!(
!mapper.irq_pending(),
"$4021 write should clear pending IRQ"
);
}
#[test]
fn read_4030_returns_irq_status_and_clears() {
let prg_rom = banked_data(PRG_BANK_SIZE, 4);
let mut mapper = create_mapper303(prg_rom);
mapper.write_prg(0x4020, 1);
mapper.write_prg(0x4021, 0);
mapper.cpu_cycle();
assert!(mapper.irq_pending());
let status = mapper.read_prg_open_bus(0x4030, 0xFF);
assert_eq!(
status & 0x01,
0x01,
"$4030 bit 0 should be 1 when IRQ pending"
);
assert!(!mapper.irq_pending(), "$4030 read should clear pending IRQ");
}
#[test]
fn read_4030_returns_zero_when_no_irq() {
let prg_rom = banked_data(PRG_BANK_SIZE, 4);
let mapper = create_mapper303(prg_rom);
let status = mapper.read_prg_open_bus(0x4030, 0xFF);
assert_eq!(status & 0x01, 0x00, "$4030 bit 0 should be 0 when no IRQ");
}
#[test]
fn irq_disables_after_firing() {
let prg_rom = banked_data(PRG_BANK_SIZE, 4);
let mut mapper = create_mapper303(prg_rom);
mapper.write_prg(0x4020, 1);
mapper.write_prg(0x4021, 0);
mapper.cpu_cycle();
assert!(mapper.irq_pending());
mapper.write_prg(0x4020, 0);
for _ in 0..10 {
mapper.cpu_cycle();
}
assert!(
!mapper.irq_pending(),
"IRQ should stay clear after acknowledge with no reload"
);
}
#[test]
fn reset_restores_power_on_state() {
let prg_rom = banked_data(PRG_BANK_SIZE, 8);
let mut mapper = create_mapper303(prg_rom);
mapper.write_prg(0x4A40, 0); mapper.write_prg(0x5100, 0); mapper.write_prg(0x4025, 0x08);
assert_eq!(mapper.read_prg(0x8000), 4);
assert_eq!(mapper.base().mirroring(), NametableLayout::Horizontal);
mapper.reset();
assert_eq!(
mapper.read_prg(0x8000),
0,
"after reset, bank 0 should be page 0"
);
assert_eq!(
mapper.read_prg(0xC000),
2,
"after reset, fixed bank should be page 2"
);
assert_eq!(mapper.base().mirroring(), NametableLayout::Vertical);
assert!(!mapper.irq_pending());
}
#[test]
fn snapshot_and_restore_preserves_prg_bank() {
let prg_rom = banked_data(PRG_BANK_SIZE, 8);
let mut mapper = create_mapper303(prg_rom);
mapper.write_prg(0x4A04, 0); mapper.write_prg(0x5100, 0);
let snap = mapper.registers_snapshot();
mapper.reset();
assert_eq!(mapper.read_prg(0x8000), 0, "after reset, should be page 0");
mapper.restore_registers(&snap);
assert_eq!(
mapper.read_prg(0x8000),
1,
"after restore, should be page 1"
);
}
#[test]
fn snapshot_and_restore_preserves_irq_state() {
let prg_rom = banked_data(PRG_BANK_SIZE, 4);
let mut mapper = create_mapper303(prg_rom);
mapper.write_prg(0x4020, 5);
mapper.write_prg(0x4021, 0);
let snap = mapper.registers_snapshot();
mapper.reset();
mapper.restore_registers(&snap);
mapper.cpu_cycle();
assert!(!mapper.irq_pending());
mapper.cpu_cycle();
assert!(!mapper.irq_pending());
mapper.cpu_cycle();
assert!(!mapper.irq_pending());
mapper.cpu_cycle();
assert!(!mapper.irq_pending());
mapper.cpu_cycle();
assert!(
mapper.irq_pending(),
"IRQ should fire after restoring counter=5 and 5 cycles"
);
}
#[test]
fn snapshot_and_restore_preserves_mirroring() {
let prg_rom = banked_data(PRG_BANK_SIZE, 4);
let mut mapper = create_mapper303(prg_rom);
mapper.write_prg(0x4025, 0x08); assert_eq!(mapper.base().mirroring(), NametableLayout::Horizontal);
let snap = mapper.registers_snapshot();
mapper.reset();
assert_eq!(
mapper.base().mirroring(),
NametableLayout::Vertical,
"after reset, mirroring should be Vertical"
);
mapper.restore_registers(&snap);
assert_eq!(
mapper.base().mirroring(),
NametableLayout::Horizontal,
"after restore, mirroring should be Horizontal"
);
}
}