use crate::cartridge::base_mapper::BaseMapper;
use crate::cartridge::mapper::{Mapper, MapperCapabilities};
const MAPPER_NUMBER: u16 = 236;
pub struct Mapper236 {
base: BaseMapper,
bank_mode: u8,
outer_bank: u8,
prg_reg: u8,
chr_reg: u8,
has_chr_rom: bool,
}
impl Mapper236 {
pub fn new(ctx: super::mapper::MapperContext) -> Self {
let has_chr_rom = !ctx.chr_rom.is_empty();
let capabilities = MapperCapabilities {
has_dynamic_mirroring: true,
has_chr_banking: has_chr_rom,
prg_bank_size_kb: 16,
chr_bank_size_kb: 8,
..Default::default()
};
let mut base = BaseMapper::new(&ctx, capabilities);
base.configure_prg_banking(16 * 1024);
if has_chr_rom {
base.configure_chr_banking(8 * 1024);
}
let mut mapper = Self {
base,
bank_mode: 0,
outer_bank: 0,
prg_reg: 0,
chr_reg: 0,
has_chr_rom,
};
mapper.apply_banks();
mapper
}
fn apply_banks(&mut self) {
let combined = self.outer_bank | self.prg_reg;
match self.bank_mode {
0x00 | 0x10 => {
self.base.select_prg_page(0, combined as i16);
self.base.select_prg_page(1, (self.outer_bank | 7) as i16);
}
0x20 => {
let bank32 = (combined & 0xFE) as i16;
self.base.select_prg_page(0, bank32);
self.base.select_prg_page(1, bank32 + 1);
}
0x30 => {
self.base.select_prg_page(0, combined as i16);
self.base.select_prg_page(1, combined as i16);
}
_ => {}
}
if self.has_chr_rom {
self.base.select_chr_page(0, self.chr_reg as i16);
}
}
}
impl Mapper for Mapper236 {
fn base(&self) -> &BaseMapper {
&self.base
}
fn base_mut(&mut self) -> &mut BaseMapper {
&mut self.base
}
fn mapper_number(&self) -> u16 {
MAPPER_NUMBER
}
fn read_prg(&self, addr: u16) -> u8 {
if let Some(value) = self.base().try_read_prg_ram(addr) {
return value;
}
if (0x8000..=0xFFFF).contains(&addr) && self.bank_mode == 0x10 {
return self.base.read_prg_rom(addr & 0xFFF0);
}
match addr {
0x8000..=0xFFFF => self.base.read_prg_rom(addr),
_ => 0,
}
}
fn write_prg(&mut self, addr: u16, value: u8) {
if self.base.try_write_prg_ram(addr, value) {
return;
}
match addr {
0x8000..=0xBFFF => {
self.base.set_mirroring_hv(addr & 0x20 != 0);
if self.has_chr_rom {
self.chr_reg = (addr & 0x07) as u8;
} else {
self.outer_bank = ((addr & 0x03) as u8) << 3;
}
self.apply_banks();
}
0xC000..=0xFFFF => {
self.bank_mode = (addr & 0x30) as u8;
self.prg_reg = (addr & 0x07) as u8;
self.apply_banks();
}
_ => {}
}
}
fn registers_snapshot(&self) -> Vec<u8> {
let mut snap = self.base.banking_snapshot();
snap.push(self.bank_mode);
snap.push(self.outer_bank);
snap.push(self.prg_reg);
snap.push(self.chr_reg);
snap
}
fn restore_registers(&mut self, data: &[u8]) {
let expected_banking_len = self.base.banking_snapshot().len();
if data.len() >= expected_banking_len + 4 {
self.base.restore_banking(&data[..expected_banking_len]);
self.bank_mode = data[expected_banking_len];
self.outer_bank = data[expected_banking_len + 1];
self.prg_reg = data[expected_banking_len + 2];
self.chr_reg = data[expected_banking_len + 3];
self.apply_banks();
} else {
self.base.restore_banking(data);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cartridge::NametableLayout;
use crate::cartridge::mapper::{MapperContext, create_mapper};
use crate::cartridge::test_helpers::banked_data;
const PRG_BANKS: usize = 8; const CHR_BANKS: usize = 8; const PRG_BANKS_RAM_VARIANT: usize = 32;
fn create_chr_rom_mapper(prg_rom: Vec<u8>, chr_rom: Vec<u8>) -> Mapper236 {
Mapper236::new(MapperContext::new_for_test(
MAPPER_NUMBER,
prg_rom,
chr_rom,
NametableLayout::Vertical,
))
}
fn create_chr_ram_mapper(prg_rom: Vec<u8>) -> Mapper236 {
Mapper236::new(MapperContext::new_for_test(
MAPPER_NUMBER,
prg_rom,
vec![],
NametableLayout::Vertical,
))
}
#[test]
fn mapper_236_is_registered_in_factory() {
let result = create_mapper(MapperContext::new_for_test(
MAPPER_NUMBER,
banked_data(16 * 1024, PRG_BANKS),
banked_data(8 * 1024, CHR_BANKS),
NametableLayout::Vertical,
));
assert!(result.is_ok(), "Mapper 236 should be registered in factory");
}
#[test]
fn power_on_lower_window_starts_at_bank_0() {
let mapper = create_chr_rom_mapper(
banked_data(16 * 1024, PRG_BANKS),
banked_data(8 * 1024, CHR_BANKS),
);
assert_eq!(
mapper.read_prg(0x8000),
0,
"$8000 window should start at bank 0"
);
}
#[test]
fn power_on_upper_window_fixed_to_last_bank() {
let mapper = create_chr_rom_mapper(
banked_data(16 * 1024, PRG_BANKS),
banked_data(8 * 1024, CHR_BANKS),
);
assert_eq!(
mapper.read_prg(0xC000),
7,
"$C000 window should start at bank 7 (UNROM last bank)"
);
}
#[test]
fn lower_latch_bit5_zero_sets_vertical_mirroring() {
let mut mapper = create_chr_rom_mapper(
banked_data(16 * 1024, PRG_BANKS),
banked_data(8 * 1024, CHR_BANKS),
);
mapper.write_prg(0x8000, 0); assert_eq!(mapper.get_mirroring(), NametableLayout::Vertical);
}
#[test]
fn lower_latch_bit5_one_sets_horizontal_mirroring() {
let mut mapper = create_chr_rom_mapper(
banked_data(16 * 1024, PRG_BANKS),
banked_data(8 * 1024, CHR_BANKS),
);
mapper.write_prg(0x8020, 0); assert_eq!(mapper.get_mirroring(), NametableLayout::Horizontal);
}
#[test]
fn mirroring_toggles_correctly() {
let mut mapper = create_chr_rom_mapper(
banked_data(16 * 1024, PRG_BANKS),
banked_data(8 * 1024, CHR_BANKS),
);
mapper.write_prg(0x8020, 0);
assert_eq!(mapper.get_mirroring(), NametableLayout::Horizontal);
mapper.write_prg(0x8000, 0);
assert_eq!(mapper.get_mirroring(), NametableLayout::Vertical);
}
#[test]
fn lower_latch_selects_chr_bank() {
let mut mapper = create_chr_rom_mapper(
banked_data(16 * 1024, PRG_BANKS),
banked_data(8 * 1024, CHR_BANKS),
);
mapper.write_prg(0x8003, 0);
assert_eq!(
mapper.read_chr(0x0000),
3,
"CHR bank 3 should be visible at $0000"
);
}
#[test]
fn lower_latch_chr_bits_select_all_eight_banks() {
let mut mapper = create_chr_rom_mapper(
banked_data(16 * 1024, PRG_BANKS),
banked_data(8 * 1024, CHR_BANKS),
);
for bank in 0u8..8 {
mapper.write_prg(0x8000 | (bank as u16), 0);
assert_eq!(
mapper.read_chr(0x0000),
bank,
"CHR bank {bank} should be selectable"
);
}
}
#[test]
fn upper_latch_mode0_lower_window_switchable() {
let mut mapper = create_chr_rom_mapper(
banked_data(16 * 1024, PRG_BANKS),
banked_data(8 * 1024, CHR_BANKS),
);
mapper.write_prg(0xC003, 0);
assert_eq!(
mapper.read_prg(0x8000),
3,
"$8000 window should be at bank 3"
);
}
#[test]
fn upper_latch_mode0_upper_window_fixed_to_last_bank() {
let mut mapper = create_chr_rom_mapper(
banked_data(16 * 1024, PRG_BANKS),
banked_data(8 * 1024, CHR_BANKS),
);
mapper.write_prg(0xC005, 0);
assert_eq!(
mapper.read_prg(0xC000),
7,
"$C000 window should be fixed at last bank (7) in UNROM mode"
);
}
#[test]
fn upper_latch_mode2_selects_32kb_bank() {
let mut mapper = create_chr_rom_mapper(
banked_data(16 * 1024, PRG_BANKS),
banked_data(8 * 1024, CHR_BANKS),
);
mapper.write_prg(0xC022, 0);
assert_eq!(
mapper.read_prg(0x8000),
2,
"$8000 window should be at bank 2"
);
assert_eq!(
mapper.read_prg(0xC000),
3,
"$C000 window should be at bank 3"
);
}
#[test]
fn upper_latch_mode2_aligns_to_even_bank_pairs() {
let mut mapper = create_chr_rom_mapper(
banked_data(16 * 1024, PRG_BANKS),
banked_data(8 * 1024, CHR_BANKS),
);
mapper.write_prg(0xC023, 0);
assert_eq!(
mapper.read_prg(0x8000),
2,
"$8000 should align to bank 2 (3 & 0xFE)"
);
assert_eq!(mapper.read_prg(0xC000), 3, "$C000 should be bank 3");
}
#[test]
fn upper_latch_mode3_both_windows_same_bank() {
let mut mapper = create_chr_rom_mapper(
banked_data(16 * 1024, PRG_BANKS),
banked_data(8 * 1024, CHR_BANKS),
);
mapper.write_prg(0xC035, 0);
assert_eq!(mapper.read_prg(0x8000), 5, "$8000 window should be bank 5");
assert_eq!(
mapper.read_prg(0xC000),
5,
"$C000 window should also be bank 5"
);
}
#[test]
fn solder_pad_mode_forces_low_4_address_bits_to_zero() {
let prg = banked_data(16 * 1024, PRG_BANKS);
let chr = banked_data(8 * 1024, CHR_BANKS);
let mut mapper = create_chr_rom_mapper(prg, chr);
mapper.write_prg(0xC010, 0); let at_8000 = mapper.read_prg(0x8000);
let at_8005 = mapper.read_prg(0x8005);
assert_eq!(
at_8005, at_8000,
"Solder pad mode should force addr bits 3-0 to zero"
);
}
#[test]
fn solder_pad_mode_reads_differ_from_normal_mode() {
let mut prg = vec![0u8; 16 * 1024 * PRG_BANKS];
prg[0] = 0xAA; prg[5] = 0x55; let chr = banked_data(8 * 1024, CHR_BANKS);
let mut mapper = create_chr_rom_mapper(prg, chr);
mapper.write_prg(0xC000, 0); let normal_at_5 = mapper.read_prg(0x8005);
mapper.write_prg(0xC010, 0); let solder_at_5 = mapper.read_prg(0x8005);
let solder_at_0 = mapper.read_prg(0x8000);
assert_eq!(
solder_at_5, solder_at_0,
"Solder pad: $8005 reads like $8000"
);
assert_ne!(
normal_at_5, solder_at_5,
"Solder pad should differ from normal read at offset > 0 (0x55 vs 0xAA)"
);
}
#[test]
fn chr_ram_variant_lower_latch_sets_outer_bank() {
let mut mapper = create_chr_ram_mapper(banked_data(16 * 1024, PRG_BANKS_RAM_VARIANT));
mapper.write_prg(0x8001, 0); assert_eq!(
mapper.read_prg(0x8000),
8,
"$8000 window should be at bank 8 with outer_bank=8"
);
}
#[test]
fn chr_ram_variant_upper_window_uses_outer_bank_last() {
let mut mapper = create_chr_ram_mapper(banked_data(16 * 1024, PRG_BANKS_RAM_VARIANT));
mapper.write_prg(0x8001, 0); assert_eq!(
mapper.read_prg(0xC000),
15,
"$C000 window should be outer_bank|7 = 15"
);
}
#[test]
fn chr_ram_is_readable_and_writable() {
let mut mapper = create_chr_ram_mapper(banked_data(16 * 1024, PRG_BANKS_RAM_VARIANT));
mapper.write_chr(0x0000, 0xAB);
mapper.write_chr(0x1FFF, 0xCD);
assert_eq!(mapper.read_chr(0x0000), 0xAB);
assert_eq!(mapper.read_chr(0x1FFF), 0xCD);
}
#[test]
fn write_data_value_is_ignored() {
let mut mapper = create_chr_rom_mapper(
banked_data(16 * 1024, PRG_BANKS),
banked_data(8 * 1024, CHR_BANKS),
);
mapper.write_prg(0xC003, 0x00);
let bank_with_0 = mapper.read_prg(0x8000);
mapper.write_prg(0xC003, 0xFF);
let bank_with_ff = mapper.read_prg(0x8000);
assert_eq!(bank_with_0, bank_with_ff, "Data value should be ignored");
assert_eq!(bank_with_0, 3);
}
#[test]
fn registers_snapshot_and_restore() {
let prg = banked_data(16 * 1024, PRG_BANKS);
let chr = banked_data(8 * 1024, CHR_BANKS);
let mut mapper = create_chr_rom_mapper(prg.clone(), chr.clone());
mapper.write_prg(0x8023, 0);
mapper.write_prg(0xC035, 0);
let snap = mapper.registers_snapshot();
let mut restored = create_chr_rom_mapper(prg, chr);
restored.restore_registers(&snap);
assert_eq!(restored.read_prg(0x8000), 5);
assert_eq!(restored.read_prg(0xC000), 5);
assert_eq!(restored.read_chr(0x0000), 3);
assert_eq!(restored.get_mirroring(), NametableLayout::Horizontal);
}
}