use crate::cartridge::BaseMapper;
use crate::cartridge::common::ChrMemory;
use crate::cartridge::mapper::{Mapper, MapperCapabilities};
pub struct Mapper51 {
base: BaseMapper,
bank: u8,
mode: u8,
}
const BAD_DUMP_CHR_ROM_ON_CHR_RAM_CRC32: u32 = 0xA912_B064;
impl Mapper51 {
pub fn new(ctx: super::mapper::MapperContext) -> Self {
let submapper = ctx.submapper;
let crc32 = ctx.crc32;
let chr_rom_data = ctx.chr_rom.clone();
let capabilities = MapperCapabilities {
has_dynamic_mirroring: true,
prg_bank_size_kb: 8,
chr_bank_size_kb: 8,
..Default::default()
};
let mut base = BaseMapper::new(&ctx, capabilities);
base.configure_prg_banking(0x2000); base.configure_prg_6000_banking();
if submapper == 0 && crc32 == BAD_DUMP_CHR_ROM_ON_CHR_RAM_CRC32 {
let mut chr_memory = ChrMemory::new_ram(0x2000);
if !chr_rom_data.is_empty() {
chr_memory.load_snapshot(&chr_rom_data);
}
base.set_chr_memory(chr_memory);
}
let mut mapper = Self {
base,
bank: 0,
mode: 1,
};
mapper.update_banks();
mapper
}
fn decode_mode(value: u8) -> u8 {
((value >> 3) & 0x02) | ((value >> 1) & 0x01)
}
fn update_banks(&mut self) {
let bank = self.bank as usize;
let prg_6000 = if self.mode & 0x01 != 0 {
(0x23 | (bank << 2)) as i16
} else {
(0x2F | (bank << 2)) as i16
};
self.base.select_prg_6000_page(prg_6000);
if self.mode & 0x01 != 0 {
self.base.select_prg_page(0, (bank << 2) as i16);
self.base.select_prg_page(1, ((bank << 2) | 1) as i16);
self.base.select_prg_page(2, ((bank << 2) | 2) as i16);
self.base.select_prg_page(3, ((bank << 2) | 3) as i16);
} else {
let mode = self.mode as usize;
self.base.select_prg_page(0, ((bank << 2) | mode) as i16);
self.base
.select_prg_page(1, (((bank << 2) | mode) + 1) as i16);
self.base.select_prg_page(2, ((bank << 2) | 0x0E) as i16);
self.base.select_prg_page(3, ((bank << 2) | 0x0F) as i16);
}
self.base.set_mirroring_hv(self.mode == 0x03);
}
}
impl Mapper for Mapper51 {
fn base(&self) -> &BaseMapper {
&self.base
}
fn base_mut(&mut self) -> &mut BaseMapper {
&mut self.base
}
fn write_prg(&mut self, addr: u16, value: u8) {
match addr {
0x6000..=0x7FFF => {
self.mode = Self::decode_mode(value);
self.update_banks();
}
0xC000..=0xDFFF => {
self.bank = value & 0x0F;
self.mode = (Self::decode_mode(value) & 0x02) | (self.mode & 0x01);
self.update_banks();
}
0x8000..=0xBFFF | 0xE000..=0xFFFF => {
self.bank = value & 0x0F;
self.update_banks();
}
_ => {}
}
}
fn read_prg_open_bus(&self, addr: u16, open_bus: u8) -> u8 {
match addr {
0x0000..=0x5FFF => open_bus,
_ => self.read_prg(addr),
}
}
fn registers_snapshot(&self) -> Vec<u8> {
vec![self.bank, self.mode]
}
fn restore_registers(&mut self, data: &[u8]) {
if data.len() >= 2 {
self.bank = data[0];
self.mode = data[1];
self.update_banks();
}
}
fn reset(&mut self) {
self.bank = 0;
self.mode = 1;
self.update_banks();
}
}
#[cfg(test)]
mod tests {
use super::Mapper51;
use crate::cartridge::NametableLayout;
use crate::cartridge::mapper::{Mapper, MapperContext, create_mapper};
use crate::cartridge::test_helpers::banked_data;
const PRG_BANKS: usize = 48;
fn make_mapper() -> Box<dyn Mapper> {
let prg = banked_data(8 * 1024, PRG_BANKS);
let chr = banked_data(8 * 1024, 1);
create_mapper(MapperContext::new_for_test(
51,
prg,
chr,
NametableLayout::Vertical,
))
.expect("Mapper 51 should be implemented")
}
fn make_mapper_direct() -> Mapper51 {
let prg = banked_data(8 * 1024, PRG_BANKS);
let chr = banked_data(8 * 1024, 1);
Mapper51::new(MapperContext::new_for_test(
51,
prg,
chr,
NametableLayout::Horizontal,
))
}
fn make_mapper_with_submapper(submapper: u8, chr: Vec<u8>, crc32: u32) -> Box<dyn Mapper> {
let prg = banked_data(8 * 1024, PRG_BANKS);
let mut ctx = MapperContext::new_for_test(51, prg, chr, NametableLayout::Vertical)
.with_submapper(submapper);
ctx.crc32 = crc32;
create_mapper(ctx).expect("Mapper 51 should be implemented")
}
#[test]
fn mapper_51_is_registered_in_factory() {
let result = create_mapper(MapperContext::new_for_test(
51,
banked_data(8 * 1024, PRG_BANKS),
banked_data(8 * 1024, 1),
NametableLayout::Vertical,
));
assert!(
result.is_ok(),
"Mapper 51 must be registered in the factory"
);
}
#[test]
fn default_6000_window_is_fixed_bank_35() {
let mapper = make_mapper();
assert_eq!(
mapper.read_prg(0x6000),
35,
"$6000 should read bank 35 by default"
);
}
#[test]
fn default_32kb_mode_maps_8000_to_bank_0() {
let mapper = make_mapper();
assert_eq!(
mapper.read_prg(0x8000),
0,
"$8000 should read bank 0 in default 32KB mode"
);
}
#[test]
fn default_32kb_mode_maps_a000_to_bank_1() {
let mapper = make_mapper();
assert_eq!(
mapper.read_prg(0xA000),
1,
"$A000 should read bank 1 in default 32KB mode"
);
}
#[test]
fn default_32kb_mode_maps_c000_to_bank_2() {
let mapper = make_mapper();
assert_eq!(
mapper.read_prg(0xC000),
2,
"$C000 should read bank 2 in default 32KB mode"
);
}
#[test]
fn default_32kb_mode_maps_e000_to_bank_3() {
let mapper = make_mapper();
assert_eq!(
mapper.read_prg(0xE000),
3,
"$E000 should read bank 3 in default 32KB mode"
);
}
#[test]
fn prg_register_selects_32kb_bank() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 1);
assert_eq!(
mapper.read_prg(0x8000),
4,
"$8000 should read bank 4 after prg=1"
);
assert_eq!(
mapper.read_prg(0xA000),
5,
"$A000 should read bank 5 after prg=1"
);
assert_eq!(
mapper.read_prg(0xC000),
6,
"$C000 should read bank 6 after prg=1"
);
assert_eq!(
mapper.read_prg(0xE000),
7,
"$E000 should read bank 7 after prg=1"
);
}
#[test]
fn prg_register_updates_6000_bank_in_32kb_mode() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 1);
assert_eq!(
mapper.read_prg(0x6000),
39,
"$6000 should read bank 39 when prg=1 in 32KB mode"
);
}
#[test]
fn mode_bit1_clear_switches_to_16kb_mode() {
let mut mapper = make_mapper();
mapper.write_prg(0x6000, 0x00);
assert_eq!(mapper.read_prg(0x8000), 0, "16KB mode $8000 reads bank 0");
assert_eq!(mapper.read_prg(0xA000), 1, "16KB mode $A000 reads bank 1");
assert_eq!(mapper.read_prg(0xC000), 14, "16KB mode $C000 reads bank 14");
assert_eq!(mapper.read_prg(0xE000), 15, "16KB mode $E000 reads bank 15");
}
#[test]
fn mode_bit1_clear_6000_window_is_bank_47() {
let mut mapper = make_mapper();
mapper.write_prg(0x6000, 0x00);
assert_eq!(
mapper.read_prg(0x6000),
47,
"$6000 in 16KB mode with prg=0 should read bank 47"
);
}
#[test]
fn default_mirroring_is_vertical_regardless_of_header() {
let mapper = make_mapper_direct();
assert_eq!(
mapper.get_mirroring(),
NametableLayout::Vertical,
"Default mode=1 must produce Vertical mirroring"
);
}
#[test]
fn mode_bit4_set_gives_horizontal_mirroring() {
let mut mapper = make_mapper_direct();
mapper.write_prg(0x6000, 0x12);
assert_eq!(
mapper.get_mirroring(),
NametableLayout::Horizontal,
"mode bit 4 set must produce Horizontal mirroring"
);
}
#[test]
fn mode_bit4_clear_gives_vertical_mirroring() {
let mut mapper = make_mapper_direct();
mapper.write_prg(0x6000, 0x00);
assert_eq!(
mapper.get_mirroring(),
NametableLayout::Vertical,
"mode bit 4 clear must produce Vertical mirroring"
);
}
#[test]
fn chr_reads_from_fixed_bank_0() {
let prg = banked_data(8 * 1024, PRG_BANKS);
let chr = banked_data(8 * 1024, 2); let mut mapper = Mapper51::new(MapperContext::new_for_test(
51,
prg,
chr,
NametableLayout::Vertical,
));
assert_eq!(mapper.read_chr(0x0000), 0, "CHR must read from bank 0");
assert_eq!(mapper.read_chr(0x1FFF), 0, "CHR end must still be bank 0");
}
#[test]
fn submapper0_with_chr_rom_header_data_still_uses_chr_ram() {
let mut mapper = make_mapper_with_submapper(0, vec![0xAA; 8 * 1024], 0xA912_B064);
assert_eq!(mapper.read_chr(0x0010), 0xAA);
mapper.write_chr(0x0010, 0x5C);
assert_eq!(
mapper.read_chr(0x0010),
0x5C,
"submapper 0 compatibility should keep CHR writable"
);
}
#[test]
fn submapper0_non_compat_crc_with_chr_rom_stays_read_only() {
let mut mapper = make_mapper_with_submapper(0, vec![0xAA; 8 * 1024], 0x1234_5678);
assert_eq!(mapper.read_chr(0x0010), 0xAA);
mapper.write_chr(0x0010, 0x5C);
assert_eq!(
mapper.read_chr(0x0010),
0xAA,
"non-compat CRC should follow normal read-only CHR-ROM behavior"
);
}
#[test]
fn reset_restores_default_state() {
let mut mapper = make_mapper_direct();
mapper.write_prg(0x8000, 5);
mapper.write_prg(0x6000, 0x00);
mapper.reset();
assert_eq!(
mapper.read_prg(0x8000),
0,
"After reset, $8000 should read bank 0"
);
assert_eq!(
mapper.read_prg(0x6000),
35,
"After reset, $6000 should read bank 35"
);
}
#[test]
fn c000_write_updates_bank_register() {
let mut mapper = make_mapper();
mapper.write_prg(0xC000, 1); assert_eq!(
mapper.read_prg(0x8000),
4,
"$C000 write with value 1 must set bank=1"
);
}
#[test]
fn c000_write_updates_mode_bit1_from_value_bit4() {
let mut mapper = make_mapper_direct();
mapper.write_prg(0xC000, 0x12); assert_eq!(
mapper.get_mirroring(),
NametableLayout::Horizontal,
"$C000 write with bit4 set must update mode bit1 → Horizontal"
);
}
#[test]
fn c000_write_preserves_mode_bit0() {
let mut mapper = make_mapper_direct();
mapper.write_prg(0x6000, 0x00); mapper.write_prg(0xC000, 0x10); assert_eq!(
mapper.get_mirroring(),
NametableLayout::Vertical,
"$C000 write with only bit4 set must not produce Horizontal (needs both bits)"
);
}
#[test]
fn mode_bit4_alone_without_mode_bit0_is_not_horizontal() {
let mut mapper = make_mapper_direct();
mapper.write_prg(0x6000, 0x10);
assert_eq!(
mapper.get_mirroring(),
NametableLayout::Vertical,
"Bit 4 alone in mode write must not produce Horizontal mirroring"
);
}
#[test]
fn bank_register_is_masked_to_4_bits() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0x1F);
assert_eq!(mapper.read_prg(0x8000), 12, "Bank must be masked to 4 bits");
}
#[test]
fn mode_write_bit4_selects_8000_offset_in_16kb_mode() {
let mut mapper = make_mapper();
mapper.write_prg(0x6000, 0x10); assert_eq!(
mapper.read_prg(0x8000),
2,
"16KB mode with mode=2: $8000 must be page 2"
);
assert_eq!(
mapper.read_prg(0xA000),
3,
"16KB mode with mode=2: $A000 must be page 3"
);
}
#[test]
fn registers_snapshot_and_restore() {
let mut mapper = make_mapper_direct();
mapper.write_prg(0x8000, 2); mapper.write_prg(0x6000, 0x00); let snap = mapper.registers_snapshot();
let mut restored = make_mapper_direct();
restored.restore_registers(&snap);
assert_eq!(
restored.read_prg(0x8000),
mapper.read_prg(0x8000),
"Restored PRG bank must match"
);
assert_eq!(
restored.read_prg(0x6000),
mapper.read_prg(0x6000),
"Restored $6000 bank must match"
);
assert_eq!(
restored.get_mirroring(),
mapper.get_mirroring(),
"Restored mirroring must match"
);
}
}