288 lines
8.6 KiB
Rust
288 lines
8.6 KiB
Rust
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<u8> {
|
|
nrom_test_rom_with_program(&[0xEA, 0x4C, 0x00, 0x80])
|
|
}
|
|
|
|
fn nrom_test_rom_with_program(program: &[u8]) -> Vec<u8> {
|
|
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<Cell<usize>>,
|
|
}
|
|
|
|
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);
|
|
}
|