use crate::nes::apu::{Apu, ApuState, SharedApu};
use crate::nes::bus::{Bus, BusState, MapperState, SharedBus};
#[cfg(test)]
use crate::nes::cartridge::TimingMode;
use crate::nes::cartridge::{Cartridge, RomDb, load_rom_db};
use crate::nes::console::ApuChannels;
#[cfg(test)]
use crate::nes::console::Config;
#[cfg(test)]
use crate::nes::console::NesConfig;
use crate::nes::cpu::lookup;
use crate::nes::cpu::opcode::{AddrMode, Mnemonic};
use crate::nes::cpu::{Cpu, CpuState};
use crate::nes::input::ControllerType;
use crate::nes::ppu::{Ppu, PpuState, SharedPpu};
use crate::platform::app_context::{IntoSharedAppContext, SharedAppContext};
use crate::platform::debugging::{Tracing, log_info};
use crate::platform::emulator::{Emulator, SystemType};
use serde::{Deserialize, Serialize};
use std::cell::RefCell;
use std::collections::VecDeque;
use std::io;
use std::path::PathBuf;
use std::rc::Rc;
pub const SAVESTATE_VERSION: u32 = 8;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SaveState {
pub version: u32,
pub cpu: CpuState,
pub ppu: PpuState,
pub apu: ApuState,
pub bus: BusState,
pub ram: Vec<u8>,
pub mapper: MapperState,
}
impl SaveState {
pub fn new(
cpu: CpuState,
ppu: PpuState,
apu: ApuState,
bus: BusState,
ram: Vec<u8>,
mapper: MapperState,
) -> Self {
Self {
version: SAVESTATE_VERSION,
cpu,
ppu,
apu,
bus,
ram,
mapper,
}
}
#[cfg(test)]
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string(self)
}
#[cfg(test)]
pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(json)
}
pub fn to_bytes(&self) -> Result<Vec<u8>, serde_json::Error> {
serde_json::to_vec(self)
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, serde_json::Error> {
serde_json::from_slice(bytes)
}
pub fn to_binary_bytes(&self) -> Result<Vec<u8>, postcard::Error> {
postcard::to_allocvec(self)
}
pub fn from_binary_bytes(bytes: &[u8]) -> Result<Self, postcard::Error> {
postcard::from_bytes(bytes)
}
}
const MAX_CPU_TRACE_LINES: usize = 512;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CpuTraceLine {
pub addr: u16,
pub bytes: Vec<u8>,
pub text: String,
}
pub struct Nes {
app_context: SharedAppContext,
rom_db: RomDb,
ppu: SharedPpu,
apu: SharedApu,
bus: SharedBus,
cpu: Cpu,
fractional_ppu_cycles: f64,
ready_to_render: bool,
recent_cpu_trace: VecDeque<CpuTraceLine>,
cpu_trace_enabled: bool,
active_controller_port1: ControllerType,
active_controller_port2: ControllerType,
}
impl Nes {
pub const SCREEN_WIDTH: u32 = 256;
pub const SCREEN_HEIGHT: u32 = 240;
pub fn new<C: IntoSharedAppContext>(app_context: C) -> Self {
let app_context = app_context.into_shared();
let config = app_context.borrow().config().clone();
let tv_system = config.nes.hardware_model.timing_mode();
let ram_init_mode = config.frontend.ram_init_mode;
let oam_dram_decay_enabled = config.nes.oam_dram_decay_enabled;
let ppu = Rc::new(RefCell::new(Ppu::new(tv_system, ram_init_mode)));
ppu.borrow_mut()
.set_oam_dram_decay_enabled(oam_dram_decay_enabled);
ppu.borrow_mut().set_famicom_emphasis(
config.nes.hardware_mode == crate::nes::console::HardwareMode::Famicom,
);
ppu.borrow_mut().set_system_palette(config.nes.palette);
let apu = Rc::new(RefCell::new(Apu::new_with_tv_system(tv_system)));
{
let channels = config.nes.apu_channels;
let mut apu = apu.borrow_mut();
apu.set_pulse1_enabled(channels.contains(ApuChannels::PULSE1));
apu.set_pulse2_enabled(channels.contains(ApuChannels::PULSE2));
apu.set_triangle_enabled(channels.contains(ApuChannels::TRIANGLE));
apu.set_noise_enabled(channels.contains(ApuChannels::NOISE));
apu.set_dmc_enabled(channels.contains(ApuChannels::DMC));
}
let memory = Rc::new(RefCell::new(Bus::new(
ppu.clone(),
apu.clone(),
app_context.clone(),
)));
let cpu = Cpu::new(tv_system, memory.clone(), ppu.clone(), apu.clone());
ppu.borrow_mut().run_ppu_cycles(1);
Self {
app_context,
rom_db: load_rom_db(),
ppu,
apu,
bus: memory,
cpu,
fractional_ppu_cycles: 0.0,
ready_to_render: false,
recent_cpu_trace: VecDeque::with_capacity(MAX_CPU_TRACE_LINES),
cpu_trace_enabled: false,
active_controller_port1: config.nes.controller_port1,
active_controller_port2: config.nes.controller_port2,
}
}
pub fn rom_db(&self) -> &RomDb {
&self.rom_db
}
pub fn current_palette(&self) -> crate::nes::ppu::NesPalette {
self.ppu.borrow().system_palette()
}
pub fn cycle_palette(&mut self) -> crate::nes::ppu::NesPalette {
self.ppu.borrow_mut().cycle_system_palette()
}
pub fn insert_cartridge(&mut self, mut cartridge: Cartridge) {
let cartridge_crc32 = cartridge.crc32();
let zapper_port = self.rom_db.default_zapper_on_port(cartridge_crc32);
let arkanoid_port = crate::nes::cartridge::default_arkanoid_on_port(cartridge_crc32);
let power_pad_port = self.rom_db.default_power_pad_on_port(cartridge_crc32);
let has_famicom_four_players_expansion = self
.rom_db
.has_famicom_four_players_expansion(cartridge_crc32);
let has_arkanoid_famicom_expansion =
self.rom_db.has_arkanoid_famicom_expansion(cartridge_crc32);
let has_zapper_famicom_expansion =
self.rom_db.has_zapper_famicom_expansion(cartridge_crc32);
let has_power_pad_famicom_expansion =
self.rom_db.has_power_pad_famicom_expansion(cartridge_crc32);
let has_nes_four_score_expansion =
self.rom_db.has_nes_four_score_expansion(cartridge_crc32);
let is_japan_region = self.rom_db.is_japan_region(cartridge_crc32);
self.app_context
.borrow_mut()
.config_mut()
.apply_rom_db_famicom_region_hint(is_japan_region);
self.app_context
.borrow_mut()
.config_mut()
.apply_rom_db_famicom_four_players_hint(has_famicom_four_players_expansion);
self.app_context
.borrow_mut()
.config_mut()
.apply_rom_db_arkanoid_famicom_hint(has_arkanoid_famicom_expansion);
self.app_context
.borrow_mut()
.config_mut()
.apply_rom_db_zapper_famicom_hint(has_zapper_famicom_expansion);
self.app_context
.borrow_mut()
.config_mut()
.apply_rom_db_power_pad_famicom_hint(has_power_pad_famicom_expansion);
self.app_context
.borrow_mut()
.config_mut()
.apply_rom_db_nes_four_score_hint(has_nes_four_score_expansion);
let is_vs_system =
cartridge.vs_ppu_type().is_some() || cartridge.vs_hardware_type().is_some();
self.app_context
.borrow_mut()
.config_mut()
.apply_rom_db_vs_system_hint(is_vs_system);
let is_playchoice10 = matches!(
cartridge.hardware_type(),
crate::nes::cartridge::HardwareType::Playchoice10
);
self.app_context
.borrow_mut()
.config_mut()
.apply_rom_db_playchoice10_hint(is_playchoice10);
let vs_controllers_swapped = self.rom_db.has_vs_swapped_controllers(cartridge_crc32);
self.app_context
.borrow_mut()
.config_mut()
.apply_rom_db_vs_controllers_swapped_hint(vs_controllers_swapped);
let is_famicom = self.app_context.borrow().config().nes.hardware_mode
== crate::nes::console::HardwareMode::Famicom;
self.ppu.borrow_mut().set_famicom_emphasis(is_famicom);
let ram_init_mode = self.app_context.borrow().config().frontend.ram_init_mode;
cartridge.initialize_ram(ram_init_mode);
{
let mut bus = self.bus.borrow_mut();
bus.map_cartridge(cartridge);
bus.sync_controller_modes_from_config();
}
self.cpu.update_mapper_capability_flags();
let app_context = self.app_context.borrow();
let config = app_context.config();
let port1_type = config.nes.controller_port1;
let port2_type = config.nes.controller_port2;
let port1_explicit = config.nes.controller_port1_explicit;
let port2_explicit = config.nes.controller_port2_explicit;
drop(app_context);
if port1_explicit || port2_explicit {
let mut bus = self.bus.borrow_mut();
bus.set_controller_type(1, port1_type);
bus.set_controller_type(2, port2_type);
self.log_hardware_summary();
return;
}
let zapper_on_expansion = self.app_context.borrow().config().nes.expansion_port
== crate::nes::console::ExpansionPort::ZapperFamicom;
let auto_controller = if zapper_port != 0 && !zapper_on_expansion {
Some((zapper_port, ControllerType::Zapper))
} else if arkanoid_port != 0 {
Some((arkanoid_port, ControllerType::Arkanoid))
} else if power_pad_port != 0 {
Some((power_pad_port, ControllerType::PowerPad))
} else {
None
};
if let Some((auto_port, auto_type)) = auto_controller {
let other_port_type = if auto_port == 1 {
port2_type
} else {
port1_type
};
let auto_label = auto_type.display_label();
log_info(format!(
"Auto-detected {} on port {} for this cartridge. Override with --controller-port{}=<type> if needed.",
auto_label, auto_port, auto_port
));
let mut bus = self.bus.borrow_mut();
bus.set_controller_type(auto_port, auto_type);
if auto_port == 1 {
self.active_controller_port1 = auto_type;
self.active_controller_port2 = other_port_type;
} else {
self.active_controller_port1 = other_port_type;
self.active_controller_port2 = auto_type;
}
let other_port = if auto_port == 1 { 2 } else { 1 };
bus.set_controller_type(other_port, other_port_type);
} else {
let mut bus = self.bus.borrow_mut();
bus.set_controller_type(1, port1_type);
bus.set_controller_type(2, port2_type);
self.active_controller_port1 = port1_type;
self.active_controller_port2 = port2_type;
}
self.log_hardware_summary();
}
pub fn active_controller_port_type(&self, port: u8) -> ControllerType {
match port {
1 => self.active_controller_port1,
2 => self.active_controller_port2,
_ => ControllerType::Joypad,
}
}
fn log_hardware_summary(&self) {
let summary = self
.app_context
.borrow()
.config()
.hardware_summary_with(self.active_controller_port1, self.active_controller_port2);
log_info(summary);
}
pub fn state_path(&self) -> Option<PathBuf> {
self.bus.borrow().cartridge_state_path()
}
pub fn save_ram(&self) -> io::Result<()> {
self.bus.borrow().save_ram()
}
pub fn debug_path(&self) -> Option<PathBuf> {
self.bus.borrow().cartridge_debug_path()
}
pub fn app_context(&self) -> &SharedAppContext {
&self.app_context
}
pub fn ppu(&self) -> &SharedPpu {
&self.ppu
}
pub fn apu(&self) -> &SharedApu {
&self.apu
}
pub fn set_audio_sample_rate(&mut self, rate: f32) {
self.apu.borrow_mut().set_sample_rate(rate);
}
pub fn bus(&self) -> &SharedBus {
&self.bus
}
pub fn cpu_ref(&self) -> &Cpu {
&self.cpu
}
pub fn cpu_mut(&mut self) -> &mut Cpu {
&mut self.cpu
}
pub fn reset(&mut self, soft_reset: bool) {
let cpu_cycle = self.cpu.get_total_cycles();
let ram_init_mode = self.app_context.borrow().config().frontend.ram_init_mode;
self.ppu.borrow_mut().reset(soft_reset, ram_init_mode);
self.apu.borrow_mut().reset(cpu_cycle, soft_reset);
self.bus.borrow_mut().reset(soft_reset, ram_init_mode);
self.cpu_mut().reset(soft_reset);
self.apu.borrow_mut().dmc_mut().reinit_timer_after_reset();
if !soft_reset {
self.start_trainer_if_present();
}
self.fractional_ppu_cycles = 0.0;
self.ready_to_render = false;
self.recent_cpu_trace.clear();
self.ppu.borrow_mut().run_ppu_cycles(1);
}
fn start_trainer_if_present(&mut self) {
if !self.bus.borrow().cartridge_has_trainer_jsr() {
return;
}
let game_vector = self.cpu.pc();
self.cpu.divert_to_trainer(game_vector);
}
pub fn run_cpu_tick(&mut self) -> u8 {
if let Some(dma_cycles) = self.cpu.handle_oam_dma_if_pending() {
return dma_cycles.min(255) as u8;
}
let cycles_before = self.cpu.get_total_cycles();
if self.cpu_trace_enabled {
let trace_line = self.capture_current_cpu_trace_line();
self.push_cpu_trace_line(trace_line);
}
self.cpu.execute();
if self.ppu.borrow_mut().poll_frame_complete() {
self.ready_to_render = true;
}
let cycles_after = self.cpu.get_total_cycles();
let cycles_consumed = cycles_after - cycles_before;
cycles_consumed.min(255) as u8
}
pub fn run(&mut self, _tracing: &Tracing) -> u8 {
self.run_cpu_tick()
}
pub fn set_cpu_trace_enabled(&mut self, enabled: bool) {
self.cpu_trace_enabled = enabled;
if !enabled {
self.recent_cpu_trace.clear();
}
}
pub fn recent_cpu_trace(&self, limit: usize) -> Vec<CpuTraceLine> {
if limit == 0 || self.recent_cpu_trace.is_empty() {
return Vec::new();
}
let start = self.recent_cpu_trace.len().saturating_sub(limit);
self.recent_cpu_trace.iter().skip(start).cloned().collect()
}
fn push_cpu_trace_line(&mut self, line: CpuTraceLine) {
if self.recent_cpu_trace.len() == MAX_CPU_TRACE_LINES {
self.recent_cpu_trace.pop_front();
}
self.recent_cpu_trace.push_back(line);
}
fn capture_current_cpu_trace_line(&self) -> CpuTraceLine {
let pc = self.cpu.pc();
let memory = self.bus.borrow();
let opcode = memory.read_cpu_for_debugger(pc);
let instruction = lookup(opcode);
let byte_len = instruction.bytes().max(1) as usize;
let mut bytes = Vec::with_capacity(byte_len);
for offset in 0..byte_len {
bytes.push(memory.read_cpu_for_debugger(pc.wrapping_add(offset as u16)));
}
CpuTraceLine {
addr: pc,
text: format_compact_trace_instruction(instruction, pc, &bytes),
bytes,
}
}
const SYSTEM_PALETTE: [(u8, u8, u8); 0x40] = crate::nes::ppu::system_palettes::PALETTE_DEFAULT;
pub fn lookup_system_palette(color_index: u8) -> (u8, u8, u8) {
Self::SYSTEM_PALETTE[(color_index & 0x3F) as usize]
}
pub fn get_screen_buffer(&self) -> std::cell::RefMut<'_, crate::nes::ppu::ScreenBuffer> {
std::cell::RefMut::map(self.ppu.borrow_mut(), |ppu| ppu.screen_buffer_mut())
}
pub fn is_ready_to_render(&self) -> bool {
self.ready_to_render
}
pub fn clear_ready_to_render(&mut self) {
self.ready_to_render = false;
}
pub fn sample_ready(&self) -> bool {
self.apu.borrow().sample_ready()
}
pub fn get_sample(&mut self) -> Option<f32> {
self.apu.borrow_mut().get_sample()
}
pub fn set_button(&mut self, controller: u8, button: crate::nes::input::Button, pressed: bool) {
self.bus
.borrow_mut()
.set_button(controller, button, pressed);
}
pub fn set_snes_button(
&mut self,
controller: u8,
button: crate::nes::input::SnesButton,
pressed: bool,
) -> bool {
self.bus
.borrow_mut()
.set_snes_button(controller, button, pressed)
}
pub fn set_power_pad_button(
&mut self,
controller: u8,
button: crate::nes::input::PowerPadButton,
pressed: bool,
) -> bool {
self.bus
.borrow_mut()
.set_power_pad_button(controller, button, pressed)
}
pub fn set_expansion_power_pad_button(
&mut self,
button: crate::nes::input::PowerPadButton,
pressed: bool,
) -> bool {
self.bus
.borrow_mut()
.set_expansion_power_pad_button(button, pressed)
}
pub fn set_vs_coin_insert(&self, slot: u8, pressed: bool) {
self.bus.borrow().set_vs_coin_insert(slot, pressed);
}
pub fn set_vs_service_button(&self, pressed: bool) {
self.bus.borrow().set_vs_service_button(pressed);
}
#[allow(dead_code)]
pub fn set_joypad_button_states(&mut self, port: u8, state: u8) {
use crate::nes::input::Button;
for button in [
Button::A,
Button::B,
Button::Select,
Button::Start,
Button::Up,
Button::Down,
Button::Left,
Button::Right,
] {
let pressed = (state & (1 << (button as u8))) != 0;
self.set_button(port, button, pressed);
}
}
pub fn get_joypad_button_states(&self, port: u8) -> u8 {
self.bus.borrow().get_joypad_button_states(port)
}
pub fn controller_input_type(&self, port: u8) -> Option<crate::nes::input::ControllerInput> {
self.bus.borrow().controller_input_type(port)
}
pub fn has_expansion_mouse_controller(&self) -> bool {
self.bus.borrow().has_expansion_mouse_controller()
}
pub fn has_expansion_zapper(&self) -> bool {
self.bus.borrow().has_expansion_zapper()
}
#[allow(dead_code)]
pub fn has_expansion_power_pad(&self) -> bool {
self.bus.borrow().has_expansion_power_pad()
}
#[allow(dead_code)]
pub fn is_zapper_active(&self, port: u8) -> bool {
self.bus.borrow().is_zapper_active(port)
}
pub fn set_mouse_x_position(&mut self, position: u8) {
self.bus.borrow_mut().set_mouse_x_position(position);
}
#[allow(dead_code)]
pub fn set_mouse_y_position(&mut self, position: u8) {
self.bus.borrow_mut().set_mouse_y_position(position);
}
pub fn add_mouse_delta(&mut self, dx: i16, dy: i16) {
self.bus.borrow_mut().add_mouse_delta(dx, dy);
}
pub fn set_mouse_left_button(&mut self, pressed: bool) {
self.bus.borrow_mut().set_mouse_left_button(pressed);
}
pub fn set_mouse_right_button(&mut self, pressed: bool) {
self.bus.borrow_mut().set_mouse_right_button(pressed);
}
pub fn has_snes_mouse(&self) -> bool {
self.bus.borrow().has_snes_mouse()
}
#[allow(dead_code)]
pub fn trace(&mut self, tracing: &Tracing) -> String {
let nestest = tracing.nestest;
let cpu_state = self.cpu.state();
let pc = cpu_state.pc;
let mut memory = self.bus.borrow_mut();
let opcode_byte = memory.read(pc, false);
let instruction = lookup(opcode_byte);
let byte1 = if instruction.bytes() > 1 {
memory.read(pc.wrapping_add(1), false)
} else {
0
};
let byte2 = if instruction.bytes() > 2 {
memory.read(pc.wrapping_add(2), false)
} else {
0
};
let hex_dump = match instruction.bytes() {
1 => format!("{:02X} ", opcode_byte),
2 => format!("{:02X} {:02X} ", opcode_byte, byte1),
3 => format!("{:02X} {:02X} {:02X}", opcode_byte, byte1, byte2),
_ => panic!("Invalid instruction byte count"),
};
let asm = match instruction.mode {
AddrMode::IMP => instruction.mnemonic.to_string(),
AddrMode::ACC => format!("{} A", instruction.mnemonic),
AddrMode::IMM => format!("{} #${:02X}", instruction.mnemonic, byte1),
AddrMode::ZP => {
let addr = byte1 as u16;
if nestest {
let mut value = memory.read(addr, false);
if (0x4000..0x4100).contains(&addr) {
value = 0xFF;
}
format!("{} ${:02X} = {:02X}", instruction.mnemonic, byte1, value)
} else {
format!("{} ${:02X}", instruction.mnemonic, byte1)
}
}
AddrMode::ZPX => {
let addr = byte1.wrapping_add(cpu_state.x) as u16;
if nestest {
let mut value = memory.read(addr, false);
if (0x4000..0x4100).contains(&addr) {
value = 0xFF;
}
format!(
"{} ${:02X},X @ {:02X} = {:02X}",
instruction.mnemonic, byte1, addr as u8, value
)
} else {
format!("{} ${:02X},X", instruction.mnemonic, byte1)
}
}
AddrMode::ZPY => {
let addr = byte1.wrapping_add(cpu_state.y) as u16;
if nestest {
let mut value = memory.read(addr, false);
if (0x4000..0x4100).contains(&addr) {
value = 0xFF;
}
format!(
"{} ${:02X},Y @ {:02X} = {:02X}",
instruction.mnemonic, byte1, addr as u8, value
)
} else {
format!("{} ${:02X},Y", instruction.mnemonic, byte1)
}
}
AddrMode::ABS => {
let addr = u16::from_le_bytes([byte1, byte2]);
if instruction.mnemonic == Mnemonic::JMP || instruction.mnemonic == Mnemonic::JSR {
format!("{} ${:04X}", instruction.mnemonic, addr)
} else if nestest {
let mut value = memory.read(addr, false);
if (0x4000..0x4100).contains(&addr) {
value = 0xFF;
}
format!("{} ${:04X} = {:02X}", instruction.mnemonic, addr, value)
} else {
format!("{} ${:04X}", instruction.mnemonic, addr)
}
}
AddrMode::ABSX => {
let addr = u16::from_le_bytes([byte1, byte2]);
if nestest {
let effective_addr = addr.wrapping_add(cpu_state.x as u16);
let value = memory.read(effective_addr, false);
format!(
"{} ${:04X},X @ {:04X} = {:02X}",
instruction.mnemonic, addr, effective_addr, value
)
} else {
format!("{} ${:04X},X", instruction.mnemonic, addr)
}
}
AddrMode::ABSY => {
let addr = u16::from_le_bytes([byte1, byte2]);
if nestest {
let effective_addr = addr.wrapping_add(cpu_state.y as u16);
let value = memory.read(effective_addr, false);
format!(
"{} ${:04X},Y @ {:04X} = {:02X}",
instruction.mnemonic, addr, effective_addr, value
)
} else {
format!("{} ${:04X},Y", instruction.mnemonic, addr)
}
}
AddrMode::INDX => {
if nestest {
let zp_addr = byte1.wrapping_add(cpu_state.x);
let addr_lo = memory.read(zp_addr as u16, false);
let addr_hi = memory.read(zp_addr.wrapping_add(1) as u16, false);
let addr = u16::from_le_bytes([addr_lo, addr_hi]);
let value = memory.read(addr, false);
format!(
"{} (${:02X},X) @ {:02X} = {:04X} = {:02X}",
instruction.mnemonic, byte1, zp_addr, addr, value
)
} else {
format!("{} (${:02X},X)", instruction.mnemonic, byte1)
}
}
AddrMode::INDY => {
if nestest {
let addr_lo = memory.read(byte1 as u16, false);
let addr_hi = memory.read(byte1.wrapping_add(1) as u16, false);
let base_addr = u16::from_le_bytes([addr_lo, addr_hi]);
let effective_addr = base_addr.wrapping_add(cpu_state.y as u16);
let value = memory.read(effective_addr, false);
format!(
"{} (${:02X}),Y = {:04X} @ {:04X} = {:02X}",
instruction.mnemonic, byte1, base_addr, effective_addr, value
)
} else {
format!("{} (${:02X}),Y", instruction.mnemonic, byte1)
}
}
AddrMode::IND => {
if nestest {
let ptr_addr = u16::from_le_bytes([byte1, byte2]);
let addr_lo = memory.read(ptr_addr, false);
let hi_addr = if ptr_addr & 0xFF == 0xFF {
ptr_addr & 0xFF00 } else {
ptr_addr.wrapping_add(1)
};
let addr_hi = memory.read(hi_addr, false);
let target_addr = u16::from_le_bytes([addr_lo, addr_hi]);
format!(
"{} (${:04X}) = {:04X}",
instruction.mnemonic, ptr_addr, target_addr
)
} else {
let ptr_addr = u16::from_le_bytes([byte1, byte2]);
format!("{} (${:04X})", instruction.mnemonic, ptr_addr)
}
}
AddrMode::REL => {
let offset = byte1 as i8;
let target = pc.wrapping_add(2).wrapping_add(offset as u16);
format!("{} ${:04X}", instruction.mnemonic, target)
}
AddrMode::ABSXW => {
let addr = u16::from_le_bytes([byte1, byte2]);
if nestest {
let effective_addr = addr.wrapping_add(cpu_state.x as u16);
let value = memory.read(effective_addr, false);
format!(
"{} ${:04X},X @ {:04X} = {:02X}",
instruction.mnemonic, addr, effective_addr, value
)
} else {
format!("{} ${:04X},X", instruction.mnemonic, addr)
}
}
AddrMode::ABSYW => {
let addr = u16::from_le_bytes([byte1, byte2]);
if nestest {
let effective_addr = addr.wrapping_add(cpu_state.y as u16);
let value = memory.read(effective_addr, false);
format!(
"{} ${:04X},Y @ {:04X} = {:02X}",
instruction.mnemonic, addr, effective_addr, value
)
} else {
format!("{} ${:04X},Y", instruction.mnemonic, addr)
}
}
AddrMode::INDYW => {
if nestest {
let addr_lo = memory.read(byte1 as u16, false);
let addr_hi = memory.read(byte1.wrapping_add(1) as u16, false);
let base_addr = u16::from_le_bytes([addr_lo, addr_hi]);
let effective_addr = base_addr.wrapping_add(cpu_state.y as u16);
let value = memory.read(effective_addr, false);
format!(
"{} (${:02X}),Y = {:04X} @ {:04X} = {:02X}",
instruction.mnemonic, byte1, base_addr, effective_addr, value
)
} else {
format!("{} (${:02X}),Y", instruction.mnemonic, byte1)
}
}
};
if nestest {
let total_cycles = self.cpu.get_total_cycles();
let ppu = self.ppu.borrow();
let mut scanline = ppu.scanline();
let mut pixel = ppu.pixel();
if pixel == 0 {
scanline -= 1;
pixel = 340;
} else {
pixel -= 1;
}
let mnem_str = instruction.mnemonic.to_string();
let (pad_before, width) = if mnem_str.len() == 4 {
(" ", 32)
} else {
(" ", 31)
};
format!(
"{:04X} {}{}{:<width$} A:{:02X} X:{:02X} Y:{:02X} P:{:02X} SP:{:02X} PPU:{:3},{:3} CYC:{}",
pc,
hex_dump,
pad_before,
asm,
cpu_state.a,
cpu_state.x,
cpu_state.y,
cpu_state.p,
cpu_state.sp,
scanline,
pixel,
total_cycles,
width = width
)
} else {
let total_cycles = self.cpu.get_total_cycles();
let ppu = self.ppu.borrow();
let scanline = ppu.scanline();
let pixel = ppu.pixel();
let apu_ticks = self.apu.borrow().debug_frame_counter_cycle();
let apu_cycles = self.apu.borrow().apu_cycle();
format!(
"{:04X} {} {:10} A:{:02X} X:{:02X} Y:{:02X} P:{:02X} SP:{:02X} PPU:{:3},{:3} CPU:{:8} PPU:{}/{}",
pc,
hex_dump,
asm,
cpu_state.a,
cpu_state.x,
cpu_state.y,
cpu_state.p,
cpu_state.sp,
scanline,
pixel,
total_cycles,
apu_ticks,
apu_cycles,
)
}
}
#[cfg(test)]
#[allow(dead_code)]
pub fn base_nametable_addr(&self) -> u16 {
self.ppu.borrow().base_nametable_addr()
}
#[cfg(test)]
#[allow(dead_code)]
pub fn read_nametable_text(&self, nametable_addr: u16, length: usize) -> String {
let ppu = self.ppu.borrow();
let mut text = String::new();
for i in 0..length {
let addr = nametable_addr.wrapping_add(i as u16);
let tile_index = ppu.read_nametable_for_debug(addr);
let ch = if (0x20..=0x7E).contains(&tile_index) {
tile_index as char
} else if tile_index == 0x00 {
' ' } else {
if (0x10..=0x19).contains(&tile_index) {
(tile_index + 0x20) as char
} else if (0x1A..=0x1F).contains(&tile_index) {
(tile_index + 0x27) as char
} else if (0xE0..=0xE9).contains(&tile_index) {
(tile_index - 0xE0 + b'0') as char
} else if (0xEA..=0xEF).contains(&tile_index) {
(tile_index - 0xEA + b'A') as char
} else {
'?' }
};
text.push(ch);
}
text
}
#[cfg(test)]
#[allow(dead_code)]
pub fn read_nametable_raw(&self, nametable_addr: u16, length: usize) -> Vec<u8> {
let ppu = self.ppu.borrow();
(0..length)
.map(|i| {
let addr = nametable_addr.wrapping_add(i as u16);
ppu.read_nametable_for_debug(addr)
})
.collect()
}
pub fn save_state(&self) -> SaveState {
SaveState::new(
self.cpu.capture_state(),
self.ppu.borrow().capture_state(),
self.apu.borrow().capture_state(),
self.bus.borrow().capture_state(),
self.bus.borrow().ram_snapshot(),
self.bus.borrow().capture_mapper_state(),
)
}
pub fn load_state(&mut self, state: &SaveState) -> Result<(), SaveStateError> {
if state.version != SAVESTATE_VERSION {
return Err(SaveStateError::IncompatibleVersion {
expected: SAVESTATE_VERSION,
found: state.version,
});
}
let current_mapper = self.bus.borrow().capture_mapper_state().mapper_number;
if state.mapper.mapper_number != current_mapper {
return Err(SaveStateError::MapperMismatch {
expected: current_mapper,
found: state.mapper.mapper_number,
});
}
self.cpu.restore_state(&state.cpu);
self.ppu.borrow_mut().restore_state(&state.ppu);
self.apu.borrow_mut().restore_state(&state.apu);
self.bus.borrow_mut().restore_state(&state.bus);
self.bus.borrow_mut().restore_ram(&state.ram);
self.bus.borrow_mut().restore_mapper_state(&state.mapper);
self.fractional_ppu_cycles = 0.0;
self.ready_to_render = false;
self.recent_cpu_trace.clear();
Ok(())
}
pub fn load_rom(&mut self, bytes: &[u8], name: &str) -> Result<(), String> {
let cart = Cartridge::load_from_file(bytes, name, Some(&self.rom_db))
.map_err(|e| e.to_string())?;
self.insert_cartridge(cart);
Ok(())
}
pub fn save_state_bytes(&self) -> Result<Vec<u8>, String> {
self.save_state()
.to_bytes()
.map_err(|e| format!("save state serialization failed: {e}"))
}
pub fn load_state_bytes(&mut self, data: &[u8]) -> Result<(), String> {
let state = SaveState::from_bytes(data)
.map_err(|e| format!("save state deserialization failed: {e}"))?;
self.load_state(&state).map_err(|e| e.to_string())
}
pub fn set_button_by_id(&mut self, port: u8, button_id: u8, pressed: bool) -> bool {
if let Some(button) = crate::nes::input::button_from_id(button_id) {
self.set_button(port, button, pressed);
true
} else {
false
}
}
}
impl Emulator for Nes {
fn system_type(&self) -> SystemType {
SystemType::Nes
}
fn allowed_shaders(&self) -> &'static [&'static str] {
&["none", "crt", "smooth", "ntsc", "pal"]
}
fn load_rom(&mut self, bytes: &[u8], name: &str) -> Result<(), String> {
Nes::load_rom(self, bytes, name)
}
fn run_tick(&mut self) -> u8 {
self.run_cpu_tick()
}
fn is_ready_to_render(&self) -> bool {
Nes::is_ready_to_render(self)
}
fn clear_ready_to_render(&mut self) {
Nes::clear_ready_to_render(self)
}
fn screen_width(&self) -> u32 {
Nes::SCREEN_WIDTH
}
fn screen_height(&self) -> u32 {
Nes::SCREEN_HEIGHT
}
fn screen_snapshot(&self) -> Vec<u8> {
self.get_screen_buffer().snapshot()
}
fn cropped_screen_snapshot(&self, h_overscan: u32, v_overscan: u32) -> Vec<u8> {
self.get_screen_buffer()
.cropped_snapshot(h_overscan, v_overscan)
}
fn screen_crc32(&self) -> u32 {
self.get_screen_buffer().crc32()
}
fn sample_ready(&self) -> bool {
Nes::sample_ready(self)
}
fn get_sample(&mut self) -> Option<f32> {
Nes::get_sample(self)
}
fn set_audio_sample_rate(&mut self, rate: f32) {
Nes::set_audio_sample_rate(self, rate)
}
fn set_button(&mut self, port: u8, button_id: u8, pressed: bool) {
if !self.set_button_by_id(port, button_id, pressed) {
#[cfg(debug_assertions)]
eprintln!("warning: invalid NES button_id: {button_id}");
}
}
fn set_joypad_button_states(&mut self, port: u8, state: u8) {
Nes::set_joypad_button_states(self, port, state)
}
fn get_joypad_button_states(&self, port: u8) -> u8 {
Nes::get_joypad_button_states(self, port)
}
fn save_state_bytes(&self) -> Result<Vec<u8>, String> {
Nes::save_state_bytes(self)
}
fn load_state_bytes(&mut self, data: &[u8]) -> Result<(), String> {
Nes::load_state_bytes(self, data)
}
fn reset(&mut self, soft_reset: bool) {
Nes::reset(self, soft_reset)
}
fn save_ram(&self) -> Result<(), String> {
Nes::save_ram(self).map_err(|e| e.to_string())
}
fn app_context(&self) -> &SharedAppContext {
Nes::app_context(self)
}
fn target_frame_duration(&self) -> std::time::Duration {
let hz = self
.app_context()
.borrow()
.config()
.nes
.hardware_model
.timing_mode()
.frame_rate_hz();
std::time::Duration::from_secs_f64(1.0 / hz)
}
}
fn format_compact_trace_instruction(
meta: &crate::nes::cpu::OpCode,
addr: u16,
bytes: &[u8],
) -> String {
let operand = match meta.mode {
AddrMode::IMP => String::new(),
AddrMode::ACC => "A".to_string(),
AddrMode::IMM => format!("#${:02X}", bytes.get(1).copied().unwrap_or(0)),
AddrMode::ZP => format!("${:02X}", bytes.get(1).copied().unwrap_or(0)),
AddrMode::ZPX => format!("${:02X},X", bytes.get(1).copied().unwrap_or(0)),
AddrMode::ZPY => format!("${:02X},Y", bytes.get(1).copied().unwrap_or(0)),
AddrMode::INDX => format!("(${:02X},X)", bytes.get(1).copied().unwrap_or(0)),
AddrMode::INDY | AddrMode::INDYW => {
format!("(${:02X}),Y", bytes.get(1).copied().unwrap_or(0))
}
AddrMode::REL => {
let off = bytes.get(1).copied().unwrap_or(0) as i8;
let next = addr.wrapping_add(2);
let target = next.wrapping_add(off as i16 as u16);
format!("${:04X}", target)
}
AddrMode::ABS => {
let lo = bytes.get(1).copied().unwrap_or(0);
let hi = bytes.get(2).copied().unwrap_or(0);
format!("${:04X}", u16::from_le_bytes([lo, hi]))
}
AddrMode::ABSX | AddrMode::ABSXW => {
let lo = bytes.get(1).copied().unwrap_or(0);
let hi = bytes.get(2).copied().unwrap_or(0);
format!("${:04X},X", u16::from_le_bytes([lo, hi]))
}
AddrMode::ABSY | AddrMode::ABSYW => {
let lo = bytes.get(1).copied().unwrap_or(0);
let hi = bytes.get(2).copied().unwrap_or(0);
format!("${:04X},Y", u16::from_le_bytes([lo, hi]))
}
AddrMode::IND => {
let lo = bytes.get(1).copied().unwrap_or(0);
let hi = bytes.get(2).copied().unwrap_or(0);
format!("(${:04X})", u16::from_le_bytes([lo, hi]))
}
};
if operand.is_empty() {
meta.mnemonic.to_string()
} else {
format!("{} {}", meta.mnemonic, operand)
}
}
mod savestate_error {
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SaveStateError {
IncompatibleVersion { expected: u32, found: u32 },
MapperMismatch { expected: u16, found: u16 },
}
impl std::fmt::Display for SaveStateError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SaveStateError::IncompatibleVersion { expected, found } => {
write!(
f,
"Incompatible save-state version: expected {}, found {}",
expected, found
)
}
SaveStateError::MapperMismatch { expected, found } => {
write!(
f,
"Mapper mismatch: cartridge uses mapper {}, but save-state is for mapper {}",
expected, found
)
}
}
}
}
impl std::error::Error for SaveStateError {}
}
pub use savestate_error::SaveStateError;
#[cfg(test)]
mod tests {
use super::*;
use crate::platform::config::ParseResult;
fn parse_cli_config(mut args: Vec<String>) -> Config {
use tempfile::NamedTempFile;
let file = NamedTempFile::new().unwrap();
args.push("--config".to_string());
args.push(file.path().to_string_lossy().to_string());
match Config::new(&args).unwrap() {
ParseResult::Config(config) => *config,
ParseResult::Help => panic!("Expected Config, got Help"),
ParseResult::Version => panic!("Expected Config, got Version"),
}
}
fn load_test_cartridge(rom_data: &[u8]) -> Cartridge {
Cartridge::load_from_file(rom_data, "nes-test-rom.nes", None)
.expect("Failed to create cartridge")
}
#[test]
fn test_ntsc_ppu_cycles_per_cpu_cycle() {
let ntsc = TimingMode::Ntsc;
assert_eq!(ntsc.ppu_cycles_per_cpu_cycle(), 3.0);
}
#[test]
fn test_pal_ppu_cycles_per_cpu_cycle() {
let pal = TimingMode::Pal;
assert_eq!(pal.ppu_cycles_per_cpu_cycle(), 3.2);
}
#[test]
fn test_ntsc_scanlines_per_frame() {
let ntsc = TimingMode::Ntsc;
assert_eq!(ntsc.scanlines_per_frame(), 262);
}
#[test]
fn test_pal_scanlines_per_frame() {
let pal = TimingMode::Pal;
assert_eq!(pal.scanlines_per_frame(), 312);
}
#[test]
fn test_ntsc_frame_rate_hz() {
let ntsc = TimingMode::Ntsc;
let fps = ntsc.frame_rate_hz();
assert!(
(fps - 60.0988).abs() < 0.01,
"NTSC frame rate should be ~60.0988 Hz, got {fps}"
);
}
#[test]
fn test_pal_frame_rate_hz() {
let pal = TimingMode::Pal;
let fps = pal.frame_rate_hz();
assert!(
(fps - 50.00698).abs() < 0.01,
"PAL frame rate should be ~50.00698 Hz, got {fps}"
);
}
#[test]
fn test_nes_new_applies_cli_disabled_apu_channels() {
let config = parse_cli_config(vec![
"neser".to_string(),
"--disable-nes-pulse1".to_string(),
"--disable-nes-pulse2".to_string(),
"--disable-nes-triangle".to_string(),
"--disable-nes-noise".to_string(),
"--disable-nes-dmc".to_string(),
]);
let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
config,
));
nes.insert_cartridge(load_test_cartridge(&create_minimal_rom()));
assert_all_apu_channels_disabled(&nes);
nes.reset(true);
assert_all_apu_channels_disabled(&nes);
nes.reset(false);
assert_all_apu_channels_disabled(&nes);
}
fn assert_all_apu_channels_disabled(nes: &Nes) {
let apu_state = nes.apu.borrow().capture_state();
assert!(!apu_state.pulse1_enabled);
assert!(!apu_state.pulse2_enabled);
assert!(!apu_state.triangle_enabled);
assert!(!apu_state.noise_enabled);
assert!(!apu_state.dmc_enabled);
}
#[test]
fn test_ntsc_ppu_runs_3x_cpu_cycles() {
let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
Config::default(),
));
nes.bus.borrow_mut().write(0x0000, 0xEA, false); nes.cpu.set_pc(0x0000);
nes.run_cpu_tick();
assert_eq!(nes.ppu.borrow().total_cycles(), 7);
}
#[test]
fn test_pal_ppu_runs_3_2x_cpu_cycles() {
let config = Config {
nes: NesConfig {
hardware_model: crate::nes::console::HardwareModel::NesPal,
..Default::default()
},
..Default::default()
};
let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
config,
));
nes.bus.borrow_mut().write(0x0000, 0xEA, false); nes.cpu.set_pc(0x0000);
nes.run_cpu_tick();
assert_eq!(nes.ppu.borrow().total_cycles(), 7);
}
#[test]
fn test_pal_ppu_accumulates_fractional_cycles() {
let config = Config {
nes: NesConfig {
hardware_model: crate::nes::console::HardwareModel::NesPal,
..Default::default()
},
..Default::default()
};
let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
config,
));
for i in 0..10 {
nes.bus.borrow_mut().write(i, 0xEA, false); }
nes.cpu.set_pc(0x0000);
for _ in 0..5 {
nes.run_cpu_tick();
}
assert_eq!(nes.ppu.borrow().total_cycles(), 33);
}
#[test]
fn test_ntsc_ppu_accumulates_over_multiple_instructions() {
let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
Config::default(),
));
for i in 0..3 {
nes.bus.borrow_mut().write(i, 0xEA, false); }
nes.cpu.set_pc(0x0000);
nes.run_cpu_tick();
nes.run_cpu_tick();
nes.run_cpu_tick();
assert_eq!(nes.ppu.borrow().total_cycles(), 19);
}
#[test]
fn test_ppu_cycles_reset_on_nes_reset() {
let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
Config::default(),
));
nes.bus.borrow_mut().write(0x0000, 0xEA, false); nes.cpu.set_pc(0x0000);
nes.run_cpu_tick();
assert_eq!(nes.ppu.borrow().total_cycles(), 7);
nes.ppu
.borrow_mut()
.reset(false, crate::nes::console::RamInitMode::Zero);
assert_eq!(nes.ppu.borrow().total_cycles(), 0);
}
#[test]
fn test_nes_reset_reestablishes_1_cycle_ppu_offset() {
let rom_data = create_minimal_rom();
let cartridge = load_test_cartridge(&rom_data);
let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
Config::default(),
));
nes.insert_cartridge(cartridge);
nes.reset(false);
assert_eq!(nes.ppu.borrow().total_cycles(), 22);
}
#[test]
fn test_nes_color_to_rgb() {
assert_eq!(Nes::lookup_system_palette(0x00), (0x54, 0x54, 0x54)); assert_eq!(Nes::lookup_system_palette(0x01), (0x00, 0x1E, 0x74)); assert_eq!(Nes::lookup_system_palette(0x16), (0x98, 0x22, 0x20)); assert_eq!(Nes::lookup_system_palette(0x2A), (0x4C, 0xD0, 0x20)); assert_eq!(Nes::lookup_system_palette(0x30), (0xEC, 0xEE, 0xEC)); assert_eq!(Nes::lookup_system_palette(0x0D), (0x00, 0x00, 0x00));
assert_eq!(Nes::lookup_system_palette(0x40), (0x54, 0x54, 0x54)); assert_eq!(Nes::lookup_system_palette(0xFF), (0x00, 0x00, 0x00)); }
#[test]
fn test_nes_provides_access_to_ppu_screen_buffer() {
let nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
Config::default(),
));
let screen_buffer = nes.get_screen_buffer();
assert_eq!(screen_buffer.width(), 256);
assert_eq!(screen_buffer.height(), 240);
}
fn create_minimal_rom() -> Vec<u8> {
let mut rom = Vec::new();
rom.extend_from_slice(b"NES\x1A"); rom.push(1); rom.push(0); rom.push(0); rom.push(0); rom.extend_from_slice(&[0; 8]);
let mut prg_rom = vec![0; 16384];
prg_rom[0x3FFC] = 0x00; prg_rom[0x3FFD] = 0x80;
prg_rom[0] = 0x4C; prg_rom[1] = 0x00; prg_rom[2] = 0x80;
rom.extend_from_slice(&prg_rom);
rom
}
fn create_minimal_playchoice10_rom() -> Vec<u8> {
let mut rom = Vec::new();
rom.extend_from_slice(b"NES\x1A");
rom.push(1); rom.push(0); rom.push(0); rom.push(0x02); rom.extend_from_slice(&[0; 8]);
let mut prg_rom = vec![0; 16384];
prg_rom[0x3FFC] = 0x00;
prg_rom[0x3FFD] = 0x80;
prg_rom[0] = 0x4C;
prg_rom[1] = 0x00;
prg_rom[2] = 0x80;
rom.extend_from_slice(&prg_rom);
rom
}
#[test]
fn test_insert_cartridge_auto_detects_playchoice10_expansion() {
let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
Config::default(),
));
let rom_data = create_minimal_playchoice10_rom();
let cartridge = load_test_cartridge(&rom_data);
nes.insert_cartridge(cartridge);
assert_eq!(
nes.app_context.borrow().config().nes.expansion_port,
crate::nes::console::ExpansionPort::Playchoice10,
"PlayChoice ROM should auto-select PlayChoice expansion mode"
);
}
#[test]
fn test_insert_cartridge_hardware_summary_reports_playchoice10() {
let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
Config::default(),
));
let rom_data = create_minimal_playchoice10_rom();
let cartridge = load_test_cartridge(&rom_data);
nes.insert_cartridge(cartridge);
let summary = nes.app_context.borrow().config().hardware_summary();
assert!(
summary.contains("PlayChoice-10"),
"hardware summary should mention PlayChoice-10 when PC10 ROM is loaded: {summary}"
);
}
#[test]
fn test_nes_reset_resets_apu_before_cpu_reset_ticks() {
let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
Config::default(),
));
let rom_data = create_minimal_rom();
let cartridge = load_test_cartridge(&rom_data);
nes.insert_cartridge(cartridge);
nes.cpu.set_total_cycles(1);
nes.reset(true);
let expected_cycle_counter = 1 + 7;
assert_eq!(
nes.apu.borrow().frame_counter().get_cycle_counter(),
expected_cycle_counter,
"APU should have advanced during CPU reset cycles"
);
}
#[test]
fn test_nes_reset_soft_reset_rewrites_last_4017_value() {
let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
Config::default(),
));
let rom_data = create_minimal_rom();
let cartridge = load_test_cartridge(&rom_data);
nes.insert_cartridge(cartridge);
nes.apu.borrow_mut().write_frame_counter(0x80);
nes.reset(true);
assert!(
nes.apu.borrow().frame_counter().get_mode(),
"Soft reset should keep the last-written $4017 mode"
);
nes.reset(false);
assert!(
!nes.apu.borrow().frame_counter().get_mode(),
"Power-on reset should behave as if $4017 was written with $00"
);
}
#[test]
fn test_oam_dma_takes_513_cycles_on_even_cpu_cycle() {
let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
Config::default(),
));
let rom_data = create_minimal_rom();
let cartridge = load_test_cartridge(&rom_data);
nes.insert_cartridge(cartridge);
nes.cpu.reset(false);
nes.cpu.set_total_cycles(8);
let cycles_before = nes.cpu.get_total_cycles();
assert_eq!(cycles_before % 2, 0, "Should start on even cycle");
nes.bus.borrow_mut().write(0x4014, 0x02, false);
nes.run_cpu_tick();
let cycles_after = nes.cpu.get_total_cycles();
assert_eq!(
cycles_after - cycles_before,
514,
"DMA should take 514 cycles on even alignment"
);
}
#[test]
fn test_oam_dma_takes_514_cycles_on_odd_cpu_cycle() {
let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
Config::default(),
));
let rom_data = create_minimal_rom();
let cartridge = load_test_cartridge(&rom_data);
nes.insert_cartridge(cartridge);
nes.cpu.reset(false);
nes.cpu.set_total_cycles(7);
let cycles_before = nes.cpu.get_total_cycles();
assert_eq!(cycles_before % 2, 1, "Should start on odd cycle");
nes.bus.borrow_mut().write(0x4014, 0x02, false);
nes.run_cpu_tick();
let cycles_after = nes.cpu.get_total_cycles();
assert_eq!(
cycles_after - cycles_before,
513,
"DMA should take 513 cycles on odd alignment"
);
}
#[test]
fn test_oam_dma_transfers_256_bytes() {
let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
Config::default(),
));
let rom_data = create_minimal_rom();
let cartridge = load_test_cartridge(&rom_data);
nes.insert_cartridge(cartridge);
nes.cpu.reset(false);
for i in 0..256u16 {
nes.bus
.borrow_mut()
.write(0x0200 + i, (i & 0xFF) as u8, false);
}
nes.bus.borrow_mut().write(0x4014, 0x02, false);
nes.run_cpu_tick();
for i in 0..256 {
nes.bus.borrow_mut().write(0x2003, i as u8, false);
let oam_byte = nes.bus.borrow_mut().read(0x2004, false);
let expected = if (i & 0x03) == 2 {
((i & 0xFF) as u8) & 0xE3
} else {
(i & 0xFF) as u8
};
assert_eq!(
oam_byte, expected,
"OAM byte {} should match source data (with attribute masking)",
i
);
}
}
#[test]
fn test_oam_dma_uses_correct_source_page() {
let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
Config::default(),
));
let rom_data = create_minimal_rom();
let cartridge = load_test_cartridge(&rom_data);
nes.insert_cartridge(cartridge);
nes.cpu.reset(false);
for i in 0..256u16 {
nes.bus.borrow_mut().write(0x0300 + i, 0xAA, false); }
nes.bus.borrow_mut().write(0x4014, 0x03, false);
nes.run_cpu_tick();
for i in 0..256 {
nes.bus.borrow_mut().write(0x2003, i as u8, false);
let oam_byte = nes.bus.borrow_mut().read(0x2004, false);
let expected = if (i & 0x03) == 2 {
0xA2
} else {
0xAA
};
assert_eq!(
oam_byte, expected,
"OAM byte {} should be from page $03 (with attribute masking)",
i
);
}
}
#[test]
fn test_ppu_advances_during_oam_dma() {
let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
Config::default(),
));
let rom_data = create_minimal_rom();
let cartridge = load_test_cartridge(&rom_data);
nes.insert_cartridge(cartridge);
nes.cpu.reset(false);
nes.cpu.set_total_cycles(8);
let initial_ppu_cycles = nes.ppu.borrow().total_cycles();
nes.bus.borrow_mut().write(0x4014, 0x02, false);
nes.run_cpu_tick();
let expected_ppu_cycles = initial_ppu_cycles + (514 * 3);
let actual_ppu_cycles = nes.ppu.borrow().total_cycles();
assert!(
actual_ppu_cycles >= expected_ppu_cycles
&& actual_ppu_cycles <= expected_ppu_cycles + 2,
"PPU should advance by 514*3 cycles during DMA on even alignment (got {}, expected {}..={})",
actual_ppu_cycles,
expected_ppu_cycles,
expected_ppu_cycles + 2
);
}
#[test]
fn test_ntsc_refresh_rate_calculation() {
let tv_system = TimingMode::Ntsc;
let even_frame_ppu_cycles = 262 * 341; let odd_frame_ppu_cycles = 262 * 341 - 1; let avg_ppu_cycles = (even_frame_ppu_cycles + odd_frame_ppu_cycles) as f64 / 2.0;
let avg_cpu_cycles = avg_ppu_cycles / tv_system.ppu_cycles_per_cpu_cycle();
assert_eq!(even_frame_ppu_cycles, 89342);
assert_eq!(odd_frame_ppu_cycles, 89341);
assert!(
(avg_cpu_cycles - 29780.5).abs() < 0.01,
"NTSC should average ~29780.5 CPU cycles per frame"
);
}
#[test]
fn test_pal_refresh_rate_calculation() {
let tv_system = TimingMode::Pal;
let frame_ppu_cycles = 312 * 341; let cpu_cycles_per_frame = frame_ppu_cycles as f64 / tv_system.ppu_cycles_per_cpu_cycle();
assert_eq!(frame_ppu_cycles, 106392);
assert!(
(cpu_cycles_per_frame - 33247.5).abs() < 0.01,
"PAL should have ~33247.5 CPU cycles per frame"
);
}
#[test]
fn test_apu_clocked_every_cpu_cycle() {
let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
Config::default(),
));
let rom_data = create_minimal_nrom_rom();
let cartridge = load_test_cartridge(&rom_data);
nes.insert_cartridge(cartridge);
nes.reset(false);
let initial_cycle = nes.apu.borrow().frame_counter().get_cycle_counter();
let cpu_cycles = nes.run_cpu_tick();
let final_cycle = nes.apu.borrow().frame_counter().get_cycle_counter();
let apu_cycles_elapsed = final_cycle - initial_cycle;
assert_eq!(
apu_cycles_elapsed, cpu_cycles as u32,
"APU should be clocked once per CPU cycle"
);
}
#[test]
fn test_apu_clocked_for_nmi_cycles_once_per_cpu_cycle() {
let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
Config::default(),
));
let rom_data = create_minimal_nrom_rom();
let cartridge = load_test_cartridge(&rom_data);
nes.insert_cartridge(cartridge);
nes.reset(false);
nes.cpu.set_nmi_pending(true);
let initial_cycle = nes.apu.borrow().frame_counter().get_cycle_counter();
let cpu_cycles = nes.run_cpu_tick();
let final_cycle = nes.apu.borrow().frame_counter().get_cycle_counter();
let apu_cycles_elapsed = final_cycle - initial_cycle;
assert_eq!(
apu_cycles_elapsed, cpu_cycles as u32,
"APU should be clocked once per CPU cycle even when NMI is serviced"
);
}
#[test]
fn test_apu_clocked_during_oam_dma() {
let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
Config::default(),
));
let rom_data = create_minimal_nrom_rom();
let cartridge = load_test_cartridge(&rom_data);
nes.insert_cartridge(cartridge);
nes.reset(false);
let initial_cycle = nes.apu.borrow().frame_counter().get_cycle_counter();
nes.bus.borrow_mut().write(0x4014, 0x02, false);
let dma_cycles = nes.run_cpu_tick();
let final_cycle = nes.apu.borrow().frame_counter().get_cycle_counter();
let apu_cycles_elapsed = final_cycle - initial_cycle;
assert!(
dma_cycles == 255,
"OAM DMA should return 255 (capped from 513/514 cycles)"
);
assert!(
apu_cycles_elapsed == 513 || apu_cycles_elapsed == 514,
"APU should be clocked 513 or 514 times during OAM DMA, got {}",
apu_cycles_elapsed
);
}
#[test]
fn test_dmc_dma_stalls_cpu_on_sample_fetch() {
let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
Config::default(),
));
let rom_data = create_minimal_nrom_rom();
let cartridge = load_test_cartridge(&rom_data);
nes.insert_cartridge(cartridge);
nes.reset(false);
{
let mut apu = nes.apu.borrow_mut();
apu.dmc_mut().write_flags_and_rate(0x0F);
apu.dmc_mut().write_sample_address(0x00);
apu.dmc_mut().write_sample_length(0x00);
apu.write_enable(0x10);
}
let mut found_stall = false;
for _ in 0..4 {
let cpu_cycles = nes.run_cpu_tick();
if cpu_cycles > 2 {
found_stall = true;
assert!(
(3..=6).contains(&cpu_cycles),
"expected DMC DMA stalling to add 1-4 cycles to a 2-cycle NOP; got {cpu_cycles}"
);
break;
}
}
assert!(
found_stall,
"expected DMC DMA stall within 4 CPU ticks after enabling DMC"
);
}
#[test]
fn test_sample_ready_initially_false() {
let nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
Config::default(),
));
assert!(!nes.sample_ready());
}
#[test]
fn test_sample_ready_after_clocking() {
let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
Config::default(),
));
let rom_data = create_minimal_nrom_rom();
let cartridge = load_test_cartridge(&rom_data);
nes.insert_cartridge(cartridge);
nes.reset(false);
for _ in 0..50 {
nes.run_cpu_tick();
if nes.sample_ready() {
break;
}
}
assert!(nes.sample_ready());
}
#[test]
fn test_get_sample_returns_value() {
let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
Config::default(),
));
let rom_data = create_minimal_nrom_rom();
let cartridge = load_test_cartridge(&rom_data);
nes.insert_cartridge(cartridge);
nes.reset(false);
for _ in 0..50 {
nes.run_cpu_tick();
if nes.sample_ready() {
break;
}
}
let sample = nes.get_sample();
assert!(sample.is_some());
let sample_value = sample.unwrap();
assert!((0.0..=1.0).contains(&sample_value));
}
#[test]
fn test_get_sample_clears_ready_flag() {
let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
Config::default(),
));
let rom_data = create_minimal_nrom_rom();
let cartridge = load_test_cartridge(&rom_data);
nes.insert_cartridge(cartridge);
nes.reset(false);
for _ in 0..50 {
nes.run_cpu_tick();
if nes.sample_ready() {
break;
}
}
assert!(nes.sample_ready());
nes.get_sample();
assert!(!nes.sample_ready());
}
#[test]
fn test_get_sample_returns_none_when_not_ready() {
let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
Config::default(),
));
let sample = nes.get_sample();
assert!(sample.is_none());
}
fn create_minimal_nrom_rom() -> Vec<u8> {
let mut rom = Vec::new();
rom.extend_from_slice(b"NES\x1A"); rom.push(2); rom.push(1); rom.push(0x00); rom.push(0x00); rom.extend_from_slice(&[0; 8]);
rom.extend_from_slice(&[0xEA; 32768]);
rom.extend_from_slice(&[0x00; 8192]);
rom
}
#[test]
fn test_nes_reset_resets_cartridge() {
let rom_data = create_minimal_nrom_rom();
let cartridge = load_test_cartridge(&rom_data);
let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
Config::default(),
));
nes.insert_cartridge(cartridge);
nes.reset(false);
nes.bus.borrow_mut().write(0x6000, 0xAB, false);
assert_eq!(nes.bus.borrow_mut().read(0x6000, false), 0xAB);
nes.reset(true);
assert_eq!(nes.bus.borrow_mut().read(0x6000, false), 0xAB);
}
#[test]
fn test_save_state_roundtrip() {
let rom_data = create_minimal_nrom_rom();
let cartridge = load_test_cartridge(&rom_data);
let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
Config::default(),
));
nes.insert_cartridge(cartridge);
nes.reset(false);
for _ in 0..1000 {
nes.run_cpu_tick();
}
let state1 = nes.save_state();
for _ in 0..500 {
nes.run_cpu_tick();
}
let state2 = nes.save_state();
assert_ne!(state1.cpu.pc, state2.cpu.pc);
nes.load_state(&state1).expect("load_state should succeed");
let state_restored = nes.save_state();
assert_eq!(state_restored.cpu.a, state1.cpu.a);
assert_eq!(state_restored.cpu.x, state1.cpu.x);
assert_eq!(state_restored.cpu.y, state1.cpu.y);
assert_eq!(state_restored.cpu.sp, state1.cpu.sp);
assert_eq!(state_restored.cpu.pc, state1.cpu.pc);
assert_eq!(state_restored.cpu.p, state1.cpu.p);
assert_eq!(state_restored.cpu.total_cycles, state1.cpu.total_cycles);
}
#[test]
fn test_save_state_version_check() {
let rom_data = create_minimal_nrom_rom();
let cartridge = load_test_cartridge(&rom_data);
let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
Config::default(),
));
nes.insert_cartridge(cartridge);
nes.reset(false);
let mut state = nes.save_state();
state.version = 9999;
let result = nes.load_state(&state);
assert!(result.is_err());
if let Err(super::SaveStateError::IncompatibleVersion { expected, found }) = result {
assert_eq!(expected, SAVESTATE_VERSION);
assert_eq!(found, 9999);
} else {
panic!("Expected IncompatibleVersion error");
}
}
#[test]
fn test_save_state_mapper_mismatch() {
let rom_data = create_minimal_nrom_rom();
let cartridge = load_test_cartridge(&rom_data);
let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
Config::default(),
));
nes.insert_cartridge(cartridge);
nes.reset(false);
let mut state = nes.save_state();
state.mapper.mapper_number = 4;
let result = nes.load_state(&state);
assert!(result.is_err());
if let Err(super::SaveStateError::MapperMismatch { expected, found }) = result {
assert_eq!(expected, 0); assert_eq!(found, 4); } else {
panic!("Expected MapperMismatch error");
}
}
#[test]
fn test_load_state_clears_joypad_button_states() {
let rom_data = create_minimal_nrom_rom();
let cartridge = load_test_cartridge(&rom_data);
let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
Config::default(),
));
nes.insert_cartridge(cartridge);
nes.reset(false);
nes.set_button(1, crate::nes::input::Button::Up, true);
assert_ne!(
nes.get_joypad_button_states(1) & (1 << crate::nes::input::Button::Up as u8),
0,
"Up should be pressed before save"
);
let state = nes.save_state();
nes.set_button(1, crate::nes::input::Button::Up, false);
nes.set_button(1, crate::nes::input::Button::Up, true);
nes.load_state(&state).expect("load_state should succeed");
assert_eq!(
nes.get_joypad_button_states(1),
0,
"All joypad buttons must be cleared after loading a save state"
);
}
#[test]
fn test_save_state_deterministic_execution() {
let rom_data = create_minimal_nrom_rom();
let cartridge = load_test_cartridge(&rom_data);
let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
Config::default(),
));
nes.insert_cartridge(cartridge);
nes.reset(false);
for _ in 0..500 {
nes.run_cpu_tick();
}
let saved_state = nes.save_state();
for _ in 0..100 {
nes.run_cpu_tick();
}
let end_state1 = nes.save_state();
nes.load_state(&saved_state).unwrap();
for _ in 0..100 {
nes.run_cpu_tick();
}
let end_state2 = nes.save_state();
assert_eq!(end_state1.cpu.pc, end_state2.cpu.pc);
assert_eq!(end_state1.cpu.a, end_state2.cpu.a);
assert_eq!(end_state1.cpu.x, end_state2.cpu.x);
assert_eq!(end_state1.cpu.y, end_state2.cpu.y);
assert_eq!(end_state1.cpu.sp, end_state2.cpu.sp);
assert_eq!(end_state1.cpu.p, end_state2.cpu.p);
assert_eq!(end_state1.cpu.total_cycles, end_state2.cpu.total_cycles);
assert_eq!(
end_state1.ppu.timing.scanline,
end_state2.ppu.timing.scanline
);
assert_eq!(end_state1.ppu.timing.pixel, end_state2.ppu.timing.pixel);
}
#[test]
fn test_insert_cartridge_enables_paddle_for_known_crc() {
let rom_data = create_minimal_nrom_rom();
let mut cartridge = load_test_cartridge(&rom_data);
cartridge.set_crc32_for_test(0x32FB0583);
let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
Config::default(),
));
nes.insert_cartridge(cartridge);
nes.bus.borrow_mut().set_mouse_x_position(0xA5);
nes.bus.borrow_mut().set_mouse_left_button(true);
nes.bus.borrow_mut().write(0x4016, 0x01, false);
nes.bus.borrow_mut().write(0x4016, 0x00, false);
let paddle_bits = nes.bus.borrow_mut().read(0x4017, false) & 0x18;
assert_eq!(paddle_bits, 0x08); }
#[test]
fn test_insert_cartridge_disables_paddle_for_unknown_crc() {
let rom_data = create_minimal_nrom_rom();
let mut cartridge = load_test_cartridge(&rom_data);
cartridge.set_crc32_for_test(0xDEADBEEF);
let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
Config::default(),
));
nes.insert_cartridge(cartridge);
nes.bus
.borrow_mut()
.set_button(2, crate::nes::input::Button::A, true);
nes.bus.borrow_mut().write(0x4016, 0x01, false);
nes.bus.borrow_mut().write(0x4016, 0x00, false);
let joypad_bit = nes.bus.borrow_mut().read(0x4017, false) & 0x01;
assert_eq!(joypad_bit, 1); }
#[test]
fn test_insert_cartridge_does_not_add_second_arkanoid_port() {
let rom_data = create_minimal_nrom_rom();
let mut cartridge = load_test_cartridge(&rom_data);
cartridge.set_crc32_for_test(0x32FB0583);
let config = Config {
nes: NesConfig {
controller_port1: crate::nes::input::ControllerType::Arkanoid,
controller_port1_explicit: true,
controller_port2: crate::nes::input::ControllerType::Joypad,
controller_port2_explicit: false,
..Default::default()
},
..Default::default()
};
let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
config,
));
nes.insert_cartridge(cartridge);
let bus_state = nes.bus.borrow().capture_state();
assert!(matches!(
bus_state.port1_controller,
crate::nes::bus::ControllerStateWrapper::Arkanoid(_)
));
assert!(matches!(
bus_state.port2_controller,
crate::nes::bus::ControllerStateWrapper::Joypad(_)
));
}
#[test]
fn test_insert_cartridge_enables_zapper_for_known_crc() {
let rom_data = create_minimal_nrom_rom();
let mut cartridge = load_test_cartridge(&rom_data);
cartridge.set_crc32_for_test(0x24598791);
let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
Config::default(),
));
nes.insert_cartridge(cartridge);
let bus_state = nes.bus.borrow().capture_state();
assert!(matches!(
bus_state.port1_controller,
crate::nes::bus::ControllerStateWrapper::Joypad(_)
));
assert!(matches!(
bus_state.port2_controller,
crate::nes::bus::ControllerStateWrapper::Joypad(_)
));
assert_eq!(
nes.app_context.borrow().config().nes.expansion_port,
crate::nes::console::ExpansionPort::ZapperFamicom
);
}
#[test]
fn test_insert_cartridge_keeps_explicit_port2_when_zapper_detected() {
let rom_data = create_minimal_nrom_rom();
let mut cartridge = load_test_cartridge(&rom_data);
cartridge.set_crc32_for_test(0x24598791);
let config = Config {
nes: NesConfig {
controller_port1: crate::nes::input::ControllerType::Joypad,
controller_port1_explicit: false,
controller_port2: crate::nes::input::ControllerType::Joypad,
controller_port2_explicit: true,
..Default::default()
},
..Default::default()
};
let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
config,
));
nes.insert_cartridge(cartridge);
let bus_state = nes.bus.borrow().capture_state();
assert!(matches!(
bus_state.port2_controller,
crate::nes::bus::ControllerStateWrapper::Joypad(_)
));
}
#[test]
fn test_insert_cartridge_does_not_apply_rom_db_controller_when_any_port_is_explicit() {
let rom_data = create_minimal_nrom_rom();
let mut cartridge = load_test_cartridge(&rom_data);
cartridge.set_crc32_for_test(0x24598791);
let config = Config {
nes: NesConfig {
controller_port1: crate::nes::input::ControllerType::Joypad,
controller_port1_explicit: true,
controller_port2: crate::nes::input::ControllerType::Joypad,
controller_port2_explicit: false,
..Default::default()
},
..Default::default()
};
let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
config,
));
nes.insert_cartridge(cartridge);
let bus_state = nes.bus.borrow().capture_state();
assert!(matches!(
bus_state.port1_controller,
crate::nes::bus::ControllerStateWrapper::Joypad(_)
));
assert!(matches!(
bus_state.port2_controller,
crate::nes::bus::ControllerStateWrapper::Joypad(_)
));
}
#[test]
fn test_insert_cartridge_auto_detects_power_pad_on_port2() {
let rom_data = create_minimal_nrom_rom();
let mut cartridge = load_test_cartridge(&rom_data);
cartridge.set_crc32_for_test(0x5734EB9E);
let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
Config::default(),
));
nes.insert_cartridge(cartridge);
let bus_state = nes.bus.borrow().capture_state();
assert!(
matches!(
bus_state.port2_controller,
crate::nes::bus::ControllerStateWrapper::PowerPad(_)
),
"Power Pad should be auto-detected on port 2 for World Class Track Meet"
);
assert!(
matches!(
bus_state.port1_controller,
crate::nes::bus::ControllerStateWrapper::Joypad(_)
),
"Port 1 should remain Joypad when Power Pad is on port 2"
);
}
#[test]
fn test_insert_cartridge_does_not_auto_detect_power_pad_when_port_is_explicit() {
let rom_data = create_minimal_nrom_rom();
let mut cartridge = load_test_cartridge(&rom_data);
cartridge.set_crc32_for_test(0x5734EB9E);
let config = Config {
nes: NesConfig {
controller_port2: crate::nes::input::ControllerType::Joypad,
controller_port2_explicit: true,
..Default::default()
},
..Default::default()
};
let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
config,
));
nes.insert_cartridge(cartridge);
let bus_state = nes.bus.borrow().capture_state();
assert!(
matches!(
bus_state.port2_controller,
crate::nes::bus::ControllerStateWrapper::Joypad(_)
),
"Explicit port config should override Power Pad auto-detection"
);
}
#[test]
fn test_switch_cartridge_from_joypad_game_to_arkanoid_auto_detects_controller() {
let rom_data = create_minimal_nrom_rom();
let mut joypad_cartridge = load_test_cartridge(&rom_data);
joypad_cartridge.set_crc32_for_test(0xDEADBEEF); let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
Config::default(),
));
nes.insert_cartridge(joypad_cartridge);
let bus_state = nes.bus.borrow().capture_state();
assert!(
matches!(
bus_state.port2_controller,
crate::nes::bus::ControllerStateWrapper::Joypad(_)
),
"Expected joypad on port 2 after non-Arkanoid ROM, got {:?}",
bus_state.port2_controller
);
let mut arkanoid_cartridge = load_test_cartridge(&rom_data);
arkanoid_cartridge.set_crc32_for_test(0x32FB0583); nes.insert_cartridge(arkanoid_cartridge);
let bus_state = nes.bus.borrow().capture_state();
assert!(
matches!(
bus_state.port2_controller,
crate::nes::bus::ControllerStateWrapper::Arkanoid(_)
),
"Expected Arkanoid on port 2 after ROM switch, got {:?}",
bus_state.port2_controller
);
assert!(
matches!(
bus_state.port1_controller,
crate::nes::bus::ControllerStateWrapper::Joypad(_)
),
"Expected joypad on port 1 after ROM switch"
);
}
#[test]
fn test_switch_cartridge_from_power_pad_to_normal_resets_to_joypad() {
let rom_data = create_minimal_nrom_rom();
let mut power_pad_cartridge = load_test_cartridge(&rom_data);
power_pad_cartridge.set_crc32_for_test(0x5734EB9E); let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
Config::default(),
));
nes.insert_cartridge(power_pad_cartridge);
assert_eq!(
nes.active_controller_port_type(2),
ControllerType::PowerPad,
"Power Pad should be active after World Class Track Meet"
);
let mut joypad_cartridge = load_test_cartridge(&rom_data);
joypad_cartridge.set_crc32_for_test(0xDEADBEEF); nes.insert_cartridge(joypad_cartridge);
let bus_state = nes.bus.borrow().capture_state();
assert!(
matches!(
bus_state.port2_controller,
crate::nes::bus::ControllerStateWrapper::Joypad(_)
),
"Port 2 bus controller should reset to joypad after switching to a normal game, got {:?}",
bus_state.port2_controller
);
assert_eq!(
nes.active_controller_port_type(2),
ControllerType::Joypad,
"active_controller_port2 should reset to joypad"
);
}
fn build_smc_rom(game_reset_vector: u16, include_trainer: bool) -> Vec<u8> {
let prg_size_units = 16u8;
let trainer_flag = if include_trainer { 0x04 } else { 0x00 };
let flags6 = 0x60 | trainer_flag; let mut rom = vec![
b'N',
b'E',
b'S',
0x1A, prg_size_units, 0, flags6, 0x00, 0,
0,
0,
0,
0,
0,
0,
0, ];
if include_trainer {
for i in 0u16..512 {
rom.push((i as u8).wrapping_add(0x55));
}
}
let prg_len = prg_size_units as usize * 16 * 1024;
let mut prg = vec![0xEA_u8; prg_len]; let reset_vec_offset = 7 * 16384 + 16380;
prg[reset_vec_offset] = (game_reset_vector & 0xFF) as u8;
prg[reset_vec_offset + 1] = (game_reset_vector >> 8) as u8;
rom.extend(prg);
rom
}
#[test]
fn test_hard_reset_with_trainer_sets_pc_to_7003() {
let game_vector = 0xC000u16;
let rom = build_smc_rom(game_vector, true);
let mut nes = Nes::new(crate::platform::app_context::AppContext::new());
nes.insert_cartridge(load_test_cartridge(&rom));
nes.reset(false); assert_eq!(
nes.cpu.pc(),
0x7003,
"CPU must start at $7003 (trainer entry) on hard reset with trainer"
);
}
#[test]
fn test_hard_reset_with_trainer_pushes_game_vector_to_stack() {
let game_vector = 0xC123u16;
let rom = build_smc_rom(game_vector, true);
let mut nes = Nes::new(crate::platform::app_context::AppContext::new());
nes.insert_cartridge(load_test_cartridge(&rom));
nes.reset(false); let sp = nes.cpu.sp();
let return_addr = game_vector.wrapping_sub(1);
let stacked_lo = nes
.bus
.borrow_mut()
.read(0x0100 | (sp.wrapping_add(1)) as u16, false);
let stacked_hi = nes
.bus
.borrow_mut()
.read(0x0100 | (sp.wrapping_add(2)) as u16, false);
let stacked = (stacked_hi as u16) << 8 | stacked_lo as u16;
assert_eq!(
stacked, return_addr,
"Stack must hold game_vector − 1 = ${:04X} for trainer RTS",
return_addr
);
}
#[test]
fn test_soft_reset_with_trainer_does_not_jump_to_7003() {
let game_vector = 0xC000u16;
let rom = build_smc_rom(game_vector, true);
let mut nes = Nes::new(crate::platform::app_context::AppContext::new());
nes.insert_cartridge(load_test_cartridge(&rom));
nes.reset(false); nes.reset(true); assert_eq!(
nes.cpu.pc(),
game_vector,
"Soft reset must NOT re-execute trainer; must go to game reset vector"
);
}
#[test]
fn test_hard_reset_without_trainer_uses_game_reset_vector() {
let game_vector = 0xD000u16;
let rom = build_smc_rom(game_vector, false);
let mut nes = Nes::new(crate::platform::app_context::AppContext::new());
nes.insert_cartridge(load_test_cartridge(&rom));
nes.reset(false); assert_eq!(
nes.cpu.pc(),
game_vector,
"Hard reset without trainer must go directly to game reset vector"
);
}
#[test]
fn test_recent_cpu_trace_is_bounded_and_returns_recent_tail() {
let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
Config::default(),
));
let mut prg_rom = vec![0xEAu8; 32 * 1024];
prg_rom[0x7FFC] = 0x00;
prg_rom[0x7FFD] = 0x80;
let cartridge = crate::nes::cartridge::Cartridge::from_parts(
prg_rom,
vec![],
crate::nes::cartridge::NametableLayout::Horizontal,
);
nes.insert_cartridge(cartridge);
nes.cpu.set_pc(0x8000);
nes.set_cpu_trace_enabled(true);
let executed = MAX_CPU_TRACE_LINES + 20;
for _ in 0..executed {
nes.run_cpu_tick();
}
let full = nes.recent_cpu_trace(usize::MAX);
assert_eq!(full.len(), MAX_CPU_TRACE_LINES);
let recent = nes.recent_cpu_trace(32);
assert_eq!(recent.len(), 32);
let expected_first = 0x8000u16.wrapping_add((executed - 32) as u16);
assert_eq!(recent[0].addr, expected_first);
}
#[test]
fn test_savestate_version() {
assert_eq!(SAVESTATE_VERSION, 8);
}
#[test]
fn test_savestate_json_roundtrip() {
let rom_data = create_minimal_nrom_rom();
let cartridge = load_test_cartridge(&rom_data);
let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
Config::default(),
));
nes.insert_cartridge(cartridge);
nes.reset(false);
for _ in 0..100 {
nes.run_cpu_tick();
}
let state = nes.save_state();
let json = state.to_json().expect("serialization should succeed");
let restored = SaveState::from_json(&json).expect("deserialization should succeed");
assert_eq!(restored.version, state.version);
assert_eq!(restored.cpu.a, state.cpu.a);
assert_eq!(restored.cpu.pc, state.cpu.pc);
assert_eq!(restored.ppu.timing.scanline, state.ppu.timing.scanline);
}
#[test]
fn test_savestate_bytes_roundtrip() {
let rom_data = create_minimal_nrom_rom();
let cartridge = load_test_cartridge(&rom_data);
let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
Config::default(),
));
nes.insert_cartridge(cartridge);
nes.reset(false);
for _ in 0..100 {
nes.run_cpu_tick();
}
let state = nes.save_state();
let bytes = state.to_bytes().expect("serialization should succeed");
let restored = SaveState::from_bytes(&bytes).expect("deserialization should succeed");
assert_eq!(restored.version, state.version);
assert_eq!(restored.cpu.x, state.cpu.x);
assert_eq!(restored.cpu.y, state.cpu.y);
}
#[test]
fn test_savestate_binary_bytes_roundtrip() {
let rom_data = create_minimal_nrom_rom();
let cartridge = load_test_cartridge(&rom_data);
let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
Config::default(),
));
nes.insert_cartridge(cartridge);
nes.reset(false);
for _ in 0..100 {
nes.run_cpu_tick();
}
let state = nes.save_state();
let bytes = state
.to_binary_bytes()
.expect("binary serialization should succeed");
let restored =
SaveState::from_binary_bytes(&bytes).expect("binary deserialization should succeed");
assert_eq!(restored.version, state.version);
assert_eq!(restored.cpu.a, state.cpu.a);
assert_eq!(restored.cpu.x, state.cpu.x);
assert_eq!(restored.cpu.y, state.cpu.y);
assert_eq!(restored.ppu.timing.scanline, state.ppu.timing.scanline);
assert_eq!(restored.ram, state.ram);
}
#[test]
fn test_savestate_binary_bytes_are_smaller_than_json() {
let rom_data = create_minimal_nrom_rom();
let cartridge = load_test_cartridge(&rom_data);
let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
Config::default(),
));
nes.insert_cartridge(cartridge);
nes.reset(false);
for _ in 0..100 {
nes.run_cpu_tick();
}
let state = nes.save_state();
let json_bytes = state.to_bytes().expect("json serialization should succeed");
let binary_bytes = state
.to_binary_bytes()
.expect("binary serialization should succeed");
assert!(
binary_bytes.len() < json_bytes.len(),
"binary ({} bytes) should be smaller than JSON ({} bytes)",
binary_bytes.len(),
json_bytes.len()
);
}
#[test]
fn test_insert_cartridge_syncs_famicom_four_player_mode_to_bus() {
use crate::nes::console::{ExpansionPort, HardwareMode};
use crate::nes::input::Button;
let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
Config::default(),
));
nes.app_context
.borrow_mut()
.config_mut()
.apply_rom_db_famicom_four_players_hint(true);
assert_eq!(
nes.app_context.borrow().config().nes.hardware_mode,
HardwareMode::Famicom
);
assert_eq!(
nes.app_context.borrow().config().nes.expansion_port,
ExpansionPort::FamicomFourPlayers
);
let rom_data = create_minimal_rom();
let cartridge = load_test_cartridge(&rom_data);
nes.insert_cartridge(cartridge);
nes.bus.borrow_mut().set_button(3, Button::A, true);
nes.bus.borrow_mut().write_for_testing(0x4016, 1);
nes.bus.borrow_mut().write_for_testing(0x4016, 0);
let value = nes.bus.borrow_mut().read_for_testing(0x4016);
assert_eq!(
value & 0x02,
0x02,
"Player 3 A button should appear on bit 1 of $4016 in Famicom four-player mode"
);
}
#[test]
fn test_nes_new_applies_configured_palette() {
let mut config = Config::default();
config.nes.palette = crate::nes::ppu::NesPalette::Smooth;
let nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
config,
));
assert_eq!(nes.current_palette(), crate::nes::ppu::NesPalette::Smooth);
assert_eq!(
nes.ppu.borrow().lookup_system_palette(0x00),
(0x6A, 0x6A, 0x6A)
);
}
#[test]
fn test_nes_cycle_palette_advances() {
let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
Config::default(),
));
assert_eq!(nes.current_palette(), crate::nes::ppu::NesPalette::Default);
assert_eq!(nes.cycle_palette(), crate::nes::ppu::NesPalette::NesDev);
assert_eq!(nes.current_palette(), crate::nes::ppu::NesPalette::NesDev);
}
#[test]
fn test_insert_cartridge_propagates_famicom_emphasis_to_ppu() {
let mut nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
Config::default(),
));
assert!(
!nes.ppu.borrow().famicom_emphasis,
"PPU should start without Famicom emphasis in NES mode"
);
nes.app_context
.borrow_mut()
.config_mut()
.apply_rom_db_famicom_four_players_hint(true);
let rom_data = create_minimal_rom();
let cartridge = load_test_cartridge(&rom_data);
nes.insert_cartridge(cartridge);
assert!(
nes.ppu.borrow().famicom_emphasis,
"PPU should have Famicom emphasis after ROM DB hint sets Famicom mode"
);
}
#[test]
fn test_save_ram_returns_ok_with_no_cartridge() {
let nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
Config::default(),
));
assert!(
nes.save_ram().is_ok(),
"save_ram with no cartridge must return Ok"
);
}
#[test]
fn test_nes_allowed_shaders_includes_expected_presets() {
use crate::platform::emulator::Emulator;
let nes = Nes::new(crate::platform::app_context::AppContext::new_with_config(
Config::default(),
));
let shaders = nes.allowed_shaders();
assert!(
shaders.contains(&"none"),
"NES must allow the 'none' (stock) shader"
);
assert!(shaders.contains(&"crt"), "NES must allow the 'crt' shader");
assert!(
shaders.contains(&"smooth"),
"NES must allow the 'smooth' shader"
);
assert!(
shaders.contains(&"ntsc"),
"NES must allow the 'ntsc' shader"
);
assert!(shaders.contains(&"pal"), "NES must allow the 'pal' shader");
assert!(
!shaders.contains(&"dmg"),
"NES must NOT allow the 'dmg' shader"
);
}
}