use crate::nes::console::{Nes, SaveState};
use crate::platform::autorun::AutorunFile;
#[derive(Debug, Clone, PartialEq, Eq)]
#[allow(dead_code)]
pub struct HeadlessPlaybackResult {
pub crc_mismatches: usize,
pub total_checkpoints_verified: usize,
}
impl HeadlessPlaybackResult {
#[allow(dead_code)]
pub fn is_ok(&self) -> bool {
self.crc_mismatches == 0
}
}
#[allow(dead_code)]
pub fn run_headless_playback(
nes: &mut Nes,
file: &AutorunFile,
start_checkpoint: Option<usize>,
) -> Result<HeadlessPlaybackResult, String> {
let (start_frame, first_checkpoint_idx) = if let Some(cp_idx) = start_checkpoint {
let cp = file
.checkpoints
.get(cp_idx)
.ok_or_else(|| format!("Checkpoint index {cp_idx} out of range"))?;
let state = SaveState::from_bytes(&cp.state_bytes)
.map_err(|e| format!("Failed to deserialize checkpoint {cp_idx} state: {e}"))?;
nes.load_state(&state)
.map_err(|e| format!("Failed to load checkpoint {cp_idx} state: {e}"))?;
(cp.frame_index as usize + 1, cp_idx + 1)
} else {
(0, 0)
};
let mut frame_idx = start_frame;
let mut cp_idx = first_checkpoint_idx;
let mut crc_mismatches = 0usize;
let mut total_verified = 0usize;
for frame in file.frames.iter().skip(start_frame) {
nes.set_joypad_button_states(1, frame.player1);
nes.set_joypad_button_states(2, frame.player2);
run_one_frame(nes);
while cp_idx < file.checkpoints.len()
&& file.checkpoints[cp_idx].frame_index as usize == frame_idx
{
let cp = &file.checkpoints[cp_idx];
let actual_crc = nes.ppu().borrow().screen_buffer().crc32();
if actual_crc != cp.screen_crc {
crc_mismatches += 1;
}
total_verified += 1;
cp_idx += 1;
}
frame_idx += 1;
}
Ok(HeadlessPlaybackResult {
crc_mismatches,
total_checkpoints_verified: total_verified,
})
}
#[allow(dead_code)]
pub fn recalculate_checkpoint_crcs(
nes: &mut Nes,
file: &mut AutorunFile,
start_checkpoint: Option<usize>,
) -> Result<usize, String> {
recalculate_checkpoint_crcs_with_progress(nes, file, start_checkpoint, |_, _| {})
}
pub fn recalculate_checkpoint_crcs_with_progress<F>(
nes: &mut Nes,
file: &mut AutorunFile,
start_checkpoint: Option<usize>,
mut on_progress: F,
) -> Result<usize, String>
where
F: FnMut(usize, usize),
{
let (start_frame, first_checkpoint_idx) = if let Some(cp_idx) = start_checkpoint {
let cp = file
.checkpoints
.get(cp_idx)
.ok_or_else(|| format!("Checkpoint index {cp_idx} out of range"))?;
let state = SaveState::from_bytes(&cp.state_bytes)
.map_err(|e| format!("Failed to deserialize checkpoint {cp_idx} state: {e}"))?;
nes.load_state(&state)
.map_err(|e| format!("Failed to load checkpoint {cp_idx} state: {e}"))?;
(cp.frame_index as usize + 1, cp_idx + 1)
} else {
(0, 0)
};
let mut frame_idx = start_frame;
let mut cp_idx = first_checkpoint_idx;
let mut updated = 0usize;
let total_to_update = file.checkpoints.len().saturating_sub(first_checkpoint_idx);
for frame in file.frames.iter().skip(start_frame) {
nes.set_joypad_button_states(1, frame.player1);
nes.set_joypad_button_states(2, frame.player2);
run_one_frame(nes);
while cp_idx < file.checkpoints.len()
&& file.checkpoints[cp_idx].frame_index as usize == frame_idx
{
let actual_crc = nes.ppu().borrow().screen_buffer().crc32();
file.checkpoints[cp_idx].screen_crc = actual_crc;
updated += 1;
on_progress(updated, total_to_update);
cp_idx += 1;
}
frame_idx += 1;
}
Ok(updated)
}
#[allow(dead_code)]
pub fn run_one_frame(nes: &mut Nes) {
while !nes.is_ready_to_render() && !nes.cpu_ref().is_halted() {
nes.run_cpu_tick();
while nes.sample_ready() {
nes.get_sample();
}
}
nes.clear_ready_to_render();
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nes::console::{Config, Nes, RamInitMode};
use crate::platform::app_context::AppContext;
use crate::platform::autorun::{AUTORUN_VERSION, AutorunCheckpoint, AutorunFile, AutorunFrame};
use crate::platform::config::FrontendConfig;
fn minimal_nrom_rom() -> Vec<u8> {
let mut rom = Vec::with_capacity(16 + 16 * 1024);
rom.extend_from_slice(b"NES\x1A");
rom.push(1); rom.push(0); rom.push(0x00); rom.push(0x00); rom.extend_from_slice(&[0u8; 8]); let mut prg = vec![0xEAu8; 16 * 1024]; prg[0x3FFC] = 0x00;
prg[0x3FFD] = 0xC0;
prg[0x0000] = 0x4C;
prg[0x0001] = 0x00;
prg[0x0002] = 0xC0;
rom.extend_from_slice(&prg);
rom
}
fn make_nes() -> Nes {
let config = Config {
frontend: FrontendConfig {
ram_init_mode: RamInitMode::Zero,
..Default::default()
},
..Default::default()
};
Nes::new(AppContext::new_with_config(config))
}
fn make_nes_with_cart(rom: &[u8]) -> Nes {
let mut nes = make_nes();
let cart = crate::nes::cartridge::Cartridge::load_from_file(rom, "test.nes", None)
.expect("load cart");
nes.insert_cartridge(cart);
nes.reset(false);
nes
}
fn run_nes_frames(nes: &mut Nes, n: u32) {
for _ in 0..n {
run_one_frame(nes);
}
}
#[test]
fn test_headless_playback_empty_recording_succeeds_with_no_checkpoints() {
let rom = minimal_nrom_rom();
let mut nes = make_nes_with_cart(&rom);
let file = AutorunFile {
version: AUTORUN_VERSION,
frames: vec![],
checkpoints: vec![],
};
let result = run_headless_playback(&mut nes, &file, None).expect("playback ok");
assert_eq!(result.total_checkpoints_verified, 0);
assert!(result.is_ok());
}
#[test]
fn test_headless_playback_matching_crc_reports_no_mismatches() {
let rom = minimal_nrom_rom();
let mut nes = make_nes_with_cart(&rom);
run_nes_frames(&mut nes, 1);
let state_after_1 = nes.save_state();
let crc_after_1 = nes.ppu().borrow().screen_buffer().crc32();
let state_bytes = state_after_1.to_bytes().expect("serialize state");
let file = AutorunFile {
version: AUTORUN_VERSION,
frames: vec![AutorunFrame {
player1: 0,
player2: 0,
}],
checkpoints: vec![AutorunCheckpoint {
frame_index: 0,
screen_crc: crc_after_1,
state_bytes,
}],
};
nes.reset(false);
let result = run_headless_playback(&mut nes, &file, None).expect("playback ok");
assert_eq!(result.total_checkpoints_verified, 1);
assert_eq!(result.crc_mismatches, 0);
assert!(result.is_ok());
}
#[test]
fn test_headless_playback_wrong_crc_reports_mismatch() {
let rom = minimal_nrom_rom();
let mut nes = make_nes_with_cart(&rom);
let state_bytes = nes.save_state().to_bytes().expect("serialize state");
let file = AutorunFile {
version: AUTORUN_VERSION,
frames: vec![AutorunFrame {
player1: 0,
player2: 0,
}],
checkpoints: vec![AutorunCheckpoint {
frame_index: 0,
screen_crc: 0xDEADBEEF, state_bytes,
}],
};
let result = run_headless_playback(&mut nes, &file, None).expect("playback ok");
assert_eq!(result.total_checkpoints_verified, 1);
assert_eq!(result.crc_mismatches, 1);
assert!(!result.is_ok());
}
#[test]
fn test_headless_playback_from_checkpoint_restores_state() {
let rom = minimal_nrom_rom();
let mut nes = make_nes_with_cart(&rom);
run_nes_frames(&mut nes, 300);
let state_at_300 = nes.save_state();
let state_bytes_300 = state_at_300.to_bytes().expect("serialize");
run_nes_frames(&mut nes, 1);
let crc_at_301 = nes.ppu().borrow().screen_buffer().crc32();
let file = AutorunFile {
version: AUTORUN_VERSION,
frames: vec![
AutorunFrame {
player1: 0,
player2: 0
};
600
],
checkpoints: vec![
AutorunCheckpoint {
frame_index: 299,
screen_crc: 0x0000, state_bytes: state_bytes_300.clone(),
},
AutorunCheckpoint {
frame_index: 300,
screen_crc: crc_at_301,
state_bytes: vec![], },
],
};
let mut nes2 = make_nes_with_cart(&rom);
let result = run_headless_playback(&mut nes2, &file, Some(0)).expect("playback ok");
assert_eq!(result.total_checkpoints_verified, 1);
assert_eq!(result.crc_mismatches, 0);
}
#[test]
fn test_recalculate_checkpoint_crcs_updates_mismatching_checkpoint_crc() {
let rom = minimal_nrom_rom();
let mut nes = make_nes_with_cart(&rom);
run_nes_frames(&mut nes, 1);
let state_after_1 = nes.save_state();
let state_bytes = state_after_1.to_bytes().expect("serialize state");
let mut file = AutorunFile {
version: AUTORUN_VERSION,
frames: vec![AutorunFrame {
player1: 0,
player2: 0,
}],
checkpoints: vec![AutorunCheckpoint {
frame_index: 0,
screen_crc: 0xDEADBEEF,
state_bytes,
}],
};
nes.reset(false);
let updated =
recalculate_checkpoint_crcs(&mut nes, &mut file, None).expect("recalculation succeeds");
assert_eq!(updated, 1);
assert_ne!(file.checkpoints[0].screen_crc, 0xDEADBEEF);
}
#[test]
fn test_recalculate_checkpoint_crcs_reports_progress_for_each_checkpoint() {
let rom = minimal_nrom_rom();
let mut nes = make_nes_with_cart(&rom);
run_nes_frames(&mut nes, 1);
let state_after_1 = nes.save_state();
let state_bytes = state_after_1.to_bytes().expect("serialize state");
let mut file = AutorunFile {
version: AUTORUN_VERSION,
frames: vec![
AutorunFrame {
player1: 0,
player2: 0,
},
AutorunFrame {
player1: 0,
player2: 0,
},
],
checkpoints: vec![
AutorunCheckpoint {
frame_index: 0,
screen_crc: 0,
state_bytes: state_bytes.clone(),
},
AutorunCheckpoint {
frame_index: 1,
screen_crc: 0,
state_bytes,
},
],
};
nes.reset(false);
let mut progress = Vec::new();
let updated =
recalculate_checkpoint_crcs_with_progress(&mut nes, &mut file, None, |done, total| {
progress.push((done, total))
})
.expect("recalculation succeeds");
assert_eq!(updated, 2);
assert_eq!(progress, vec![(1, 2), (2, 2)]);
}
}