use crate::cartridge::BaseMapper;
use crate::cartridge::mapper::{Mapper, MapperCapabilities};
use super::cpu_cycle_irq::{CpuCycleIrq, CpuCycleIrqMode};
pub struct Mapper50 {
base: BaseMapper,
prg_reg: u8,
irq: CpuCycleIrq,
}
impl Mapper50 {
const PRG_BANK_SIZE: usize = 0x2000;
const BANK_AT_6000: usize = 0x0F;
pub fn new(ctx: super::mapper::MapperContext) -> Self {
let mirroring = ctx.mirroring;
let capabilities = MapperCapabilities {
has_irq: 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.set_mirroring(mirroring);
let mut mapper = Self {
base,
prg_reg: 0,
irq: CpuCycleIrq::new(CpuCycleIrqMode::UpAutoDisable { threshold: 0x1000 }),
};
mapper.update_banks();
mapper
}
fn update_banks(&mut self) {
self.base.select_prg_6000_page(Self::BANK_AT_6000 as i16);
self.base.select_prg_page(0, 0x08);
self.base.select_prg_page(1, 0x09);
self.base
.select_prg_page(2, Self::unscramble_prg(self.prg_reg) as i16);
self.base.select_prg_page(3, 0x0B);
}
fn unscramble_prg(reg: u8) -> usize {
((reg & 0x08) as usize) | (((reg & 0x01) << 2) as usize) | (((reg & 0x06) >> 1) as usize) }
}
impl Mapper for Mapper50 {
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 > 0x5FFF {
return;
}
match addr & 0x4120 {
0x4020 => {
self.prg_reg = value & 0x0F;
self.update_banks();
}
0x4120 => {
if (value & 0x01) != 0 {
self.irq.set_enabled(true);
} else {
self.irq.set_enabled(false);
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);
vec![
self.prg_reg,
flags,
(self.irq.counter() & 0xFF) as u8,
(self.irq.counter() >> 8) as u8,
]
}
fn restore_registers(&mut self, data: &[u8]) {
if data.len() >= 4 {
self.prg_reg = 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.update_banks();
}
}
}
#[cfg(test)]
mod tests {
use super::Mapper50;
use crate::cartridge::NametableLayout;
use crate::cartridge::mapper::{Mapper, MapperContext, create_mapper};
use crate::cartridge::test_helpers::banked_data;
const PRG_BANKS: usize = 19;
fn make_mapper() -> Box<dyn Mapper> {
let prg = banked_data(8 * 1024, PRG_BANKS);
let chr = banked_data(8 * 1024, 1);
create_mapper(MapperContext::new_for_test(
50,
prg,
chr,
NametableLayout::Vertical,
))
.expect("Mapper 50 should be implemented")
}
fn make_mapper_direct() -> Mapper50 {
let prg = banked_data(8 * 1024, PRG_BANKS);
let chr = banked_data(8 * 1024, 1);
Mapper50::new(MapperContext::new_for_test(
50,
prg,
chr,
NametableLayout::Vertical,
))
}
#[test]
fn mapper_50_is_registered_in_factory() {
let result = create_mapper(MapperContext::new_for_test(
50,
banked_data(8 * 1024, PRG_BANKS),
banked_data(8 * 1024, 1),
NametableLayout::Vertical,
));
assert!(
result.is_ok(),
"Mapper 50 must be registered in the factory"
);
}
#[test]
fn prg_6000_is_fixed_bank_0x0f() {
let mapper = make_mapper();
assert_eq!(
mapper.read_prg(0x6000),
0x0F % PRG_BANKS as u8,
"$6000 window must read from fixed bank $0F"
);
}
#[test]
fn prg_8000_is_fixed_bank_0x08() {
let mapper = make_mapper();
assert_eq!(
mapper.read_prg(0x8000),
0x08,
"$8000 window must read from fixed bank $08"
);
}
#[test]
fn prg_a000_is_fixed_bank_0x09() {
let mapper = make_mapper();
assert_eq!(
mapper.read_prg(0xA000),
0x09,
"$A000 window must read from fixed bank $09"
);
}
#[test]
fn prg_e000_is_fixed_bank_0x0b() {
let mapper = make_mapper();
assert_eq!(
mapper.read_prg(0xE000),
0x0B,
"$E000 window must read from fixed bank $0B"
);
}
#[test]
fn prg_c000_defaults_to_bank_0() {
let mapper = make_mapper();
assert_eq!(mapper.read_prg(0xC000), 0, "$C000 must default to bank 0");
}
#[test]
fn prg_c000_selects_bank_via_4020_register() {
let mut mapper = make_mapper();
mapper.write_prg(0x4020, 0x02);
assert_eq!(
mapper.read_prg(0xC000),
1,
"$4020=2 must select bank 1 at $C000"
);
}
#[test]
fn prg_register_unscrambling_bit0_m_maps_to_bit2() {
let mut mapper = make_mapper();
mapper.write_prg(0x4020, 0x01);
assert_eq!(
mapper.read_prg(0xC000),
4,
"$4020=1 (M bit) must map to bank 4 (bit2)"
);
}
#[test]
fn prg_register_unscrambling_bit3_h_maps_to_bit3() {
let mut mapper = make_mapper();
mapper.write_prg(0x4020, 0x08);
assert_eq!(
mapper.read_prg(0xC000),
8,
"$4020=8 (H bit) must map to bank 8 (bit3)"
);
}
#[test]
fn prg_register_only_uses_low_nibble() {
let mut mapper = make_mapper();
mapper.write_prg(0x4020, 0xF2); assert_eq!(mapper.read_prg(0xC000), 1);
}
#[test]
fn irq_not_pending_initially() {
let mapper = make_mapper_direct();
assert!(!mapper.irq_pending());
}
#[test]
fn irq_does_not_fire_while_disabled() {
let mut mapper = make_mapper_direct();
for _ in 0..0x2000 {
mapper.cpu_cycle();
}
assert!(!mapper.irq_pending());
}
#[test]
fn irq_fires_after_4096_cycles_when_enabled() {
let mut mapper = make_mapper_direct();
mapper.write_prg(0x4120, 0x01); for _ in 0..0x1000 {
mapper.cpu_cycle();
}
assert!(mapper.irq_pending(), "IRQ must fire after 4096 CPU cycles");
}
#[test]
fn irq_does_not_fire_before_4096_cycles() {
let mut mapper = make_mapper_direct();
mapper.write_prg(0x4120, 0x01); for _ in 0..0x0FFF {
mapper.cpu_cycle();
}
assert!(
!mapper.irq_pending(),
"IRQ must not fire before 4096 cycles"
);
}
#[test]
fn irq_auto_disables_when_it_fires() {
let mut mapper = make_mapper_direct();
mapper.write_prg(0x4120, 0x01); for _ in 0..0x1000 {
mapper.cpu_cycle();
}
assert!(mapper.irq_pending());
let count_before = mapper.irq.counter();
mapper.cpu_cycle();
assert_eq!(
mapper.irq.counter(),
count_before.wrapping_add(0), "Counter must stop after IRQ fires"
);
}
#[test]
fn irq_acknowledged_and_reset_by_writing_4120_with_e_clear() {
let mut mapper = make_mapper_direct();
mapper.write_prg(0x4120, 0x01); for _ in 0..0x1000 {
mapper.cpu_cycle();
}
assert!(mapper.irq_pending());
mapper.write_prg(0x4120, 0x00); assert!(!mapper.irq_pending());
}
#[test]
fn irq_counter_resets_on_disable() {
let mut mapper = make_mapper_direct();
mapper.write_prg(0x4120, 0x01); for _ in 0..0x1000 {
mapper.cpu_cycle();
}
mapper.write_prg(0x4120, 0x00); mapper.write_prg(0x4120, 0x01); for _ in 0..0x0FFF {
mapper.cpu_cycle();
}
assert!(
!mapper.irq_pending(),
"IRQ must not fire until 4096 cycles after re-enable"
);
}
#[test]
fn registers_snapshot_and_restore() {
let mut mapper = make_mapper_direct();
mapper.write_prg(0x4020, 0x04); mapper.write_prg(0x4120, 0x01); for _ in 0..100 {
mapper.cpu_cycle();
}
let snap = mapper.registers_snapshot();
let mut restored = make_mapper_direct();
restored.restore_registers(&snap);
assert_eq!(
restored.read_prg(0xC000),
mapper.read_prg(0xC000),
"Restored PRG bank must match"
);
}
}