use crate::nes::cartridge::BaseMapper;
use crate::nes::cartridge::NametableLayout;
use crate::nes::cartridge::mapper::{Mapper, MapperCapabilities};
pub struct Vrc1Mapper {
base: BaseMapper,
pub(crate) prg_bank: [u8; 3],
pub(crate) chr_bank_low: [u8; 2],
pub(crate) chr_bank_high: [u8; 2],
}
impl Vrc1Mapper {
const PRG_BANK_SIZE: usize = 0x2000; const CHR_BANK_SIZE: usize = 0x1000;
pub fn new(ctx: crate::nes::cartridge::mapper::MapperContext) -> Self {
let mirroring = ctx.mirroring;
let capabilities = MapperCapabilities {
has_chr_banking: true,
has_dynamic_mirroring: true,
max_prg_ram_kb: 8,
prg_bank_size_kb: 8,
chr_bank_size_kb: 4,
..Default::default()
};
let mut base = BaseMapper::new(&ctx, capabilities);
base.configure_prg_banking(Self::PRG_BANK_SIZE);
base.configure_chr_banking(Self::CHR_BANK_SIZE);
base.set_mirroring(mirroring);
let mut mapper = Self {
base,
prg_bank: [0; 3],
chr_bank_low: [0; 2],
chr_bank_high: [0; 2],
};
mapper.update_prg_banks();
mapper.update_chr_banks();
mapper
}
fn update_prg_banks(&mut self) {
self.base.select_prg_page(0, self.prg_bank[0] as i16);
self.base.select_prg_page(1, self.prg_bank[1] as i16);
self.base.select_prg_page(2, self.prg_bank[2] as i16);
self.base.select_prg_page(3, -1); }
fn update_chr_banks(&mut self) {
let bank0 = ((self.chr_bank_high[0] as i16) << 4) | (self.chr_bank_low[0] as i16);
let bank1 = ((self.chr_bank_high[1] as i16) << 4) | (self.chr_bank_low[1] as i16);
self.base.select_chr_page(0, bank0);
self.base.select_chr_page(1, bank1);
}
}
impl Mapper for Vrc1Mapper {
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) {
if self.base.try_write_prg_ram(addr, value) {
return;
}
match addr {
0x8000..=0x8FFF => {
self.prg_bank[0] = value & 0x0F;
self.update_prg_banks();
}
0x9000..=0x9FFF => {
self.base.set_mirroring_hv((value & 0x01) != 0);
self.chr_bank_high[0] = (value >> 1) & 0x01;
self.chr_bank_high[1] = (value >> 2) & 0x01;
self.update_chr_banks();
}
0xA000..=0xAFFF => {
self.prg_bank[1] = value & 0x0F;
self.update_prg_banks();
}
0xC000..=0xCFFF => {
self.prg_bank[2] = value & 0x0F;
self.update_prg_banks();
}
0xE000..=0xEFFF => {
self.chr_bank_low[0] = value & 0x0F;
self.update_chr_banks();
}
0xF000..=0xFFFF => {
self.chr_bank_low[1] = value & 0x0F;
self.update_chr_banks();
}
_ => {}
}
}
fn registers_snapshot(&self) -> Vec<u8> {
let mirroring_bit = u8::from(self.get_mirroring() == NametableLayout::Horizontal);
vec![
self.prg_bank[0],
self.prg_bank[1],
self.prg_bank[2],
self.chr_bank_low[0],
self.chr_bank_low[1],
self.chr_bank_high[0],
self.chr_bank_high[1],
mirroring_bit,
]
}
fn restore_registers(&mut self, data: &[u8]) {
if data.len() >= 8 {
self.prg_bank[0] = data[0];
self.prg_bank[1] = data[1];
self.prg_bank[2] = data[2];
self.chr_bank_low[0] = data[3];
self.chr_bank_low[1] = data[4];
self.chr_bank_high[0] = data[5];
self.chr_bank_high[1] = data[6];
self.base.set_mirroring_hv((data[7] & 0x01) != 0);
self.update_prg_banks();
self.update_chr_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 = 11; const CHR_BANKS: usize = 11;
fn make_mapper() -> Vrc1Mapper {
let prg = banked_data(8 * 1024, PRG_BANKS);
let chr = banked_data(4 * 1024, CHR_BANKS);
Vrc1Mapper::new(MapperContext::new_for_test(
75,
prg,
chr,
NametableLayout::Vertical,
))
}
#[test]
fn mapper_75_is_registered() {
let result = create_mapper(MapperContext::new_for_test(
75,
banked_data(8 * 1024, PRG_BANKS),
banked_data(4 * 1024, CHR_BANKS),
NametableLayout::Vertical,
));
assert!(
result.is_ok(),
"Mapper 75 must be registered in the factory"
);
}
#[test]
fn power_on_prg_bank0_at_8000_is_bank_0() {
let mapper = make_mapper();
assert_eq!(mapper.read_prg(0x8000), 0, "$8000 must start at PRG bank 0");
}
#[test]
fn power_on_prg_bank1_at_a000_is_bank_0() {
let mapper = make_mapper();
assert_eq!(mapper.read_prg(0xA000), 0, "$A000 must start at PRG bank 0");
}
#[test]
fn power_on_prg_bank2_at_c000_is_bank_0() {
let mapper = make_mapper();
assert_eq!(mapper.read_prg(0xC000), 0, "$C000 must start at PRG bank 0");
}
#[test]
fn power_on_prg_e000_is_fixed_to_last_bank() {
let mapper = make_mapper();
assert_eq!(
mapper.read_prg(0xE000),
(PRG_BANKS - 1) as u8,
"$E000-$FFFF must always map to the last PRG bank"
);
}
#[test]
fn prg_bank0_select_via_8000_register() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 1);
assert_eq!(
mapper.read_prg(0x8000),
1,
"$8000 must switch to PRG bank 1 after writing 1 to $8000"
);
assert_eq!(mapper.read_prg(0xA000), 0, "$A000 must remain at bank 0");
assert_eq!(mapper.read_prg(0xC000), 0, "$C000 must remain at bank 0");
}
#[test]
fn prg_bank1_select_via_a000_register() {
let mut mapper = make_mapper();
mapper.write_prg(0xA000, 2);
assert_eq!(
mapper.read_prg(0xA000),
2,
"$A000 must switch to PRG bank 2 after writing 2 to $A000"
);
assert_eq!(mapper.read_prg(0x8000), 0, "$8000 must remain at bank 0");
assert_eq!(mapper.read_prg(0xC000), 0, "$C000 must remain at bank 0");
}
#[test]
fn prg_bank2_select_via_c000_register() {
let mut mapper = make_mapper();
mapper.write_prg(0xC000, 3);
assert_eq!(
mapper.read_prg(0xC000),
3,
"$C000 must switch to PRG bank 3 after writing 3 to $C000"
);
assert_eq!(mapper.read_prg(0x8000), 0, "$8000 must remain at bank 0");
assert_eq!(mapper.read_prg(0xA000), 0, "$A000 must remain at bank 0");
}
#[test]
fn prg_fixed_bank_at_e000_never_changes() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 5);
mapper.write_prg(0xA000, 6);
mapper.write_prg(0xC000, 7);
assert_eq!(
mapper.read_prg(0xE000),
(PRG_BANKS - 1) as u8,
"$E000 fixed window must not change after PRG bank writes"
);
}
#[test]
fn chr_bank0_low_bits_via_e000_register() {
let mut mapper = make_mapper();
mapper.write_prg(0xE000, 3);
assert_eq!(
mapper.read_chr(0x0000),
3,
"$0000-$0FFF must map to CHR bank 3 after writing 3 to $E000"
);
assert_eq!(
mapper.read_chr(0x1000),
0,
"$1000-$1FFF must remain at CHR bank 0"
);
}
#[test]
fn chr_bank1_low_bits_via_f000_register() {
let mut mapper = make_mapper();
mapper.write_prg(0xF000, 4);
assert_eq!(
mapper.read_chr(0x1000),
4,
"$1000-$1FFF must map to CHR bank 4 after writing 4 to $F000"
);
assert_eq!(
mapper.read_chr(0x0000),
0,
"$0000-$0FFF must remain at CHR bank 0"
);
}
#[test]
fn chr_bank0_high_bit_via_9000_bit1() {
let mut mapper = make_mapper();
mapper.write_prg(0x9000, 0x02); assert_eq!(
mapper.chr_bank_high[0], 1,
"CHR bank 0 high bit must be set after writing bit 1 of $9000"
);
}
#[test]
fn chr_bank1_high_bit_via_9000_bit2() {
let mut mapper = make_mapper();
mapper.write_prg(0x9000, 0x04); assert_eq!(
mapper.chr_bank_high[1], 1,
"CHR bank 1 high bit must be set after writing bit 2 of $9000"
);
}
#[test]
fn chr_bank0_uses_combined_high_and_low_bits() {
let prg = banked_data(8 * 1024, PRG_BANKS);
let chr = banked_data(4 * 1024, CHR_BANKS);
let mut mapper = Vrc1Mapper::new(MapperContext::new_for_test(
75,
prg,
chr,
NametableLayout::Vertical,
));
mapper.write_prg(0x9000, 0x02); mapper.write_prg(0xE000, 0x01); assert_eq!(
mapper.read_chr(0x0000),
17 % CHR_BANKS as u8,
"$0000 must reflect combined CHR bank 0 high+low bits"
);
}
#[test]
fn chr_bank1_uses_combined_high_and_low_bits() {
let prg = banked_data(8 * 1024, PRG_BANKS);
let chr = banked_data(4 * 1024, CHR_BANKS);
let mut mapper = Vrc1Mapper::new(MapperContext::new_for_test(
75,
prg,
chr,
NametableLayout::Vertical,
));
mapper.write_prg(0x9000, 0x04); mapper.write_prg(0xF000, 0x02); assert_eq!(
mapper.read_chr(0x1000),
18 % CHR_BANKS as u8,
"$1000 must reflect combined CHR bank 1 high+low bits"
);
}
#[test]
fn mirroring_defaults_to_vertical() {
let mapper = make_mapper();
assert_eq!(
mapper.get_mirroring(),
NametableLayout::Vertical,
"Power-on mirroring must match header (Vertical)"
);
}
#[test]
fn mirroring_switches_to_horizontal_via_9000_bit0() {
let mut mapper = make_mapper();
mapper.write_prg(0x9000, 0x01); assert_eq!(
mapper.get_mirroring(),
NametableLayout::Horizontal,
"$9000 bit 0 = 1 must select Horizontal mirroring"
);
}
#[test]
fn mirroring_switches_to_vertical_when_bit0_clear() {
let mut mapper = make_mapper();
mapper.write_prg(0x9000, 0x01); mapper.write_prg(0x9000, 0x00); assert_eq!(
mapper.get_mirroring(),
NametableLayout::Vertical,
"$9000 bit 0 = 0 must select Vertical mirroring"
);
}
#[test]
fn registers_snapshot_round_trips() {
let mut original = make_mapper();
original.write_prg(0x8000, 1); original.write_prg(0xA000, 2); original.write_prg(0xC000, 3); original.write_prg(0xE000, 5); original.write_prg(0xF000, 7); original.write_prg(0x9000, 0x07);
let snap = original.registers_snapshot();
let mut restored = make_mapper();
restored.restore_registers(&snap);
assert_eq!(
restored.prg_bank, original.prg_bank,
"PRG banks must be preserved"
);
assert_eq!(
restored.chr_bank_low, original.chr_bank_low,
"CHR low bits must be preserved"
);
assert_eq!(
restored.chr_bank_high, original.chr_bank_high,
"CHR high bits must be preserved"
);
assert_eq!(
restored.get_mirroring(),
original.get_mirroring(),
"Mirroring must be preserved"
);
}
}