use crate::nes::cartridge::base_mapper::BaseMapper;
use crate::nes::cartridge::hardware_type::HardwareType;
use crate::nes::cartridge::mmc2_mmc4_latch::{LatchTriggerMode, Mmc2Mmc4Latch};
use crate::nes::cartridge::{Mapper, MapperCapabilities, NametableLayout};
pub struct MMC2Mapper {
base: BaseMapper,
prg_bank_8k: u8,
chr_latch: Mmc2Mmc4Latch,
}
impl MMC2Mapper {
pub fn new(ctx: crate::nes::cartridge::mapper::MapperContext) -> Self {
let has_prg_ram = matches!(ctx.hardware_type, HardwareType::Playchoice10)
|| (ctx.prg_ram_size_specified && ctx.prg_ram_banks_8k > 0);
let capabilities = MapperCapabilities {
has_chr_banking: true,
has_dynamic_mirroring: true,
max_prg_ram_kb: if has_prg_ram { 8 } else { 0 },
prg_bank_size_kb: 8,
chr_bank_size_kb: 4,
..Default::default()
};
let mut base = BaseMapper::new(&ctx, capabilities);
base.configure_prg_banking(8 * 1024);
base.configure_chr_banking(4 * 1024);
base.select_prg_page(0, 0);
base.select_prg_page(1, -3);
base.select_prg_page(2, -2);
base.select_prg_page(3, -1);
base.select_chr_page(0, 0);
base.select_chr_page(1, 0);
Self {
base,
prg_bank_8k: 0,
chr_latch: Mmc2Mmc4Latch::new(LatchTriggerMode::Mmc2),
}
}
fn update_prg_banks(&mut self) {
self.base.select_prg_page(0, self.prg_bank_8k as i16);
}
fn update_chr_pages(&mut self) {
let bank0 = self.chr_latch.selected_bank_0();
let bank1 = self.chr_latch.selected_bank_1();
self.base.select_chr_page(0, bank0 as i16);
self.base.select_chr_page(1, bank1 as i16);
}
fn update_latches_for_chr_read(&mut self, addr: u16) {
self.chr_latch.update_for_chr_read(addr);
}
}
impl Mapper for MMC2Mapper {
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) {
if self.base.try_write_prg_ram(addr, value) {
return;
}
match addr {
0xA000..=0xAFFF => {
self.prg_bank_8k = value & 0x0F;
self.update_prg_banks();
}
0xB000..=0xBFFF => {
self.chr_latch.bank_0_fd = value & 0x1F;
self.update_chr_pages();
}
0xC000..=0xCFFF => {
self.chr_latch.bank_0_fe = value & 0x1F;
self.update_chr_pages();
}
0xD000..=0xDFFF => {
self.chr_latch.bank_1_fd = value & 0x1F;
self.update_chr_pages();
}
0xE000..=0xEFFF => {
self.chr_latch.bank_1_fe = value & 0x1F;
self.update_chr_pages();
}
0xF000..=0xFFFF => {
self.base.set_mirroring_hv((value & 0x01) != 0);
}
_ => {}
}
}
fn read_chr(&mut self, addr: u16) -> u8 {
let value = self.base.read_chr_banked(addr);
self.update_latches_for_chr_read(addr);
self.update_chr_pages();
value
}
fn ppu_address_changed(&mut self, addr: u16) {
let _ = addr;
}
fn registers_snapshot(&self) -> Vec<u8> {
vec![
self.prg_bank_8k,
self.chr_latch.bank_0_fd,
self.chr_latch.bank_0_fe,
self.chr_latch.bank_1_fd,
self.chr_latch.bank_1_fe,
(self.chr_latch.latch0_is_fd as u8) | ((self.chr_latch.latch1_is_fd as u8) << 1),
match self.base.mirroring() {
NametableLayout::Vertical => 0,
NametableLayout::Horizontal => 1,
_ => 1,
},
]
}
fn restore_registers(&mut self, data: &[u8]) {
if data.len() >= 7 {
self.prg_bank_8k = data[0];
self.chr_latch.bank_0_fd = data[1];
self.chr_latch.bank_0_fe = data[2];
self.chr_latch.bank_1_fd = data[3];
self.chr_latch.bank_1_fe = data[4];
self.chr_latch.latch0_is_fd = (data[5] & 1) != 0;
self.chr_latch.latch1_is_fd = (data[5] & 2) != 0;
let mirroring = match data[6] {
0 => NametableLayout::Vertical,
1 => NametableLayout::Horizontal,
_ => NametableLayout::Horizontal,
};
self.base.set_mirroring(mirroring);
self.update_prg_banks();
self.update_chr_pages();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nes::cartridge::mapper::MapperContext;
fn filled_banks(bank_size: usize, banks: usize) -> Vec<u8> {
(0..banks)
.flat_map(|bank| vec![bank as u8; bank_size])
.collect()
}
#[test]
fn test_mmc2_prg_bank_8000_is_switchable_and_upper_banks_are_fixed() {
let prg_banks = 8;
let prg_rom = filled_banks(0x2000, prg_banks);
let chr_rom = filled_banks(0x1000, 8);
let mapper = MMC2Mapper::new(MapperContext::new_for_test(
9,
prg_rom,
chr_rom,
NametableLayout::Vertical,
));
assert_eq!(mapper.read_prg(0x8000), 0);
assert_eq!(mapper.read_prg(0xA000), (prg_banks - 3) as u8);
assert_eq!(mapper.read_prg(0xC000), (prg_banks - 2) as u8);
assert_eq!(mapper.read_prg(0xE000), (prg_banks - 1) as u8);
}
#[test]
fn test_mmc2_registers_snapshot_preserves_latches_and_mirroring() {
let prg_rom = filled_banks(0x2000, 4);
let chr_rom = filled_banks(0x1000, 8);
let mut mapper = MMC2Mapper::new(MapperContext::new_for_test(
9,
prg_rom.clone(),
chr_rom.clone(),
NametableLayout::Vertical,
));
mapper.write_prg(0xA000, 0x03); mapper.write_prg(0xB000, 0x01); mapper.write_prg(0xC000, 0x02); mapper.write_prg(0xD000, 0x03); mapper.write_prg(0xE000, 0x04); mapper.write_prg(0xF000, 0x01);
mapper.read_chr(0x0FD8); mapper.read_chr(0x1FE8);
let saved = mapper.registers_snapshot();
let mut restored = MMC2Mapper::new(MapperContext::new_for_test(
9,
prg_rom,
chr_rom,
NametableLayout::Vertical,
));
restored.restore_registers(&saved);
assert_eq!(restored.get_mirroring(), NametableLayout::Horizontal);
assert_eq!(restored.read_prg(0x8000), 3);
assert_eq!(restored.read_chr(0x0000), 1);
assert_eq!(restored.read_chr(0x1000), 4);
}
#[test]
fn test_mmc2_chr_latches_select_between_fd_and_fe_banks() {
let chr_rom = filled_banks(0x1000, 8);
let prg_rom = filled_banks(0x2000, 8);
let mut mapper = MMC2Mapper::new(MapperContext::new_for_test(
9,
prg_rom,
chr_rom,
NametableLayout::Vertical,
));
mapper.write_prg(0xB000, 1); mapper.write_prg(0xC000, 2); mapper.write_prg(0xD000, 3); mapper.write_prg(0xE000, 4);
assert_eq!(mapper.read_chr(0x0FD8), 2);
assert_eq!(mapper.read_chr(0x0000), 1);
assert_eq!(mapper.read_chr(0x0FE8), 1);
assert_eq!(mapper.read_chr(0x0000), 2);
assert_eq!(mapper.read_chr(0x1FD8), 4);
assert_eq!(mapper.read_chr(0x1000), 3);
assert_eq!(mapper.read_chr(0x1FE8), 3);
assert_eq!(mapper.read_chr(0x1000), 4);
}
#[test]
fn test_mmc2_latch0_only_switches_on_exact_addresses() {
let chr_rom = filled_banks(0x1000, 8);
let prg_rom = filled_banks(0x2000, 8);
let mut mapper = MMC2Mapper::new(MapperContext::new_for_test(
9,
prg_rom,
chr_rom,
NametableLayout::Vertical,
));
mapper.write_prg(0xB000, 1); mapper.write_prg(0xC000, 2);
assert_eq!(mapper.read_chr(0x0000), 2);
assert_eq!(mapper.read_chr(0x0FDF), 2);
assert_eq!(mapper.read_chr(0x0000), 2);
assert_eq!(mapper.read_chr(0x0FD8), 2);
assert_eq!(mapper.read_chr(0x0000), 1);
assert_eq!(mapper.read_chr(0x0FEF), 1);
assert_eq!(mapper.read_chr(0x0000), 1);
assert_eq!(mapper.read_chr(0x0FE8), 1);
assert_eq!(mapper.read_chr(0x0000), 2);
}
#[test]
fn test_mmc2_ppu_address_changed_does_not_switch_latches() {
let chr_rom = filled_banks(0x1000, 8);
let prg_rom = filled_banks(0x2000, 8);
let mut mapper = MMC2Mapper::new(MapperContext::new_for_test(
9,
prg_rom,
chr_rom,
NametableLayout::Vertical,
));
mapper.write_prg(0xB000, 1);
mapper.write_prg(0xC000, 2);
assert_eq!(mapper.read_chr(0x0000), 2);
mapper.ppu_address_changed(0x0FD8);
assert_eq!(mapper.read_chr(0x0000), 2);
}
#[test]
fn test_mmc2_open_bus() {
let mapper = MMC2Mapper::new(MapperContext::new_for_test(
9,
vec![0; 128 * 1024],
vec![0; 128 * 1024],
NametableLayout::Horizontal,
));
assert_eq!(mapper.read_prg_open_bus(0x5000, 0x11), 0x11);
assert_eq!(mapper.read_prg_open_bus(0x5FFF, 0x22), 0x22);
}
#[test]
fn test_mmc2_standard_board_has_no_prg_ram_window() {
let mut mapper = MMC2Mapper::new(
MapperContext::new_for_test(
9,
vec![0; 128 * 1024],
vec![0; 128 * 1024],
NametableLayout::Horizontal,
)
.with_unspecified_prg_ram_size(),
);
mapper.write_prg(0x6000, 0xA5);
assert_eq!(mapper.wram_size(), 0);
assert_eq!(mapper.read_prg_open_bus(0x6000, 0x3C), 0x3C);
assert_eq!(mapper.read_prg_open_bus(0x7FFF, 0x7E), 0x7E);
}
}