use crate::cartridge::BaseMapper;
use crate::cartridge::common::A12RisingEdgeDetector;
use crate::cartridge::{Mapper, MapperCapabilities};
pub struct Mapper91 {
base: BaseMapper,
prg_regs: [u8; 2],
chr_regs: [u8; 4],
irq_counter: u8,
irq_reload: bool,
irq_enabled: bool,
irq_asserted: bool,
a12: A12RisingEdgeDetector,
}
impl Mapper91 {
const PRG_BANK_SIZE: usize = 0x2000; const CHR_BANK_SIZE: usize = 0x0800; const IRQ_LATCH: u8 = 7;
pub fn new(ctx: super::mapper::MapperContext) -> Self {
let mirroring = ctx.mirroring;
let capabilities = MapperCapabilities {
has_irq: true,
has_chr_banking: true,
prg_bank_size_kb: 8,
chr_bank_size_kb: 2,
..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);
let mut mapper = Self {
base,
prg_regs: [0; 2],
chr_regs: [0; 4],
irq_counter: 0,
irq_reload: false,
irq_enabled: false,
irq_asserted: false,
a12: A12RisingEdgeDetector::new(3),
};
mapper.base.set_mirroring(mirroring);
mapper.update_banks();
mapper
}
fn update_banks(&mut self) {
self.base.select_prg_page(0, self.prg_regs[0] as i16);
self.base.select_prg_page(1, self.prg_regs[1] as i16);
self.base.select_prg_page(2, -2);
self.base.select_prg_page(3, -1);
self.base.select_chr_page(0, self.chr_regs[0] as i16);
self.base.select_chr_page(1, self.chr_regs[1] as i16);
self.base.select_chr_page(2, self.chr_regs[2] as i16);
self.base.select_chr_page(3, self.chr_regs[3] as i16);
}
fn clock_irq(&mut self) {
let old_counter = self.irq_counter;
let was_reload = self.irq_reload;
if self.irq_counter == 0 || self.irq_reload {
self.irq_counter = Self::IRQ_LATCH;
self.irq_reload = false;
} else {
self.irq_counter = self.irq_counter.wrapping_sub(1);
}
if self.irq_counter == 0 && self.irq_enabled {
let decremented = old_counter == 1;
let reload_triggered = was_reload;
if decremented || reload_triggered {
self.irq_asserted = true;
}
}
}
}
impl Mapper for Mapper91 {
fn base(&self) -> &BaseMapper {
&self.base
}
fn base_mut(&mut self) -> &mut BaseMapper {
&mut self.base
}
fn mapper_number(&self) -> u16 {
91
}
fn capabilities(&self) -> MapperCapabilities {
MapperCapabilities {
has_irq: true,
has_chr_banking: true,
prg_bank_size_kb: 8,
chr_bank_size_kb: 2,
..Default::default()
}
}
fn reset(&mut self) {
self.prg_regs = [0; 2];
self.chr_regs = [0; 4];
self.irq_counter = 0;
self.irq_reload = false;
self.irq_enabled = false;
self.irq_asserted = false;
self.a12 = A12RisingEdgeDetector::new(3);
self.update_banks();
}
fn write_prg(&mut self, addr: u16, value: u8) {
if !(0x6000..=0x7FFF).contains(&addr) {
return;
}
match addr & 0x7003 {
0x6000 => {
self.chr_regs[0] = value;
self.update_banks();
}
0x6001 => {
self.chr_regs[1] = value;
self.update_banks();
}
0x6002 => {
self.chr_regs[2] = value;
self.update_banks();
}
0x6003 => {
self.chr_regs[3] = value;
self.update_banks();
}
0x7000 => {
self.prg_regs[0] = value & 0x0F;
self.update_banks();
}
0x7001 => {
self.prg_regs[1] = value & 0x0F;
self.update_banks();
}
0x7002 => {
self.irq_enabled = false;
self.irq_asserted = false;
}
0x7003 => {
self.irq_counter = 0;
self.irq_reload = true;
self.irq_enabled = true;
}
_ => {}
}
}
fn ppu_address_changed(&mut self, addr: u16) {
if self.a12.update(addr) {
self.clock_irq();
}
}
fn cpu_cycle(&mut self) {
self.a12.cpu_tick();
}
fn irq_pending(&self) -> bool {
self.irq_asserted
}
fn registers_snapshot(&self) -> Vec<u8> {
let mut snap = Vec::with_capacity(11);
snap.extend_from_slice(&self.prg_regs);
snap.extend_from_slice(&self.chr_regs);
snap.push(self.irq_counter);
let flags = (self.irq_reload as u8)
| ((self.irq_enabled as u8) << 1)
| ((self.irq_asserted as u8) << 2);
snap.push(flags);
snap.push(self.a12.prev_a12() as u8);
snap.push(self.a12.current_a12() as u8);
snap.push(self.a12.a12_low_cycles());
snap
}
fn restore_registers(&mut self, data: &[u8]) {
if data.len() >= 8 {
self.prg_regs.copy_from_slice(&data[0..2]);
self.chr_regs.copy_from_slice(&data[2..6]);
self.irq_counter = data[6];
let flags = data[7];
self.irq_reload = (flags & 1) != 0;
self.irq_enabled = (flags & 2) != 0;
self.irq_asserted = (flags & 4) != 0;
self.update_banks();
}
if data.len() >= 11 {
self.a12.set_prev_a12(data[8] != 0);
self.a12.set_current_a12(data[9] != 0);
self.a12.set_a12_low_cycles(data[10]);
}
}
}
#[cfg(test)]
mod tests {
use crate::cartridge::NametableLayout;
use crate::cartridge::mapper::{Mapper, MapperContext, create_mapper};
use crate::cartridge::test_helpers::banked_data;
const PRG_BANKS: usize = 6; const CHR_2K_BANKS: usize = 12;
fn make_mapper() -> Box<dyn Mapper> {
let prg = banked_data(8 * 1024, PRG_BANKS);
let chr = banked_data(2 * 1024, CHR_2K_BANKS);
create_mapper(MapperContext::new_for_test(
91,
prg,
chr,
NametableLayout::Vertical,
))
.expect("Mapper 91 should be registered")
}
#[test]
fn mapper91_is_registered_in_factory() {
let result = create_mapper(MapperContext::new_for_test(
91,
banked_data(8 * 1024, PRG_BANKS),
banked_data(2 * 1024, CHR_2K_BANKS),
NametableLayout::Vertical,
));
assert!(
result.is_ok(),
"Mapper 91 must be registered in the factory"
);
}
#[test]
fn prg_slots_2_and_3_are_fixed_to_last_two_banks() {
let mapper = make_mapper();
assert_eq!(
mapper.read_prg(0xC000),
4,
"$C000 must be fixed to second-last PRG bank"
);
assert_eq!(
mapper.read_prg(0xE000),
5,
"$E000 must be fixed to last PRG bank"
);
}
#[test]
fn prg_slot0_switchable_via_7000() {
let mut mapper = make_mapper();
mapper.write_prg(0x7000, 2);
assert_eq!(
mapper.read_prg(0x8000),
2,
"$8000 must map to PRG bank 2 after $7000 write"
);
}
#[test]
fn prg_slot1_switchable_via_7001() {
let mut mapper = make_mapper();
mapper.write_prg(0x7001, 3);
assert_eq!(
mapper.read_prg(0xA000),
3,
"$A000 must map to PRG bank 3 after $7001 write"
);
}
#[test]
fn prg_slot0_masks_to_4_bits() {
let mut mapper = make_mapper();
mapper.write_prg(0x7000, 0xFF);
assert_eq!(
mapper.read_prg(0x8000),
3,
"$7000 must mask to low 4 bits (0xFF & 0x0F = 15 → bank 15 % 6 = 3)"
);
}
#[test]
fn prg_fixed_banks_not_affected_by_slot0_switch() {
let mut mapper = make_mapper();
mapper.write_prg(0x7000, 1);
assert_eq!(mapper.read_prg(0xC000), 4, "$C000 must stay at second-last");
assert_eq!(mapper.read_prg(0xE000), 5, "$E000 must stay at last");
}
#[test]
fn chr_slot0_switchable_via_6000() {
let mut mapper = make_mapper();
mapper.write_prg(0x6000, 3);
assert_eq!(
mapper.read_chr(0x0000),
3,
"PPU $0000 must be CHR bank 3 after $6000=3"
);
}
#[test]
fn chr_slot1_switchable_via_6001() {
let mut mapper = make_mapper();
mapper.write_prg(0x6001, 5);
assert_eq!(
mapper.read_chr(0x0800),
5,
"PPU $0800 must be CHR bank 5 after $6001=5"
);
}
#[test]
fn chr_slot2_switchable_via_6002() {
let mut mapper = make_mapper();
mapper.write_prg(0x6002, 7);
assert_eq!(
mapper.read_chr(0x1000),
7,
"PPU $1000 must be CHR bank 7 after $6002=7"
);
}
#[test]
fn chr_slot3_switchable_via_6003() {
let mut mapper = make_mapper();
mapper.write_prg(0x6003, 9);
assert_eq!(
mapper.read_chr(0x1800),
9,
"PPU $1800 must be CHR bank 9 after $6003=9"
);
}
#[test]
fn chr_slots_are_independent() {
let mut mapper = make_mapper();
mapper.write_prg(0x6000, 2);
mapper.write_prg(0x6001, 4);
mapper.write_prg(0x6002, 6);
mapper.write_prg(0x6003, 8);
assert_eq!(mapper.read_chr(0x0000), 2);
assert_eq!(mapper.read_chr(0x0800), 4);
assert_eq!(mapper.read_chr(0x1000), 6);
assert_eq!(mapper.read_chr(0x1800), 8);
}
#[test]
fn chr_register_decoding_uses_addr_mask_0x7003() {
let mut mapper = make_mapper();
mapper.write_prg(0x6100, 3); assert_eq!(
mapper.read_chr(0x0000),
3,
"$6100 write must be decoded same as $6000 (addr & 0x7003)"
);
}
#[test]
fn mirroring_is_fixed_from_header() {
let prg = banked_data(8 * 1024, PRG_BANKS);
let chr = banked_data(2 * 1024, CHR_2K_BANKS);
let mapper = create_mapper(MapperContext::new_for_test(
91,
prg,
chr,
NametableLayout::Horizontal,
))
.expect("mapper 91 must be registered");
assert_eq!(
mapper.get_mirroring(),
NametableLayout::Horizontal,
"Mirroring must come from header"
);
}
#[test]
fn mirroring_cannot_be_changed_by_register_writes() {
let mut mapper = make_mapper(); mapper.write_prg(0x6000, 0);
mapper.write_prg(0x7000, 0);
mapper.write_prg(0x7002, 0);
mapper.write_prg(0x7003, 0);
assert_eq!(
mapper.get_mirroring(),
NametableLayout::Vertical,
"Mirroring must remain vertical"
);
}
#[test]
fn irq_not_pending_on_power_on() {
let mapper = make_mapper();
assert!(!mapper.irq_pending(), "IRQ must not be pending on power-on");
}
#[test]
fn irq_disabled_and_acked_by_7002_write() {
let mut mapper = make_mapper();
mapper.write_prg(0x7003, 0);
mapper.write_prg(0x7002, 0);
assert!(
!mapper.irq_pending(),
"IRQ must not be pending after $7002 write"
);
for _ in 0..10 {
mapper.ppu_address_changed(0x0FFF);
for _ in 0..3 {
mapper.cpu_cycle();
}
mapper.ppu_address_changed(0x1000);
}
assert!(!mapper.irq_pending(), "IRQ must not fire when disabled");
}
#[test]
fn irq_fires_after_eight_a12_rising_edges() {
let mut mapper = make_mapper();
mapper.write_prg(0x7003, 0);
let trigger_edge = |m: &mut Box<dyn Mapper>| {
m.ppu_address_changed(0x0000); for _ in 0..3 {
m.cpu_cycle();
}
m.ppu_address_changed(0x1000); };
trigger_edge(&mut mapper);
assert!(!mapper.irq_pending(), "No IRQ after edge 1 (reloaded to 7)");
for i in 2..=8 {
trigger_edge(&mut mapper);
if i < 8 {
assert!(!mapper.irq_pending(), "No IRQ after edge {i}");
}
}
assert!(
mapper.irq_pending(),
"IRQ must fire after 8 A12 rising edges"
);
}
#[test]
fn irq_can_be_acknowledged_after_firing() {
let mut mapper = make_mapper();
mapper.write_prg(0x7003, 0);
let trigger_edge = |m: &mut Box<dyn Mapper>| {
m.ppu_address_changed(0x0000);
for _ in 0..3 {
m.cpu_cycle();
}
m.ppu_address_changed(0x1000);
};
for _ in 0..9 {
trigger_edge(&mut mapper);
}
assert!(mapper.irq_pending(), "IRQ must be pending before ack");
mapper.write_prg(0x7002, 0); assert!(!mapper.irq_pending(), "IRQ must be cleared after $7002 ack");
}
#[test]
fn registers_snapshot_round_trip() {
let mut mapper = make_mapper();
mapper.write_prg(0x7000, 1); mapper.write_prg(0x7001, 2); mapper.write_prg(0x6000, 3); mapper.write_prg(0x6001, 4); mapper.write_prg(0x6002, 5); mapper.write_prg(0x6003, 6);
let snap = mapper.registers_snapshot();
let mut mapper2 = make_mapper();
mapper2.restore_registers(&snap);
assert_eq!(
mapper2.read_prg(0x8000),
1,
"PRG slot 0 must survive snapshot"
);
assert_eq!(
mapper2.read_prg(0xA000),
2,
"PRG slot 1 must survive snapshot"
);
assert_eq!(
mapper2.read_chr(0x0000),
3,
"CHR slot 0 must survive snapshot"
);
assert_eq!(
mapper2.read_chr(0x0800),
4,
"CHR slot 1 must survive snapshot"
);
assert_eq!(
mapper2.read_chr(0x1000),
5,
"CHR slot 2 must survive snapshot"
);
assert_eq!(
mapper2.read_chr(0x1800),
6,
"CHR slot 3 must survive snapshot"
);
}
#[test]
fn a12_state_is_included_in_snapshot_round_trip() {
let mut mapper = make_mapper();
mapper.ppu_address_changed(0x1000);
mapper.cpu_cycle();
let snap = mapper.registers_snapshot();
assert!(snap.len() >= 11, "Snapshot must include A12 detector state");
let mut mapper2 = make_mapper();
mapper2.restore_registers(&snap);
let edge = {
mapper2.ppu_address_changed(0x1000);
mapper2.irq_pending()
};
assert!(
!edge,
"Restoring A12 state must not cause a spurious rising edge"
);
}
#[test]
fn reset_restores_power_on_state() {
let mut mapper = make_mapper();
mapper.write_prg(0x7000, 2);
mapper.write_prg(0x7001, 3);
mapper.write_prg(0x6000, 4);
mapper.write_prg(0x6001, 5);
mapper.write_prg(0x6002, 6);
mapper.write_prg(0x6003, 7);
mapper.write_prg(0x7003, 0);
mapper.reset();
assert_eq!(
mapper.read_prg(0x8000),
0,
"PRG slot 0 must reset to bank 0"
);
assert_eq!(
mapper.read_prg(0xA000),
0,
"PRG slot 1 must reset to bank 0"
);
assert_eq!(
mapper.read_prg(0xC000),
4,
"PRG slot 2 must stay fixed after reset"
);
assert_eq!(
mapper.read_prg(0xE000),
5,
"PRG slot 3 must stay fixed after reset"
);
assert_eq!(
mapper.read_chr(0x0000),
0,
"CHR slot 0 must reset to bank 0"
);
assert_eq!(
mapper.read_chr(0x0800),
0,
"CHR slot 1 must reset to bank 0"
);
assert_eq!(
mapper.read_chr(0x1000),
0,
"CHR slot 2 must reset to bank 0"
);
assert_eq!(
mapper.read_chr(0x1800),
0,
"CHR slot 3 must reset to bank 0"
);
assert!(!mapper.irq_pending(), "IRQ must not be pending after reset");
}
#[test]
fn reset_clears_irq_state_and_a12_detector() {
let mut mapper = make_mapper();
mapper.write_prg(0x7003, 0);
let trigger_a12_rising_edge = |m: &mut Box<dyn Mapper>| {
m.ppu_address_changed(0x0000);
for _ in 0..3 {
m.cpu_cycle();
}
m.ppu_address_changed(0x1000);
};
for _ in 0..9 {
trigger_a12_rising_edge(&mut mapper);
}
assert!(mapper.irq_pending(), "IRQ must be pending before reset");
mapper.reset();
assert!(!mapper.irq_pending(), "IRQ must be cleared by reset");
mapper.write_prg(0x7003, 0); trigger_a12_rising_edge(&mut mapper);
assert!(
!mapper.irq_pending(),
"After reset, first edge must reload counter (not fire immediately)"
);
}
#[test]
fn write_prg_ignores_addresses_outside_6000_7fff() {
let mut mapper = make_mapper();
mapper.write_prg(0xE000, 5);
assert_eq!(
mapper.read_chr(0x0000),
0,
"Write to $E000 must be ignored (outside $6000-$7FFF)"
);
mapper.write_prg(0x8000, 3);
assert_eq!(
mapper.read_prg(0x8000),
0,
"Write to $8000 must be ignored (outside $6000-$7FFF)"
);
}
}