use crate::nes::cartridge::base_mapper::BaseMapper;
use crate::nes::cartridge::{Mapper, MapperCapabilities};
pub struct Mapper298 {
base: BaseMapper,
prg_regs: [u8; 2],
swap_prg: bool,
chr_regs: [u8; 8],
irq_counter: u8,
irq_reload: u8,
irq_scaler: i16,
irq_enabled: bool,
irq_pending: bool,
}
impl Mapper298 {
const MAPPER_NUMBER: u16 = 298;
const PRG_BANK_SIZE: usize = 0x2000; const CHR_BANK_SIZE: usize = 0x0400;
pub fn new(ctx: crate::nes::cartridge::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: 0,
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_SIZE);
base.set_mirroring(mirroring);
let mut mapper = Self {
base,
prg_regs: [0; 2],
swap_prg: false,
chr_regs: [0; 8],
irq_counter: 0,
irq_reload: 0,
irq_scaler: 0,
irq_enabled: false,
irq_pending: false,
};
mapper.update_prg();
mapper.update_chr();
mapper
}
fn update_prg(&mut self) {
if self.swap_prg {
self.base.select_prg_page(0, -2);
self.base.select_prg_page(2, self.prg_regs[0] as i16);
} else {
self.base.select_prg_page(0, self.prg_regs[0] as i16);
self.base.select_prg_page(2, -2);
}
self.base.select_prg_page(1, self.prg_regs[1] as i16);
self.base.select_prg_page(3, -1);
}
fn update_chr(&mut self) {
for i in 0..8 {
self.base.select_chr_page(i, self.chr_regs[i] as i16);
}
}
fn write_chr_nibble(&mut self, addr: u16, value: u8) {
let effective = (addr & 0xF003) | ((addr & 0x0C) >> 2);
let slot = ((((effective >> 11).wrapping_sub(6)) | (effective & 0x01)) & 0x07) as usize;
let shift = (effective & 0x02) << 1; let mask: u8 = 0xF0 >> shift; self.chr_regs[slot] = (self.chr_regs[slot] & mask) | ((value & 0x0F) << shift);
self.base.select_chr_page(slot, self.chr_regs[slot] as i16);
}
}
impl Mapper for Mapper298 {
fn base(&self) -> &BaseMapper {
&self.base
}
fn base_mut(&mut self) -> &mut BaseMapper {
&mut self.base
}
fn mapper_number(&self) -> u16 {
Self::MAPPER_NUMBER
}
fn write_prg(&mut self, addr: u16, value: u8) {
if self.base.try_write_prg_ram(addr, value) {
return;
}
if (0xB000..=0xEFFF).contains(&addr) {
self.write_chr_nibble(addr, value);
return;
}
match addr & 0xF003 {
0x8000 => {
self.prg_regs[0] = value;
self.update_prg();
}
0x9000 => {
self.base.set_mirroring_hv(value & 0x01 != 0);
}
0x9001 => {
self.swap_prg = value != 0;
self.update_prg();
}
0xA000 => {
self.prg_regs[1] = value;
self.update_prg();
}
0xF000 => {
self.irq_reload = (self.irq_reload & 0xF0) | (value & 0x0F);
}
0xF002 => {
self.irq_reload = (self.irq_reload & 0x0F) | (value << 4);
}
0xF001 => {
self.irq_enabled = (value & 0x02) != 0;
if self.irq_enabled {
self.irq_scaler = 341;
self.irq_counter = self.irq_reload;
}
self.irq_pending = false;
}
0xF003 => {
self.irq_pending = false;
}
_ => {}
}
}
fn cpu_cycle(&mut self) {
if !self.irq_enabled {
return;
}
self.irq_scaler -= 3;
if self.irq_scaler <= 0 {
self.irq_scaler += 341;
let (new_counter, overflow) = self.irq_counter.overflowing_add(1);
self.irq_counter = new_counter;
if overflow {
self.irq_pending = true;
}
}
}
fn irq_pending(&self) -> bool {
self.irq_pending
}
fn reset(&mut self) {
self.prg_regs = [0; 2];
self.swap_prg = false;
self.chr_regs = [0; 8];
self.irq_counter = 0;
self.irq_reload = 0;
self.irq_scaler = 0;
self.irq_enabled = false;
self.irq_pending = false;
self.update_prg();
self.update_chr();
}
fn capabilities(&self) -> MapperCapabilities {
MapperCapabilities {
has_irq: true,
has_chr_banking: true,
has_dynamic_mirroring: true,
max_prg_ram_kb: 0,
prg_bank_size_kb: 8,
chr_bank_size_kb: 1,
..Default::default()
}
}
fn registers_snapshot(&self) -> Vec<u8> {
let mut snap = self.base.banking_snapshot();
snap.push(self.prg_regs[0]);
snap.push(self.prg_regs[1]);
snap.push(self.swap_prg as u8);
snap.extend_from_slice(&self.chr_regs);
snap.push(self.irq_counter);
snap.push(self.irq_reload);
snap.push((self.irq_scaler & 0xFF) as u8);
snap.push(((self.irq_scaler >> 8) & 0xFF) as u8);
snap.push(self.irq_enabled as u8);
snap.push(self.irq_pending as u8);
snap
}
fn restore_registers(&mut self, data: &[u8]) {
let banking_len = self.base.banking_snapshot().len();
const EXTRA: usize = 17;
if data.len() < banking_len + EXTRA {
let safe = data.len().min(banking_len);
self.base.restore_banking(&data[..safe]);
return;
}
self.base.restore_banking(&data[..banking_len]);
let e = &data[banking_len..];
self.prg_regs[0] = e[0];
self.prg_regs[1] = e[1];
self.swap_prg = e[2] != 0;
self.chr_regs.copy_from_slice(&e[3..11]);
self.irq_counter = e[11];
self.irq_reload = e[12];
self.irq_scaler = (e[13] as i16) | ((e[14] as i16) << 8);
self.irq_enabled = e[15] != 0;
self.irq_pending = e[16] != 0;
self.update_prg();
self.update_chr();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nes::cartridge::NametableLayout;
use crate::nes::cartridge::mapper::{MapperContext, create_mapper};
use crate::nes::cartridge::test_helpers::banked_data;
const PRG_BANK_SIZE: usize = 8 * 1024;
const CHR_BANK_SIZE: usize = 1024;
fn make_mapper(prg_banks: usize, chr_banks: usize) -> Mapper298 {
Mapper298::new(
MapperContext::new_for_test(
Mapper298::MAPPER_NUMBER,
banked_data(PRG_BANK_SIZE, prg_banks),
banked_data(CHR_BANK_SIZE, chr_banks),
NametableLayout::Vertical,
)
.with_prg_ram_banks(0),
)
}
#[test]
fn mapper_298_is_registered() {
let result = create_mapper(
MapperContext::new_for_test(
Mapper298::MAPPER_NUMBER,
banked_data(PRG_BANK_SIZE, 8),
banked_data(CHR_BANK_SIZE, 8),
NametableLayout::Vertical,
)
.with_prg_ram_banks(0),
);
assert!(result.is_ok(), "Mapper 298 must be registered in factory");
}
#[test]
fn power_on_slot0_is_bank0() {
let mapper = make_mapper(8, 8);
assert_eq!(
mapper.read_prg(0x8000),
0,
"slot $8000 = bank 0 at power-on"
);
}
#[test]
fn power_on_slot1_is_bank0() {
let mapper = make_mapper(8, 8);
assert_eq!(
mapper.read_prg(0xA000),
0,
"slot $A000 = bank 0 at power-on"
);
}
#[test]
fn power_on_slot2_fixed_second_last() {
let mapper = make_mapper(8, 8);
assert_eq!(
mapper.read_prg(0xC000),
6,
"slot $C000 fixed to second-last bank"
);
}
#[test]
fn power_on_slot3_fixed_last() {
let mapper = make_mapper(8, 8);
assert_eq!(mapper.read_prg(0xE000), 7, "slot $E000 fixed to last bank");
}
#[test]
fn write_8000_sets_prg_slot0() {
let mut mapper = make_mapper(8, 8);
mapper.write_prg(0x8000, 3);
assert_eq!(mapper.read_prg(0x8000), 3, "$8000 = PRG bank 3");
}
#[test]
fn write_a000_sets_prg_slot1() {
let mut mapper = make_mapper(8, 8);
mapper.write_prg(0xA000, 5);
assert_eq!(mapper.read_prg(0xA000), 5, "$A000 = PRG bank 5");
}
#[test]
fn prg_slot3_stays_fixed_last_after_register_writes() {
let mut mapper = make_mapper(8, 8);
mapper.write_prg(0x8000, 2);
mapper.write_prg(0xA000, 3);
assert_eq!(mapper.read_prg(0xE000), 7, "$E000 stays last bank");
}
#[test]
fn swap_prg_nonzero_swaps_slots_0_and_2() {
let mut mapper = make_mapper(8, 8);
mapper.write_prg(0x8000, 2); mapper.write_prg(0x9001, 0x01); assert_eq!(
mapper.read_prg(0x8000),
6,
"slot $8000 = second-last (6) in swap mode"
);
assert_eq!(
mapper.read_prg(0xC000),
2,
"slot $C000 = prg_regs[0] in swap mode"
);
}
#[test]
fn swap_prg_zero_restores_normal_layout() {
let mut mapper = make_mapper(8, 8);
mapper.write_prg(0x8000, 2);
mapper.write_prg(0x9001, 0x01); mapper.write_prg(0x9001, 0x00); assert_eq!(
mapper.read_prg(0x8000),
2,
"slot $8000 = prg_regs[0] after swap off"
);
assert_eq!(
mapper.read_prg(0xC000),
6,
"slot $C000 = second-last after swap off"
);
}
#[test]
fn swap_prg_slot1_unchanged_by_swap() {
let mut mapper = make_mapper(8, 8);
mapper.write_prg(0xA000, 4);
mapper.write_prg(0x9001, 0x01);
assert_eq!(mapper.read_prg(0xA000), 4, "slot $A000 unchanged by swap");
}
fn write_chr_bank(mapper: &mut Mapper298, slot: usize, bank: u8) {
let group_base: u16 = 0xB000 + ((slot as u16 / 2) * 0x1000);
let pair_offset: u16 = slot as u16 & 0x01; mapper.write_prg(group_base + pair_offset, bank & 0x0F);
mapper.write_prg(group_base + pair_offset + 2, (bank >> 4) & 0x0F);
}
#[test]
fn chr_slot0_write_and_read() {
let chr_banks = 16_usize;
let mut mapper = make_mapper(4, chr_banks);
write_chr_bank(&mut mapper, 0, 5);
assert_eq!(mapper.read_chr(0x0000), 5, "CHR slot 0 = bank 5");
}
#[test]
fn chr_slot1_write_and_read() {
let chr_banks = 16_usize;
let mut mapper = make_mapper(4, chr_banks);
write_chr_bank(&mut mapper, 1, 3);
assert_eq!(mapper.read_chr(0x0400), 3, "CHR slot 1 = bank 3");
}
#[test]
fn chr_slot7_write_and_read() {
let chr_banks = 16_usize;
let mut mapper = make_mapper(4, chr_banks);
write_chr_bank(&mut mapper, 7, 9);
assert_eq!(
mapper.read_chr(0x1C00),
9 % chr_banks as u8,
"CHR slot 7 = bank 9"
);
}
#[test]
fn chr_all_slots_independent() {
let chr_banks = 16_usize;
let mut mapper = make_mapper(4, chr_banks);
for slot in 0..8 {
write_chr_bank(&mut mapper, slot, slot as u8 + 1);
}
for slot in 0..8 {
let ppu_addr = (slot as u16) * 0x0400;
assert_eq!(
mapper.read_chr(ppu_addr),
(slot as u8 + 1) % chr_banks as u8,
"CHR slot {slot} should be bank {}",
slot + 1
);
}
}
#[test]
fn chr_low_nibble_write_preserves_high() {
let chr_banks = 16_usize;
let mut mapper = make_mapper(4, chr_banks);
write_chr_bank(&mut mapper, 0, 0xAB);
mapper.write_prg(0xB000, 0x05);
assert_eq!(
mapper.chr_regs[0], 0xA5,
"Low nibble overwrite preserves high nibble"
);
}
#[test]
fn chr_high_nibble_write_preserves_low() {
let chr_banks = 16_usize;
let mut mapper = make_mapper(4, chr_banks);
write_chr_bank(&mut mapper, 0, 0xAB);
mapper.write_prg(0xB002, 0x05);
assert_eq!(
mapper.chr_regs[0], 0x5B,
"High nibble overwrite preserves low nibble"
);
}
#[test]
fn mirroring_bit0_one_sets_horizontal() {
let mut mapper = make_mapper(4, 8);
mapper.write_prg(0x9000, 0x01);
assert_eq!(
mapper.get_mirroring(),
NametableLayout::Horizontal,
"$9000 bit 0 = 1 → horizontal"
);
}
#[test]
fn mirroring_bit0_zero_sets_vertical() {
let mut mapper = make_mapper(4, 8);
mapper.write_prg(0x9000, 0x01); mapper.write_prg(0x9000, 0x00);
assert_eq!(
mapper.get_mirroring(),
NametableLayout::Vertical,
"$9000 bit 0 = 0 → vertical"
);
}
#[test]
fn f000_sets_irq_reload_low_nibble() {
let mut mapper = make_mapper(4, 8);
mapper.write_prg(0xF000, 0x0A);
assert_eq!(
mapper.irq_reload & 0x0F,
0x0A,
"$F000 sets IRQ reload low nibble"
);
}
#[test]
fn f002_sets_irq_reload_high_nibble() {
let mut mapper = make_mapper(4, 8);
mapper.write_prg(0xF002, 0x05);
assert_eq!(
mapper.irq_reload & 0xF0,
0x50,
"$F002 sets IRQ reload high nibble"
);
}
#[test]
fn f000_f002_combine_full_reload_byte() {
let mut mapper = make_mapper(4, 8);
mapper.write_prg(0xF000, 0x0B); mapper.write_prg(0xF002, 0x0A); assert_eq!(mapper.irq_reload, 0xAB, "Full reload byte = 0xAB");
}
#[test]
fn irq_not_pending_at_power_on() {
let mapper = make_mapper(4, 8);
assert!(!mapper.irq_pending(), "IRQ not pending at power-on");
}
#[test]
fn f001_with_bit1_enables_irq_and_loads_counter() {
let mut mapper = make_mapper(4, 8);
mapper.write_prg(0xF000, 0x0A); mapper.write_prg(0xF002, 0x0B); mapper.write_prg(0xF001, 0x02); assert!(
mapper.irq_enabled,
"IRQ should be enabled after $F001 write with bit 1"
);
assert_eq!(mapper.irq_counter, 0xBA, "Counter loaded from reload");
assert!(
!mapper.irq_pending(),
"IRQ not immediately pending on enable"
);
}
#[test]
fn f001_without_bit1_disables_irq() {
let mut mapper = make_mapper(4, 8);
mapper.write_prg(0xF001, 0x02); mapper.write_prg(0xF001, 0x00); assert!(
!mapper.irq_enabled,
"IRQ should be disabled after $F001 write without bit 1"
);
}
#[test]
fn f003_acknowledges_irq() {
let mut mapper = make_mapper(4, 8);
mapper.irq_pending = true;
mapper.write_prg(0xF003, 0x00);
assert!(!mapper.irq_pending(), "IRQ cleared after $F003 write");
}
#[test]
fn f001_clears_pending_irq() {
let mut mapper = make_mapper(4, 8);
mapper.irq_pending = true;
mapper.write_prg(0xF001, 0x02);
assert!(!mapper.irq_pending(), "$F001 must clear pending IRQ");
}
#[test]
fn cpu_cycle_does_nothing_when_irq_disabled() {
let mut mapper = make_mapper(4, 8);
for _ in 0..500 {
mapper.cpu_cycle();
}
assert!(!mapper.irq_pending(), "No IRQ when disabled");
assert_eq!(mapper.irq_scaler, 0, "Scaler stays 0 when disabled");
}
#[test]
fn irq_fires_after_counter_overflows_from_255() {
let mut mapper = make_mapper(4, 8);
mapper.write_prg(0xF000, 0x0F); mapper.write_prg(0xF002, 0x0F); mapper.write_prg(0xF001, 0x02); assert_eq!(mapper.irq_counter, 0xFF);
for _ in 0..115 {
mapper.cpu_cycle();
}
assert!(
mapper.irq_pending(),
"IRQ fires when counter overflows from 0xFF to 0"
);
}
#[test]
fn irq_does_not_fire_before_counter_overflow() {
let mut mapper = make_mapper(4, 8);
mapper.write_prg(0xF000, 0x0E);
mapper.write_prg(0xF002, 0x0F); mapper.write_prg(0xF001, 0x02);
for _ in 0..115 {
mapper.cpu_cycle();
}
assert!(
!mapper.irq_pending(),
"IRQ must not fire on first counter tick from 0xFE"
);
}
#[test]
fn capabilities_match_spec() {
let mapper = make_mapper(4, 8);
let caps = mapper.capabilities();
assert!(caps.has_irq, "must have IRQ");
assert!(caps.has_chr_banking, "must have CHR banking");
assert!(caps.has_dynamic_mirroring, "must have dynamic mirroring");
assert!(!caps.has_expansion_audio, "no expansion audio");
assert_eq!(caps.max_prg_ram_kb, 0, "no PRG-RAM");
}
#[test]
fn reset_restores_power_on_prg_layout() {
let mut mapper = make_mapper(8, 8);
mapper.write_prg(0x8000, 3);
mapper.write_prg(0xA000, 5);
mapper.write_prg(0x9001, 0x01);
mapper.reset();
assert_eq!(
mapper.read_prg(0x8000),
0,
"slot $8000 = bank 0 after reset"
);
assert_eq!(
mapper.read_prg(0xA000),
0,
"slot $A000 = bank 0 after reset"
);
assert_eq!(
mapper.read_prg(0xC000),
6,
"slot $C000 = second-last after reset"
);
assert_eq!(mapper.read_prg(0xE000), 7, "slot $E000 = last after reset");
assert!(!mapper.swap_prg, "swap_prg cleared after reset");
}
#[test]
fn reset_clears_irq_state() {
let mut mapper = make_mapper(4, 8);
mapper.write_prg(0xF001, 0x02);
mapper.irq_pending = true;
mapper.reset();
assert!(!mapper.irq_enabled, "irq_enabled cleared after reset");
assert!(!mapper.irq_pending(), "irq_pending cleared after reset");
assert_eq!(mapper.irq_counter, 0, "irq_counter cleared after reset");
}
#[test]
fn registers_snapshot_round_trips() {
let mut mapper = make_mapper(8, 16);
mapper.write_prg(0x8000, 3);
mapper.write_prg(0xA000, 5);
mapper.write_prg(0x9001, 0x01);
write_chr_bank(&mut mapper, 0, 0xAB);
write_chr_bank(&mut mapper, 7, 0x05);
mapper.write_prg(0xF000, 0x09);
mapper.write_prg(0xF002, 0x06); mapper.write_prg(0xF001, 0x02);
let snap = mapper.registers_snapshot();
let mut restored = make_mapper(8, 16);
restored.restore_registers(&snap);
assert_eq!(
restored.prg_regs, mapper.prg_regs,
"prg_regs survive round-trip"
);
assert_eq!(
restored.swap_prg, mapper.swap_prg,
"swap_prg survives round-trip"
);
assert_eq!(
restored.chr_regs, mapper.chr_regs,
"chr_regs survive round-trip"
);
assert_eq!(
restored.irq_reload, mapper.irq_reload,
"irq_reload survives round-trip"
);
assert_eq!(
restored.irq_enabled, mapper.irq_enabled,
"irq_enabled survives round-trip"
);
assert_eq!(
restored.read_prg(0x8000),
6,
"PRG slot 0 correct in restored"
);
assert_eq!(
restored.read_prg(0xC000),
3,
"PRG slot 2 (swap) correct in restored"
);
}
#[test]
fn restore_with_truncated_snapshot_does_not_panic() {
let mut mapper = make_mapper(4, 8);
mapper.restore_registers(&[0u8; 4]);
}
#[test]
fn chr_ram_writable_when_no_chr_rom() {
let mut mapper = Mapper298::new(
MapperContext::new_for_test(
Mapper298::MAPPER_NUMBER,
banked_data(PRG_BANK_SIZE, 4),
vec![],
NametableLayout::Vertical,
)
.with_prg_ram_banks(0),
);
mapper.write_chr(0x0100, 0xAB);
assert_eq!(
mapper.read_chr(0x0100),
0xAB,
"CHR-RAM must be writable when no CHR-ROM"
);
}
}