use crate::cartridge::BaseMapper;
use crate::cartridge::common::A12RisingEdgeDetector;
use crate::cartridge::{Mapper, MapperCapabilities, NametableLayout};
pub struct Mapper48 {
base: BaseMapper,
prg_bank: [u8; 2],
chr_bank_2k: [u8; 2],
chr_bank_1k: [u8; 4],
irq_latch: u8,
irq_counter: u8,
irq_reload: bool,
irq_enabled: bool,
irq_pending: bool,
a12_detector: A12RisingEdgeDetector,
}
impl Mapper48 {
const PRG_BANK_SIZE: usize = 0x2000; const CHR_BANK_1K_SIZE: usize = 0x0400; const REGISTER_MASK: u16 = 0xE003;
pub fn new(ctx: super::mapper::MapperContext) -> Self {
let mirroring = ctx.mirroring;
let capabilities = MapperCapabilities {
has_irq: true,
has_chr_banking: true,
has_dynamic_mirroring: true,
max_prg_ram_kb: 8,
prg_bank_size_kb: 8,
chr_bank_size_kb: 1,
..Default::default()
};
let mut base = BaseMapper::new(&ctx, capabilities);
base.configure_prg_banking(Self::PRG_BANK_SIZE);
base.configure_chr_banking(Self::CHR_BANK_1K_SIZE);
base.set_mirroring(mirroring);
let mut mapper = Self {
base,
prg_bank: [0; 2],
chr_bank_2k: [0; 2],
chr_bank_1k: [0; 4],
irq_latch: 0,
irq_counter: 0,
irq_reload: false,
irq_enabled: false,
irq_pending: false,
a12_detector: A12RisingEdgeDetector::new(3),
};
mapper.update_banks();
mapper
}
fn update_banks(&mut self) {
self.base.select_prg_page(0, self.prg_bank[0] as i16);
self.base.select_prg_page(1, self.prg_bank[1] as i16);
self.base.select_prg_page(2, -2);
self.base.select_prg_page(3, -1);
let b0 = self.chr_bank_2k[0] as i16;
self.base.select_chr_page(0, b0 * 2);
self.base.select_chr_page(1, b0 * 2 + 1);
let b1 = self.chr_bank_2k[1] as i16;
self.base.select_chr_page(2, b1 * 2);
self.base.select_chr_page(3, b1 * 2 + 1);
for i in 0..4 {
self.base.select_chr_page(4 + i, self.chr_bank_1k[i] as i16);
}
}
fn clock_irq_counter(&mut self) {
if self.irq_counter == 0 || self.irq_reload {
self.irq_counter = self.irq_latch;
self.irq_reload = false;
} else {
self.irq_counter -= 1;
}
if self.irq_counter == 0 && self.irq_enabled {
self.irq_pending = true;
}
}
}
impl Mapper for Mapper48 {
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 self.base.try_write_prg_ram(addr, value) {
return;
}
match addr & Self::REGISTER_MASK {
0x8000 => {
self.prg_bank[0] = value & 0x3F;
self.update_banks();
}
0x8001 => {
self.prg_bank[1] = value & 0x3F;
self.update_banks();
}
0x8002 => {
self.chr_bank_2k[0] = value;
self.update_banks();
}
0x8003 => {
self.chr_bank_2k[1] = value;
self.update_banks();
}
0xA000 => {
self.chr_bank_1k[0] = value;
self.update_banks();
}
0xA001 => {
self.chr_bank_1k[1] = value;
self.update_banks();
}
0xA002 => {
self.chr_bank_1k[2] = value;
self.update_banks();
}
0xA003 => {
self.chr_bank_1k[3] = value;
self.update_banks();
}
0xC000 => self.irq_latch = value ^ 0xFF,
0xC001 => {
self.irq_counter = 0;
self.irq_reload = true;
}
0xC002 => self.irq_enabled = true,
0xC003 => {
self.irq_enabled = false;
self.irq_pending = false;
}
0xE000 => {
self.base.set_mirroring_hv((value & 0x40) != 0);
}
_ => {}
}
}
fn ppu_address_changed(&mut self, addr: u16) {
if self.a12_detector.update(addr) {
self.clock_irq_counter();
}
}
fn cpu_cycle(&mut self) {
self.a12_detector.cpu_tick();
}
fn irq_pending(&self) -> bool {
self.irq_pending
}
fn registers_snapshot(&self) -> Vec<u8> {
let mut snap = Vec::with_capacity(12);
snap.push(if self.base.mirroring() == NametableLayout::Horizontal {
1
} else {
0
});
snap.extend_from_slice(&self.prg_bank);
snap.extend_from_slice(&self.chr_bank_2k);
snap.extend_from_slice(&self.chr_bank_1k);
snap.push(self.irq_latch);
snap.push(self.irq_counter);
let irq_flags = (self.irq_reload as u8)
| ((self.irq_enabled as u8) << 1)
| ((self.irq_pending as u8) << 2);
snap.push(irq_flags);
snap
}
fn restore_registers(&mut self, data: &[u8]) {
if data.len() >= 12 {
self.base.set_mirroring_hv(data[0] != 0);
self.prg_bank.copy_from_slice(&data[1..3]);
self.chr_bank_2k.copy_from_slice(&data[3..5]);
self.chr_bank_1k.copy_from_slice(&data[5..9]);
self.irq_latch = data[9];
self.irq_counter = data[10];
let flags = data[11];
self.irq_reload = (flags & 1) != 0;
self.irq_enabled = (flags & 2) != 0;
self.irq_pending = (flags & 4) != 0;
self.update_banks();
}
}
}
#[cfg(test)]
mod tests {
use crate::cartridge::NametableLayout;
use crate::cartridge::mapper::{Mapper, MapperContext, create_mapper};
use crate::cartridge::test_helpers::banked_data;
fn create_tc0690(
prg_rom: Vec<u8>,
chr_rom: Vec<u8>,
mirroring: NametableLayout,
) -> std::io::Result<Box<dyn Mapper>> {
create_mapper(MapperContext::new_for_test(48, prg_rom, chr_rom, mirroring))
}
fn trigger_scanline(mapper: &mut Box<dyn Mapper>) {
mapper.ppu_address_changed(0x0FFF);
for _ in 0..3 {
mapper.cpu_cycle();
}
mapper.ppu_address_changed(0x1000);
}
#[test]
fn mapper_48_is_registered() {
let result = create_tc0690(
banked_data(8 * 1024, 6),
banked_data(1024, 8),
NametableLayout::Vertical,
);
assert!(
result.is_ok(),
"Mapper 48 must be registered in the factory"
);
}
#[test]
fn power_on_prg_8000_bank_0_and_a000_bank_1() {
let prg = banked_data(8 * 1024, 6);
let mapper =
create_tc0690(prg, banked_data(1024, 8), NametableLayout::Vertical).expect("mapper 48");
assert_eq!(mapper.read_prg(0x8000), 0, "$8000 must default to bank 0");
assert_eq!(mapper.read_prg(0xA000), 0, "$A000 must default to bank 0");
}
#[test]
fn prg_8000_switchable_via_register() {
let prg = banked_data(8 * 1024, 6);
let mut mapper =
create_tc0690(prg, banked_data(1024, 8), NametableLayout::Vertical).expect("mapper 48");
mapper.write_prg(0x8000, 2);
assert_eq!(mapper.read_prg(0x8000), 2);
assert_eq!(mapper.read_prg(0x9FFF), 2);
mapper.write_prg(0x8000, 3);
assert_eq!(mapper.read_prg(0x8000), 3);
}
#[test]
fn prg_a000_switchable_via_register() {
let prg = banked_data(8 * 1024, 6);
let mut mapper =
create_tc0690(prg, banked_data(1024, 8), NametableLayout::Vertical).expect("mapper 48");
mapper.write_prg(0x8001, 1);
assert_eq!(mapper.read_prg(0xA000), 1);
assert_eq!(mapper.read_prg(0xBFFF), 1);
mapper.write_prg(0x8001, 4);
assert_eq!(mapper.read_prg(0xA000), 4);
}
#[test]
fn prg_c000_and_e000_fixed_to_last_two_banks() {
let prg = banked_data(8 * 1024, 6);
let mut mapper =
create_tc0690(prg, banked_data(1024, 8), NametableLayout::Vertical).expect("mapper 48");
mapper.write_prg(0x8000, 0);
mapper.write_prg(0x8001, 0);
assert_eq!(
mapper.read_prg(0xC000),
4,
"$C000 must be fixed to second-last bank"
);
assert_eq!(mapper.read_prg(0xDFFF), 4);
assert_eq!(
mapper.read_prg(0xE000),
5,
"$E000 must be fixed to last bank"
);
assert_eq!(mapper.read_prg(0xFFFF), 5);
}
#[test]
fn chr_2kb_bank0_at_ppu_0000() {
let chr = banked_data(2 * 1024, 3);
let mut mapper = create_tc0690(banked_data(8 * 1024, 2), chr, NametableLayout::Vertical)
.expect("mapper 48");
mapper.write_prg(0x8002, 1);
assert_eq!(mapper.read_chr(0x0000), 1);
assert_eq!(mapper.read_chr(0x07FF), 1);
}
#[test]
fn chr_2kb_bank1_at_ppu_0800() {
let chr = banked_data(2 * 1024, 3);
let mut mapper = create_tc0690(banked_data(8 * 1024, 2), chr, NametableLayout::Vertical)
.expect("mapper 48");
mapper.write_prg(0x8003, 2);
assert_eq!(mapper.read_chr(0x0800), 2);
assert_eq!(mapper.read_chr(0x0FFF), 2);
}
#[test]
fn chr_1kb_bank2_at_ppu_1000() {
let chr = banked_data(1024, 48);
let mut mapper = create_tc0690(banked_data(8 * 1024, 2), chr, NametableLayout::Vertical)
.expect("mapper 48");
mapper.write_prg(0xA000, 10);
assert_eq!(mapper.read_chr(0x1000), 10);
assert_eq!(mapper.read_chr(0x13FF), 10);
}
#[test]
fn chr_1kb_bank3_at_ppu_1400() {
let chr = banked_data(1024, 48);
let mut mapper = create_tc0690(banked_data(8 * 1024, 2), chr, NametableLayout::Vertical)
.expect("mapper 48");
mapper.write_prg(0xA001, 20);
assert_eq!(mapper.read_chr(0x1400), 20);
assert_eq!(mapper.read_chr(0x17FF), 20);
}
#[test]
fn chr_1kb_bank4_at_ppu_1800() {
let chr = banked_data(1024, 48);
let mut mapper = create_tc0690(banked_data(8 * 1024, 2), chr, NametableLayout::Vertical)
.expect("mapper 48");
mapper.write_prg(0xA002, 30);
assert_eq!(mapper.read_chr(0x1800), 30);
assert_eq!(mapper.read_chr(0x1BFF), 30);
}
#[test]
fn chr_1kb_bank5_at_ppu_1c00() {
let chr = banked_data(1024, 48);
let mut mapper = create_tc0690(banked_data(8 * 1024, 2), chr, NametableLayout::Vertical)
.expect("mapper 48");
mapper.write_prg(0xA003, 40);
assert_eq!(mapper.read_chr(0x1C00), 40);
assert_eq!(mapper.read_chr(0x1FFF), 40);
}
#[test]
fn mirroring_controlled_by_e000_bit_6() {
let mut mapper = create_tc0690(
banked_data(8 * 1024, 2),
banked_data(1024, 8),
NametableLayout::Vertical,
)
.expect("mapper 48");
mapper.write_prg(0xE000, 0x00);
assert_eq!(mapper.get_mirroring(), NametableLayout::Vertical);
mapper.write_prg(0xE000, 0x40);
assert_eq!(mapper.get_mirroring(), NametableLayout::Horizontal);
}
#[test]
fn irq_latch_xored_with_ff() {
let prg = banked_data(8 * 1024, 2);
let chr = banked_data(1024, 8);
let mut mapper = create_tc0690(prg, chr, NametableLayout::Vertical).expect("mapper 48");
mapper.write_prg(0xC000, 5); mapper.write_prg(0xC001, 0); mapper.write_prg(0xC002, 0);
assert!(
!mapper.irq_pending(),
"IRQ must not be pending before enough scanlines"
);
for _ in 0..0xFA_u8 {
trigger_scanline(&mut mapper);
}
assert!(
!mapper.irq_pending(),
"IRQ must not yet fire after 0xFA clocks"
);
trigger_scanline(&mut mapper);
assert!(
mapper.irq_pending(),
"IRQ must fire after 0xFB clocks (latch 0xFA = 5 XOR 0xFF)"
);
}
#[test]
fn irq_fires_after_n_scanlines() {
let prg = banked_data(8 * 1024, 2);
let chr = banked_data(1024, 8);
let mut mapper = create_tc0690(prg, chr, NametableLayout::Vertical).expect("mapper 48");
mapper.write_prg(0xC000, 0xFF); mapper.write_prg(0xC001, 0); mapper.write_prg(0xC002, 0);
trigger_scanline(&mut mapper);
assert!(
mapper.irq_pending(),
"IRQ must fire after 1 scanline when latch=0"
);
}
#[test]
fn irq_enable_c002_disable_c003() {
let prg = banked_data(8 * 1024, 2);
let chr = banked_data(1024, 8);
let mut mapper = create_tc0690(prg, chr, NametableLayout::Vertical).expect("mapper 48");
mapper.write_prg(0xC000, 0xFF);
mapper.write_prg(0xC001, 0);
mapper.write_prg(0xC002, 0);
trigger_scanline(&mut mapper);
assert!(
mapper.irq_pending(),
"IRQ must be pending after scanline with enabled IRQ"
);
mapper.write_prg(0xC003, 0);
assert!(
!mapper.irq_pending(),
"IRQ must be cleared after writing $C003"
);
trigger_scanline(&mut mapper);
assert!(!mapper.irq_pending(), "IRQ must not fire when disabled");
}
#[test]
fn registers_snapshot_round_trips() {
let prg = banked_data(8 * 1024, 6);
let chr = banked_data(1024, 48);
let mut mapper =
create_tc0690(prg.clone(), chr.clone(), NametableLayout::Vertical).expect("mapper 48");
mapper.write_prg(0x8000, 2);
mapper.write_prg(0x8001, 3);
mapper.write_prg(0x8002, 1);
mapper.write_prg(0x8003, 2);
mapper.write_prg(0xA000, 10);
mapper.write_prg(0xA001, 20);
mapper.write_prg(0xA002, 30);
mapper.write_prg(0xA003, 40);
mapper.write_prg(0xE000, 0x40); mapper.write_prg(0xC000, 0xFE); mapper.write_prg(0xC001, 0);
mapper.write_prg(0xC002, 0);
let snap = mapper.registers_snapshot();
let mut restored =
create_tc0690(prg, chr, NametableLayout::Vertical).expect("mapper 48 restore");
restored.restore_registers(&snap);
assert_eq!(restored.get_mirroring(), NametableLayout::Horizontal);
assert_eq!(restored.read_prg(0x8000), 2);
assert_eq!(restored.read_prg(0xA000), 3);
assert_eq!(restored.read_chr(0x1000), 10);
assert_eq!(restored.read_chr(0x1400), 20);
assert_eq!(restored.read_chr(0x1800), 30);
assert_eq!(restored.read_chr(0x1C00), 40);
assert!(restored.irq_pending() || !restored.irq_pending()); }
}