use crate::cartridge::BaseMapper;
use crate::cartridge::common::{DEFAULT_PRG_RAM_SIZE, PrgRam};
use crate::cartridge::vrc_irq::VrcIrq;
use crate::cartridge::{Mapper, MapperCapabilities, NametableLayout};
pub struct VRC7Mapper {
base: BaseMapper,
prg_ram: PrgRam,
prg_banks_8k: [u8; 3],
chr_banks_1k: [u8; 8],
control: u8,
irq: VrcIrq,
}
impl VRC7Mapper {
pub fn new(ctx: super::mapper::MapperContext) -> Self {
let mirroring = ctx.mirroring;
let capabilities = MapperCapabilities {
has_irq: true,
has_chr_banking: true,
has_dynamic_mirroring: true,
has_expansion_audio: true,
max_prg_ram_kb: 0,
prg_bank_size_kb: 8,
chr_bank_size_kb: 1,
trainer_jsr: false,
..Default::default()
};
let mut base = BaseMapper::new(&ctx, capabilities);
base.configure_prg_banking(0x2000);
base.configure_chr_banking(0x0400);
let mut mapper = Self {
base,
prg_ram: PrgRam::new(DEFAULT_PRG_RAM_SIZE),
prg_banks_8k: [0; 3],
chr_banks_1k: [0; 8],
control: 0,
irq: VrcIrq::new(341, 3),
};
mapper.base.set_mirroring(mirroring);
mapper.update_banks();
mapper
}
fn normalize_addr(addr: u16) -> u16 {
let mut a = addr;
if (a & 0x0010) != 0 && (a & 0xF010) != 0x9010 {
a |= 0x0008;
a &= !0x0010;
}
a & 0xF038
}
fn update_mirroring(&mut self) {
let new_mirroring = match self.control & 0x03 {
0 => NametableLayout::Vertical,
1 => NametableLayout::Horizontal,
2 => NametableLayout::SingleScreen,
3 => NametableLayout::SingleScreenUpper,
_ => unreachable!(),
};
self.base.set_mirroring(new_mirroring);
}
fn update_banks(&mut self) {
for i in 0..3 {
self.base
.select_prg_page(i, (self.prg_banks_8k[i] & 0x3F) as i16);
}
self.base.select_prg_page(3, -1);
for i in 0..8 {
self.base.select_chr_page(i, self.chr_banks_1k[i] as i16);
}
}
}
impl Mapper for VRC7Mapper {
fn base(&self) -> &BaseMapper {
&self.base
}
fn base_mut(&mut self) -> &mut BaseMapper {
&mut self.base
}
fn read_prg(&self, addr: u16) -> u8 {
if let Some(value) = self.prg_ram.try_read(addr)
&& (self.control & 0x80) != 0
{
return value;
}
match addr {
0x8000..=0xFFFF => self.base.read_prg_banked(addr),
_ => 0,
}
}
fn write_prg(&mut self, addr: u16, value: u8) {
if (self.control & 0x80) != 0 && self.prg_ram.try_write(addr, value) {
return;
}
if !(0x8000..=0xFFFF).contains(&addr) {
return;
}
let reg = Self::normalize_addr(addr);
match reg {
0x8000 => {
self.prg_banks_8k[0] = value & 0x3F;
self.update_banks();
}
0x8008 => {
self.prg_banks_8k[1] = value & 0x3F;
self.update_banks();
}
0x9000 => {
self.prg_banks_8k[2] = value & 0x3F;
self.update_banks();
}
0x9010 | 0x9030 => {}
0xA000 => {
self.chr_banks_1k[0] = value;
self.update_banks();
}
0xA008 => {
self.chr_banks_1k[1] = value;
self.update_banks();
}
0xB000 => {
self.chr_banks_1k[2] = value;
self.update_banks();
}
0xB008 => {
self.chr_banks_1k[3] = value;
self.update_banks();
}
0xC000 => {
self.chr_banks_1k[4] = value;
self.update_banks();
}
0xC008 => {
self.chr_banks_1k[5] = value;
self.update_banks();
}
0xD000 => {
self.chr_banks_1k[6] = value;
self.update_banks();
}
0xD008 => {
self.chr_banks_1k[7] = value;
self.update_banks();
}
0xE000 => {
self.control = value;
self.update_mirroring();
}
0xE008 => {
self.irq.write_latch(value);
}
0xF000 => {
self.irq.write_control(value);
}
0xF008 => {
self.irq.write_acknowledge();
}
_ => {}
}
}
fn cpu_cycle(&mut self) {
self.irq.tick();
}
fn irq_pending(&self) -> bool {
self.irq.pending()
}
fn expansion_audio_sample(&self) -> f32 {
0.0
}
fn wram_size(&self) -> usize {
self.prg_ram.size()
}
fn wram_snapshot(&self) -> Vec<u8> {
self.prg_ram.snapshot()
}
fn load_wram_snapshot(&mut self, data: &[u8]) {
self.prg_ram.load_snapshot(data);
}
fn initialize_ram(&mut self, mode: crate::console::RamInitMode) {
self.prg_ram.initialize(mode);
self.base.initialize_ram(mode);
}
fn read_prg_open_bus(&self, addr: u16, open_bus: u8) -> u8 {
if (self.control & 0x80) == 0 && (0x6000..=0x7FFF).contains(&addr) {
return open_bus;
}
self.read_prg(addr)
}
fn registers_snapshot(&self) -> Vec<u8> {
let mut snapshot = Vec::with_capacity(20);
snapshot.extend_from_slice(&self.prg_banks_8k);
snapshot.extend_from_slice(&self.chr_banks_1k);
snapshot.push(self.control);
snapshot.push(self.irq.latch());
snapshot.push(self.irq.counter());
let flags = (self.irq.enabled() as u8)
| ((self.irq.mode_cycle() as u8) << 1)
| ((self.irq.enable_after_ack() as u8) << 2)
| ((self.irq.pending() as u8) << 3);
snapshot.push(flags);
snapshot.extend_from_slice(&self.irq.prescaler().to_le_bytes());
snapshot.push(self.base.mirroring().to_snapshot_byte());
snapshot
}
fn restore_registers(&mut self, data: &[u8]) {
if data.len() >= 20 {
self.prg_banks_8k.copy_from_slice(&data[0..3]);
self.chr_banks_1k.copy_from_slice(&data[3..11]);
self.control = data[11];
self.irq.write_latch(data[12]);
self.irq.set_counter(data[13]);
let flags = data[14];
self.irq.set_enabled((flags & 1) != 0);
self.irq.set_mode_cycle((flags & 2) != 0);
self.irq.set_enable_after_ack((flags & 4) != 0);
self.irq.set_asserted((flags & 8) != 0);
self.irq
.set_prescaler(i32::from_le_bytes([data[15], data[16], data[17], data[18]]));
self.base
.set_mirroring(NametableLayout::from_snapshot_byte(data[19]));
self.update_banks();
}
}
fn capabilities(&self) -> MapperCapabilities {
MapperCapabilities {
has_irq: true,
has_chr_banking: true,
has_dynamic_mirroring: true,
has_expansion_audio: true,
max_prg_ram_kb: 8,
prg_bank_size_kb: 8,
chr_bank_size_kb: 1,
trainer_jsr: false,
..Default::default()
}
}
}
#[cfg(test)]
mod tests {
use crate::cartridge::NametableLayout;
use crate::cartridge::mapper::{Mapper, MapperContext, create_mapper};
use crate::cartridge::test_helpers::banked_data;
fn create_vrc7(
prg_rom: Vec<u8>,
chr_rom: Vec<u8>,
mirroring: NametableLayout,
) -> Box<dyn Mapper> {
create_mapper(MapperContext::new_for_test(85, prg_rom, chr_rom, mirroring))
.expect("VRC7 (mapper 85) should be implemented")
}
fn prg_rom(num_banks: usize) -> Vec<u8> {
banked_data(8 * 1024, num_banks)
}
fn chr_rom(num_banks: usize) -> Vec<u8> {
banked_data(1024, num_banks)
}
#[test]
fn prg_bank0_switched_via_8000() {
let mut mapper = create_vrc7(prg_rom(48), chr_rom(8), NametableLayout::Horizontal);
mapper.write_prg(0x8000, 7);
assert_eq!(mapper.read_prg(0x8000), 7);
}
#[test]
fn prg_bank1_switched_via_8008() {
let mut mapper = create_vrc7(prg_rom(48), chr_rom(8), NametableLayout::Horizontal);
mapper.write_prg(0x8008, 5);
assert_eq!(mapper.read_prg(0xA000), 5);
}
#[test]
fn prg_bank1_switched_via_8010_bit4_remap() {
let mut mapper = create_vrc7(prg_rom(48), chr_rom(8), NametableLayout::Horizontal);
mapper.write_prg(0x8010, 5);
assert_eq!(mapper.read_prg(0xA000), 5);
}
#[test]
fn prg_bank2_switched_via_9000() {
let mut mapper = create_vrc7(prg_rom(48), chr_rom(8), NametableLayout::Horizontal);
mapper.write_prg(0x9000, 3);
assert_eq!(mapper.read_prg(0xC000), 3);
}
#[test]
fn prg_bank3_is_fixed_to_last_bank() {
let mut mapper = create_vrc7(prg_rom(48), chr_rom(8), NametableLayout::Horizontal);
mapper.write_prg(0x8000, 0);
assert_eq!(mapper.read_prg(0xE000), 47);
}
#[test]
fn prg_bank_mask_applied_correctly() {
let mut mapper = create_vrc7(prg_rom(48), chr_rom(8), NametableLayout::Horizontal);
mapper.write_prg(0x8000, 0xFF);
assert_eq!(mapper.read_prg(0x8000), 63 % 48);
}
#[test]
fn chr_bank0_switched_via_a000() {
let mut mapper = create_vrc7(prg_rom(8), chr_rom(48), NametableLayout::Horizontal);
mapper.write_prg(0xA000, 5);
assert_eq!(mapper.read_chr(0x0000), 5);
}
#[test]
fn chr_bank1_switched_via_a008() {
let mut mapper = create_vrc7(prg_rom(8), chr_rom(48), NametableLayout::Horizontal);
mapper.write_prg(0xA008, 6);
assert_eq!(mapper.read_chr(0x0400), 6);
}
#[test]
fn chr_bank1_switched_via_a010_bit4_remap() {
let mut mapper = create_vrc7(prg_rom(8), chr_rom(48), NametableLayout::Horizontal);
mapper.write_prg(0xA010, 6);
assert_eq!(mapper.read_chr(0x0400), 6);
}
#[test]
fn chr_bank7_switched_via_d008() {
let mut mapper = create_vrc7(prg_rom(8), chr_rom(48), NametableLayout::Horizontal);
mapper.write_prg(0xD008, 9);
assert_eq!(mapper.read_chr(0x1C00), 9);
}
#[test]
fn mirroring_bits00_selects_vertical() {
let mut mapper = create_vrc7(prg_rom(8), chr_rom(8), NametableLayout::Horizontal);
mapper.write_prg(0xE000, 0x00); assert_eq!(mapper.base().mirroring(), NametableLayout::Vertical);
}
#[test]
fn mirroring_bits01_selects_horizontal() {
let mut mapper = create_vrc7(prg_rom(8), chr_rom(8), NametableLayout::Vertical);
mapper.write_prg(0xE000, 0x01); assert_eq!(mapper.base().mirroring(), NametableLayout::Horizontal);
}
#[test]
fn mirroring_bits10_selects_single_screen_lower() {
let mut mapper = create_vrc7(prg_rom(8), chr_rom(8), NametableLayout::Horizontal);
mapper.write_prg(0xE000, 0x02);
assert_eq!(mapper.base().mirroring(), NametableLayout::SingleScreen);
}
#[test]
fn mirroring_bits11_selects_single_screen_upper() {
let mut mapper = create_vrc7(prg_rom(8), chr_rom(8), NametableLayout::Horizontal);
mapper.write_prg(0xE000, 0x03);
assert_eq!(
mapper.base().mirroring(),
NametableLayout::SingleScreenUpper
);
}
#[test]
fn prg_ram_disabled_by_default() {
let mapper = create_vrc7(prg_rom(8), chr_rom(8), NametableLayout::Horizontal);
assert_eq!(mapper.read_prg(0x6000), 0);
}
#[test]
fn prg_ram_enabled_via_e000_bit7() {
let mut mapper = create_vrc7(prg_rom(8), chr_rom(8), NametableLayout::Horizontal);
mapper.write_prg(0xE000, 0x80);
mapper.write_prg(0x6000, 0xAB);
assert_eq!(mapper.read_prg(0x6000), 0xAB);
}
#[test]
fn prg_ram_write_ignored_when_disabled() {
let mut mapper = create_vrc7(prg_rom(8), chr_rom(8), NametableLayout::Horizontal);
mapper.write_prg(0x6000, 0xCD);
mapper.write_prg(0xE000, 0x80);
assert_ne!(mapper.read_prg(0x6000), 0xCD);
}
#[test]
fn read_prg_open_bus_returns_rom_data_for_prg_rom_addresses() {
let mut mapper = create_vrc7(prg_rom(8), chr_rom(8), NametableLayout::Horizontal);
mapper.write_prg(0x8000, 3); let rom_value = mapper.read_prg(0x8000);
assert_eq!(mapper.read_prg_open_bus(0x8000, 0xFF), rom_value);
assert_ne!(mapper.read_prg_open_bus(0x8000, 0xFF), 0xFF);
}
#[test]
fn read_prg_open_bus_returns_open_bus_for_disabled_ram() {
let mapper = create_vrc7(prg_rom(8), chr_rom(8), NametableLayout::Horizontal);
assert_eq!(mapper.read_prg_open_bus(0x6000, 0xAB), 0xAB);
}
#[test]
fn read_prg_open_bus_returns_ram_data_when_enabled() {
let mut mapper = create_vrc7(prg_rom(8), chr_rom(8), NametableLayout::Horizontal);
mapper.write_prg(0xE000, 0x80); mapper.write_prg(0x6000, 0x55);
assert_eq!(mapper.read_prg_open_bus(0x6000, 0xFF), 0x55);
}
#[test]
fn irq_fires_in_cycle_mode_after_latch_overflow() {
let mut mapper = create_vrc7(prg_rom(8), chr_rom(8), NametableLayout::Horizontal);
mapper.write_prg(0xE008, 0xFE);
mapper.write_prg(0xF000, 0b0000_0110);
assert!(!mapper.irq_pending());
mapper.cpu_cycle(); assert!(!mapper.irq_pending());
mapper.cpu_cycle(); assert!(mapper.irq_pending());
}
#[test]
fn irq_acknowledge_clears_pending() {
let mut mapper = create_vrc7(prg_rom(8), chr_rom(8), NametableLayout::Horizontal);
mapper.write_prg(0xE008, 0xFE);
mapper.write_prg(0xF000, 0b0000_0110);
mapper.cpu_cycle();
mapper.cpu_cycle();
assert!(mapper.irq_pending());
mapper.write_prg(0xF008, 0); assert!(!mapper.irq_pending());
}
#[test]
fn registers_snapshot_restore_roundtrip() {
let mut mapper = create_vrc7(prg_rom(48), chr_rom(48), NametableLayout::Horizontal);
mapper.write_prg(0x8000, 5);
mapper.write_prg(0x8008, 10);
mapper.write_prg(0x9000, 15);
mapper.write_prg(0xA000, 2);
mapper.write_prg(0xD008, 9);
mapper.write_prg(0xE000, 0x80);
let snapshot = mapper.registers_snapshot();
let mut restored = create_vrc7(prg_rom(48), chr_rom(48), NametableLayout::Horizontal);
restored.restore_registers(&snapshot);
assert_eq!(restored.read_prg(0x8000), 5);
assert_eq!(restored.read_prg(0xA000), 10);
assert_eq!(restored.read_prg(0xC000), 15);
assert_eq!(restored.read_chr(0x0000), 2);
assert_eq!(restored.read_chr(0x1C00), 9);
assert_eq!(restored.base().mirroring(), NametableLayout::Vertical);
}
#[test]
fn expansion_audio_sample_is_zero_silence_stub() {
let mapper = create_vrc7(prg_rom(8), chr_rom(8), NametableLayout::Horizontal);
assert_eq!(mapper.expansion_audio_sample(), 0.0);
}
#[test]
fn capabilities_reports_irq_and_expansion_audio() {
let mapper = create_vrc7(prg_rom(8), chr_rom(8), NametableLayout::Horizontal);
let caps = mapper.capabilities();
assert!(caps.has_irq);
assert!(caps.has_expansion_audio);
assert!(caps.has_chr_banking);
assert!(caps.has_dynamic_mirroring);
}
}