use crate::cartridge::{Cartridge, ChrRomContentStatus};
use crate::rom_loader::ines::mappers::{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::{Device, Ram, Rom};
use std::cmp::PartialEq;
use std::io;
use std::ops::Rem;
const PRG_ROM_PAGE_SIZE: usize = SIZE_8K;
const CHR_MEM_PAGE_SIZE: usize = SIZE_1K;
const NAME_TABLE_PAGE_SIZE: usize = SIZE_1K;
const MAX_PRG_ROM_PAGES: usize = u8::MAX as usize;
const MAX_CHR_ROM_PAGES: usize = 256;
#[derive(Default, Copy, Clone, PartialEq, Eq, Debug)]
enum Mode {
#[default]
Normal,
Inverted,
}
type NameTablePage = Ram<NAME_TABLE_PAGE_SIZE>;
#[derive(Default)]
struct NameTableRam {
ram_pages: [NameTablePage; 4],
mode: Mirroring,
}
trait ChrMem: Send {
fn write_chr_mem(
&mut self,
data: u8,
addr: u16,
mode: Mode,
bank_regs: &[u8; 8],
) -> ChrRomContentStatus;
fn read_chr_mem(&self, addr: u16, mode: Mode, bank_regs: &[u8; 8]) -> u8;
fn page_count(&self) -> u16;
#[cfg(test)]
fn has_chr_ram() -> bool {
false
}
}
#[derive(Default)]
struct Cart<TChrMem> {
prg_ram: Ram<SIZE_8K>,
prg_rom: Vec<Rom<PRG_ROM_PAGE_SIZE>>,
chr_mem: TChrMem,
name_tables_ram: NameTableRam,
bank_select: u8,
bank_registers: [u8; 8],
prg_mode: Mode,
chr_mode: Mode,
is_ram_writable: bool,
irq_active: bool,
irq_latch: u8,
irq_counter: u8,
irq_reload_flag: bool,
irq_enable: bool,
prev_a12: 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 mapper 004. Up until 256K allowed".into(),
));
}
if (header.chr_rom_size as usize % CHR_MEM_PAGE_SIZE) != 0 {
return Err(RomError::RomFormat(
"Wrong chr rom size using mapper 004. It must be a multiple of 1K".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 mapper 004. Up until ~2MB allowed".into(),
));
}
if (header.prg_rom_size as usize % PRG_ROM_PAGE_SIZE) != 0 {
return Err(RomError::RomFormat(
"Wrong prg rom size using mapper 004. It must be a multiple of 8K".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 num_prg_pages = header.prg_rom_size as usize / PRG_ROM_PAGE_SIZE;
let num_chr_pages = header.chr_rom_size as usize / CHR_MEM_PAGE_SIZE;
let mut prg_rom = Vec::with_capacity(num_prg_pages);
prg_rom_data.chunks(PRG_ROM_PAGE_SIZE).for_each(|chunk| {
prg_rom.push(Rom::<PRG_ROM_PAGE_SIZE>::with_data(chunk));
});
let mut chr_mem = Vec::with_capacity(num_chr_pages);
chr_rom_data.chunks(CHR_MEM_PAGE_SIZE).for_each(|chunk| {
chr_mem.push(Rom::<CHR_MEM_PAGE_SIZE>::with_data(chunk));
});
if chr_mem.len() > 0 {
let mut cart = Cart {
prg_rom,
chr_mem,
irq_enable: true,
..Default::default()
};
match header.mirroring {
Mirroring::Horizontal => cart.name_tables_ram.set_horizontal(),
Mirroring::Vertical => cart.name_tables_ram.set_vertical(),
Mirroring::FourScreen => cart.name_tables_ram.set_four_screen(),
}
Ok(Box::new(cart))
} else {
let mut cart = Cart {
prg_rom,
chr_mem: Ram::<SIZE_8K>::default(),
irq_enable: true,
..Default::default()
};
match header.mirroring {
Mirroring::Horizontal => cart.name_tables_ram.set_horizontal(),
Mirroring::Vertical => cart.name_tables_ram.set_vertical(),
Mirroring::FourScreen => cart.name_tables_ram.set_four_screen(),
}
Ok(Box::new(cart))
}
}
fn name_table_page_index(addr: u16, mode: Mirroring) -> usize {
let page_index = addr as usize / SIZE_1K;
match mode {
Mirroring::Horizontal => page_index / 2,
Mirroring::Vertical => page_index % 2,
Mirroring::FourScreen => page_index,
}
}
fn is_even<T>(n: T) -> bool
where
T: Rem<Output = T> + PartialEq + From<u8> + Copy,
{
n % T::from(2) == T::from(0)
}
impl Device for NameTableRam {
fn with_data(data: &[u8]) -> Self
where
Self: Sized,
{
let ranges: [_; 4] =
core::array::from_fn(|i| i * NAME_TABLE_PAGE_SIZE..(i + 1) * NAME_TABLE_PAGE_SIZE);
assert_eq!(data.len(), ranges.last().unwrap().end);
Self {
ram_pages: [
Ram::with_data(&data[ranges[0].clone()]),
Ram::with_data(&data[ranges[1].clone()]),
Ram::with_data(&data[ranges[2].clone()]),
Ram::with_data(&data[ranges[3].clone()]),
],
mode: Default::default(),
}
}
fn init_data(&mut self, data: &[u8]) {
assert_eq!(data.len(), SIZE_4K);
for i in 0..4 {
self.ram_pages[i].init_data(&data[i * SIZE_1K..(i + 1) * SIZE_1K]);
}
}
fn read(&self, addr: u16) -> u8 {
let addr_in_page = addr % SIZE_1K as u16;
let page_index = name_table_page_index(addr, self.mode);
self.ram_pages[page_index].read(addr_in_page)
}
fn write(&mut self, data: u8, addr: u16) {
let addr_in_page = addr % SIZE_1K as u16;
let page_index = name_table_page_index(addr, self.mode);
self.ram_pages[page_index].write(data, addr_in_page);
}
fn addr_space_size() -> u32
where
Self: Sized,
{
NameTablePage::addr_space_size() * 4
}
fn addr_space_size_dyn(&self) -> u32 {
self.ram_pages[0].addr_space_size_dyn() * 4
}
}
impl NameTableRam {
fn set_four_screen(&mut self) {
self.mode = Mirroring::FourScreen;
}
fn set_vertical(&mut self) {
if self.mode != Mirroring::FourScreen {
self.mode = Mirroring::Vertical;
}
}
fn set_horizontal(&mut self) {
if self.mode != Mirroring::FourScreen {
self.mode = Mirroring::Horizontal;
}
}
}
impl<TChrMem: ChrMem> Cartridge for Cart<TChrMem> {
fn read_cpu_mapped(&self, addr: u16) -> u8 {
let addr = addr + CART_CPU_MAP_BEGIN_ADDR;
match addr {
0x000..=LAST_UNREACHABLE_ADDRESS => unreachable!(),
CART_CPU_MAP_BEGIN_ADDR..=0x5FFF => 0, 0x6000..=0x7FFF => self.read_prg_ram(addr - 0x6000),
0x8000..=0xFFFF => self.read_prg_rom(addr - 0x8000),
}
}
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..=0x5FFF => (), 0x6000..=0x7FFF => self.write_prg_ram(addr - 0x6000, data),
0x8000..=0x9FFF => {
if is_even(addr) {
self.write_bank_select(data);
} else {
self.write_bank_data(data);
}
}
0xA000..=0xBFFF => {
if is_even(addr) {
if is_even(data) {
self.name_tables_ram.set_vertical();
} else {
self.name_tables_ram.set_horizontal();
};
} else {
self.is_ram_writable = (data & 0x80) != 0;
}
}
0xC000..=0xDFFF => {
if is_even(addr) {
self.irq_latch = data;
} else {
self.irq_reload_flag = true;
}
}
0xE000..=0xFFFF => {
if is_even(addr) {
self.irq_enable = false;
self.irq_active = false;
} else {
self.irq_enable = true;
}
}
}
ChrRomContentStatus::Unchanged
}
fn read_ppu_mapped(&mut self, addr: u16) -> u8 {
self.irq_update(addr);
match addr {
0x0000..=0x1FFF => self
.chr_mem
.read_chr_mem(addr, self.chr_mode, &self.bank_registers),
0x2000..=0x3EFF => self.name_tables_ram.read(addr - 0x2000),
_ => unreachable!(),
}
}
fn write_ppu_mapped(&mut self, data: u8, addr: u16) -> ChrRomContentStatus {
self.irq_update(addr);
match addr {
0x0000..=0x1FFF => {
self.chr_mem
.write_chr_mem(data, addr, self.chr_mode, &self.bank_registers)
}
0x2000..=0x3EFF => {
self.name_tables_ram.write(data, addr - 0x2000);
ChrRomContentStatus::Unchanged
}
_ => unreachable!(),
}
}
fn irq_pin(&self) -> bool {
self.irq_active
}
fn requires_cycle_accurate_sprites(&self) -> bool {
true
}
fn notify_vram_addr_change(&mut self, _old_addr: u16, new_addr: u16) {
self.irq_update(new_addr);
}
}
impl ChrMem for Ram<SIZE_8K> {
fn write_chr_mem(
&mut self,
data: u8,
addr: u16,
_mode: Mode,
_bank_regs: &[u8; 8],
) -> ChrRomContentStatus {
self.write(data, addr);
ChrRomContentStatus::Changed
}
fn read_chr_mem(&self, addr: u16, _mode: Mode, _bank_regs: &[u8; 8]) -> u8 {
self.read(addr)
}
fn page_count(&self) -> u16 {
8
}
#[cfg(test)]
fn has_chr_ram() -> bool {
true
}
}
fn chr_mem_page_index(chr_addr: u16, mode: Mode, bank_registers: &[u8; 8]) -> usize {
let eighth = chr_addr / SIZE_1K as u16;
match (eighth, mode) {
(0, Mode::Normal) | (4, Mode::Inverted) => bank_registers[0] as usize,
(1, Mode::Normal) | (5, Mode::Inverted) => bank_registers[0] as usize + 1,
(2, Mode::Normal) | (6, Mode::Inverted) => bank_registers[1] as usize,
(3, Mode::Normal) | (7, Mode::Inverted) => bank_registers[1] as usize + 1,
(4, Mode::Normal) | (0, Mode::Inverted) => bank_registers[2] as usize,
(5, Mode::Normal) | (1, Mode::Inverted) => bank_registers[3] as usize,
(6, Mode::Normal) | (2, Mode::Inverted) => bank_registers[4] as usize,
(7, Mode::Normal) | (3, Mode::Inverted) => bank_registers[5] as usize,
_ => unreachable!(),
}
}
impl ChrMem for Vec<Rom<SIZE_1K>> {
fn write_chr_mem(
&mut self,
data: u8,
addr: u16,
mode: Mode,
bank_regs: &[u8; 8],
) -> ChrRomContentStatus {
debug_assert!(addr < SIZE_8K as u16);
let addr_in_page = addr % SIZE_8K as u16;
let page_index = chr_mem_page_index(addr, mode, bank_regs);
self[page_index].write(data, addr_in_page);
ChrRomContentStatus::Changed
}
fn read_chr_mem(&self, addr: u16, mode: Mode, bank_regs: &[u8; 8]) -> u8 {
debug_assert!(addr < SIZE_8K as u16);
let addr_in_page = addr % SIZE_8K as u16;
let page_index = chr_mem_page_index(addr, mode, bank_regs);
self[page_index].read(addr_in_page)
}
fn page_count(&self) -> u16 {
self.len() as u16
}
}
impl<TChrMem: ChrMem> Cart<TChrMem> {
#[cfg(test)]
fn has_chr_ram(&self) -> bool {
TChrMem::has_chr_ram()
}
fn write_prg_ram(&mut self, addr: u16, data: u8) {
if self.is_ram_writable {
self.prg_ram.write(data, addr);
}
}
fn write_bank_select(&mut self, data: u8) {
self.bank_select = data % 8;
self.prg_mode = if (data & 0x40) != 0 {
Mode::Inverted
} else {
Mode::Normal
};
self.chr_mode = if (data & 0x80) != 0 {
Mode::Inverted
} else {
Mode::Normal
};
#[cfg(feature = "mapper_debug_log")]
if false {
println!("write_bank_select bank:{}", self.bank_select);
println!(
"prg_mode: {:?}, chr_mode: {:?}",
self.prg_mode, self.chr_mode
);
println!();
}
}
fn write_bank_data(&mut self, data: u8) {
let data = if self.bank_select < 6 {
let data = (data as u16 % self.chr_mem.page_count()) as u8;
if self.bank_select < 2 {
data & !1 } else {
data
}
} else {
(data as u16 % self.prg_rom.len() as u16) as u8
};
self.bank_registers[self.bank_select as usize] = data;
#[cfg(feature = "mapper_debug_log")]
{
println!(
"write_bank_data\tbank:{}\tvalue:{}",
self.bank_select, self.bank_registers[self.bank_select as usize]
);
}
}
fn read_prg_ram(&self, addr: u16) -> u8 {
debug_assert!(addr < SIZE_8K as u16);
self.prg_ram.read(addr)
}
fn read_prg_rom(&self, addr: u16) -> u8 {
debug_assert!(addr < SIZE_32K as u16);
let quarter = addr / SIZE_8K as u16;
let addr_in_page = addr % SIZE_8K as u16;
match (quarter, self.prg_mode) {
(0, Mode::Normal) | (2, Mode::Inverted) => {
self.prg_rom[self.bank_registers[6] as usize].read(addr_in_page)
}
(1, _) => self.prg_rom[self.bank_registers[7] as usize].read(addr_in_page),
(2, Mode::Normal) | (0, Mode::Inverted) => {
self.prg_rom[self.second_to_last_prg_rom_page_index()].read(addr_in_page)
}
(3, _) => self.prg_rom[self.last_prg_rom_page_index()].read(addr_in_page),
_ => unreachable!(),
}
}
fn irq_update(&mut self, addr: u16) {
let current_a12 = (addr & 0x1000) != 0;
if current_a12 && !self.prev_a12 {
if self.irq_reload_flag || (self.irq_counter == 0) {
self.irq_counter = self.irq_latch;
self.irq_reload_flag = false;
} else {
self.irq_counter = self.irq_counter.wrapping_sub(1);
}
if self.irq_enable && (self.irq_counter == 0) {
self.irq_active = true;
}
}
self.prev_a12 = current_a12;
}
fn last_prg_rom_page_index(&self) -> usize {
self.prg_rom.len() - 1
}
fn second_to_last_prg_rom_page_index(&self) -> usize {
self.last_prg_rom_page_index() - 1
}
}
#[cfg(test)]
mod tests {
use super::*;
fn build_pages() -> Vec<u8> {
let mut data = vec![0u8; NAME_TABLE_PAGE_SIZE * 4];
for i in 0..4 {
let val = (i as u8).wrapping_mul(0x11);
for b in data
.iter_mut()
.take((i + 1) * NAME_TABLE_PAGE_SIZE)
.skip(i * NAME_TABLE_PAGE_SIZE)
{
*b = val;
}
}
data
}
#[test]
fn horizontal_mapping_read_write() {
let data = build_pages();
let mut nt = NameTableRam::with_data(&data);
assert_eq!(nt.read(0x000), 0x00);
assert_eq!(nt.read(0x400), 0x00); assert_eq!(nt.read(0x800), 0x11);
assert_eq!(nt.read(0xC00), 0x11);
nt.write(0xAA, 0x400); assert_eq!(nt.read(0x000), 0xAA);
assert_eq!(nt.read(0x400), 0xAA);
}
#[test]
fn vertical_mapping_read_write() {
let data = build_pages();
let mut nt = NameTableRam::with_data(&data);
nt.set_vertical();
assert_eq!(nt.read(0x000), 0x00);
assert_eq!(nt.read(0x400), 0x11);
assert_eq!(nt.read(0x800), 0x00);
assert_eq!(nt.read(0xC00), 0x11);
nt.write(0xBB, 0x800); assert_eq!(nt.read(0x000), 0xBB);
assert_eq!(nt.read(0x800), 0xBB);
}
#[test]
fn four_screen_mapping_read_write() {
let data = build_pages();
let mut nt = NameTableRam::with_data(&data);
nt.set_four_screen();
assert_eq!(nt.read(0x000), 0x00);
assert_eq!(nt.read(0x400), 0x11);
assert_eq!(nt.read(0x800), 0x22);
assert_eq!(nt.read(0xC00), 0x33);
nt.write(0xCC, 0xC00); assert_eq!(nt.read(0xC00), 0xCC);
}
#[test]
fn is_even_various_types() {
assert!(is_even(0u8));
assert!(is_even(2u8));
assert!(!is_even(3u8));
assert!(is_even(4u16));
assert!(!is_even(5u16));
assert!(is_even(10i32));
assert!(!is_even(11i32));
}
#[test]
fn name_table_page_index_function() {
assert_eq!(name_table_page_index(0x000, Mirroring::Horizontal), 0);
assert_eq!(name_table_page_index(0x400, Mirroring::Horizontal), 0);
assert_eq!(name_table_page_index(0x800, Mirroring::Horizontal), 1);
assert_eq!(name_table_page_index(0x000, Mirroring::Vertical), 0);
assert_eq!(name_table_page_index(0x400, Mirroring::Vertical), 1);
assert_eq!(name_table_page_index(0x800, Mirroring::Vertical), 0);
assert_eq!(name_table_page_index(0x000, Mirroring::FourScreen), 0);
assert_eq!(name_table_page_index(0x400, Mirroring::FourScreen), 1);
assert_eq!(name_table_page_index(0x800, Mirroring::FourScreen), 2);
}
fn build_cart(prg_pages: usize, chr_pages: usize) -> Cart<Vec<Rom<SIZE_1K>>> {
let mut prg = Vec::new();
for i in 0..prg_pages {
let data = vec![i as u8; PRG_ROM_PAGE_SIZE];
prg.push(Rom::<PRG_ROM_PAGE_SIZE>::with_data(&data));
}
let mut chr = Vec::new();
for i in 0..chr_pages {
let data = vec![i as u8; CHR_MEM_PAGE_SIZE];
chr.push(Rom::<CHR_MEM_PAGE_SIZE>::with_data(&data));
}
Cart {
prg_rom: prg,
chr_mem: chr,
bank_select: 0,
bank_registers: [0u8; 8],
prg_mode: Mode::Normal,
chr_mode: Mode::Normal,
is_ram_writable: false,
..Default::default()
}
}
#[test]
fn write_bank_select_and_data() {
let mut cart = build_cart(4, 8);
cart.write_bank_select(0);
assert_eq!(cart.bank_select, 0);
assert_eq!(cart.prg_mode, Mode::Normal);
assert_eq!(cart.chr_mode, Mode::Normal);
cart.write_bank_data(3);
assert_eq!(cart.bank_registers[0], 2);
cart.write_bank_select(6);
cart.write_bank_data(6); assert_eq!(cart.bank_registers[6], 2);
}
#[test]
fn read_prg_rom_mapping_quarters() {
let mut cart = build_cart(4, 0);
cart.bank_registers[6] = 1; cart.bank_registers[7] = 2; cart.prg_mode = Mode::Normal;
let val_q0 = cart.read_prg_rom(0 * SIZE_8K as u16 + 0x000);
assert_eq!(val_q0, 1u8);
let val_q1 = cart.read_prg_rom(1 * SIZE_8K as u16 + 0x123);
assert_eq!(val_q1, 2u8);
let val_q2 = cart.read_prg_rom(2 * SIZE_8K as u16 + 0x1);
assert_eq!(val_q2, 2u8);
}
#[test]
fn chr_mem_page_index_and_write_behavior() {
let mut cart = build_cart(2, 8);
cart.bank_registers[0] = 4;
cart.bank_registers[1] = 6;
cart.bank_registers[2] = 8;
cart.bank_registers[3] = 9;
cart.bank_registers[4] = 10;
cart.bank_registers[5] = 11;
cart.chr_mode = Mode::Normal;
assert_eq!(
chr_mem_page_index(0 * SIZE_1K as u16, cart.chr_mode, &cart.bank_registers),
cart.bank_registers[0] as usize
);
assert_eq!(
chr_mem_page_index(1 * SIZE_1K as u16, cart.chr_mode, &cart.bank_registers),
cart.bank_registers[0] as usize + 1
);
assert_eq!(
chr_mem_page_index(2 * SIZE_1K as u16, cart.chr_mode, &cart.bank_registers),
cart.bank_registers[1] as usize
);
cart.chr_mode = Mode::Inverted;
assert_eq!(
chr_mem_page_index(0 * SIZE_1K as u16, cart.chr_mode, &cart.bank_registers),
cart.bank_registers[2] as usize
);
assert_eq!(
chr_mem_page_index(4 * SIZE_1K as u16, cart.chr_mode, &cart.bank_registers),
cart.bank_registers[0] as usize
);
}
#[test]
fn has_chr_ram_and_indices_helpers() {
let cart_with_chr = build_cart(1, 1);
assert!(!cart_with_chr.has_chr_ram());
let cart1 = build_cart(3, 0);
assert_eq!(cart1.last_prg_rom_page_index(), 2);
assert_eq!(cart1.second_to_last_prg_rom_page_index(), 1);
}
#[test]
fn name_table_addr_space_and_init() {
assert_eq!(
NameTableRam::addr_space_size(),
(NAME_TABLE_PAGE_SIZE as u32) * 4
);
let nt = NameTableRam::default();
assert_eq!(nt.addr_space_size_dyn(), (NAME_TABLE_PAGE_SIZE as u32) * 4);
let data = build_pages();
let mut nt2 = NameTableRam::default();
nt2.init_data(&data);
assert_eq!(nt2.read(0x000), 0x00);
assert_eq!(nt2.read(0x400), 0x00);
nt2.set_four_screen();
assert_eq!(nt2.read(0x800), 0x22);
}
#[test]
fn set_four_screen_prevents_changes() {
let data = build_pages();
let mut nt = NameTableRam::with_data(&data);
nt.set_four_screen();
nt.set_horizontal();
nt.set_vertical();
assert_eq!(nt.read(0x000), 0x00);
assert_eq!(nt.read(0x400), 0x11);
assert_eq!(nt.read(0x800), 0x22);
assert_eq!(nt.read(0xC00), 0x33);
}
fn build_cart_with_prg_ram(prg_pages: usize) -> Cart<Ram<SIZE_8K>> {
let mut prg = Vec::new();
for i in 0..prg_pages {
let data = vec![i as u8; PRG_ROM_PAGE_SIZE];
prg.push(Rom::<PRG_ROM_PAGE_SIZE>::with_data(&data));
}
Cart {
prg_rom: prg,
bank_select: 0,
bank_registers: [0u8; 8],
prg_mode: Mode::Normal,
chr_mode: Mode::Normal,
is_ram_writable: false,
..Default::default()
}
}
#[test]
fn write_cpu_mapped_bank_select_and_data_via_wrapper() {
let mut cart = build_cart(4, 8);
let sel_addr = (0x8000 - CART_CPU_MAP_BEGIN_ADDR) as u16;
cart.write_cpu_mapped(0xC0, sel_addr);
assert_eq!(cart.bank_select, 0xC0 % 8);
assert_eq!(cart.prg_mode, Mode::Inverted);
assert_eq!(cart.chr_mode, Mode::Inverted);
let data_addr = (0x8001 - CART_CPU_MAP_BEGIN_ADDR) as u16;
cart.write_cpu_mapped(3, data_addr);
assert_eq!(cart.bank_registers[cart.bank_select as usize], 2);
}
#[test]
fn write_cpu_mapped_mirroring_and_prg_ram_write_enable() {
let mut cart = build_cart_with_prg_ram(2);
cart.name_tables_ram.set_vertical();
let a_even = (0xA000 - CART_CPU_MAP_BEGIN_ADDR) as u16;
cart.write_cpu_mapped(1, a_even);
assert_eq!(
cart.read_ppu_mapped(0x2000),
cart.read_ppu_mapped(0x2000 + 0x400)
);
let a_odd = (0xA001 - CART_CPU_MAP_BEGIN_ADDR) as u16;
cart.write_cpu_mapped(0x80, a_odd); let ram_write_addr = (0x6000 - CART_CPU_MAP_BEGIN_ADDR) as u16;
cart.write_cpu_mapped(0x7A, ram_write_addr);
let val = cart.read_cpu_mapped(ram_write_addr);
assert_eq!(val, 0x7A);
cart.write_cpu_mapped(0x00, a_odd); cart.write_cpu_mapped(0x55, ram_write_addr);
let val2 = cart.read_cpu_mapped(ram_write_addr);
assert_eq!(val2, 0x7A);
}
#[test]
fn read_write_ppu_mapped_chr_and_name_table() {
let mut cart = build_cart(4, 8);
cart.bank_registers[0] = 4;
cart.chr_mode = Mode::Normal;
let chr_val = cart.read_ppu_mapped(0x0000);
assert_eq!(chr_val, 4u8);
let nt_write_res = cart.write_ppu_mapped(0xAB, 0x2000);
assert_eq!(nt_write_res, ChrRomContentStatus::Unchanged);
assert_eq!(cart.read_ppu_mapped(0x2000), 0xAB);
}
fn cart_addr(cpu_addr: u16) -> u16 {
cpu_addr - CART_CPU_MAP_BEGIN_ADDR
}
#[test]
fn prg_rom_normal_mode_full_layout() {
let mut cart = build_cart(4, 8);
cart.bank_registers[6] = 0;
cart.bank_registers[7] = 1;
cart.prg_mode = Mode::Normal;
assert_eq!(cart.read_prg_rom(0 * SIZE_8K as u16), 0u8); assert_eq!(cart.read_prg_rom(1 * SIZE_8K as u16), 1u8); assert_eq!(cart.read_prg_rom(2 * SIZE_8K as u16), 2u8); assert_eq!(cart.read_prg_rom(3 * SIZE_8K as u16), 3u8); }
#[test]
fn prg_rom_inverted_mode_full_layout() {
let mut cart = build_cart(4, 8);
cart.bank_registers[6] = 0;
cart.bank_registers[7] = 1;
cart.prg_mode = Mode::Inverted;
assert_eq!(cart.read_prg_rom(0 * SIZE_8K as u16), 2u8); assert_eq!(cart.read_prg_rom(1 * SIZE_8K as u16), 1u8); assert_eq!(cart.read_prg_rom(2 * SIZE_8K as u16), 0u8); assert_eq!(cart.read_prg_rom(3 * SIZE_8K as u16), 3u8); }
#[test]
fn write_bank_data_chr_pair_clears_lsb() {
let mut cart = build_cart(4, 8);
cart.bank_select = 0;
cart.write_bank_data(5); assert_eq!(cart.bank_registers[0], 4);
cart.bank_select = 1;
cart.write_bank_data(7);
assert_eq!(cart.bank_registers[1], 6);
}
#[test]
fn write_bank_data_chr_single_keeps_value() {
let mut cart = build_cart(4, 8);
cart.bank_select = 2;
cart.write_bank_data(5);
assert_eq!(cart.bank_registers[2], 5);
cart.bank_select = 5;
cart.write_bank_data(7);
assert_eq!(cart.bank_registers[5], 7);
}
#[test]
fn write_bank_data_prg_modulo_page_count() {
let mut cart = build_cart(4, 8);
cart.bank_select = 6;
cart.write_bank_data(7); assert_eq!(cart.bank_registers[6], 3);
cart.bank_select = 7;
cart.write_bank_data(9); assert_eq!(cart.bank_registers[7], 1);
}
#[test]
fn write_bank_data_chr_with_256_pages_does_not_panic() {
let mut cart = build_cart(2, 256);
cart.bank_select = 0;
cart.write_bank_data(0xFF); assert_eq!(cart.bank_registers[0], 0xFE);
cart.bank_select = 3;
cart.write_bank_data(0xFF); assert_eq!(cart.bank_registers[3], 0xFF);
cart.bank_select = 5;
cart.write_bank_data(200); assert_eq!(cart.bank_registers[5], 200);
assert_eq!(cart.chr_mem.page_count(), 256);
}
#[test]
fn irq_default_inactive() {
let cart = build_cart(2, 4);
assert!(!cart.irq_pin());
assert!(!cart.irq_active);
assert!(!cart.irq_enable);
}
#[test]
fn irq_enable_disable_via_cpu_mapped() {
let mut cart = build_cart(2, 4);
cart.write_cpu_mapped(0x00, cart_addr(0xE000));
assert!(!cart.irq_enable);
assert!(!cart.irq_active);
cart.write_cpu_mapped(0x00, cart_addr(0xE001));
assert!(cart.irq_enable);
assert!(!cart.irq_active);
}
#[test]
fn irq_latch_and_reload_flag() {
let mut cart = build_cart(2, 4);
cart.write_cpu_mapped(0x05, cart_addr(0xC000));
assert_eq!(cart.irq_latch, 0x05);
cart.write_cpu_mapped(0x00, cart_addr(0xC001));
assert!(cart.irq_reload_flag);
cart.read_ppu_mapped(0x0000);
cart.read_ppu_mapped(0x1000);
assert_eq!(cart.irq_counter, 0x05);
assert!(!cart.irq_reload_flag);
}
#[test]
fn irq_a12_rising_edge_decrements_counter() {
let mut cart = build_cart(2, 4);
cart.irq_latch = 0x10;
cart.irq_counter = 0x10;
cart.irq_enable = true;
for _ in 0..0x10 {
cart.read_ppu_mapped(0x0000); cart.read_ppu_mapped(0x1000); }
assert_eq!(cart.irq_counter, 0);
assert!(cart.irq_pin());
}
#[test]
fn irq_not_asserted_when_disabled() {
let mut cart = build_cart(2, 4);
cart.irq_latch = 0x01;
cart.irq_counter = 0x01;
cart.irq_enable = false;
cart.read_ppu_mapped(0x0000);
cart.read_ppu_mapped(0x1000); assert_eq!(cart.irq_counter, 0);
assert!(!cart.irq_pin());
}
#[test]
fn irq_falling_edge_does_not_decrement() {
let mut cart = build_cart(2, 4);
cart.irq_latch = 0x10;
cart.irq_counter = 0x10;
cart.read_ppu_mapped(0x0000);
cart.read_ppu_mapped(0x1000);
assert_eq!(cart.irq_counter, 0x0F);
cart.read_ppu_mapped(0x0000);
assert_eq!(cart.irq_counter, 0x0F);
cart.read_ppu_mapped(0x1000);
assert_eq!(cart.irq_counter, 0x0E);
}
#[test]
fn irq_counter_reloads_on_zero_or_reload_flag() {
let mut cart = build_cart(2, 4);
cart.irq_latch = 0x03;
cart.irq_counter = 0x03;
cart.irq_enable = false;
let expected_after_each_edge = [0x02u8, 0x01, 0x00];
for &expected in &expected_after_each_edge {
cart.read_ppu_mapped(0x0000);
cart.read_ppu_mapped(0x1000);
assert_eq!(cart.irq_counter, expected);
}
cart.read_ppu_mapped(0x0000);
cart.read_ppu_mapped(0x1000);
assert_eq!(cart.irq_counter, 0x03);
}
#[test]
fn irq_acknowledge_clears_pin() {
let mut cart = build_cart(2, 4);
cart.irq_latch = 0x01;
cart.irq_counter = 0x01;
cart.irq_enable = true;
cart.read_ppu_mapped(0x0000);
cart.read_ppu_mapped(0x1000); assert!(cart.irq_pin());
cart.write_cpu_mapped(0x00, cart_addr(0xE000));
assert!(!cart.irq_pin());
assert!(!cart.irq_enable);
}
#[test]
fn requires_cycle_accurate_sprites() {
let cart = build_cart(2, 4);
assert!(cart.requires_cycle_accurate_sprites());
}
#[test]
fn should_reload_and_set_irq_every_clock_when_reload_is_zero() {
let mut cart = build_cart(2, 4);
cart.write_cpu_mapped(0x00, cart_addr(0xC000));
cart.write_cpu_mapped(0x00, cart_addr(0xE001));
assert!(cart.irq_enable);
assert!(!cart.irq_active);
for _ in 0..16 {
cart.read_ppu_mapped(0x0000); assert!(!cart.irq_pin());
cart.read_ppu_mapped(0x1000); assert!(
cart.irq_pin(),
"IRQ should be asserted after A12 rising edge"
);
assert_eq!(
cart.irq_counter, 0x00,
"counter should reload to 0 (latch value)"
);
cart.write_cpu_mapped(0x00, cart_addr(0xE000));
assert!(!cart.irq_pin());
cart.write_cpu_mapped(0x00, cart_addr(0xE001));
}
}
#[test]
fn load_accepts_max_chr_rom_size() {
use crate::rom_loader::ines::HeaderData;
let mut header = [0u8; 16];
header[0..4].copy_from_slice(b"NES\x1A");
header[4] = 4; header[5] = 32; header[6] = 0b0100_0000; header[7] = 0x00;
let header_data = HeaderData::new(&header);
let prg = vec![0u8; header_data.prg_rom_size as usize];
let chr = vec![0u8; header_data.chr_rom_size as usize];
let mut payload = Vec::new();
payload.extend_from_slice(&prg);
payload.extend_from_slice(&chr);
let cart = load(&header_data, &mut payload.as_slice());
assert!(
cart.is_ok(),
"MMC3 with 256 KB CHR ROM must load: got {}",
if cart.is_err() { "Err" } else { "Ok" }
);
}
#[test]
fn load_error_message_references_mapper_004() {
use crate::rom_loader::ines::HeaderData;
let mut header = [0u8; 16];
header[0..4].copy_from_slice(b"NES\x1A");
header[4] = 2;
header[5] = 255; header[6] = 0b0100_0000;
header[7] = 0x00;
let header_data = HeaderData::new(&header);
let mut empty: &[u8] = &[];
let result = load(&header_data, &mut empty);
let err = match result {
Ok(_) => panic!("expected an error for oversized CHR ROM"),
Err(e) => e,
};
let msg = format!("{err}");
assert!(
msg.contains("mapper 004"),
"error should name mapper 004, got: {msg}"
);
assert!(
!msg.contains("basic mapper"),
"error should not say basic mapper, got: {msg}"
);
}
}