use crate::nes::cartridge::BaseMapper;
use crate::nes::cartridge::mapper::{Mapper, MapperCapabilities};
pub struct Vrc3Mapper {
base: BaseMapper,
prg_bank: u8,
irq_latch: u16,
irq_counter: u16,
irq_mode_8bit: bool,
pub(crate) irq_enable: bool,
irq_enable_on_ack: bool,
irq_pending: bool,
}
impl Vrc3Mapper {
const PRG_BANK_SIZE: usize = 0x4000;
pub fn new(ctx: crate::nes::cartridge::mapper::MapperContext) -> Self {
let mirroring = ctx.mirroring;
let capabilities = MapperCapabilities {
has_irq: true,
max_prg_ram_kb: 8,
prg_bank_size_kb: 16,
..Default::default()
};
let mut base = BaseMapper::new(&ctx, capabilities);
base.configure_prg_banking(Self::PRG_BANK_SIZE);
base.set_mirroring(mirroring);
let mut mapper = Self {
base,
prg_bank: 0,
irq_latch: 0,
irq_counter: 0,
irq_mode_8bit: false,
irq_enable: false,
irq_enable_on_ack: false,
irq_pending: false,
};
mapper.update_banks();
mapper
}
fn update_banks(&mut self) {
self.base.select_prg_page(0, self.prg_bank as i16);
self.base.select_prg_page(1, -1); }
fn acknowledge_irq(&mut self) {
self.irq_pending = false;
self.irq_enable = self.irq_enable_on_ack;
}
fn tick_counter(&mut self) {
if self.irq_mode_8bit {
let low = (self.irq_counter & 0x00FF) as u8;
let (new_low, overflow) = low.overflowing_add(1);
self.irq_counter = (self.irq_counter & 0xFF00) | (new_low as u16);
if overflow {
self.irq_pending = true;
self.irq_counter = (self.irq_counter & 0xFF00) | (self.irq_latch & 0x00FF);
}
} else {
let (new_counter, overflow) = self.irq_counter.overflowing_add(1);
self.irq_counter = new_counter;
if overflow {
self.irq_pending = true;
self.irq_counter = self.irq_latch;
}
}
}
}
impl Mapper for Vrc3Mapper {
fn base(&self) -> &BaseMapper {
&self.base
}
fn base_mut(&mut self) -> &mut BaseMapper {
&mut self.base
}
fn read_prg(&self, addr: u16) -> u8 {
match addr {
0x6000..=0x7FFF => self.base.try_read_prg_ram(addr).unwrap_or(0),
0x8000..=0xFFFF => self.base.read_prg_banked(addr),
_ => 0,
}
}
fn write_prg(&mut self, addr: u16, value: u8) {
match addr {
0x6000..=0x7FFF => {
self.base.try_write_prg_ram(addr, value);
}
0x8000..=0x8FFF => {
self.irq_latch = (self.irq_latch & 0xFFF0) | ((value as u16) & 0x000F);
}
0x9000..=0x9FFF => {
self.irq_latch = (self.irq_latch & 0xFF0F) | (((value as u16) & 0x000F) << 4);
}
0xA000..=0xAFFF => {
self.irq_latch = (self.irq_latch & 0xF0FF) | (((value as u16) & 0x000F) << 8);
}
0xB000..=0xBFFF => {
self.irq_latch = (self.irq_latch & 0x0FFF) | (((value as u16) & 0x000F) << 12);
}
0xC000..=0xCFFF => {
self.irq_enable_on_ack = (value & 0x01) != 0;
self.irq_mode_8bit = (value & 0x04) != 0;
self.irq_pending = false;
let enable = (value & 0x02) != 0;
self.irq_enable = enable;
if enable {
self.irq_counter = self.irq_latch;
}
}
0xD000..=0xDFFF => {
self.acknowledge_irq();
}
0xF000..=0xFFFF => {
self.prg_bank = value & 0x07;
self.update_banks();
}
_ => {}
}
}
fn cpu_cycle(&mut self) {
if !self.irq_enable {
return;
}
self.tick_counter();
}
fn irq_pending(&self) -> bool {
self.irq_pending
}
fn registers_snapshot(&self) -> Vec<u8> {
let flags = (self.irq_mode_8bit as u8)
| ((self.irq_enable as u8) << 1)
| ((self.irq_enable_on_ack as u8) << 2)
| ((self.irq_pending as u8) << 3);
vec![
self.prg_bank,
(self.irq_latch & 0xFF) as u8,
(self.irq_latch >> 8) as u8,
(self.irq_counter & 0xFF) as u8,
(self.irq_counter >> 8) as u8,
flags,
]
}
fn restore_registers(&mut self, data: &[u8]) {
if data.len() >= 6 {
self.prg_bank = data[0];
self.irq_latch = (data[1] as u16) | ((data[2] as u16) << 8);
self.irq_counter = (data[3] as u16) | ((data[4] as u16) << 8);
self.irq_mode_8bit = (data[5] & 0x01) != 0;
self.irq_enable = (data[5] & 0x02) != 0;
self.irq_enable_on_ack = (data[5] & 0x04) != 0;
self.irq_pending = (data[5] & 0x08) != 0;
self.update_banks();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nes::cartridge::NametableLayout;
use crate::nes::cartridge::mapper::{MapperContext, create_mapper};
use crate::nes::cartridge::test_helpers::banked_data;
const PRG_BANKS: usize = 3;
fn make_mapper() -> Vrc3Mapper {
let prg = banked_data(16 * 1024, PRG_BANKS);
Vrc3Mapper::new(MapperContext::new_for_test(
73,
prg,
vec![],
NametableLayout::Horizontal,
))
}
#[test]
fn mapper_73_is_registered() {
let result = create_mapper(MapperContext::new_for_test(
73,
banked_data(16 * 1024, PRG_BANKS),
vec![],
NametableLayout::Horizontal,
));
assert!(result.is_ok(), "Mapper 73 must be registered");
}
#[test]
fn power_on_prg_bank_at_8000_is_bank_0() {
let mapper = make_mapper();
assert_eq!(mapper.read_prg(0x8000), 0, "$8000 must start at bank 0");
}
#[test]
fn prg_c000_is_fixed_to_last_bank() {
let mapper = make_mapper();
assert_eq!(
mapper.read_prg(0xC000),
(PRG_BANKS - 1) as u8,
"$C000 must always map to last bank"
);
}
#[test]
fn prg_bank_select_via_f000_register() {
let mut mapper = make_mapper();
mapper.write_prg(0xF000, 1);
assert_eq!(
mapper.read_prg(0x8000),
1,
"$8000 must switch to bank 1 after writing 1 to $F000"
);
assert_eq!(
mapper.read_prg(0xC000),
(PRG_BANKS - 1) as u8,
"$C000 must still be last bank after bank switch"
);
}
#[test]
fn prg_ram_readable_and_writable_at_6000() {
let mut mapper = make_mapper();
mapper.write_prg(0x6000, 0xAB);
assert_eq!(mapper.read_prg(0x6000), 0xAB);
mapper.write_prg(0x7FFF, 0xCD);
assert_eq!(mapper.read_prg(0x7FFF), 0xCD);
}
#[test]
fn chr_ram_readable_and_writable() {
let mut mapper = make_mapper();
mapper.write_chr(0x0000, 0x42);
assert_eq!(mapper.read_chr(0x0000), 0x42);
mapper.write_chr(0x1FFF, 0x77);
assert_eq!(mapper.read_chr(0x1FFF), 0x77);
}
#[test]
fn irq_not_pending_on_power_on() {
let mapper = make_mapper();
assert!(!mapper.irq_pending(), "IRQ must not be pending on power-on");
}
#[test]
fn irq_fires_after_n_cycles_in_16bit_mode() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0x0D); mapper.write_prg(0x9000, 0x0F); mapper.write_prg(0xA000, 0x0F); mapper.write_prg(0xB000, 0x0F); mapper.write_prg(0xC000, 0x02); assert!(!mapper.irq_pending());
mapper.cpu_cycle(); assert!(!mapper.irq_pending());
mapper.cpu_cycle(); assert!(!mapper.irq_pending());
mapper.cpu_cycle(); assert!(mapper.irq_pending(), "IRQ must fire after 3 cycles");
}
#[test]
fn irq_fires_faster_in_8bit_mode() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0x0D); mapper.write_prg(0x9000, 0x0F); mapper.write_prg(0xA000, 0x00);
mapper.write_prg(0xB000, 0x00);
mapper.write_prg(0xC000, 0x06); assert!(!mapper.irq_pending());
mapper.cpu_cycle(); assert!(!mapper.irq_pending());
mapper.cpu_cycle(); assert!(!mapper.irq_pending());
mapper.cpu_cycle(); assert!(
mapper.irq_pending(),
"IRQ must fire in 8-bit mode after 3 cycles"
);
}
#[test]
fn irq_acknowledge_via_d000() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0x0E);
mapper.write_prg(0x9000, 0x0F);
mapper.write_prg(0xA000, 0x0F);
mapper.write_prg(0xB000, 0x0F);
mapper.write_prg(0xC000, 0x02); mapper.cpu_cycle();
mapper.cpu_cycle(); assert!(mapper.irq_pending());
mapper.write_prg(0xD000, 0); assert!(
!mapper.irq_pending(),
"IRQ must be cleared after $D000 write"
);
}
#[test]
fn irq_re_enable_via_d000_using_a_bit() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0x0E);
mapper.write_prg(0x9000, 0x0F);
mapper.write_prg(0xA000, 0x0F);
mapper.write_prg(0xB000, 0x0F);
mapper.write_prg(0xC000, 0x03); mapper.cpu_cycle();
mapper.cpu_cycle(); assert!(mapper.irq_pending());
mapper.write_prg(0xD000, 0);
assert!(!mapper.irq_pending());
assert!(mapper.irq_enable, "E must be set from A after $D000 ack");
}
#[test]
fn irq_reloads_on_c000_with_e_set() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0x05); mapper.write_prg(0x9000, 0x00);
mapper.write_prg(0xA000, 0x00);
mapper.write_prg(0xB000, 0x00); mapper.write_prg(0xC000, 0x02); assert_eq!(
mapper.irq_counter, 0x0005,
"counter must be loaded from latch"
);
}
#[test]
fn irq_counter_not_incremented_when_disabled() {
let mut mapper = make_mapper();
for _ in 0..100 {
mapper.cpu_cycle();
}
assert_eq!(
mapper.irq_counter, 0,
"Counter must not increment when disabled"
);
assert!(!mapper.irq_pending());
}
#[test]
fn registers_snapshot_round_trips() {
let mut original = make_mapper();
original.write_prg(0xF000, 2); original.write_prg(0x8000, 0x0A); original.write_prg(0x9000, 0x0B); original.write_prg(0xA000, 0x0C); original.write_prg(0xB000, 0x0D); original.write_prg(0xC000, 0x07); original.cpu_cycle();
let snap = original.registers_snapshot();
let mut restored = make_mapper();
restored.restore_registers(&snap);
assert_eq!(restored.prg_bank, original.prg_bank);
assert_eq!(restored.irq_latch, original.irq_latch);
assert_eq!(restored.irq_counter, original.irq_counter);
assert_eq!(restored.irq_mode_8bit, original.irq_mode_8bit);
assert_eq!(restored.irq_enable, original.irq_enable);
assert_eq!(restored.irq_enable_on_ack, original.irq_enable_on_ack);
assert_eq!(restored.irq_pending, original.irq_pending);
}
#[test]
fn wram_snapshot_round_trip() {
let mut mapper = make_mapper();
mapper.write_prg(0x6000, 0x11);
mapper.write_prg(0x6100, 0x22);
mapper.write_prg(0x7FFF, 0x33);
let snapshot = mapper.wram_snapshot();
assert_eq!(snapshot.len(), 8192);
assert_eq!(snapshot[0x0000], 0x11);
assert_eq!(snapshot[0x0100], 0x22);
assert_eq!(snapshot[0x1FFF], 0x33);
let mut mapper2 = make_mapper();
mapper2.load_wram_snapshot(&snapshot);
assert_eq!(mapper2.read_prg(0x6000), 0x11);
assert_eq!(mapper2.read_prg(0x6100), 0x22);
assert_eq!(mapper2.read_prg(0x7FFF), 0x33);
}
}