mirror of
https://github.com/gopher64/gopher64.git
synced 2026-11-15 02:19:43 +01:00
Add savestate test
This commit is contained in:
1 parent
f6b9fcd3d2
commit
2ebd1dde60
7 files changed
+135
-2
No files matched your search
+14
-2
@@ -109,7 +109,11 @@ pub fn run_game(
|
||||
cpu::init(device);
|
||||
|
||||
ui::storage::init(&mut device.ui, &device.cart.rom);
|
||||
ui::storage::load_saves(&mut device.ui, &mut device.netplay, device.cart.cic_seed);
|
||||
|
||||
if device.ui.state_test.is_none() {
|
||||
ui::storage::load_saves(&mut device.ui, &mut device.netplay, device.cart.cic_seed);
|
||||
}
|
||||
|
||||
ui::storage::format_saves(device);
|
||||
|
||||
if !retroachievements::get_hardcore() {
|
||||
@@ -120,7 +124,9 @@ pub fn run_game(
|
||||
|
||||
retroachievements::unload_game(discord_watch_tx, discord_handle);
|
||||
|
||||
ui::storage::write_saves(device);
|
||||
if device.ui.state_test.is_none() {
|
||||
ui::storage::write_saves(device);
|
||||
}
|
||||
|
||||
if let Some(netplay) = &mut device.netplay {
|
||||
netplay::close(netplay);
|
||||
@@ -144,6 +150,9 @@ fn init_rng_rtc(device: &mut Device) {
|
||||
rng_seed = netplay::receive_rng(netplay);
|
||||
}
|
||||
}
|
||||
if device.ui.state_test.is_some() {
|
||||
rng_seed = ui::state_test::RNG_SEED;
|
||||
}
|
||||
device.rng = rand::rngs::Xoshiro256PlusPlus::seed_from_u64(rng_seed);
|
||||
|
||||
device.cart.rtc_timestamp = std::time::SystemTime::now()
|
||||
@@ -157,6 +166,9 @@ fn init_rng_rtc(device: &mut Device) {
|
||||
device.cart.rtc_timestamp = netplay::receive_rtc(netplay);
|
||||
}
|
||||
}
|
||||
if device.ui.state_test.is_some() {
|
||||
device.cart.rtc_timestamp = ui::state_test::RTC_TIMESTAMP;
|
||||
}
|
||||
}
|
||||
|
||||
fn swap_rom(contents: Vec<u8>) -> Option<Vec<u8>> {
|
||||
|
||||
@@ -58,6 +58,11 @@ pub fn process(device: &mut device::Device, channel: usize) {
|
||||
let offset = device.pif.channels[channel].rx_buf.unwrap();
|
||||
let input = if let Some(netplay) = &mut device.netplay {
|
||||
netplay.inputs[channel].0
|
||||
} else if device.ui.state_test.is_some() {
|
||||
ui::input::InputData {
|
||||
data: 0,
|
||||
pak_change_pressed: false,
|
||||
}
|
||||
} else {
|
||||
ui::input::get(&mut device.ui, channel)
|
||||
};
|
||||
|
||||
+17
@@ -97,6 +97,13 @@ pub struct Args {
|
||||
help = "Password for RetroAchievements"
|
||||
)]
|
||||
pub ra_password: Option<String>,
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "FRAMES",
|
||||
value_parser = clap::value_parser!(u64).range(1..),
|
||||
help = "Savestate test: run FRAMES, save state, run FRAMES and hash, load state, run FRAMES and hash again, then compare the hashes"
|
||||
)]
|
||||
pub savestate_test: Option<u64>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "gui")]
|
||||
@@ -161,8 +168,18 @@ pub fn run(args: Args, arg_count: usize) -> std::io::Result<()> {
|
||||
return Err(Error::other("Savestate slot must be between 0 and 9"));
|
||||
}
|
||||
|
||||
if args.savestate_test.is_some() && args.netplay_server_addr.is_some() {
|
||||
return Err(Error::other("--savestate-test cannot be used with netplay"));
|
||||
}
|
||||
|
||||
let mut device = device::Device::new(true);
|
||||
|
||||
if let Some(frames) = args.savestate_test {
|
||||
device.ui.state_test = Some(ui::state_test::StateTest::new(frames));
|
||||
device.ui.speed_limiter.enabled = false;
|
||||
device.ui.audio.gain = 0.0;
|
||||
}
|
||||
|
||||
device.ui.config.ui.recent_roms.retain(|x| *x != game);
|
||||
device.ui.config.ui.recent_roms.insert(0, game);
|
||||
device.ui.config.ui.recent_roms.truncate(5);
|
||||
|
||||
@@ -122,6 +122,8 @@ impl SavestateData {
|
||||
}
|
||||
|
||||
pub fn process_savestates(device: &mut device::Device) {
|
||||
ui::state_test::frame(device);
|
||||
|
||||
let save_rewind = if device.netplay.is_none()
|
||||
&& device.ui.config.emulation.rewind
|
||||
&& device.vi.elapsed_time - device.ui.savestate.last_rewind_saved > 1.0
|
||||
|
||||
@@ -14,6 +14,7 @@ pub mod macos;
|
||||
pub mod netplay;
|
||||
#[cfg(feature = "gui")]
|
||||
pub mod retroachievements;
|
||||
pub mod state_test;
|
||||
pub mod storage;
|
||||
pub mod usb;
|
||||
pub mod video;
|
||||
@@ -153,6 +154,7 @@ pub struct Ui {
|
||||
pub usb: Usb,
|
||||
pub speed_limiter: SpeedLimiter,
|
||||
pub savestate: savestates::Savestate,
|
||||
pub state_test: Option<state_test::StateTest>,
|
||||
}
|
||||
|
||||
pub fn sdl_hints() {
|
||||
@@ -333,6 +335,7 @@ impl Ui {
|
||||
dirs,
|
||||
speed_limiter: SpeedLimiter::default(),
|
||||
savestate: savestates::Savestate::default(),
|
||||
state_test: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
use crate::{device, savestates, ui};
|
||||
use sha2::digest::Digest;
|
||||
|
||||
// fixed so that the reported hash is reproducible across runs
|
||||
pub const RNG_SEED: u64 = 0x6770_6865_7236_3400;
|
||||
pub const RTC_TIMESTAMP: i64 = 946684800; // 2000-01-01 00:00:00 UTC
|
||||
|
||||
enum Phase {
|
||||
BeforeSave,
|
||||
AfterSave,
|
||||
AfterLoad,
|
||||
}
|
||||
|
||||
/// Savestate round-trip test (--savestate-test FRAMES):
|
||||
/// run FRAMES, save state, run FRAMES and hash the device (A),
|
||||
/// load the state, run FRAMES and hash the device again (B). A and B must match.
|
||||
pub struct StateTest {
|
||||
frames: u64,
|
||||
frame_counter: u64,
|
||||
phase: Phase,
|
||||
hash: String,
|
||||
finished: bool,
|
||||
}
|
||||
|
||||
impl StateTest {
|
||||
pub fn new(frames: u64) -> Self {
|
||||
Self {
|
||||
frames,
|
||||
frame_counter: 0,
|
||||
phase: Phase::BeforeSave,
|
||||
hash: String::new(),
|
||||
finished: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finished(&self) -> bool {
|
||||
self.finished
|
||||
}
|
||||
}
|
||||
|
||||
fn hash_device(device: &device::Device) -> String {
|
||||
ui::video::idle();
|
||||
let data = postcard::to_stdvec(device).unwrap();
|
||||
sha2::Sha256::digest(&data)
|
||||
.iter()
|
||||
.map(|b| format!("{b:02x}"))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn frame(device: &mut device::Device) {
|
||||
let Some(mut test) = device.ui.state_test.take() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if !test.finished() {
|
||||
test.frame_counter += 1;
|
||||
if test.frame_counter >= test.frames {
|
||||
test.frame_counter = 0;
|
||||
advance(device, &mut test);
|
||||
}
|
||||
}
|
||||
|
||||
device.ui.state_test = Some(test);
|
||||
}
|
||||
|
||||
fn advance(device: &mut device::Device, test: &mut StateTest) {
|
||||
match test.phase {
|
||||
Phase::BeforeSave => {
|
||||
savestates::create_savestate(device, false, None);
|
||||
test.phase = Phase::AfterSave;
|
||||
}
|
||||
Phase::AfterSave => {
|
||||
test.hash = hash_device(device);
|
||||
savestates::load_savestate(device, false, None);
|
||||
test.phase = Phase::AfterLoad;
|
||||
}
|
||||
Phase::AfterLoad => {
|
||||
let hash = hash_device(device);
|
||||
if hash == test.hash {
|
||||
println!("Savestate test passed: {hash}");
|
||||
} else {
|
||||
eprintln!(
|
||||
"Savestate test failed: hash before load {} does not match hash after load {hash}",
|
||||
test.hash
|
||||
);
|
||||
}
|
||||
test.finished = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -226,6 +226,10 @@ pub fn check_callback(device: &mut device::Device) -> (bool, bool) {
|
||||
let mut speed_limiter_toggled = false;
|
||||
let callback = unsafe { rdp_check_callback() };
|
||||
device.cpu.running = callback.emu_running;
|
||||
if let Some(test) = &device.ui.state_test {
|
||||
device.cpu.running &= !test.finished();
|
||||
return (false, false);
|
||||
}
|
||||
if device.netplay.is_none() {
|
||||
if callback.save_state {
|
||||
device.ui.savestate.save_state = true;
|
||||
|
||||
Reference in new issue
Block a user