use crate::cartridge::BaseMapper;
use crate::cartridge::mapper::{Mapper, MapperCapabilities};
use crate::cartridge::mmc1::MMC1Mapper;
pub struct Mapper105 {
inner: MMC1Mapper,
irq_counter: u8,
irq_reload: u8,
irq_enabled: bool,
irq_pending: bool,
last_chr_bank_0: u8,
}
impl Mapper105 {
const SNAPSHOT_SIZE: usize = 5;
const CHR_BANK_MASK: u8 = 0x1F;
const MMC1_WRITE_COMPLETE_COUNT: u8 = 4;
const MMC1_CHR_BANK0_REGISTER_ADDR: u16 = 0xA000;
const MMC1_WRITE_COUNT_IDX: usize = 1;
const MMC1_CHR_BANK0_IDX: usize = 3;
const MMC1_LAST_CHR_REG_ADDR_LO_IDX: usize = 23;
const MMC1_LAST_CHR_REG_ADDR_HI_IDX: usize = 24;
const MMC1_MIN_REG_SNAPSHOT_SIZE: usize = Self::MMC1_LAST_CHR_REG_ADDR_HI_IDX + 1;
pub fn new(ctx: super::mapper::MapperContext) -> Self {
let mut inner = MMC1Mapper::new(ctx);
Self::force_timer_disable_bit_on_powerup(&mut inner);
let chr_bank_0 = Self::chr_bank_0_from_mapper(&inner);
let mut mapper = Self {
inner,
irq_counter: 0,
irq_reload: 0,
irq_enabled: false,
irq_pending: false,
last_chr_bank_0: chr_bank_0,
};
mapper.apply_timer_control(chr_bank_0);
mapper
}
fn force_timer_disable_bit_on_powerup(inner: &mut MMC1Mapper) {
let mut regs = inner.registers_snapshot();
if regs.len() >= 4 {
regs[3] |= 0x10;
inner.restore_registers(®s);
}
}
fn chr_bank_0_from_mapper(inner: &MMC1Mapper) -> u8 {
inner
.registers_snapshot()
.get(Self::MMC1_CHR_BANK0_IDX)
.copied()
.unwrap_or(0)
& Self::CHR_BANK_MASK
}
fn apply_timer_control(&mut self, chr_bank_0: u8) {
self.irq_reload = chr_bank_0 & 0x0F;
if (chr_bank_0 & 0x10) != 0 {
self.irq_enabled = false;
self.irq_pending = false;
self.irq_counter = 0;
} else {
self.irq_enabled = true;
self.irq_pending = false;
self.irq_counter = self.irq_reload.max(1);
}
}
fn maybe_update_timer_from_chr_register(&mut self) {
let chr_bank_0 = Self::chr_bank_0_from_mapper(&self.inner);
if chr_bank_0 != self.last_chr_bank_0 {
self.last_chr_bank_0 = chr_bank_0;
self.apply_timer_control(chr_bank_0);
}
}
fn apply_timer_on_chr_bank0_commit(&mut self, before: &[u8], after: &[u8]) {
if before.len() < Self::MMC1_MIN_REG_SNAPSHOT_SIZE
|| after.len() < Self::MMC1_MIN_REG_SNAPSHOT_SIZE
{
return;
}
let committed = before[Self::MMC1_WRITE_COUNT_IDX] == Self::MMC1_WRITE_COMPLETE_COUNT
&& after[Self::MMC1_WRITE_COUNT_IDX] == 0;
let committed_reg = u16::from_le_bytes([
after[Self::MMC1_LAST_CHR_REG_ADDR_LO_IDX],
after[Self::MMC1_LAST_CHR_REG_ADDR_HI_IDX],
]);
if committed && committed_reg == Self::MMC1_CHR_BANK0_REGISTER_ADDR {
self.last_chr_bank_0 = after[Self::MMC1_CHR_BANK0_IDX] & Self::CHR_BANK_MASK;
self.apply_timer_control(self.last_chr_bank_0);
}
}
fn tick_irq_timer(&mut self) {
if !self.irq_enabled {
return;
}
if self.irq_counter > 0 {
self.irq_counter -= 1;
}
if self.irq_counter == 0 {
self.irq_pending = true;
self.irq_enabled = false;
}
}
}
impl Mapper for Mapper105 {
fn base(&self) -> &BaseMapper {
self.inner.base()
}
fn base_mut(&mut self) -> &mut BaseMapper {
self.inner.base_mut()
}
fn read_prg(&self, addr: u16) -> u8 {
self.inner.read_prg(addr)
}
fn read_prg_open_bus(&self, addr: u16, open_bus: u8) -> u8 {
self.inner.read_prg_open_bus(addr, open_bus)
}
fn write_prg(&mut self, addr: u16, value: u8) {
let before = self.inner.registers_snapshot();
self.inner.write_prg(addr, value);
let after = self.inner.registers_snapshot();
self.apply_timer_on_chr_bank0_commit(&before, &after);
}
fn write_chr(&mut self, addr: u16, value: u8) {
self.inner.write_chr(addr, value);
}
fn read_chr(&mut self, addr: u16) -> u8 {
self.inner.read_chr(addr)
}
fn cpu_cycle(&mut self) {
self.inner.cpu_cycle();
self.tick_irq_timer();
}
fn irq_pending(&self) -> bool {
self.irq_pending
}
fn get_mirroring(&self) -> crate::cartridge::NametableLayout {
self.inner.get_mirroring()
}
fn wram_size(&self) -> usize {
self.inner.wram_size()
}
fn wram_snapshot(&self) -> Vec<u8> {
self.inner.wram_snapshot()
}
fn load_wram_snapshot(&mut self, data: &[u8]) {
self.inner.load_wram_snapshot(data);
}
fn initialize_ram(&mut self, mode: crate::console::RamInitMode) {
self.inner.initialize_ram(mode);
}
fn registers_snapshot(&self) -> Vec<u8> {
let mut snap =
Vec::with_capacity(Self::SNAPSHOT_SIZE + self.inner.registers_snapshot().len());
snap.push(self.irq_enabled as u8);
snap.push(self.irq_pending as u8);
snap.push(self.last_chr_bank_0);
snap.push(self.irq_counter);
snap.push(self.irq_reload);
snap.extend(self.inner.registers_snapshot());
snap
}
fn restore_registers(&mut self, data: &[u8]) {
if data.len() >= Self::SNAPSHOT_SIZE {
self.irq_enabled = data[0] != 0;
self.irq_pending = data[1] != 0;
self.last_chr_bank_0 = data[2] & Self::CHR_BANK_MASK;
self.irq_counter = data[3];
self.irq_reload = data[4];
self.inner.restore_registers(&data[Self::SNAPSHOT_SIZE..]);
self.maybe_update_timer_from_chr_register();
}
}
fn reset(&mut self) {
self.inner.reset();
Self::force_timer_disable_bit_on_powerup(&mut self.inner);
self.last_chr_bank_0 = Self::chr_bank_0_from_mapper(&self.inner);
self.apply_timer_control(self.last_chr_bank_0);
}
fn capabilities(&self) -> MapperCapabilities {
let mut caps = self.inner.capabilities();
caps.has_irq = true;
caps
}
}
#[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_16K: usize = 11;
const CHR_BANKS_4K: usize = 9;
fn write_mmc1_register<M: Mapper + ?Sized>(mapper: &mut M, addr: u16, value: u8) {
for bit in 0..5 {
mapper.cpu_cycle();
mapper.cpu_cycle();
mapper.write_prg(addr, (value >> bit) & 0x01);
}
}
fn make_mapper() -> Box<dyn Mapper> {
let prg_rom = banked_data(16 * 1024, PRG_BANKS_16K);
let chr_rom = banked_data(4 * 1024, CHR_BANKS_4K);
create_mapper(MapperContext::new_for_test(
105,
prg_rom,
chr_rom,
NametableLayout::Horizontal,
))
.expect("Mapper 105 should be implemented")
}
#[test]
fn mapper_105_is_registered() {
let prg_rom = banked_data(16 * 1024, PRG_BANKS_16K);
let chr_rom = banked_data(4 * 1024, CHR_BANKS_4K);
let result = create_mapper(MapperContext::new_for_test(
105,
prg_rom,
chr_rom,
NametableLayout::Horizontal,
));
assert!(result.is_ok(), "Mapper 105 must be registered");
}
#[test]
fn mapper_105_prg_bank_switching_matches_mmc1_mode_3_at_8000_bfff() {
let mut mapper = make_mapper();
write_mmc1_register(mapper.as_mut(), 0xE000, 5);
assert_eq!(mapper.read_prg(0x8000), 5);
}
#[test]
fn mapper_105_mirroring_modes_are_selectable_via_control_register() {
let mut mapper = make_mapper();
write_mmc1_register(mapper.as_mut(), 0x8000, 0b00000);
assert_eq!(mapper.get_mirroring(), NametableLayout::SingleScreenLower);
write_mmc1_register(mapper.as_mut(), 0x8000, 0b00001);
assert_eq!(mapper.get_mirroring(), NametableLayout::SingleScreenUpper);
write_mmc1_register(mapper.as_mut(), 0x8000, 0b00010);
assert_eq!(mapper.get_mirroring(), NametableLayout::Vertical);
write_mmc1_register(mapper.as_mut(), 0x8000, 0b00011);
assert_eq!(mapper.get_mirroring(), NametableLayout::Horizontal);
}
#[test]
fn mapper_105_irq_fires_after_configured_cycle_count() {
let mut mapper = make_mapper();
write_mmc1_register(mapper.as_mut(), 0xA000, 0b00011);
assert!(!mapper.irq_pending());
mapper.cpu_cycle();
mapper.cpu_cycle();
assert!(
!mapper.irq_pending(),
"IRQ should not fire before countdown"
);
mapper.cpu_cycle();
assert!(
mapper.irq_pending(),
"IRQ should fire when countdown reaches zero"
);
write_mmc1_register(mapper.as_mut(), 0xA000, 0b10011);
assert!(
!mapper.irq_pending(),
"setting timer control bit must clear pending IRQ"
);
}
#[test]
fn mapper_105_timer_bit4_toggle_and_zero_reload_behavior() {
let mut mapper = make_mapper();
write_mmc1_register(mapper.as_mut(), 0xA000, 0b00000);
assert!(!mapper.irq_pending());
mapper.cpu_cycle();
assert!(
mapper.irq_pending(),
"reload 0 should be treated as 1 cycle"
);
write_mmc1_register(mapper.as_mut(), 0xA000, 0b10000);
assert!(!mapper.irq_pending());
mapper.cpu_cycle();
assert!(
!mapper.irq_pending(),
"timer should remain disabled while bit4 is set"
);
}
#[test]
fn mapper_105_rewriting_same_chr_bank0_value_rearms_and_clears_irq() {
let mut mapper = make_mapper();
write_mmc1_register(mapper.as_mut(), 0xA000, 0b00001);
mapper.cpu_cycle();
assert!(mapper.irq_pending(), "initial timer arm should fire IRQ");
write_mmc1_register(mapper.as_mut(), 0xA000, 0b00001);
assert!(
!mapper.irq_pending(),
"same-value rewrite must clear pending IRQ and restart timer"
);
mapper.cpu_cycle();
assert!(
mapper.irq_pending(),
"re-armed timer should fire again after configured countdown"
);
}
}