use crate::runtime::{ button_pressed, set_button_pressed, AudioOutput, ClientRuntime, EmulationState, HostConfig, InputProvider, JoypadButton, NesRuntime, NoopClock, NullAudio, NullInput, NullVideo, RuntimeHostLoop, VideoMode, VideoOutput, FRAME_RGBA_BYTES, JOYPAD_BUTTONS_COUNT, JOYPAD_BUTTON_ORDER, }; use std::cell::Cell; use std::rc::Rc; fn nrom_test_rom() -> Vec { nrom_test_rom_with_program(&[0xEA, 0x4C, 0x00, 0x80]) } fn nrom_test_rom_with_program(program: &[u8]) -> Vec { let mut rom = vec![0u8; 16 + 16 * 1024 + 8 * 1024]; rom[0..4].copy_from_slice(b"NES\x1A"); rom[4] = 1; // 16 KiB PRG rom[5] = 1; // 8 KiB CHR let prg_offset = 16; let reset_vec = prg_offset + 0x3FFC; rom[reset_vec] = 0x00; rom[reset_vec + 1] = 0x80; rom[prg_offset..prg_offset + program.len()].copy_from_slice(program); rom } #[test] fn runtime_runs_frame_and_renders() { let rom = nrom_test_rom(); let mut rt = NesRuntime::from_rom_bytes(&rom).expect("runtime init"); rt.run_until_frame_complete().expect("frame should run"); let frame = rt.frame_rgba(); assert_eq!(frame.len(), FRAME_RGBA_BYTES); assert_eq!(rt.frame_number(), 1); } #[test] fn runtime_state_roundtrip() { let rom = nrom_test_rom(); let mut rt = NesRuntime::from_rom_bytes(&rom).expect("runtime init"); rt.set_buttons([true, false, true, false, true, false, true, false]); rt.step_instruction().expect("step"); let state = rt.save_state(); rt.run_until_frame_complete().expect("run"); rt.load_state(&state).expect("load state"); assert_eq!( rt.buttons(), [true, false, true, false, true, false, true, false] ); } #[test] fn timing_mode_defaults_to_ntsc_for_ines1() { let rom = nrom_test_rom(); let rt = NesRuntime::from_rom_bytes(&rom).expect("runtime init"); assert_eq!(rt.video_mode(), VideoMode::Ntsc); } #[test] fn joypad_button_helpers_match_public_order() { let mut buttons = [false; JOYPAD_BUTTONS_COUNT]; for &button in &JOYPAD_BUTTON_ORDER { assert!(!button_pressed(&buttons, button)); set_button_pressed(&mut buttons, button, true); assert!(button_pressed(&buttons, button)); } assert!(button_pressed(&buttons, JoypadButton::A)); assert!(button_pressed(&buttons, JoypadButton::Select)); } #[test] fn audio_mixer_generates_samples() { let rom = nrom_test_rom(); let mut rt = NesRuntime::from_rom_bytes(&rom).expect("runtime init"); let mut mixer = rt.default_audio_mixer(48_000); let mut out = Vec::new(); rt.run_until_frame_complete_with_audio(&mut mixer, &mut out) .expect("run frame with audio"); assert!(!out.is_empty()); assert_eq!(mixer.sample_rate(), 48_000); } #[test] fn audio_mixer_accounts_for_oam_dma_stall_cycles() { let rom = nrom_test_rom_with_program(&[ 0xA9, 0x00, // LDA #$00 0x8D, 0x14, 0x40, // STA $4014 0x4C, 0x00, 0x80, // JMP $8000 ]); let runtime = NesRuntime::from_rom_bytes(&rom).expect("runtime init"); let mode = runtime.video_mode(); let mut host = RuntimeHostLoop::with_config(runtime, HostConfig::new(48_000, false)); let total_samples = host .run_frames_unpaced(120, &mut NullInput, &mut NullVideo, &mut NullAudio) .expect("run frames"); let expected = ((host.runtime().frame_number() as f64) * 48_000.0 / mode.frame_hz()).round(); let drift_pct = ((total_samples as f64 - expected).abs() / expected) * 100.0; assert!( drift_pct <= 2.5, "audio drift too high with OAM DMA: {drift_pct:.3}% (samples={total_samples}, expected={expected:.0})" ); } struct FixedInput; impl InputProvider for FixedInput { fn poll_buttons(&mut self) -> [bool; JOYPAD_BUTTONS_COUNT] { [true, false, false, false, false, false, false, false] } } #[derive(Default)] struct MockVideo { frames: usize, last_len: usize, } impl VideoOutput for MockVideo { fn present_rgba(&mut self, frame: &[u8], _width: usize, _height: usize) { self.frames += 1; self.last_len = frame.len(); } } #[derive(Default)] struct MockAudio { total_samples: usize, } impl AudioOutput for MockAudio { fn push_samples(&mut self, samples: &[f32]) { self.total_samples += samples.len(); } } #[derive(Clone)] struct CountingClock { waits: Rc>, } impl CountingClock { fn new() -> Self { Self { waits: Rc::new(Cell::new(0)), } } fn waits(&self) -> usize { self.waits.get() } } impl crate::runtime::FrameClock for CountingClock { fn wait_next_frame(&mut self) { self.waits.set(self.waits.get().saturating_add(1)); } } #[test] fn host_loop_runs_single_frame() { let rom = nrom_test_rom(); let rt = NesRuntime::from_rom_bytes(&rom).expect("runtime init"); let mut host = RuntimeHostLoop::with_clock(rt, 48_000, NoopClock); let mut input = FixedInput; let mut video = MockVideo::default(); let mut audio = MockAudio::default(); let stats = host .run_frame(&mut input, &mut video, &mut audio) .expect("host frame should run"); assert_eq!(stats.frame_number, 1); assert!(stats.audio_samples > 0); assert_eq!(host.runtime().frame_number(), 1); assert_eq!(video.frames, 1); assert_eq!(video.last_len, FRAME_RGBA_BYTES); assert!(audio.total_samples > 0); } #[test] fn host_loop_runs_multiple_frames() { let rom = nrom_test_rom(); let rt = NesRuntime::from_rom_bytes(&rom).expect("runtime init"); let config = HostConfig::new(48_000, false); let mut host = RuntimeHostLoop::with_config(rt, config); let mut input = FixedInput; let mut video = MockVideo::default(); let mut audio = MockAudio::default(); let total_samples = host .run_frames(3, &mut input, &mut video, &mut audio) .expect("host frames should run"); assert_eq!(host.runtime().frame_number(), 3); assert_eq!(video.frames, 3); assert!(audio.total_samples > 0); assert_eq!(total_samples, audio.total_samples); } #[test] fn client_runtime_respects_pause_and_step() { let rom = nrom_test_rom(); let rt = NesRuntime::from_rom_bytes(&rom).expect("runtime init"); let host = RuntimeHostLoop::with_clock(rt, 48_000, NoopClock); let mut client = ClientRuntime::with_host_loop(host); let mut input = FixedInput; let mut video = MockVideo::default(); let mut audio = MockAudio::default(); client.pause(); assert_eq!(client.state(), EmulationState::Paused); let skipped = client .tick(&mut input, &mut video, &mut audio) .expect("paused tick should succeed"); assert!(skipped.is_none()); assert_eq!(client.host().runtime().frame_number(), 0); let step_stats = client .step_frame(&mut input, &mut video, &mut audio) .expect("manual step should run"); assert_eq!(step_stats.frame_number, 1); assert_eq!(client.host().runtime().frame_number(), 1); client.resume(); let tick_stats = client .tick(&mut input, &mut video, &mut audio) .expect("running tick should succeed"); assert_eq!(tick_stats.expect("must run").frame_number, 2); } #[test] fn run_frame_unpaced_does_not_call_clock() { let rom = nrom_test_rom(); let rt = NesRuntime::from_rom_bytes(&rom).expect("runtime init"); let clock = CountingClock::new(); let clock_probe = clock.clone(); let mut host = RuntimeHostLoop::with_clock(rt, 48_000, clock); let mut input = FixedInput; let mut video = MockVideo::default(); let mut audio = MockAudio::default(); host.run_frame_unpaced(&mut input, &mut video, &mut audio) .expect("frame should run"); assert_eq!(clock_probe.waits(), 0); host.run_frame(&mut input, &mut video, &mut audio) .expect("paced frame should run"); assert_eq!(clock_probe.waits(), 1); } #[test] fn client_step_frame_is_unpaced() { let rom = nrom_test_rom(); let rt = NesRuntime::from_rom_bytes(&rom).expect("runtime init"); let clock = CountingClock::new(); let clock_probe = clock.clone(); let host = RuntimeHostLoop::with_clock(rt, 48_000, clock); let mut client = ClientRuntime::with_host_loop(host); let mut input = FixedInput; let mut video = MockVideo::default(); let mut audio = MockAudio::default(); client.pause(); client .step_frame(&mut input, &mut video, &mut audio) .expect("manual step should run"); assert_eq!(clock_probe.waits(), 0); client.resume(); client .tick(&mut input, &mut video, &mut audio) .expect("running tick should run"); assert_eq!(clock_probe.waits(), 1); }