use crate::cartridge::base_mapper::BaseMapper;
use crate::cartridge::mmc3::MMC3Mapper;
use crate::cartridge::{Mapper, MapperCapabilities, NametableLayout};
use crate::trace_mapper;
pub struct Mapper52 {
pub(crate) mmc3: MMC3Mapper,
outer: u8,
locked: bool,
submapper: u8,
chr_ram_8k: Vec<u8>,
legacy_1mb_profile: bool,
}
impl Mapper52 {
const MAPPER_NUMBER: u8 = 52;
const WRAM_START: u16 = 0x6000;
const WRAM_END: u16 = 0x7FFF;
const PRG_BANK_SIZE: usize = 0x2000; const PRG_BANK_MASK: usize = Self::PRG_BANK_SIZE - 1;
const CHR_1K_SIZE: usize = 0x0400; const CHR_BANK_MASK: usize = Self::CHR_1K_SIZE - 1;
fn is_legacy_1mb_profile(submapper: u8, prg_len: usize, chr_len: usize) -> bool {
submapper == 0 && prg_len == 1024 * 1024 && chr_len == 1024 * 1024
}
pub fn new(ctx: super::mapper::MapperContext) -> Self {
let legacy_1mb_profile =
Self::is_legacy_1mb_profile(ctx.submapper, ctx.prg_rom.len(), ctx.chr_rom.len());
let prg_rom = ctx.prg_rom;
let chr_rom = ctx.chr_rom;
let mirroring = ctx.mirroring;
let submapper = ctx.submapper;
Self {
mmc3: MMC3Mapper::new_with_irq_mode(prg_rom, chr_rom, mirroring, false),
outer: 0,
locked: false,
submapper,
chr_ram_8k: if submapper == 13 || submapper == 14 {
vec![0; 8 * 1024]
} else {
Vec::new()
},
legacy_1mb_profile,
}
}
#[allow(dead_code)]
pub fn new_with_submapper(
prg_rom: Vec<u8>,
chr_rom: Vec<u8>,
mirroring: NametableLayout,
submapper: u8,
) -> Self {
let legacy_1mb_profile =
Self::is_legacy_1mb_profile(submapper, prg_rom.len(), chr_rom.len());
Self {
mmc3: MMC3Mapper::new_with_irq_mode(prg_rom, chr_rom, mirroring, false),
outer: 0,
locked: false,
submapper,
chr_ram_8k: if submapper == 13 || submapper == 14 {
vec![0; 8 * 1024]
} else {
Vec::new()
},
legacy_1mb_profile,
}
}
fn apply_prg_block(&self, mmc3_raw_bank: usize) -> usize {
let b = ((self.outer >> 2) & 0x01) as usize; let p = ((self.outer >> 1) & 0x01) as usize; let p_bit = (self.outer & 0x01) as usize; let s = ((self.outer >> 3) & 0x01) as usize;
let a17 = if s != 0 {
p_bit
} else {
(mmc3_raw_bank >> 4) & 1
};
let low = mmc3_raw_bank & 0x0F;
(b << 6) | (p << 5) | (a17 << 4) | low
}
fn apply_chr_block(&self, mmc3_raw_bank: usize) -> usize {
if self.legacy_1mb_profile {
let mode_a = ((self.outer >> 6) & 0x01) as usize;
let chr_block_bit2_b = ((self.outer >> 5) & 0x01) as usize;
let chr_block_bit0_c = ((self.outer >> 4) & 0x01) as usize;
let chr_block_bit1_e = ((self.outer >> 2) & 0x01) as usize;
let mmc3_chr_a17 = (mmc3_raw_bank >> 7) & 1;
let block = (chr_block_bit2_b << 2)
| (chr_block_bit1_e << 1)
| if mode_a != 0 {
chr_block_bit0_c
} else {
mmc3_chr_a17
};
let low = mmc3_raw_bank & 0x7F;
return (block << 7) | low;
}
let t = ((self.outer >> 6) & 0x01) as usize;
let c_lo = ((self.outer >> 4) & 0x01) as usize;
let a17 = if t != 0 {
c_lo
} else {
(mmc3_raw_bank >> 7) & 1
};
let low = mmc3_raw_bank & 0x7F;
let final_bank = if self.submapper == 14 {
let bb = ((self.outer >> 1) & 0x03) as usize;
(bb << 8) | (a17 << 7) | low
} else {
let b = ((self.outer >> 2) & 0x01) as usize;
let c_bit = ((self.outer >> 5) & 0x01) as usize;
(b << 9) | (c_bit << 8) | (a17 << 7) | low
};
trace_mapper!(3; "[52] CHR bank: outer=0x{:02X} T={} B={} C={} c={} raw={} -> final={}",
self.outer,
t,
(self.outer >> 2) & 1,
(self.outer >> 5) & 1,
c_lo,
mmc3_raw_bank,
final_bank
);
final_bank
}
fn chr_ram_selected(&self) -> bool {
match self.submapper {
13 => (self.outer & 0x03) == 0x03,
14 => (self.outer & 0x20) != 0,
_ => false,
}
}
fn is_wram_window(addr: u16) -> bool {
(Self::WRAM_START..=Self::WRAM_END).contains(&addr)
}
}
impl Mapper for Mapper52 {
fn base(&self) -> &BaseMapper {
&self.mmc3.base
}
fn base_mut(&mut self) -> &mut BaseMapper {
&mut self.mmc3.base
}
fn mmc3_delegate(&self) -> Option<&MMC3Mapper> {
Some(&self.mmc3)
}
fn mmc3_delegate_mut(&mut self) -> Option<&mut MMC3Mapper> {
Some(&mut self.mmc3)
}
fn read_prg_open_bus(&self, addr: u16, open_bus: u8) -> u8 {
if Self::is_wram_window(addr) {
return self.mmc3.read_prg_open_bus(addr, open_bus);
}
if addr < Self::WRAM_START {
return open_bus;
}
self.read_prg(addr)
}
fn read_prg(&self, addr: u16) -> u8 {
if Self::is_wram_window(addr) {
return self.mmc3.read_prg(addr); }
let raw_bank = self.mmc3.mapped_prg_bank(addr);
let final_bank = self.apply_prg_block(raw_bank);
trace_mapper!(3; "[52] PRG read: addr=${:04X} outer=0x{:02X} raw={} -> final={}",
addr, self.outer, raw_bank, final_bank);
let offset = (addr as usize) & Self::PRG_BANK_MASK;
self.mmc3.read_prg_at_bank(final_bank, offset)
}
fn write_prg(&mut self, addr: u16, value: u8) {
if (0x6000..=0x7FFF).contains(&addr) {
if !self.mmc3.is_prg_ram_writable() {
trace_mapper!(2; "[52] $6000 write BLOCKED (WRAM not writable): addr=${:04X} value=0x{:02X}", addr, value);
return;
}
if self.locked {
trace_mapper!(2; "[52] WRAM write (outer locked): addr=${:04X} value=0x{:02X}", addr, value);
self.mmc3.write_prg(addr, value);
} else {
self.locked = (value & 0x80) != 0;
self.outer = value;
let snap = self.mmc3.registers_snapshot();
let (_bs, _r) = (snap[0], &snap[1..=8]);
trace_mapper!(1; "[52] OUTER REG <- 0x{:02X} locked={} (B={} P={} p={} S={} C={} c={} T={}) | mmc3: bs=0x{:02X} chr=[{},{},{},{},{},{}] prg=[{},{}]",
value, self.locked,
(value >> 2) & 1, (value >> 1) & 1, value & 1,
(value >> 3) & 1, (value >> 5) & 1, (value >> 4) & 1, (value >> 6) & 1,
_bs, _r[0], _r[1], _r[2], _r[3], _r[4], _r[5], _r[6], _r[7]);
}
} else {
self.mmc3.write_prg(addr, value);
}
}
fn read_chr(&mut self, addr: u16) -> u8 {
if self.chr_ram_selected() {
return self.chr_ram_8k[(addr as usize) & 0x1FFF];
}
let raw_bank = self.mmc3.raw_chr_1k_bank(addr);
let final_bank = self.apply_chr_block(raw_bank);
let offset = (addr as usize) & Self::CHR_BANK_MASK;
self.mmc3.read_chr_1k_at(final_bank, offset)
}
fn write_chr(&mut self, addr: u16, value: u8) {
if self.chr_ram_selected() {
let index = (addr as usize) & 0x1FFF;
self.chr_ram_8k[index] = value;
return;
}
let raw_bank = self.mmc3.raw_chr_1k_bank(addr);
let final_bank = self.apply_chr_block(raw_bank);
let offset = (addr as usize) & Self::CHR_BANK_MASK;
self.mmc3.write_chr_1k_at(final_bank, offset, value);
}
fn chr_ram_snapshot(&self) -> Vec<u8> {
if self.chr_ram_8k.is_empty() {
self.mmc3.chr_ram_snapshot()
} else {
self.chr_ram_8k.clone()
}
}
fn restore_chr_ram(&mut self, data: &[u8]) {
if self.chr_ram_8k.is_empty() {
self.mmc3.restore_chr_ram(data);
return;
}
let len = data.len().min(self.chr_ram_8k.len());
self.chr_ram_8k[..len].copy_from_slice(&data[..len]);
}
fn mapper_number(&self) -> u16 {
u16::from(Self::MAPPER_NUMBER)
}
fn wram_size(&self) -> usize {
0x2000 }
fn load_wram_snapshot(&mut self, data: &[u8]) {
Mapper::load_wram_snapshot(&mut self.mmc3, data);
}
fn registers_snapshot(&self) -> Vec<u8> {
let mut snap = self.mmc3.registers_snapshot();
snap.push(self.outer);
snap.push(self.locked as u8);
snap
}
fn restore_registers(&mut self, data: &[u8]) {
if data.len() >= 2 {
let (outer_and_lock, mmc3_data) = data.split_at(data.len() - 2);
self.outer = mmc3_data[0];
self.locked = mmc3_data[1] != 0;
self.mmc3.restore_registers(outer_and_lock);
}
}
fn reset(&mut self) {
self.mmc3.reset();
self.outer = 0;
self.locked = false;
}
fn capabilities(&self) -> MapperCapabilities {
MapperCapabilities {
has_irq: true,
has_chr_banking: true,
has_dynamic_mirroring: true,
prg_bank_size_kb: 8,
chr_bank_size_kb: 1,
..Default::default()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cartridge::mapper::{MapperContext, create_mapper};
use crate::cartridge::test_helpers::banked_data;
const PRG_BANKS: usize = 32;
const CHR_1K_BANKS: usize = 512;
fn make_mapper() -> Mapper52 {
let prg = banked_data(8 * 1024, PRG_BANKS);
let chr = banked_data(1024, CHR_1K_BANKS);
Mapper52::new(MapperContext::new_for_test(
52,
prg,
chr,
NametableLayout::Vertical,
))
}
fn make_mapper_with_submapper(submapper: u8, chr: Vec<u8>) -> Mapper52 {
let prg = banked_data(8 * 1024, PRG_BANKS);
Mapper52::new(
MapperContext::new_for_test(52, prg, chr, NametableLayout::Vertical)
.with_submapper(submapper),
)
}
#[test]
fn mapper_52_is_registered() {
let result = create_mapper(MapperContext::new_for_test(
52,
banked_data(8 * 1024, PRG_BANKS),
banked_data(1024, CHR_1K_BANKS),
NametableLayout::Vertical,
));
assert!(result.is_ok(), "Mapper 52 must be registered");
}
#[test]
fn default_outer_zero_passes_mmc3_banks_unchanged() {
let mapper = make_mapper();
assert_eq!(
mapper.read_prg(0xE000),
(PRG_BANKS - 1) as u8,
"Default outer=0: last bank must be passthrough"
);
}
#[test]
fn outer_p_bit_selects_prg_a18() {
let mut mapper = make_mapper();
mapper.write_prg(0x6000, 0x02);
assert_eq!(mapper.outer, 0x02, "Outer register must be set");
}
#[test]
fn lock_bit_prevents_outer_register_update() {
let mut mapper = make_mapper();
mapper.write_prg(0xA001, 0x80); mapper.write_prg(0x6000, 0x80); assert!(mapper.locked, "L=1 must set locked");
mapper.write_prg(0x6000, 0x02); assert_eq!(mapper.outer, 0x80, "Locked register must not change");
}
#[test]
fn reset_clears_lock() {
let mut mapper = make_mapper();
mapper.write_prg(0xA001, 0x80);
mapper.write_prg(0x6000, 0x80); assert!(mapper.locked);
mapper.reset();
assert!(!mapper.locked, "Reset must clear lock");
}
#[test]
fn outer_register_works_when_prg_ram_enabled() {
let mut mapper = make_mapper();
mapper.write_prg(0x6000, 0x02);
assert_eq!(
mapper.outer, 0x02,
"Outer register must update when PRG-RAM is enabled"
);
}
#[test]
fn outer_register_blocked_when_wram_disabled() {
let mut mapper = make_mapper();
mapper.write_prg(0xA001, 0x00); mapper.write_prg(0x6000, 0x02); assert_eq!(
mapper.outer, 0x00,
"Outer register must NOT update when WRAM is disabled"
);
mapper.write_prg(0xA001, 0x80); mapper.write_prg(0x6000, 0x02);
assert_eq!(
mapper.outer, 0x02,
"Outer register must update when WRAM is enabled"
);
}
#[test]
fn outer_register_blocked_when_wram_write_protected() {
let mut mapper = make_mapper();
mapper.write_prg(0xA001, 0xC0); mapper.write_prg(0x6000, 0x02); assert_eq!(
mapper.outer, 0x00,
"Outer register must NOT update when WRAM is write-protected"
);
}
#[test]
fn wram_reads_return_data_written_when_locked() {
let mut mapper = make_mapper();
mapper.write_prg(0x6000, 0x80);
assert!(mapper.locked);
mapper.write_prg(0x6000, 0xAB);
mapper.write_prg(0x6001, 0xCD);
assert_eq!(
mapper.read_prg(0x6000),
0xAB,
"WRAM read must return written value"
);
assert_eq!(
mapper.read_prg(0x6001),
0xCD,
"WRAM read at +1 must return written value"
);
}
#[test]
fn wram_is_not_written_when_setting_outer_register() {
let mut mapper = make_mapper();
mapper.write_prg(0x6000, 0x80); assert_eq!(
mapper.read_prg(0x6000),
0x00,
"Outer register write must not corrupt WRAM"
);
}
#[test]
fn wram_disabled_reads_return_open_bus_value() {
let mut mapper = make_mapper();
mapper.write_prg(0xA001, 0x00);
let open_bus = 0x5A;
assert_eq!(mapper.read_prg_open_bus(0x6000, open_bus), open_bus);
}
#[test]
fn submapper13_chr_ram_is_selected_when_prg_a18_and_a17_are_high() {
let chr_rom = vec![0x55; 1024 * CHR_1K_BANKS];
let mut mapper = make_mapper_with_submapper(13, chr_rom);
mapper.write_prg(0x6000, 0x03);
mapper.write_chr(0x0123, 0xAB);
assert_eq!(
mapper.read_chr(0x0123),
0xAB,
"Submapper 13 must route CHR reads/writes to 8KB CHR-RAM when PRG A18/A17 are both 1"
);
}
#[test]
fn submapper14_bit5_selects_chr_ram() {
let chr_rom = vec![0x33; 1024 * CHR_1K_BANKS];
let mut mapper = make_mapper_with_submapper(14, chr_rom);
mapper.write_prg(0x6000, 0x20);
mapper.write_chr(0x0042, 0xBE);
assert_eq!(
mapper.read_chr(0x0042),
0xBE,
"Submapper 14 must select CHR-RAM when outer bit 5 (R) is set"
);
}
#[test]
fn submapper14_uses_shared_bb_bits_for_chr_a19_a18() {
let chr_1k_banks = 1024;
let mut chr_rom = vec![0; chr_1k_banks * 1024];
for bank in 0..chr_1k_banks {
let value = ((bank >> 8) & 0xFF) as u8;
let start = bank * 1024;
let end = start + 1024;
chr_rom[start..end].fill(value);
}
let mut mapper = make_mapper_with_submapper(14, chr_rom);
mapper.write_prg(0x8000, 0x82);
mapper.write_prg(0x8001, 0x00);
mapper.write_prg(0x6000, 0x42);
assert_eq!(
mapper.read_chr(0x1000),
0x01,
"Submapper 14 must use shared BB bits (outer[2:1]) as CHR A19..A18"
);
}
#[test]
fn submapper0_1mb_profile_uses_legacy_chr_outer_decode() {
let prg = banked_data(8 * 1024, 128); let chr_1k_banks = 1024; let mut chr = vec![0; chr_1k_banks * 1024];
for bank in 0..chr_1k_banks {
let value = ((bank / 128) & 0xFF) as u8;
let start = bank * 1024;
let end = start + 1024;
chr[start..end].fill(value);
}
let mut mapper = Mapper52::new(MapperContext::new_for_test(
52,
prg,
chr,
NametableLayout::Vertical,
));
mapper.write_prg(0x8000, 0x82);
mapper.write_prg(0x8001, 43);
mapper.write_prg(0x6000, 0xCC);
assert_eq!(
mapper.read_chr(0x1000),
2,
"1MiB mapper52 carts must decode CHR outer bits using legacy xABCDEFG mapping"
);
}
}