#[allow(dead_code)]
pub trait StateSnapshot {
fn snapshot(&self) -> Vec<u8>;
fn restore(&mut self, data: &[u8]);
}
pub const DEFAULT_PRG_RAM_SIZE: usize = 8192;
pub const DEFAULT_CHR_RAM_SIZE: usize = 8192;
#[derive(Clone)]
pub struct PrgRam {
data: Vec<u8>,
}
impl PrgRam {
pub fn new(size: usize) -> Self {
Self {
data: vec![0; size],
}
}
#[inline]
pub fn try_read(&self, addr: u16) -> Option<u8> {
if (0x6000..=0x7FFF).contains(&addr) {
let offset = (addr - 0x6000) as usize;
Some(self.data.get(offset).copied().unwrap_or(0))
} else {
None
}
}
#[inline]
pub fn try_write(&mut self, addr: u16, value: u8) -> bool {
if (0x6000..=0x7FFF).contains(&addr) {
let offset = (addr - 0x6000) as usize;
if offset < self.data.len() {
self.data[offset] = value;
}
true
} else {
false
}
}
#[inline]
pub fn size(&self) -> usize {
self.data.len()
}
pub fn snapshot(&self) -> Vec<u8> {
self.data.clone()
}
#[inline]
pub fn read_at_offset(&self, offset: usize) -> u8 {
self.data.get(offset).copied().unwrap_or(0)
}
#[inline]
pub fn write_at_offset(&mut self, offset: usize, value: u8) {
if offset < self.data.len() {
self.data[offset] = value;
}
}
pub fn load_snapshot(&mut self, data: &[u8]) {
let to_copy = data.len().min(self.data.len());
self.data[..to_copy].copy_from_slice(&data[..to_copy]);
}
pub fn initialize(&mut self, mode: crate::nes::console::RamInitMode) {
crate::nes::console::initialize_ram(&mut self.data, mode);
}
}
impl StateSnapshot for PrgRam {
fn snapshot(&self) -> Vec<u8> {
PrgRam::snapshot(self)
}
fn restore(&mut self, data: &[u8]) {
self.load_snapshot(data);
}
}
#[derive(Clone)]
pub struct ChrMemory {
data: Vec<u8>,
is_ram: bool,
}
impl ChrMemory {
pub fn new(chr_rom: Vec<u8>) -> Self {
if chr_rom.is_empty() {
Self {
data: vec![0; DEFAULT_CHR_RAM_SIZE],
is_ram: true,
}
} else {
Self {
data: chr_rom,
is_ram: false,
}
}
}
pub fn new_ram(size: usize) -> Self {
Self {
data: vec![0; size],
is_ram: true,
}
}
#[inline]
pub fn read(&self, addr: u16) -> u8 {
let index = (addr & 0x1FFF) as usize;
self.data.get(index).copied().unwrap_or(0)
}
#[inline]
pub fn write(&mut self, addr: u16, value: u8) {
if self.is_ram {
let index = (addr & 0x1FFF) as usize;
if index < self.data.len() {
self.data[index] = value;
}
}
}
#[cfg(test)]
#[inline]
pub fn is_ram(&self) -> bool {
self.is_ram
}
#[inline]
pub fn size(&self) -> usize {
self.data.len()
}
#[inline]
pub fn read_at_index(&self, index: usize) -> u8 {
self.data.get(index).copied().unwrap_or(0)
}
#[inline]
pub fn write_at_index(&mut self, index: usize, value: u8) {
if self.is_ram && index < self.data.len() {
self.data[index] = value;
}
}
pub fn snapshot(&self) -> Vec<u8> {
if self.is_ram {
self.data.clone()
} else {
Vec::new()
}
}
pub fn load_snapshot(&mut self, data: &[u8]) {
if !self.is_ram {
return;
}
let to_copy = data.len().min(self.data.len());
self.data[..to_copy].copy_from_slice(&data[..to_copy]);
}
pub fn initialize(&mut self, mode: crate::nes::console::RamInitMode) {
if self.is_ram {
crate::nes::console::initialize_ram(&mut self.data, mode);
}
}
}
impl StateSnapshot for ChrMemory {
fn snapshot(&self) -> Vec<u8> {
ChrMemory::snapshot(self)
}
fn restore(&mut self, data: &[u8]) {
self.load_snapshot(data);
}
}
#[cfg(test)]
#[derive(Clone)]
pub struct BankedRom {
data: Vec<u8>,
bank_size: usize,
}
#[cfg(test)]
impl BankedRom {
pub fn new(data: Vec<u8>, bank_size: usize) -> Self {
Self { data, bank_size }
}
#[inline]
pub fn num_banks(&self) -> usize {
if self.bank_size == 0 || self.data.is_empty() {
return 0;
}
self.data.len() / self.bank_size
}
#[inline]
pub fn read(&self, bank: usize, offset: usize) -> u8 {
let num_banks = self.num_banks();
if num_banks == 0 {
return 0;
}
let bank = bank % num_banks;
let index = bank * self.bank_size + offset;
self.data.get(index).copied().unwrap_or(0)
}
#[inline]
pub fn read_with_base(&self, bank: usize, base_addr: u16, addr: u16) -> u8 {
let offset = addr.wrapping_sub(base_addr) as usize;
self.read(bank, offset)
}
}
#[cfg(test)]
#[derive(Clone, Copy, Debug)]
pub struct BankSwitch {
num_banks: usize,
bank: u8,
}
#[cfg(test)]
impl BankSwitch {
pub fn new(num_banks: usize) -> Self {
Self { num_banks, bank: 0 }
}
pub fn from_rom(rom_data: &[u8], bank_size: usize) -> Self {
let num_banks = if rom_data.is_empty() || bank_size == 0 {
0
} else {
rom_data.len() / bank_size
};
Self::new(num_banks)
}
pub fn set(&mut self, value: u8) {
self.bank = value;
}
pub fn current(&self) -> usize {
if self.num_banks == 0 {
0
} else {
(self.bank as usize) % self.num_banks
}
}
#[cfg(test)]
pub fn offset(&self, bank_size: usize) -> usize {
self.current() * bank_size
}
pub fn raw(&self) -> u8 {
self.bank
}
}
#[cfg(test)]
impl StateSnapshot for BankSwitch {
fn snapshot(&self) -> Vec<u8> {
vec![self.bank]
}
fn restore(&mut self, data: &[u8]) {
if let Some(&value) = data.first() {
self.bank = value;
}
}
}
#[allow(dead_code)]
pub struct A12IrqCounter {
latch: u8,
counter: u8,
reload: bool,
enabled: bool,
asserted: bool,
a12_detector: A12RisingEdgeDetector,
alternate_behavior: bool,
}
#[allow(dead_code)]
impl A12IrqCounter {
pub fn new(alternate_behavior: bool) -> Self {
Self {
latch: 0,
counter: 0,
reload: false,
enabled: false,
asserted: false,
a12_detector: A12RisingEdgeDetector::new(3),
alternate_behavior,
}
}
pub fn set_latch(&mut self, value: u8) {
self.latch = value;
}
pub fn request_reload(&mut self) {
self.counter = 0;
self.reload = true;
}
pub fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
if !enabled {
self.asserted = false;
}
}
pub fn is_pending(&self) -> bool {
self.asserted
}
pub fn cpu_cycle(&mut self) {
self.a12_detector.cpu_tick();
}
pub fn ppu_address_changed(&mut self, addr: u16) {
if self.a12_detector.update(addr) {
self.clock_counter();
}
}
fn clock_counter(&mut self) {
let old_counter = self.counter;
let was_reload = self.reload;
if self.counter == 0 || self.reload {
self.counter = self.latch;
self.reload = false;
} else {
self.counter = self.counter.wrapping_sub(1);
}
let should_fire = if self.alternate_behavior {
let decremented_to_zero = old_counter == 1 && self.counter == 0;
let reload_triggered_to_zero = was_reload && self.counter == 0;
decremented_to_zero || reload_triggered_to_zero
} else {
self.counter == 0
};
if should_fire && self.enabled {
self.asserted = true;
}
}
#[cfg(test)]
pub fn counter(&self) -> u8 {
self.counter
}
}
pub struct A12RisingEdgeDetector {
prev_a12: bool,
current_a12: bool,
a12_low_cycles: u8,
threshold: u8,
}
impl A12RisingEdgeDetector {
pub fn new(threshold: u8) -> Self {
Self {
prev_a12: false,
current_a12: false,
a12_low_cycles: 0,
threshold,
}
}
pub fn update(&mut self, addr: u16) -> bool {
let a12 = (addr & 0x1000) != 0;
self.current_a12 = a12;
let rising = a12 && !self.prev_a12;
self.prev_a12 = a12;
rising && self.a12_low_cycles >= self.threshold
}
pub fn cpu_tick(&mut self) {
if self.current_a12 {
self.a12_low_cycles = 0;
} else {
self.a12_low_cycles = self.a12_low_cycles.saturating_add(1);
}
}
pub fn prev_a12(&self) -> bool {
self.prev_a12
}
pub fn current_a12(&self) -> bool {
self.current_a12
}
pub fn a12_low_cycles(&self) -> u8 {
self.a12_low_cycles
}
pub fn set_prev_a12(&mut self, value: bool) {
self.prev_a12 = value;
}
pub fn set_current_a12(&mut self, value: bool) {
self.current_a12 = value;
}
pub fn set_a12_low_cycles(&mut self, value: u8) {
self.a12_low_cycles = value;
}
}
#[derive(Default)]
#[allow(dead_code)]
pub struct VrcIrqCounter {
latch: u8,
counter: u8,
enabled: bool,
mode_cycle: bool,
enable_after_ack: bool,
asserted: bool,
prescaler: i32,
}
#[allow(dead_code)]
impl VrcIrqCounter {
const PRESCALER_INIT: i32 = 341;
const PRESCALER_STEP: i32 = 3;
pub fn new() -> Self {
Self {
latch: 0,
counter: 0,
enabled: false,
mode_cycle: false,
enable_after_ack: false,
asserted: false,
prescaler: 0,
}
}
pub fn set_latch_low(&mut self, value: u8) {
self.latch = (self.latch & 0xF0) | (value & 0x0F);
}
pub fn set_latch_high(&mut self, value: u8) {
self.latch = (self.latch & 0x0F) | ((value & 0x0F) << 4);
}
pub fn write_control(&mut self, value: u8) {
self.asserted = false;
self.prescaler = Self::PRESCALER_INIT;
self.mode_cycle = (value & 0b0000_0100) != 0;
let enable = (value & 0b0000_0010) != 0;
self.enable_after_ack = (value & 0b0000_0001) != 0;
if enable {
self.enabled = true;
self.counter = self.latch;
} else {
self.enabled = false;
}
}
pub fn acknowledge(&mut self) {
self.asserted = false;
self.enabled = self.enable_after_ack;
}
pub fn is_pending(&self) -> bool {
self.asserted
}
pub fn clock(&mut self) {
if !self.enabled {
return;
}
if self.mode_cycle {
self.clock_counter();
return;
}
self.prescaler -= Self::PRESCALER_STEP;
if self.prescaler <= 0 {
self.prescaler += Self::PRESCALER_INIT;
self.clock_counter();
}
}
fn clock_counter(&mut self) {
if self.counter == 0xFF {
self.counter = self.latch;
self.asserted = true;
} else {
self.counter = self.counter.wrapping_add(1);
}
}
#[cfg(test)]
pub fn counter(&self) -> u8 {
self.counter
}
#[cfg(test)]
pub fn latch(&self) -> u8 {
self.latch
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nes::cartridge::test_helpers::banked_data;
#[test]
fn test_prg_ram_read_write() {
let mut prg_ram = PrgRam::new(8192);
assert!(prg_ram.try_write(0x6000, 0x42));
assert_eq!(prg_ram.try_read(0x6000), Some(0x42));
assert!(prg_ram.try_write(0x7FFF, 0xAB));
assert_eq!(prg_ram.try_read(0x7FFF), Some(0xAB));
assert_eq!(prg_ram.try_read(0x5FFF), None);
assert_eq!(prg_ram.try_read(0x8000), None);
assert!(!prg_ram.try_write(0x5FFF, 0xFF));
assert!(!prg_ram.try_write(0x8000, 0xFF));
}
#[test]
fn test_prg_ram_snapshot() {
let mut prg_ram = PrgRam::new(8192);
prg_ram.try_write(0x6000, 0x42);
prg_ram.try_write(0x7FFF, 0xAB);
let snapshot = prg_ram.snapshot();
assert_eq!(snapshot.len(), 8192);
assert_eq!(snapshot[0], 0x42);
assert_eq!(snapshot[0x1FFF], 0xAB);
let mut prg_ram2 = PrgRam::new(8192);
prg_ram2.load_snapshot(&snapshot);
assert_eq!(prg_ram2.try_read(0x6000), Some(0x42));
assert_eq!(prg_ram2.try_read(0x7FFF), Some(0xAB));
}
#[test]
fn test_chr_memory_rom() {
let chr_rom = vec![0xAA; 8192];
let mut chr = ChrMemory::new(chr_rom);
assert!(!chr.is_ram());
assert_eq!(chr.read(0x0000), 0xAA);
chr.write(0x0000, 0x55);
assert_eq!(chr.read(0x0000), 0xAA);
}
#[test]
fn test_chr_memory_ram() {
let mut chr = ChrMemory::new(vec![]);
assert!(chr.is_ram());
assert_eq!(chr.size(), DEFAULT_CHR_RAM_SIZE);
assert_eq!(chr.read(0x0000), 0x00);
chr.write(0x0000, 0x55);
assert_eq!(chr.read(0x0000), 0x55);
}
#[test]
fn test_chr_memory_address_masking() {
let mut chr = ChrMemory::new_ram(8192);
assert_eq!(chr.size(), 8192);
chr.write(0x0100, 0x42);
assert_eq!(chr.read(0x0100), 0x42);
assert_eq!(chr.read(0x2100), 0x42); }
#[test]
fn test_banked_rom_single_bank() {
let mut rom = vec![0; 8192];
rom[0] = 0xAA;
rom[8191] = 0xBB;
let banked = BankedRom::new(rom, 8192);
assert_eq!(banked.read(0, 0), 0xAA);
assert_eq!(banked.read(0, 8191), 0xBB);
}
#[test]
fn test_banked_rom_multiple_banks() {
let mut rom = vec![0; 64 * 1024];
for bank in 0..4 {
let start = bank * 16 * 1024;
rom[start] = (bank + 10) as u8;
rom[start + 16 * 1024 - 1] = (bank + 20) as u8;
}
let banked = BankedRom::new(rom, 16 * 1024);
assert_eq!(banked.read(0, 0), 10);
assert_eq!(banked.read(0, 16 * 1024 - 1), 20);
assert_eq!(banked.read(1, 0), 11);
assert_eq!(banked.read(1, 16 * 1024 - 1), 21);
assert_eq!(banked.read(3, 0), 13);
assert_eq!(banked.read(3, 16 * 1024 - 1), 23);
}
#[test]
fn test_banked_rom_wrapping() {
let mut rom = vec![0; 16 * 1024];
rom[0] = 0x11;
rom[8192] = 0x22;
let banked = BankedRom::new(rom, 8192);
assert_eq!(banked.read(0, 0), 0x11);
assert_eq!(banked.read(1, 0), 0x22);
assert_eq!(banked.read(2, 0), 0x11);
assert_eq!(banked.read(3, 0), 0x22);
}
#[test]
fn test_banked_rom_out_of_bounds_offset() {
let rom = vec![0xAA; 8192];
let banked = BankedRom::new(rom, 8192);
assert_eq!(banked.read(0, 8192), 0);
assert_eq!(banked.read(0, 10000), 0);
}
#[test]
fn test_banked_rom_num_banks() {
let rom = vec![0; 64 * 1024]; let banked = BankedRom::new(rom, 16 * 1024);
assert_eq!(banked.num_banks(), 4);
}
#[test]
fn test_banked_rom_empty() {
let rom = vec![];
let banked = BankedRom::new(rom, 8192);
assert_eq!(banked.num_banks(), 0);
assert_eq!(banked.read(0, 0), 0);
}
#[test]
fn test_banked_rom_read_with_base_address() {
let mut rom = vec![0; 32 * 1024];
for bank in 0..2 {
let start = bank * 16 * 1024;
rom[start] = (bank + 100) as u8;
}
let banked = BankedRom::new(rom, 16 * 1024);
assert_eq!(banked.read_with_base(0, 0x8000, 0x8000), 100);
assert_eq!(banked.read_with_base(1, 0xC000, 0xC000), 101);
}
#[test]
fn test_banked_rom_with_cpu_addressing() {
const PRG_BANK_SIZE: usize = 0x4000; let rom = banked_data(PRG_BANK_SIZE, 8);
let banked = BankedRom::new(rom, PRG_BANK_SIZE);
assert_eq!(banked.read_with_base(0, 0x8000, 0x8000), 0);
assert_eq!(banked.read_with_base(0, 0x8000, 0x8001), 0);
assert_eq!(banked.read_with_base(0, 0x8000, 0xBFFF), 0);
assert_eq!(banked.read_with_base(3, 0x8000, 0x8000), 3);
assert_eq!(banked.read_with_base(3, 0x8000, 0x8001), 3);
}
#[test]
fn test_banked_rom_empty_rom() {
const PRG_BANK_SIZE: usize = 0x4000;
let empty_rom = vec![];
let banked = BankedRom::new(empty_rom, PRG_BANK_SIZE);
assert_eq!(banked.num_banks(), 0);
assert_eq!(banked.read(0, 0), 0);
assert_eq!(banked.read(1, 0), 0);
}
#[test]
fn test_banked_rom_bounds_checking() {
const BANK_SIZE: usize = 1024;
let rom = banked_data(BANK_SIZE, 4);
let banked = BankedRom::new(rom, BANK_SIZE);
assert_eq!(banked.read(0, 0), 0);
assert_eq!(banked.read(0, BANK_SIZE - 1), 0);
assert_eq!(banked.read(3, BANK_SIZE - 1), 3);
assert_eq!(banked.read(0, BANK_SIZE * 2), 2);
assert_eq!(banked.read(0, 10000), 0);
assert_eq!(banked.read(99, 10000), 0);
}
#[test]
fn test_state_snapshot_prg_ram() {
let mut prg_ram = PrgRam::new(8192);
prg_ram.try_write(0x6000, 0x42);
prg_ram.try_write(0x7FFF, 0xAB);
let snapshot = prg_ram.snapshot();
assert_eq!(snapshot.len(), 8192);
assert_eq!(snapshot[0], 0x42);
assert_eq!(snapshot[0x1FFF], 0xAB);
let mut prg_ram2 = PrgRam::new(8192);
prg_ram2.restore(&snapshot);
assert_eq!(prg_ram2.try_read(0x6000), Some(0x42));
assert_eq!(prg_ram2.try_read(0x7FFF), Some(0xAB));
}
#[test]
fn test_state_snapshot_chr_memory() {
let mut chr = ChrMemory::new_ram(8192);
chr.write(0x0000, 0x11);
chr.write(0x1FFF, 0x22);
let snapshot = chr.snapshot();
assert_eq!(snapshot.len(), 8192);
assert_eq!(snapshot[0], 0x11);
assert_eq!(snapshot[0x1FFF], 0x22);
let mut chr2 = ChrMemory::new_ram(8192);
chr2.restore(&snapshot);
assert_eq!(chr2.read(0x0000), 0x11);
assert_eq!(chr2.read(0x1FFF), 0x22);
}
#[test]
fn test_state_snapshot_chr_rom_empty() {
let chr_rom_data = vec![0xAA; 8192];
let chr = ChrMemory::new(chr_rom_data);
let snapshot = chr.snapshot();
assert!(snapshot.is_empty(), "CHR-ROM snapshot should be empty");
}
#[test]
fn test_state_snapshot_chr_rom_restore_is_noop() {
let chr_rom_data = vec![0xAA; 8192];
let mut chr = ChrMemory::new(chr_rom_data);
let restore_data = vec![0x55; 8192];
chr.restore(&restore_data);
assert_eq!(chr.read(0x0000), 0xAA);
assert_eq!(chr.read(0x1FFF), 0xAA);
assert!(!chr.is_ram(), "Should still be ROM, not RAM");
}
#[test]
fn test_bank_switch_basic() {
let mut bank = BankSwitch::new(4);
assert_eq!(bank.current(), 0);
assert_eq!(bank.raw(), 0);
bank.set(2);
assert_eq!(bank.current(), 2);
assert_eq!(bank.raw(), 2);
}
#[test]
fn test_bank_switch_wrapping() {
let mut bank = BankSwitch::new(4);
bank.set(5);
assert_eq!(bank.current(), 1);
assert_eq!(bank.raw(), 5);
bank.set(8);
assert_eq!(bank.current(), 0);
bank.set(255);
assert_eq!(bank.current(), 255 % 4);
}
#[test]
fn test_bank_switch_empty_rom() {
let mut bank = BankSwitch::new(0);
assert_eq!(bank.current(), 0);
bank.set(5);
assert_eq!(bank.current(), 0);
assert_eq!(bank.raw(), 5);
}
#[test]
fn test_bank_switch_offset_calculation() {
let mut bank = BankSwitch::new(4);
const BANK_SIZE: usize = 0x8000;
assert_eq!(bank.offset(BANK_SIZE), 0);
bank.set(1);
assert_eq!(bank.offset(BANK_SIZE), 0x8000);
bank.set(2);
assert_eq!(bank.offset(BANK_SIZE), 0x10000);
bank.set(3);
assert_eq!(bank.offset(BANK_SIZE), 0x18000);
bank.set(5);
assert_eq!(bank.offset(BANK_SIZE), 0x8000);
}
#[test]
fn test_bank_switch_snapshot() {
let mut bank = BankSwitch::new(8);
bank.set(5);
let snapshot = bank.snapshot();
assert_eq!(snapshot, vec![5]);
let mut bank2 = BankSwitch::new(8);
bank2.restore(&snapshot);
assert_eq!(bank2.current(), 5);
assert_eq!(bank2.raw(), 5);
}
#[test]
fn test_bank_switch_snapshot_empty_data() {
let mut bank = BankSwitch::new(4);
bank.set(3);
bank.restore(&[]);
assert_eq!(bank.raw(), 3); }
#[test]
fn test_bank_switch_from_rom() {
let rom_data = vec![0u8; 32 * 1024];
let bank = BankSwitch::from_rom(&rom_data, 8 * 1024);
assert_eq!(bank.current(), 0);
let empty_rom: Vec<u8> = vec![];
let empty_bank = BankSwitch::from_rom(&empty_rom, 8 * 1024);
assert_eq!(empty_bank.current(), 0);
let zero_bank = BankSwitch::from_rom(&rom_data, 0);
assert_eq!(zero_bank.current(), 0);
}
fn run_detector_cpu_ticks(det: &mut A12RisingEdgeDetector, n: u32) {
for _ in 0..n {
det.cpu_tick();
}
}
#[test]
fn test_a12_detector_new_defaults() {
let det = A12RisingEdgeDetector::new(3);
assert!(!det.prev_a12());
assert!(!det.current_a12());
assert_eq!(det.a12_low_cycles(), 0);
}
#[test]
fn test_a12_detector_no_debounce_detects_first_rising_edge() {
let mut det = A12RisingEdgeDetector::new(0);
assert!(det.update(0x1000)); }
#[test]
fn test_a12_detector_no_debounce_no_false_positive_when_staying_high() {
let mut det = A12RisingEdgeDetector::new(0);
assert!(det.update(0x1000)); assert!(!det.update(0x1000)); assert!(!det.update(0x1FFF)); }
#[test]
fn test_a12_detector_no_debounce_no_edge_on_falling() {
let mut det = A12RisingEdgeDetector::new(0);
det.update(0x1000); assert!(!det.update(0x0000)); }
#[test]
fn test_a12_detector_no_debounce_detects_repeated_rising_edges() {
let mut det = A12RisingEdgeDetector::new(0);
assert!(det.update(0x1000)); assert!(!det.update(0x0000)); assert!(det.update(0x1000)); }
#[test]
fn test_a12_detector_debounce_rejects_edge_without_enough_low_cycles() {
let mut det = A12RisingEdgeDetector::new(3);
det.update(0x0000); run_detector_cpu_ticks(&mut det, 2);
assert!(!det.update(0x1000)); }
#[test]
fn test_a12_detector_debounce_accepts_edge_with_enough_low_cycles() {
let mut det = A12RisingEdgeDetector::new(3);
det.update(0x0000); run_detector_cpu_ticks(&mut det, 3); assert!(det.update(0x1000)); }
#[test]
fn test_a12_detector_debounce_accepts_edge_with_excess_low_cycles() {
let mut det = A12RisingEdgeDetector::new(3);
det.update(0x0000); run_detector_cpu_ticks(&mut det, 10); assert!(det.update(0x1000)); }
#[test]
fn test_a12_detector_cpu_tick_resets_low_cycles_when_a12_high() {
let mut det = A12RisingEdgeDetector::new(3);
det.update(0x0000); run_detector_cpu_ticks(&mut det, 5); assert!(det.a12_low_cycles() > 0);
det.update(0x1000); det.cpu_tick(); assert_eq!(det.a12_low_cycles(), 0);
}
#[test]
fn test_a12_detector_cpu_tick_increments_low_cycles_when_a12_low() {
let mut det = A12RisingEdgeDetector::new(3);
det.update(0x0000); det.cpu_tick();
assert_eq!(det.a12_low_cycles(), 1);
det.cpu_tick();
assert_eq!(det.a12_low_cycles(), 2);
det.cpu_tick();
assert_eq!(det.a12_low_cycles(), 3);
}
#[test]
fn test_a12_detector_low_cycles_saturate() {
let mut det = A12RisingEdgeDetector::new(3);
det.update(0x0000); run_detector_cpu_ticks(&mut det, 300);
assert_eq!(det.a12_low_cycles(), 255);
}
#[test]
fn test_a12_detector_debounce_resets_after_detection() {
let mut det = A12RisingEdgeDetector::new(3);
det.update(0x0000); run_detector_cpu_ticks(&mut det, 3);
assert!(det.update(0x1000));
det.cpu_tick();
assert_eq!(det.a12_low_cycles(), 0);
det.update(0x0000);
run_detector_cpu_ticks(&mut det, 1);
assert!(!det.update(0x1000)); }
#[test]
fn test_a12_detector_update_tracks_current_a12() {
let mut det = A12RisingEdgeDetector::new(3);
assert!(!det.current_a12());
det.update(0x1000);
assert!(det.current_a12());
det.update(0x0000);
assert!(!det.current_a12());
}
#[test]
fn test_a12_detector_snapshot_restore() {
let mut det = A12RisingEdgeDetector::new(3);
det.update(0x0000); run_detector_cpu_ticks(&mut det, 2);
det.update(0x1000);
let prev = det.prev_a12();
let curr = det.current_a12();
let low = det.a12_low_cycles();
let mut det2 = A12RisingEdgeDetector::new(3);
det2.set_prev_a12(prev);
det2.set_current_a12(curr);
det2.set_a12_low_cycles(low);
assert_eq!(det2.prev_a12(), prev);
assert_eq!(det2.current_a12(), curr);
assert_eq!(det2.a12_low_cycles(), low);
}
#[test]
fn test_a12_detector_addr_bit12_extraction() {
let mut det = A12RisingEdgeDetector::new(0);
assert!(det.update(0x1000));
assert!(!det.update(0x0FFF));
assert!(det.update(0x1FFF));
}
fn run_cpu_cycles(irq: &mut A12IrqCounter, n: u32) {
for _ in 0..n {
irq.cpu_cycle();
}
}
fn trigger_a12_rising_edge(irq: &mut A12IrqCounter) {
irq.ppu_address_changed(0x0000); run_cpu_cycles(irq, 3);
irq.ppu_address_changed(0x1000); }
#[test]
fn test_a12_irq_new_defaults() {
let irq = A12IrqCounter::new(false);
assert!(!irq.is_pending());
assert_eq!(irq.counter(), 0);
}
#[test]
fn test_a12_irq_set_latch() {
let mut irq = A12IrqCounter::new(false);
irq.set_latch(10);
assert_eq!(irq.counter(), 0);
}
#[test]
fn test_a12_irq_reload_loads_latch_into_counter() {
let mut irq = A12IrqCounter::new(false);
irq.set_latch(5);
irq.request_reload();
irq.set_enabled(true);
trigger_a12_rising_edge(&mut irq);
assert_eq!(irq.counter(), 5);
}
#[test]
fn test_a12_irq_counter_decrements_on_each_clock() {
let mut irq = A12IrqCounter::new(false);
irq.set_latch(3);
irq.request_reload();
irq.set_enabled(true);
trigger_a12_rising_edge(&mut irq);
assert_eq!(irq.counter(), 3);
trigger_a12_rising_edge(&mut irq);
assert_eq!(irq.counter(), 2);
trigger_a12_rising_edge(&mut irq);
assert_eq!(irq.counter(), 1);
}
#[test]
fn test_a12_irq_fires_when_counter_reaches_zero_normal() {
let mut irq = A12IrqCounter::new(false); irq.set_latch(2);
irq.request_reload();
irq.set_enabled(true);
trigger_a12_rising_edge(&mut irq);
assert!(!irq.is_pending());
trigger_a12_rising_edge(&mut irq);
assert!(!irq.is_pending());
trigger_a12_rising_edge(&mut irq);
assert!(irq.is_pending());
}
#[test]
fn test_a12_irq_does_not_fire_when_disabled() {
let mut irq = A12IrqCounter::new(false);
irq.set_latch(1);
irq.request_reload();
trigger_a12_rising_edge(&mut irq);
trigger_a12_rising_edge(&mut irq);
assert!(!irq.is_pending());
}
#[test]
fn test_a12_irq_disable_acknowledges_pending() {
let mut irq = A12IrqCounter::new(false);
irq.set_latch(1);
irq.request_reload();
irq.set_enabled(true);
trigger_a12_rising_edge(&mut irq); trigger_a12_rising_edge(&mut irq);
assert!(irq.is_pending());
irq.set_enabled(false);
assert!(!irq.is_pending());
}
#[test]
fn test_a12_irq_counter_reloads_on_zero_naturally() {
let mut irq = A12IrqCounter::new(false);
irq.set_latch(1);
irq.request_reload();
irq.set_enabled(true);
trigger_a12_rising_edge(&mut irq);
assert_eq!(irq.counter(), 1);
trigger_a12_rising_edge(&mut irq);
assert_eq!(irq.counter(), 0);
assert!(irq.is_pending());
trigger_a12_rising_edge(&mut irq);
assert_eq!(irq.counter(), 1);
}
#[test]
fn test_a12_irq_debounce_rejects_short_low() {
let mut irq = A12IrqCounter::new(false);
irq.set_latch(5);
irq.request_reload();
irq.set_enabled(true);
irq.ppu_address_changed(0x0000); run_cpu_cycles(&mut irq, 2); irq.ppu_address_changed(0x1000);
assert_eq!(irq.counter(), 0);
}
#[test]
fn test_a12_irq_debounce_accepts_sufficient_low() {
let mut irq = A12IrqCounter::new(false);
irq.set_latch(5);
irq.request_reload();
irq.set_enabled(true);
irq.ppu_address_changed(0x0000); run_cpu_cycles(&mut irq, 3);
irq.ppu_address_changed(0x1000);
assert_eq!(irq.counter(), 5);
}
#[test]
fn test_a12_irq_no_edge_when_already_high() {
let mut irq = A12IrqCounter::new(false);
irq.set_latch(5);
irq.request_reload();
irq.set_enabled(true);
trigger_a12_rising_edge(&mut irq);
assert_eq!(irq.counter(), 5);
run_cpu_cycles(&mut irq, 3);
irq.ppu_address_changed(0x1000);
assert_eq!(irq.counter(), 5);
}
#[test]
fn test_a12_irq_alternate_fires_on_decrement_to_zero() {
let mut irq = A12IrqCounter::new(true); irq.set_latch(2);
irq.request_reload();
irq.set_enabled(true);
trigger_a12_rising_edge(&mut irq);
assert!(!irq.is_pending());
trigger_a12_rising_edge(&mut irq);
assert!(!irq.is_pending());
trigger_a12_rising_edge(&mut irq);
assert!(irq.is_pending());
}
#[test]
fn test_a12_irq_alternate_fires_on_reload_triggered_to_zero() {
let mut irq = A12IrqCounter::new(true); irq.set_latch(0); irq.request_reload();
irq.set_enabled(true);
trigger_a12_rising_edge(&mut irq);
assert!(irq.is_pending());
}
#[test]
fn test_a12_irq_alternate_no_fire_on_natural_zero_reload() {
let mut irq = A12IrqCounter::new(true); irq.set_latch(0); irq.set_enabled(true);
irq.request_reload();
trigger_a12_rising_edge(&mut irq);
assert!(irq.is_pending());
irq.set_enabled(false);
irq.set_enabled(true);
trigger_a12_rising_edge(&mut irq);
assert!(!irq.is_pending());
}
#[test]
fn test_a12_irq_normal_fires_on_natural_zero_reload() {
let mut irq = A12IrqCounter::new(false); irq.set_latch(0); irq.set_enabled(true);
trigger_a12_rising_edge(&mut irq);
assert!(irq.is_pending());
}
#[test]
fn test_vrc_irq_new_defaults() {
let irq = VrcIrqCounter::new();
assert!(!irq.is_pending());
assert_eq!(irq.counter(), 0);
assert_eq!(irq.latch(), 0);
}
#[test]
fn test_vrc_irq_set_latch_nibbles() {
let mut irq = VrcIrqCounter::new();
irq.set_latch_low(0x0A); assert_eq!(irq.latch(), 0x0A);
irq.set_latch_high(0x05); assert_eq!(irq.latch(), 0x5A);
}
#[test]
fn test_vrc_irq_set_latch_preserves_other_nibble() {
let mut irq = VrcIrqCounter::new();
irq.set_latch_high(0x0F); irq.set_latch_low(0x03); assert_eq!(irq.latch(), 0xF3);
irq.set_latch_high(0x02); assert_eq!(irq.latch(), 0x23);
}
#[test]
fn test_vrc_irq_cpu_cycle_mode_counts_up() {
let mut irq = VrcIrqCounter::new();
irq.set_latch_low(0x00);
irq.set_latch_high(0x00);
irq.write_control(0b0000_0110);
assert_eq!(irq.counter(), 0);
irq.clock();
assert_eq!(irq.counter(), 1);
irq.clock();
assert_eq!(irq.counter(), 2);
}
#[test]
fn test_vrc_irq_cpu_cycle_mode_fires_on_overflow() {
let mut irq = VrcIrqCounter::new();
irq.set_latch_low(0x0E);
irq.set_latch_high(0x0F); irq.write_control(0b0000_0110);
assert_eq!(irq.counter(), 0xFE);
irq.clock(); assert!(!irq.is_pending());
irq.clock(); assert!(irq.is_pending());
assert_eq!(irq.counter(), 0xFE); }
#[test]
fn test_vrc_irq_scanline_mode_prescaler() {
let mut irq = VrcIrqCounter::new();
irq.set_latch_low(0x0E);
irq.set_latch_high(0x0F); irq.write_control(0b0000_0010);
assert_eq!(irq.counter(), 0xFE);
for _ in 0..113 {
irq.clock();
}
assert_eq!(irq.counter(), 0xFE);
irq.clock(); assert_eq!(irq.counter(), 0xFF);
}
#[test]
fn test_vrc_irq_acknowledge_clears_and_restores_enable() {
let mut irq = VrcIrqCounter::new();
irq.write_control(0b0000_0111);
irq.set_latch_low(0x0F);
irq.set_latch_high(0x0F); irq.write_control(0b0000_0110); irq.clock();
assert!(irq.is_pending());
irq.acknowledge();
assert!(!irq.is_pending());
}
#[test]
fn test_vrc_irq_write_control_acknowledges() {
let mut irq = VrcIrqCounter::new();
irq.set_latch_low(0x0F);
irq.set_latch_high(0x0F); irq.write_control(0b0000_0110); irq.clock();
assert!(irq.is_pending());
irq.write_control(0b0000_0000); assert!(!irq.is_pending());
}
#[test]
fn test_vrc_irq_disabled_does_not_count() {
let mut irq = VrcIrqCounter::new();
irq.set_latch_low(0x05);
irq.write_control(0b0000_0000);
irq.clock();
irq.clock();
assert_eq!(irq.counter(), 0); }
#[test]
fn test_vrc_irq_enable_loads_counter_from_latch() {
let mut irq = VrcIrqCounter::new();
irq.set_latch_low(0x0A);
irq.set_latch_high(0x0B); irq.write_control(0b0000_0010);
assert_eq!(irq.counter(), 0xBA);
}
}