use crate::nes::cartridge::base_mapper::BaseMapper;
use crate::nes::cartridge::mmc3::MMC3Mapper;
use crate::nes::cartridge::{Mapper, MapperCapabilities, NametableLayout};
const CHR_RAM_SIZE: usize = 8 * 1024;
const PRG_BANK_MASK: usize = 0x1FFF; const CHR_1KB_BANK_MASK: usize = 0x03FF;
pub struct Mapper296 {
mmc3: MMC3Mapper,
config_reg: u8,
outer_reg: u8,
outer_reg2: u8,
chr_ram: Vec<u8>,
unrom_bank: u8,
cnrom_bank: u8,
mmc1_shift: u8,
mmc1_count: u8,
mmc1_control: u8,
mmc1_chr0: u8,
mmc1_chr1: u8,
mmc1_prg: u8,
}
impl Mapper296 {
const MAPPER_NUMBER: u16 = 296;
const MODE_MASK: u8 = 0x03;
const CHR_RAM_BIT: u8 = 0x04;
const PRG_A25_BIT: u8 = 0x01;
const CHR_A25_BIT: u8 = 0x02;
const PRG_A26_BIT: u8 = 0x04;
const CHR_A26_BIT: u8 = 0x08;
const A27_BIT: u8 = 0x01;
const MMC1_PRG_MODE_MASK: u8 = 0x0C; const MMC1_CHR_MODE_BIT: u8 = 0x10;
pub fn new(ctx: crate::nes::cartridge::mapper::MapperContext) -> Self {
let mmc3 = MMC3Mapper::new_with_irq_mode_and_prg_ram_banks(
ctx.prg_rom,
ctx.chr_rom,
ctx.mirroring,
false,
0,
);
Self {
mmc3,
config_reg: 0,
outer_reg: 0,
outer_reg2: 0,
chr_ram: vec![0u8; CHR_RAM_SIZE],
unrom_bank: 0,
cnrom_bank: 0,
mmc1_shift: 0,
mmc1_count: 0,
mmc1_control: 0x0C, mmc1_chr0: 0,
mmc1_chr1: 0,
mmc1_prg: 0,
}
}
fn mapper_mode(&self) -> u8 {
self.config_reg & Self::MODE_MASK
}
fn chr_ram_enabled(&self) -> bool {
(self.config_reg & Self::CHR_RAM_BIT) != 0
}
fn prg_8kb_outer_offset(&self) -> usize {
let a25 = ((self.outer_reg & Self::PRG_A25_BIT) != 0) as usize;
let a26 = ((self.outer_reg & Self::PRG_A26_BIT) != 0) as usize;
let a27 = ((self.outer_reg2 & Self::A27_BIT) != 0) as usize;
(a27 << 14) | (a26 << 13) | (a25 << 12)
}
fn chr_1kb_outer_offset(&self) -> usize {
let a25 = ((self.outer_reg & Self::CHR_A25_BIT) != 0) as usize;
let a26 = ((self.outer_reg & Self::CHR_A26_BIT) != 0) as usize;
let a27 = ((self.outer_reg2 & Self::A27_BIT) != 0) as usize;
(a27 << 17) | (a26 << 16) | (a25 << 15)
}
fn chr_8kb_outer_offset(&self) -> usize {
let a25 = ((self.outer_reg & Self::CHR_A25_BIT) != 0) as usize;
let a26 = ((self.outer_reg & Self::CHR_A26_BIT) != 0) as usize;
let a27 = ((self.outer_reg2 & Self::A27_BIT) != 0) as usize;
(a27 << 14) | (a26 << 13) | (a25 << 12)
}
fn read_prg_mmc3(&self, addr: u16) -> u8 {
let inner_bank = self.mmc3.mapped_prg_bank(addr);
let bank = self.prg_8kb_outer_offset() | inner_bank;
let offset = (addr as usize) & PRG_BANK_MASK;
self.mmc3.read_prg_at_bank(bank, offset)
}
fn read_prg_unrom(&self, addr: u16) -> u8 {
let outer = self.prg_8kb_outer_offset();
let count = self.mmc3.base.prg_bank_count();
let bank = match addr {
0x8000..=0x9FFF => outer | (self.unrom_bank as usize * 2),
0xA000..=0xBFFF => outer | (self.unrom_bank as usize * 2 + 1),
0xC000..=0xDFFF => outer | count.saturating_sub(2),
0xE000..=0xFFFF => outer | count.saturating_sub(1),
_ => 0,
};
let offset = (addr as usize) & PRG_BANK_MASK;
self.mmc3.read_prg_at_bank(bank, offset)
}
fn read_prg_cnrom(&self, addr: u16) -> u8 {
let outer = self.prg_8kb_outer_offset();
let slot = ((addr as usize).saturating_sub(0x8000)) >> 13;
let bank = outer | (slot & 0x03);
let offset = (addr as usize) & PRG_BANK_MASK;
self.mmc3.read_prg_at_bank(bank, offset)
}
fn read_prg_mmc1(&self, addr: u16) -> u8 {
let outer = self.prg_8kb_outer_offset();
let count = self.mmc3.base.prg_bank_count();
let prg_mode = (self.mmc1_control & Self::MMC1_PRG_MODE_MASK) >> 2;
let bank_16kb = (self.mmc1_prg & 0x0F) as usize; let bank = match prg_mode {
0 | 1 => {
let base = (bank_16kb & !1) * 2; let slot = ((addr as usize - 0x8000) >> 13) & 0x03;
outer | (base + slot)
}
2 => match addr {
0x8000..=0x9FFF => outer,
0xA000..=0xBFFF => outer | 1,
0xC000..=0xDFFF => outer | (bank_16kb * 2),
0xE000..=0xFFFF => outer | (bank_16kb * 2 + 1),
_ => 0,
},
_ => match addr {
0x8000..=0x9FFF => outer | (bank_16kb * 2),
0xA000..=0xBFFF => outer | (bank_16kb * 2 + 1),
0xC000..=0xDFFF => outer | count.saturating_sub(2),
0xE000..=0xFFFF => outer | count.saturating_sub(1),
_ => 0,
},
};
let offset = (addr as usize) & PRG_BANK_MASK;
self.mmc3.read_prg_at_bank(bank, offset)
}
fn read_chr_mmc3(&mut self, addr: u16) -> u8 {
let inner_bank = self.mmc3.mapped_chr_1k_bank(addr);
let bank = self.chr_1kb_outer_offset() | inner_bank;
let offset = (addr as usize) & CHR_1KB_BANK_MASK;
self.mmc3.read_chr_1k_at(bank, offset)
}
fn read_chr_cnrom(&mut self, addr: u16) -> u8 {
let outer = self.chr_8kb_outer_offset();
let bank_8kb = outer | (self.cnrom_bank as usize & 0x03);
let bank_1kb = bank_8kb * 8 + (((addr as usize) & 0x1FFF) >> 10);
let offset = (addr as usize) & CHR_1KB_BANK_MASK;
self.mmc3.read_chr_1k_at(bank_1kb, offset)
}
fn read_chr_mmc1(&mut self, addr: u16) -> u8 {
let outer_1kb = self.chr_1kb_outer_offset();
let chr_mode_8kb = (self.mmc1_control & Self::MMC1_CHR_MODE_BIT) == 0;
let bank_1kb = if chr_mode_8kb {
let base_4kb = (self.mmc1_chr0 & 0xFE) as usize;
outer_1kb | (base_4kb * 4 + (((addr as usize) & 0x1FFF) >> 10))
} else {
let (bank_4kb, sub) = if (addr & 0x1000) == 0 {
(self.mmc1_chr0 as usize, (addr as usize & 0x0FFF) >> 10)
} else {
(self.mmc1_chr1 as usize, (addr as usize & 0x0FFF) >> 10)
};
outer_1kb | (bank_4kb * 4 + sub)
};
let offset = (addr as usize) & CHR_1KB_BANK_MASK;
self.mmc3.read_chr_1k_at(bank_1kb, offset)
}
fn read_chr_unrom(&mut self, addr: u16) -> u8 {
let outer_1kb = self.chr_1kb_outer_offset();
let bank_1kb = outer_1kb | (((addr as usize) & 0x1FFF) >> 10);
let offset = (addr as usize) & CHR_1KB_BANK_MASK;
self.mmc3.read_chr_1k_at(bank_1kb, offset)
}
fn mmc1_mirroring(&self) -> NametableLayout {
match self.mmc1_control & 0x03 {
0 => NametableLayout::SingleScreenLower,
1 => NametableLayout::SingleScreenUpper,
2 => NametableLayout::Vertical,
_ => NametableLayout::Horizontal,
}
}
fn write_mmc1_serial(&mut self, addr: u16, value: u8) {
if value & 0x80 != 0 {
self.mmc1_shift = 0;
self.mmc1_count = 0;
self.mmc1_control |= 0x0C; return;
}
self.mmc1_shift |= (value & 1) << self.mmc1_count;
self.mmc1_count += 1;
if self.mmc1_count == 5 {
let val = self.mmc1_shift;
self.mmc1_shift = 0;
self.mmc1_count = 0;
match addr & 0x6000 {
0x0000 => {
self.mmc1_control = val & 0x1F; self.mmc3.base.set_mirroring(self.mmc1_mirroring());
}
0x2000 => self.mmc1_chr0 = val & 0x1F, 0x4000 => self.mmc1_chr1 = val & 0x1F, 0x6000 => self.mmc1_prg = val & 0x1F, _ => {}
}
}
}
}
impl Mapper for Mapper296 {
fn base(&self) -> &BaseMapper {
&self.mmc3.base
}
fn base_mut(&mut self) -> &mut BaseMapper {
&mut self.mmc3.base
}
fn mmc3_delegate(&self) -> Option<&MMC3Mapper> {
if self.mapper_mode() == 0 {
Some(&self.mmc3)
} else {
None
}
}
fn mmc3_delegate_mut(&mut self) -> Option<&mut MMC3Mapper> {
if self.mapper_mode() == 0 {
Some(&mut self.mmc3)
} else {
None
}
}
fn mapper_number(&self) -> u16 {
Self::MAPPER_NUMBER
}
fn capabilities(&self) -> MapperCapabilities {
MapperCapabilities {
has_irq: self.mapper_mode() == 0, 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 wram_size(&self) -> usize {
0
}
fn read_prg(&self, addr: u16) -> u8 {
match addr {
0x8000..=0xFFFF => match self.mapper_mode() {
0 => self.read_prg_mmc3(addr),
1 => self.read_prg_mmc1(addr),
2 => self.read_prg_unrom(addr),
3 => self.read_prg_cnrom(addr),
_ => 0,
},
_ => 0,
}
}
fn read_prg_open_bus(&self, addr: u16, open_bus: u8) -> u8 {
match addr {
0x8000..=0xFFFF => self.read_prg(addr),
_ => open_bus,
}
}
fn write_prg(&mut self, addr: u16, value: u8) {
if self.mmc3.base.try_write_prg_ram(addr, value) {
return;
}
match addr {
0x411D => self.config_reg = value & 0x07,
0x411E => { }
0x412C => self.outer_reg = value & 0x0F,
0x412E => self.outer_reg2 = value & 0x01,
0x8000..=0xFFFF => match self.mapper_mode() {
0 => self.mmc3.write_prg(addr, value),
1 => self.write_mmc1_serial(addr, value),
2 => self.unrom_bank = value & 0x0F,
3 => self.cnrom_bank = value & 0x03,
_ => {}
},
_ => {}
}
}
fn read_chr(&mut self, addr: u16) -> u8 {
if self.chr_ram_enabled() {
return self.chr_ram[(addr as usize) & 0x1FFF];
}
match self.mapper_mode() {
0 => self.read_chr_mmc3(addr),
1 => self.read_chr_mmc1(addr),
2 => self.read_chr_unrom(addr),
3 => self.read_chr_cnrom(addr),
_ => 0,
}
}
fn write_chr(&mut self, addr: u16, value: u8) {
if self.chr_ram_enabled() {
self.chr_ram[(addr as usize) & 0x1FFF] = value;
} else {
self.mmc3.write_chr(addr, value);
}
}
fn registers_snapshot(&self) -> Vec<u8> {
let mut snap = self.mmc3.registers_snapshot();
snap.extend_from_slice(&[
self.config_reg,
self.outer_reg,
self.outer_reg2,
self.unrom_bank,
self.cnrom_bank,
self.mmc1_shift,
self.mmc1_count,
self.mmc1_control,
self.mmc1_chr0,
self.mmc1_chr1,
self.mmc1_prg,
]);
snap
}
fn restore_registers(&mut self, data: &[u8]) {
const TAIL_LEN: usize = 11;
if data.len() < TAIL_LEN {
return;
}
let (mmc3_data, tail) = data.split_at(data.len() - TAIL_LEN);
self.config_reg = tail[0];
self.outer_reg = tail[1];
self.outer_reg2 = tail[2];
self.unrom_bank = tail[3];
self.cnrom_bank = tail[4];
self.mmc1_shift = tail[5];
self.mmc1_count = tail[6];
self.mmc1_control = tail[7];
self.mmc1_chr0 = tail[8];
self.mmc1_chr1 = tail[9];
self.mmc1_prg = tail[10];
self.mmc3.restore_registers(mmc3_data);
}
fn initialize_ram(&mut self, mode: crate::nes::console::RamInitMode) {
self.mmc3.initialize_ram(mode);
crate::nes::console::initialize_ram(&mut self.chr_ram, mode);
}
fn wram_snapshot(&self) -> Vec<u8> {
Vec::new()
}
fn load_wram_snapshot(&mut self, _data: &[u8]) {
}
fn chr_ram_snapshot(&self) -> Vec<u8> {
self.chr_ram.clone()
}
fn restore_chr_ram(&mut self, data: &[u8]) {
let len = data.len().min(CHR_RAM_SIZE);
self.chr_ram[..len].copy_from_slice(&data[..len]);
}
fn reset(&mut self) {
self.mmc3.reset();
self.config_reg = 0;
self.outer_reg = 0;
self.outer_reg2 = 0;
self.unrom_bank = 0;
self.cnrom_bank = 0;
self.mmc1_shift = 0;
self.mmc1_count = 0;
self.mmc1_control = 0x0C;
self.mmc1_chr0 = 0;
self.mmc1_chr1 = 0;
self.mmc1_prg = 0;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nes::cartridge::NametableLayout;
use crate::nes::cartridge::mapper::MapperContext;
const PRG_8KB_BANKS: usize = 32; const CHR_1KB_BANKS: usize = 64;
fn make_prg_rom(banks: usize) -> Vec<u8> {
let mut prg = vec![0u8; banks * 0x2000];
for bank in 0..banks {
let start = bank * 0x2000;
for b in prg[start..start + 0x2000].iter_mut() {
*b = (bank & 0xFF) as u8;
}
}
prg
}
fn make_chr_rom(banks_1kb: usize) -> Vec<u8> {
let mut chr = vec![0u8; banks_1kb * 0x0400];
for bank in 0..banks_1kb {
let start = bank * 0x0400;
for b in chr[start..start + 0x0400].iter_mut() {
*b = (bank & 0xFF) as u8;
}
}
chr
}
fn create_mapper() -> Mapper296 {
Mapper296::new(
MapperContext::new_for_test(
296,
make_prg_rom(PRG_8KB_BANKS),
make_chr_rom(CHR_1KB_BANKS),
NametableLayout::Horizontal,
)
.with_prg_ram_banks(0),
)
}
#[test]
fn test_mapper_number_is_296() {
assert_eq!(create_mapper().mapper_number(), 296);
}
#[test]
fn test_config_reg_sets_mode_bits() {
let mut m = create_mapper();
m.write_prg(0x411D, 0x03); assert_eq!(m.mapper_mode(), 3);
m.write_prg(0x411D, 0x00); assert_eq!(m.mapper_mode(), 0);
}
#[test]
fn test_config_reg_sets_chr_ram_bit() {
let mut m = create_mapper();
assert!(!m.chr_ram_enabled());
m.write_prg(0x411D, 0x04);
assert!(m.chr_ram_enabled());
m.write_prg(0x411D, 0x00);
assert!(!m.chr_ram_enabled());
}
#[test]
fn test_config_reg_masks_upper_bits() {
let mut m = create_mapper();
m.write_prg(0x411D, 0xFF);
assert_eq!(m.config_reg, 0x07, "only bits 2:0 are stored");
}
#[test]
fn test_outer_reg_412c_written_and_stored() {
let mut m = create_mapper();
m.write_prg(0x412C, 0x0A);
assert_eq!(m.outer_reg, 0x0A);
}
#[test]
fn test_outer_reg2_412e_written_and_stored() {
let mut m = create_mapper();
m.write_prg(0x412E, 0x01);
assert_eq!(m.outer_reg2, 0x01);
}
#[test]
fn test_outer_regs_masked_to_valid_bits() {
let mut m = create_mapper();
m.write_prg(0x412C, 0xFF);
assert_eq!(m.outer_reg, 0x0F, "$412C: only bits 3:0 stored");
m.write_prg(0x412E, 0xFF);
assert_eq!(m.outer_reg2, 0x01, "$412E: only bit 0 stored");
}
#[test]
fn test_prg_8kb_outer_offset_a25() {
let mut m = create_mapper();
m.write_prg(0x412C, 0x01); assert_eq!(m.prg_8kb_outer_offset(), 1 << 12);
}
#[test]
fn test_prg_8kb_outer_offset_a26() {
let mut m = create_mapper();
m.write_prg(0x412C, 0x04); assert_eq!(m.prg_8kb_outer_offset(), 1 << 13);
}
#[test]
fn test_prg_8kb_outer_offset_a27() {
let mut m = create_mapper();
m.write_prg(0x412E, 0x01); assert_eq!(m.prg_8kb_outer_offset(), 1 << 14);
}
#[test]
fn test_chr_1kb_outer_offset_a25() {
let mut m = create_mapper();
m.write_prg(0x412C, 0x02); assert_eq!(m.chr_1kb_outer_offset(), 1 << 15);
}
#[test]
fn test_mmc3_mode_prg_bank_selection_r6() {
let mut m = create_mapper();
m.write_prg(0x8000, 0x06); m.write_prg(0x8001, 5); assert_eq!(m.read_prg(0x8000), 5, "MMC3 mode: $8000 reads bank 5");
}
#[test]
fn test_mmc3_mode_prg_outer_bank_extends_bank_number() {
let mut m = create_mapper();
m.write_prg(0x8000, 0x06); m.write_prg(0x8001, 0); let without_outer = m.read_prg(0x8000);
assert_eq!(without_outer, 0, "bank 0 reads 0");
m.write_prg(0x412C, 0x01);
assert_eq!(
m.prg_8kb_outer_offset(),
4096,
"outer A25 gives 4096 bank offset"
);
}
#[test]
fn test_mmc3_mode_chr_bank_selection() {
let mut m = create_mapper();
m.write_prg(0x8000, 0x02); m.write_prg(0x8001, 7); let val = m.read_chr(0x1000);
assert_eq!(val, 7, "MMC3 mode: PPU $1000 → 1KB bank 7 → filled with 7");
}
#[test]
fn test_unrom_mode_prg_bank_switch_at_8000() {
let mut m = create_mapper();
m.write_prg(0x411D, 0x02); m.write_prg(0x8000, 3); assert_eq!(m.read_prg(0x8000), 6, "$8000 first 8KB → bank 6");
assert_eq!(m.read_prg(0xA000), 7, "$A000 second 8KB → bank 7");
}
#[test]
fn test_unrom_mode_fixed_last_bank() {
let mut m = create_mapper();
m.write_prg(0x411D, 0x02); m.write_prg(0x8000, 0); assert_eq!(m.read_prg(0xC000), 30, "$C000 → second-to-last bank = 30");
assert_eq!(m.read_prg(0xE000), 31, "$E000 → last bank = 31");
}
#[test]
fn test_unrom_bank_register_masked() {
let mut m = create_mapper();
m.write_prg(0x411D, 0x02);
m.write_prg(0x9000, 0xFF); assert_eq!(m.unrom_bank, 0x0F, "UNROM bank uses bits 3:0 only");
}
#[test]
fn test_cnrom_mode_chr_bank_selection() {
let mut m = create_mapper();
m.write_prg(0x411D, 0x03); m.write_prg(0x8000, 2); let val = m.read_chr(0x0000);
assert_eq!(
val, 16,
"CNROM mode: CHR bank 2 → 1KB bank 16 (2*8) → filled with 16"
);
}
#[test]
fn test_cnrom_mode_prg_fixed_32kb() {
let mut m = create_mapper();
m.write_prg(0x411D, 0x03); assert_eq!(m.read_prg(0x8000), 0, "CNROM $8000 → bank 0");
assert_eq!(m.read_prg(0xA000), 1, "CNROM $A000 → bank 1");
assert_eq!(m.read_prg(0xC000), 2, "CNROM $C000 → bank 2");
assert_eq!(m.read_prg(0xE000), 3, "CNROM $E000 → bank 3");
}
#[test]
fn test_cnrom_bank_register_masked_to_2_bits() {
let mut m = create_mapper();
m.write_prg(0x411D, 0x03);
m.write_prg(0x8000, 0xFF);
assert_eq!(m.cnrom_bank, 0x03, "CNROM bank uses bits 1:0 only");
}
#[test]
fn test_chr_ram_mode_read_write() {
let mut m = create_mapper();
m.write_prg(0x411D, 0x04); m.write_chr(0x0050, 0xAB);
assert_eq!(m.read_chr(0x0050), 0xAB, "CHR-RAM: written byte reads back");
}
#[test]
fn test_chr_ram_mode_does_not_read_chr_rom() {
let mut m = create_mapper();
let rom_val = m.read_chr(0x0000);
assert_eq!(rom_val, 0, "CHR-ROM bank 0 = 0");
m.write_prg(0x411D, 0x04);
m.write_chr(0x0000, 0x42);
assert_eq!(
m.read_chr(0x0000),
0x42,
"CHR-RAM reads back the written byte"
);
}
#[test]
fn test_chr_ram_wraps_at_8kb() {
let mut m = create_mapper();
m.write_prg(0x411D, 0x04);
m.write_chr(0x1FFF, 0x55);
assert_eq!(m.read_chr(0x1FFF), 0x55);
}
#[test]
fn test_mmc1_shift_register_loads_after_5_writes() {
let mut m = create_mapper();
m.write_prg(0x411D, 0x01); for bit in [1u8, 1, 1, 0, 0] {
m.write_prg(0xE000, bit);
}
assert_eq!(m.mmc1_prg, 0x07, "MMC1: shift register loaded correctly");
}
#[test]
fn test_mmc1_reset_via_bit7() {
let mut m = create_mapper();
m.write_prg(0x411D, 0x01);
m.write_prg(0xE000, 0x01); m.write_prg(0xE000, 0x01);
assert_eq!(m.mmc1_count, 2);
m.write_prg(0xE000, 0x80); assert_eq!(m.mmc1_count, 0, "MMC1 reset clears shift register");
assert_eq!(m.mmc1_shift, 0);
}
#[test]
fn test_mmc1_control_register_set() {
let mut m = create_mapper();
m.write_prg(0x411D, 0x01);
for bit in [1u8, 1, 0, 0, 0] {
m.write_prg(0x8000, bit);
}
assert_eq!(m.mmc1_control, 0x03);
}
#[test]
fn test_mmc1_prg_mode3_fixed_last_bank() {
let mut m = create_mapper();
m.write_prg(0x411D, 0x01);
assert_eq!(m.read_prg(0xC000), 30, "MMC1 mode3: $C000 → bank 30");
assert_eq!(m.read_prg(0xE000), 31, "MMC1 mode3: $E000 → bank 31");
}
#[test]
fn test_registers_snapshot_roundtrip() {
let mut m = create_mapper();
m.write_prg(0x411D, 0x06); m.write_prg(0x412C, 0x05);
m.write_prg(0x412E, 0x01);
m.write_prg(0x411D, 0x02); m.write_prg(0x8000, 0x0B); let snap = m.registers_snapshot();
m.config_reg = 0;
m.outer_reg = 0;
m.outer_reg2 = 0;
m.unrom_bank = 0;
m.restore_registers(&snap);
assert_eq!(m.config_reg, 0x02);
assert_eq!(m.outer_reg, 0x05);
assert_eq!(m.outer_reg2, 0x01);
assert_eq!(m.unrom_bank, 0x0B);
}
#[test]
fn test_chr_ram_snapshot_roundtrip() {
let mut m = create_mapper();
m.write_prg(0x411D, 0x04); m.write_chr(0x0100, 0xDE);
m.write_chr(0x0200, 0xAD);
let snap = m.chr_ram_snapshot();
m.chr_ram.fill(0);
m.restore_chr_ram(&snap);
assert_eq!(m.read_chr(0x0100), 0xDE);
assert_eq!(m.read_chr(0x0200), 0xAD);
}
#[test]
fn test_reset_clears_all_registers() {
let mut m = create_mapper();
m.write_prg(0x411D, 0x07);
m.write_prg(0x412C, 0x0F);
m.write_prg(0x412E, 0x01);
m.write_prg(0x411D, 0x02);
m.write_prg(0x8000, 0x0A); m.reset();
assert_eq!(m.config_reg, 0);
assert_eq!(m.outer_reg, 0);
assert_eq!(m.outer_reg2, 0);
assert_eq!(m.unrom_bank, 0);
assert_eq!(m.cnrom_bank, 0);
assert_eq!(m.mmc1_control, 0x0C, "MMC1 control resets to 0x0C");
}
#[test]
fn test_chr_1kb_outer_offset_a26() {
let mut m = create_mapper();
m.write_prg(0x412C, 0x08); assert_eq!(m.chr_1kb_outer_offset(), 1 << 16);
}
#[test]
fn test_chr_1kb_outer_offset_a27() {
let mut m = create_mapper();
m.write_prg(0x412E, 0x01); assert_eq!(m.chr_1kb_outer_offset(), 1 << 17);
}
#[test]
fn test_mmc1_chr_8kb_mode_bank0_selects_8kb_block() {
let mut m = create_mapper();
m.write_prg(0x411D, 0x01); for bit in [0u8, 1, 0, 0, 0] {
m.write_prg(0xA000, bit);
}
let val = m.read_chr(0x0000);
assert_eq!(val, 8, "MMC1 8KB CHR: chr0=2 → 1KB bank 8 → value 8");
let mut m2 = create_mapper();
m2.write_prg(0x411D, 0x01);
for bit in [1u8, 1, 0, 0, 0] {
m2.write_prg(0xA000, bit);
}
assert_eq!(
m2.read_chr(0x0000),
8,
"MMC1 8KB CHR: chr0=3 (bit0 ignored) → same 1KB bank 8"
);
}
#[test]
fn test_mmc1_chr_4kb_mode_banks_at_0000_and_1000() {
let mut m = create_mapper();
m.write_prg(0x411D, 0x01); for bit in [0u8, 0, 0, 0, 1] {
m.write_prg(0x8000, bit);
}
for bit in [0u8, 1, 0, 0, 0] {
m.write_prg(0xA000, bit);
}
for bit in [1u8, 1, 0, 0, 0] {
m.write_prg(0xC000, bit);
}
assert_eq!(
m.read_chr(0x0000),
8,
"4KB CHR: bank 2 at $0000 → 1KB bank 8"
);
assert_eq!(
m.read_chr(0x1000),
12,
"4KB CHR: bank 3 at $1000 → 1KB bank 12"
);
}
#[test]
fn test_mmc1_mirroring_single_screen_lower() {
let mut m = create_mapper();
m.write_prg(0x411D, 0x01);
for bit in [0u8, 0, 0, 0, 0] {
m.write_prg(0x8000, bit);
}
assert_eq!(m.mmc1_mirroring(), NametableLayout::SingleScreenLower);
}
#[test]
fn test_mmc1_mirroring_single_screen_upper() {
let mut m = create_mapper();
m.write_prg(0x411D, 0x01);
for bit in [1u8, 0, 0, 0, 0] {
m.write_prg(0x8000, bit);
}
assert_eq!(m.mmc1_mirroring(), NametableLayout::SingleScreenUpper);
}
#[test]
fn test_mmc1_mirroring_vertical() {
let mut m = create_mapper();
m.write_prg(0x411D, 0x01);
for bit in [0u8, 1, 0, 0, 0] {
m.write_prg(0x8000, bit);
}
assert_eq!(m.mmc1_mirroring(), NametableLayout::Vertical);
}
#[test]
fn test_mmc1_mirroring_horizontal() {
let mut m = create_mapper();
m.write_prg(0x411D, 0x01);
for bit in [1u8, 1, 0, 0, 0] {
m.write_prg(0x8000, bit);
}
assert_eq!(m.mmc1_mirroring(), NametableLayout::Horizontal);
}
#[test]
fn test_unrom_chr_reads_from_chr_rom_bank0() {
let mut m = create_mapper();
m.write_prg(0x411D, 0x02); let val = m.read_chr(0x0000);
assert_eq!(val, 0, "UNROM CHR: PPU $0000 → bank 0 → 0");
let val = m.read_chr(0x1C00);
assert_eq!(val, 7, "UNROM CHR: PPU $1C00 → 1KB bank 7 → 7");
}
#[test]
fn test_wram_size_is_zero() {
assert_eq!(create_mapper().wram_size(), 0);
}
#[test]
fn test_wram_snapshot_is_empty() {
assert!(create_mapper().wram_snapshot().is_empty());
}
#[test]
fn test_load_wram_snapshot_is_noop() {
let mut m = create_mapper();
m.load_wram_snapshot(&[0xAB; 1024]); assert_eq!(m.wram_size(), 0);
}
#[test]
fn test_capabilities_mmc3_mode_has_irq() {
let mut m = create_mapper();
m.write_prg(0x411D, 0x00); assert!(m.capabilities().has_irq);
}
#[test]
fn test_capabilities_non_mmc3_mode_no_irq() {
let mut m = create_mapper();
m.write_prg(0x411D, 0x02); assert!(!m.capabilities().has_irq);
}
#[test]
fn test_capabilities_has_chr_banking_and_dynamic_mirroring() {
let m = create_mapper();
let caps = m.capabilities();
assert!(caps.has_chr_banking);
assert!(caps.has_dynamic_mirroring);
assert_eq!(caps.prg_bank_size_kb, 8);
assert_eq!(caps.chr_bank_size_kb, 1);
}
}