use crate::cartridge::{Cartridge, ChrRomContentStatus};
use crate::rom_loader::ines::mappers::{
NameTableRam, CART_CPU_MAP_BEGIN_ADDR, LAST_UNREACHABLE_ADDRESS,
};
use crate::rom_loader::ines::{HeaderData, Mirroring};
use crate::{LoadRomResult, RomError};
use devices6502::size_const::*;
use devices6502::*;
use std::io;
const PRG_ROM_PAGE_SIZE: usize = SIZE_16K;
const CHR_MEM_PAGE_SIZE: usize = SIZE_8K;
const CHR_RAM_NUM_PAGES: usize = 2;
const MAX_PRG_ROM_PAGES: usize = 16;
const MAX_CHR_ROM_PAGES: usize = u8::MAX as usize;
#[derive(Default)]
struct Cart {
prg_rom: Vec<Rom<PRG_ROM_PAGE_SIZE>>,
chr_mem: Vec<Ram<CHR_MEM_PAGE_SIZE>>,
name_tables_ram: NameTableRam,
use_chr_ram: bool,
bank_register: u8,
chr_banked: bool,
prg_banked: bool,
}
pub fn load(header: &HeaderData, reader: &mut impl io::Read) -> LoadRomResult {
if header.chr_rom_size as usize > (MAX_CHR_ROM_PAGES * CHR_MEM_PAGE_SIZE) {
return Err(RomError::RomFormat(
"Wrong chr rom size using basic mapper (000, 002, 003). Up until ~2MB allowed".into(),
));
}
if (header.chr_rom_size as usize % CHR_MEM_PAGE_SIZE) != 0 {
return Err(RomError::RomFormat(
"Wrong chr rom size using basic mapper (000, 002, 003). It must be a multiple of 8K"
.into(),
));
}
if header.prg_rom_size as usize > (MAX_PRG_ROM_PAGES * PRG_ROM_PAGE_SIZE) {
return Err(RomError::RomFormat(
"Wrong prg rom size using basic mapper (000, 002, 003). Up until 256K allowed".into(),
));
}
if (header.prg_rom_size as usize % PRG_ROM_PAGE_SIZE) != 0 {
return Err(RomError::RomFormat(
"Wrong prg rom size using basic mapper (000, 002, 003). It must be a multiple of 16K"
.into(),
));
}
if header.mapper == 0 && header.prg_rom_size > (2 * PRG_ROM_PAGE_SIZE) as u64 {
return Err(RomError::RomFormat(
"NROM (mapper 0) supports at most 32 KB of PRG ROM (2 x 16 KB banks)".into(),
));
}
let mut prg_rom_data = Vec::with_capacity(header.prg_rom_size as usize);
prg_rom_data.resize(header.prg_rom_size as usize, 0u8);
reader.read_exact(prg_rom_data.as_mut_slice())?;
let mut chr_rom_data = Vec::new();
chr_rom_data.resize(header.chr_rom_size as usize, 0u8);
reader.read_exact(&mut chr_rom_data)?;
let chr_rom_data = if header.chr_rom_size == 0 {
None } else {
Some(chr_rom_data.as_slice())
};
let mut cart = match header.mirroring {
Mirroring::Horizontal => Box::new(Cart::new_horizontal(
prg_rom_data.as_slice(),
chr_rom_data,
header.mapper,
)),
Mirroring::Vertical => Box::new(Cart::new_vertical(
prg_rom_data.as_slice(),
chr_rom_data,
header.mapper,
)),
Mirroring::FourScreen => Box::new(Cart::new_four_screen(
prg_rom_data.as_slice(),
chr_rom_data,
header.mapper,
)),
};
cart.use_chr_ram = header.chr_rom_size == 0;
Ok(cart)
}
impl Cartridge for Cart {
fn read_cpu_mapped(&self, addr: u16) -> u8 {
let addr = addr + CART_CPU_MAP_BEGIN_ADDR;
match addr {
0x0000..=LAST_UNREACHABLE_ADDRESS => unreachable!(),
CART_CPU_MAP_BEGIN_ADDR..=0x7FFF => 0,
0x8000..=0xBFFF => match self.prg_rom.len() {
0 => unreachable!(), 1 => unsafe { self.prg_rom.get_unchecked(0).read(addr - 0x8000) }, 2..=MAX_PRG_ROM_PAGES => {
let bank = if self.prg_banked {
self.bank_register as usize % self.prg_rom.len()
} else {
0
};
unsafe { self.prg_rom.get_unchecked(bank).read(addr - 0x8000) }
}
_ => unreachable!(),
},
0xC000..=0xFFFF => match self.prg_rom.len() {
0 => unreachable!(), _ => unsafe { self.prg_rom.last().unwrap_unchecked().read(addr - 0xC000) },
},
}
}
fn write_cpu_mapped(&mut self, data: u8, addr: u16) -> ChrRomContentStatus {
let addr = addr + CART_CPU_MAP_BEGIN_ADDR;
match addr {
0x0000..=LAST_UNREACHABLE_ADDRESS => unreachable!(),
CART_CPU_MAP_BEGIN_ADDR..=0x7FFF => (),
0x8000..=0xFFFF => {
self.bank_register = data;
}
};
ChrRomContentStatus::Unchanged
}
fn read_ppu_mapped(&mut self, addr: u16) -> u8 {
match addr {
0x0000..=0x1FFF => {
let bank = if !self.use_chr_ram && self.chr_banked {
self.bank_register as usize
} else {
0
};
let bank = bank % self.chr_mem.len();
unsafe { self.chr_mem.get_unchecked(bank).read(addr) }
}
0x2000..=0x3EFF => match &self.name_tables_ram {
NameTableRam::Horizontal(name_tables_ram) => name_tables_ram.read(addr - 0x2000),
NameTableRam::Vertical(name_tables_ram) => name_tables_ram.read(addr - 0x2000),
NameTableRam::FourScreens(name_tables_ram) => name_tables_ram.read(addr - 0x2000),
},
_ => unreachable!(),
}
}
fn write_ppu_mapped(&mut self, data: u8, addr: u16) -> ChrRomContentStatus {
match addr {
0x0000..=0x1FFF => {
if self.use_chr_ram == true {
let bank = 0;
unsafe {
self.chr_mem.get_unchecked_mut(bank).write(data, addr);
}
}
}
0x2000..=0x3EFF => match &mut self.name_tables_ram {
NameTableRam::Horizontal(name_tables_ram) => {
name_tables_ram.write(data, addr - 0x2000)
}
NameTableRam::Vertical(name_tables_ram) => {
name_tables_ram.write(data, addr - 0x2000)
}
NameTableRam::FourScreens(name_tables_ram) => {
name_tables_ram.write(data, addr - 0x2000)
}
},
_ => unreachable!(),
}
ChrRomContentStatus::Unchanged
}
}
impl Default for NameTableRam {
fn default() -> Self {
Self::Horizontal(Default::default())
}
}
impl Cart {
fn new(prg_rom: &[u8], chr_rom: Option<&[u8]>, mapper: u8) -> Self {
let prg_pages_count = prg_rom.len() / PRG_ROM_PAGE_SIZE;
let chr_pages_count = chr_rom.map_or(CHR_RAM_NUM_PAGES, |chr_rom| {
chr_rom.len() / CHR_MEM_PAGE_SIZE
});
let mut cart = Self::default();
cart.prg_rom.reserve(prg_pages_count);
cart.chr_mem.reserve(chr_pages_count);
cart.prg_banked = mapper == 2;
cart.chr_banked = mapper == 3 && chr_rom.is_some();
for page_index in 0..prg_pages_count {
let start = page_index * PRG_ROM_PAGE_SIZE;
let end = start + PRG_ROM_PAGE_SIZE;
cart.prg_rom.push(Device::with_data(&prg_rom[start..end]));
}
if let Some(chr_rom) = chr_rom {
for page_index in 0..chr_pages_count {
let start = page_index * CHR_MEM_PAGE_SIZE;
let end = start + CHR_MEM_PAGE_SIZE;
cart.chr_mem.push(Device::with_data(&chr_rom[start..end]));
cart.use_chr_ram = false;
}
} else {
cart.use_chr_ram = true;
cart.chr_mem.resize(CHR_RAM_NUM_PAGES, Default::default());
}
cart
}
pub fn new_horizontal(prg_rom: &[u8], chr_rom: Option<&[u8]>, mapper: u8) -> Self {
Self::new(prg_rom, chr_rom, mapper)
}
pub fn new_vertical(prg_rom: &[u8], chr_rom: Option<&[u8]>, mapper: u8) -> Self {
let mut new_cart = Self::new(prg_rom, chr_rom, mapper);
new_cart.name_tables_ram = NameTableRam::Vertical(Default::default());
new_cart
}
pub fn new_four_screen(prg_rom: &[u8], chr_rom: Option<&[u8]>, mapper: u8) -> Self {
let mut new_cart = Self::new(prg_rom, chr_rom, mapper);
new_cart.name_tables_ram = NameTableRam::FourScreens(Default::default());
new_cart
}
}
#[cfg(test)]
mod tests {
use super::*;
fn uxrom_with_chr_ram() -> Cart {
let prg_rom = vec![0u8; 8 * PRG_ROM_PAGE_SIZE];
Cart::new(prg_rom.as_slice(), None, 2)
}
fn uxrom_with_2_prg_banks() -> Cart {
let prg_rom = vec![0u8; 2 * PRG_ROM_PAGE_SIZE];
Cart::new(prg_rom.as_slice(), None, 2)
}
fn cnrom_with_chr_rom(num_chr_banks: usize) -> Cart {
let prg_rom = vec![0u8; 2 * PRG_ROM_PAGE_SIZE];
let chr_rom = vec![0u8; num_chr_banks * CHR_MEM_PAGE_SIZE];
Cart::new(prg_rom.as_slice(), Some(chr_rom.as_slice()), 3)
}
fn uxrom_with_chr_rom(num_chr_banks: usize) -> Cart {
let prg_rom = vec![0u8; 8 * PRG_ROM_PAGE_SIZE];
let chr_rom = vec![0u8; num_chr_banks * CHR_MEM_PAGE_SIZE];
Cart::new(prg_rom.as_slice(), Some(chr_rom.as_slice()), 2)
}
#[test]
fn uxrom_chr_ram_survives_bank_register_changes() {
let mut cart = uxrom_with_chr_ram();
cart.write_ppu_mapped(0xAB, 0x0100);
assert_eq!(cart.read_ppu_mapped(0x0100), 0xAB);
cart.write_cpu_mapped(5, 0x8000 - CART_CPU_MAP_BEGIN_ADDR);
assert_eq!(cart.read_ppu_mapped(0x0100), 0xAB);
for prg_bank in [0u8, 1, 3, 7, 15] {
cart.write_cpu_mapped(prg_bank, 0xC000 - CART_CPU_MAP_BEGIN_ADDR);
assert_eq!(
cart.read_ppu_mapped(0x0100),
0xAB,
"CHR RAM data lost when bank register was {prg_bank}"
);
}
}
#[test]
fn uxrom_2_bank_prg_switches() {
let mut cart = uxrom_with_2_prg_banks();
assert_eq!(cart.read_cpu_mapped(0x8000 - CART_CPU_MAP_BEGIN_ADDR), 0);
cart.prg_rom[1].init_data(&vec![0x5A; PRG_ROM_PAGE_SIZE]);
assert_eq!(cart.read_cpu_mapped(0xC000 - CART_CPU_MAP_BEGIN_ADDR), 0x5A);
cart.write_cpu_mapped(1, 0x8000 - CART_CPU_MAP_BEGIN_ADDR);
assert_eq!(
cart.read_cpu_mapped(0x8000 - CART_CPU_MAP_BEGIN_ADDR),
0x5A,
"$8000-$BFFF must reflect the switchable bank when prg_banked is true"
);
assert_eq!(cart.read_cpu_mapped(0xC000 - CART_CPU_MAP_BEGIN_ADDR), 0x5A);
cart.write_cpu_mapped(0, 0x8000 - CART_CPU_MAP_BEGIN_ADDR);
assert_eq!(cart.read_cpu_mapped(0x8000 - CART_CPU_MAP_BEGIN_ADDR), 0);
assert_eq!(cart.read_cpu_mapped(0xC000 - CART_CPU_MAP_BEGIN_ADDR), 0x5A);
}
#[test]
fn uxrom_chr_ram_addressable_across_full_window() {
let mut cart = uxrom_with_chr_ram();
cart.write_ppu_mapped(0x11, 0x0000);
cart.write_ppu_mapped(0x22, 0x0FFF);
cart.write_ppu_mapped(0x33, 0x1000);
cart.write_ppu_mapped(0x44, 0x1FFF);
cart.write_cpu_mapped(1, 0x8000 - CART_CPU_MAP_BEGIN_ADDR);
assert_eq!(cart.read_ppu_mapped(0x0000), 0x11);
assert_eq!(cart.read_ppu_mapped(0x0FFF), 0x22);
assert_eq!(cart.read_ppu_mapped(0x1000), 0x33);
assert_eq!(cart.read_ppu_mapped(0x1FFF), 0x44);
}
#[test]
fn cnrom_chr_rom_still_banked_by_register() {
let mut cart = cnrom_with_chr_rom(4);
for bank in 0..4 {
cart.chr_mem[bank].init_data(&vec![0x10 + bank as u8; CHR_MEM_PAGE_SIZE]);
}
assert_eq!(cart.read_ppu_mapped(0x0000), 0x10);
cart.write_cpu_mapped(1, 0x8000 - CART_CPU_MAP_BEGIN_ADDR);
assert_eq!(cart.read_ppu_mapped(0x0000), 0x11);
cart.write_cpu_mapped(3, 0x8000 - CART_CPU_MAP_BEGIN_ADDR);
assert_eq!(cart.read_ppu_mapped(0x0000), 0x13);
}
#[test]
fn uxrom_chr_rom_fixed_at_bank_zero() {
let mut cart = uxrom_with_chr_rom(4);
for bank in 0..4 {
cart.chr_mem[bank].init_data(&vec![0x20 + bank as u8; CHR_MEM_PAGE_SIZE]);
}
cart.write_cpu_mapped(2, 0x8000 - CART_CPU_MAP_BEGIN_ADDR);
assert_eq!(cart.read_ppu_mapped(0x0000), 0x20);
}
fn cart_for_mirroring(mirroring: crate::rom_loader::ines::Mirroring) -> Cart {
let prg = vec![0u8; 2 * PRG_ROM_PAGE_SIZE];
let chr = vec![0u8; 2 * CHR_MEM_PAGE_SIZE];
let mut cart = Cart::new(prg.as_slice(), Some(chr.as_slice()), 0);
cart.name_tables_ram = match mirroring {
crate::rom_loader::ines::Mirroring::Horizontal => NameTableRam::Horizontal(
crate::rom_loader::ines::mappers::HorizontalNameTablesRam::default(),
),
crate::rom_loader::ines::Mirroring::Vertical => NameTableRam::Vertical(
crate::rom_loader::ines::mappers::VerticalNameTablesRam::default(),
),
crate::rom_loader::ines::Mirroring::FourScreen => NameTableRam::FourScreens(
crate::rom_loader::ines::mappers::FourScreensNameTablesRam::default(),
),
};
cart
}
#[test]
fn vertical_mirroring_two_name_tables() {
let mut cart = cart_for_mirroring(crate::rom_loader::ines::Mirroring::Vertical);
cart.write_ppu_mapped(1, 0x2000);
cart.write_ppu_mapped(2, 0x2401);
cart.write_ppu_mapped(3, 0x2802);
cart.write_ppu_mapped(4, 0x2C03);
assert_eq!(cart.read_ppu_mapped(0x2000), 1, "screen-0 byte 0");
assert_eq!(cart.read_ppu_mapped(0x2401), 2, "screen-1 byte 1");
assert_eq!(cart.read_ppu_mapped(0x2802), 3, "screen-0 mirror at $2802");
assert_eq!(cart.read_ppu_mapped(0x2C03), 4, "screen-1 mirror at $2C03");
assert_eq!(cart.read_ppu_mapped(0x3000), 1);
assert_eq!(cart.read_ppu_mapped(0x3401), 2);
assert_eq!(cart.read_ppu_mapped(0x3802), 3);
assert_eq!(cart.read_ppu_mapped(0x3C03), 4);
}
#[test]
fn horizontal_mirroring_two_distinct_tiles() {
let mut cart = cart_for_mirroring(crate::rom_loader::ines::Mirroring::Horizontal);
cart.write_ppu_mapped(0x11, 0x2000);
assert_eq!(cart.read_ppu_mapped(0x2000), 0x11, "lower tile byte 0");
assert_eq!(
cart.read_ppu_mapped(0x2400),
0x11,
"$2400 mirrors $2000 inside the same 1KB tile"
);
cart.write_ppu_mapped(0x22, 0x2800);
assert_eq!(cart.read_ppu_mapped(0x2800), 0x22, "upper tile byte 0");
assert_eq!(
cart.read_ppu_mapped(0x2C00),
0x22,
"$2C00 mirrors $2800 inside the same 1KB tile"
);
assert_ne!(
cart.read_ppu_mapped(0x2000),
cart.read_ppu_mapped(0x2800),
"the two name tables must hold distinct data"
);
assert_eq!(cart.read_ppu_mapped(0x3000), 0x11);
assert_eq!(cart.read_ppu_mapped(0x3400), 0x11);
assert_eq!(cart.read_ppu_mapped(0x3800), 0x22);
assert_eq!(cart.read_ppu_mapped(0x3C00), 0x22);
}
#[test]
fn four_screen_mirroring_all_four_independent() {
let mut cart = cart_for_mirroring(crate::rom_loader::ines::Mirroring::FourScreen);
cart.write_ppu_mapped(1, 0x2000);
cart.write_ppu_mapped(2, 0x2400);
cart.write_ppu_mapped(3, 0x2800);
cart.write_ppu_mapped(4, 0x2C00);
assert_eq!(cart.read_ppu_mapped(0x2000), 1, "screen-0 byte 0");
assert_eq!(cart.read_ppu_mapped(0x2400), 2, "screen-1 byte 0");
assert_eq!(cart.read_ppu_mapped(0x2800), 3, "screen-2 byte 0");
assert_eq!(cart.read_ppu_mapped(0x2C00), 4, "screen-3 byte 0");
assert_eq!(cart.read_ppu_mapped(0x3000), 1);
assert_eq!(cart.read_ppu_mapped(0x3400), 2);
assert_eq!(cart.read_ppu_mapped(0x3800), 3);
assert_eq!(cart.read_ppu_mapped(0x3C00), 4);
}
#[test]
fn horizontal_mirroring_does_not_collapse_into_single_tile() {
let mut cart = cart_for_mirroring(crate::rom_loader::ines::Mirroring::Horizontal);
cart.write_ppu_mapped(0xAA, 0x2000);
let r0 = cart.read_ppu_mapped(0x2000);
let r1 = cart.read_ppu_mapped(0x2400);
assert_eq!(r0, 0xAA, "$2000 must read what we just wrote there");
assert_eq!(r1, 0xAA, "$2400 mirrors $2000 within the same tile");
cart.write_ppu_mapped(0xBB, 0x2800);
let r2 = cart.read_ppu_mapped(0x2800);
let r3 = cart.read_ppu_mapped(0x2C00);
assert_eq!(r2, 0xBB, "$2800 must read what we just wrote there");
assert_eq!(r3, 0xBB, "$2C00 mirrors $2800 within the same tile");
assert_ne!(
r0, r2,
"lower NT and upper NT must not alias — a single tile storage would fail here"
);
assert_ne!(r1, r3, "lower NT mirror and upper NT mirror must not alias");
}
}