diff --git a/parallel-rdp/interface.cpp b/parallel-rdp/interface.cpp index 7395fcca..95fa7936 100644 --- a/parallel-rdp/interface.cpp +++ b/parallel-rdp/interface.cpp @@ -773,7 +773,8 @@ void rdp_check_framebuffers(uint32_t address, uint32_t length) { } size_t rdp_state_size() { - return sizeof(RDP_DEVICE) + 0x1000 + processor->get_hidden_rdram_size(); + return sizeof(RDP_DEVICE) + 0x1000 + processor->get_hidden_rdram_size() + + sizeof(RDP::CommandProcessor::RenderStateSnapshot); } void rdp_save_state(uint8_t *state) { @@ -787,6 +788,12 @@ void rdp_save_state(uint8_t *state) { memcpy(state + sizeof(RDP_DEVICE) + 0x1000, g_hidden_rdram, processor->get_hidden_rdram_size()); } + + RDP::CommandProcessor::RenderStateSnapshot snapshot = {}; + processor->save_render_state(snapshot); + memcpy(state + sizeof(RDP_DEVICE) + 0x1000 + + processor->get_hidden_rdram_size(), + &snapshot, sizeof(snapshot)); } void rdp_load_state(GFX_INFO _gfx_info, const uint8_t *state, @@ -805,6 +812,13 @@ void rdp_load_state(GFX_INFO _gfx_info, const uint8_t *state, memcpy(g_hidden_rdram, state + sizeof(RDP_DEVICE) + 0x1000, processor->get_hidden_rdram_size()); } + + RDP::CommandProcessor::RenderStateSnapshot snapshot = {}; + memcpy(&snapshot, + state + sizeof(RDP_DEVICE) + 0x1000 + + processor->get_hidden_rdram_size(), + sizeof(snapshot)); + processor->load_render_state(snapshot); } static void push_onscreen_message(void *data) { diff --git a/parallel-rdp/parallel-rdp-standalone b/parallel-rdp/parallel-rdp-standalone index 5d0ab119..2b13717d 160000 --- a/parallel-rdp/parallel-rdp-standalone +++ b/parallel-rdp/parallel-rdp-standalone @@ -1 +1 @@ -Subproject commit 5d0ab119bdd69e745396e8659a0f141f622bcb92 +Subproject commit 2b13717d9ea59e4abe46878ea0aba0d098ae0ec4 diff --git a/src/device.rs b/src/device.rs index 9b6a4511..a09fa876 100644 --- a/src/device.rs +++ b/src/device.rs @@ -15,7 +15,7 @@ const N64_EXTENSIONS_UNCOMPRESSED: [&str; 8] = use rand::{Rng, SeedableRng}; -use crate::{cheats, netplay, retroachievements, savestates, ui}; +use crate::{cheats, netplay, retroachievements, ui}; use std::io::Read; pub mod ai; @@ -60,7 +60,7 @@ pub fn run_game( } if let Some(slot) = game_settings.load_savestate_slot { device.ui.storage.save_state_slot = slot; - device.savestate.load_state = true; + device.ui.savestate.load_state = true; } cart::rom::init(device, rom_contents); // cart needs to come before rdram @@ -261,7 +261,6 @@ pub struct Device { #[serde(skip)] pub ui: ui::Ui, pub byte_swap: usize, - pub savestate: savestates::Savestate, pub cpu: cpu::Cpu, pub pif: pif::Pif, pub cart: cart::Cart, @@ -304,6 +303,11 @@ impl Device { self.vru.clone_from(&device.vru); self.transferpaks.clone_from(&device.transferpaks); self.cheats.clone_from(&device.cheats); + + self.cart.rom = Vec::new(); + for transferpak in self.transferpaks.iter_mut() { + transferpak.cart.rom = Vec::new(); + } } pub fn new(with_ui: bool) -> Box { @@ -322,15 +326,6 @@ impl Device { ui::Ui::default() }, byte_swap, - savestate: savestates::Savestate { - save_state: false, - load_state: false, - load_rewind: false, - last_rewind_saved: 0.0, - rewind_pool: std::sync::Arc::new(tokio::sync::Mutex::new( - std::collections::BTreeMap::new(), - )), - }, cpu: cpu::Cpu { cop0: cop0::Cop0 { regs: [0; cop0::COP0_REGS_COUNT], diff --git a/src/device/controller/vru.rs b/src/device/controller/vru.rs index a8492e5b..8afcbaab 100644 --- a/src/device/controller/vru.rs +++ b/src/device/controller/vru.rs @@ -1,4 +1,5 @@ use crate::device; +use crate::savestates; #[cfg(all(feature = "gui", not(target_os = "android")))] use crate::ui; @@ -27,6 +28,7 @@ pub struct Vru { pub word_buffer: [u16; 40], pub words: Vec, pub talking: bool, + #[serde(serialize_with = "savestates::serialize_sorted_map")] pub word_mappings: rustc_hash::FxHashMap, } diff --git a/src/device/cop0.rs b/src/device/cop0.rs index 75e48a39..abfda992 100644 --- a/src/device/cop0.rs +++ b/src/device/cop0.rs @@ -100,7 +100,9 @@ pub struct Cop0 { pub instrs: [fn(&mut device::Device, u32); 32], #[serde(skip, default = "savestates::default_instructions")] pub instrs2: [fn(&mut device::Device, u32); 32], + #[serde(serialize_with = "savestates::serialize_sorted_map")] pub tlb_lut_r: rustc_hash::FxHashMap, + #[serde(serialize_with = "savestates::serialize_sorted_map")] pub tlb_lut_w: rustc_hash::FxHashMap, pub tlb_entries: [device::tlb::TlbEntry; 32], pub is_event: bool, diff --git a/src/savestates.rs b/src/savestates.rs index 19342da7..3976c386 100644 --- a/src/savestates.rs +++ b/src/savestates.rs @@ -56,6 +56,29 @@ where Ok(unsafe { std::mem::transmute::(bytes) }) } +/// Serialize an `FxHashMap` in sorted-key order so the output is deterministic regardless +/// of insertion history. Without this, two logically-identical maps (e.g. the TLB lookup +/// tables after a savestate round-trip) can serialize to different bytes, which would make +/// the serialized device state — and any hash of it — non-canonical. +pub fn serialize_sorted_map( + map: &rustc_hash::FxHashMap, + serializer: S, +) -> Result +where + S: serde::Serializer, + K: serde::Serialize + Ord, + V: serde::Serialize, +{ + use serde::ser::SerializeMap; + let mut entries: Vec<(&K, &V)> = map.iter().collect(); + entries.sort_by(|a, b| a.0.cmp(b.0)); + let mut map_ser = serializer.serialize_map(Some(entries.len()))?; + for (k, v) in entries { + map_ser.serialize_entry(k, v)?; + } + map_ser.end() +} + pub fn serialize_m128i_array(value: &[__m128i], serializer: S) -> Result where S: serde::Serializer, @@ -70,13 +93,12 @@ where seq.end() } -#[derive(Clone, serde::Serialize, serde::Deserialize)] +#[derive(Default)] pub struct Savestate { pub save_state: bool, pub load_state: bool, pub load_rewind: bool, pub last_rewind_saved: f64, - #[serde(skip)] pub rewind_pool: std::sync::Arc>>, } @@ -97,21 +119,21 @@ static SAVES_CLONE: std::sync::LazyLock> pub fn process_savestates(device: &mut device::Device) { let save_rewind = if device.netplay.is_none() && device.ui.config.emulation.rewind - && device.vi.elapsed_time - device.savestate.last_rewind_saved > 1.0 + && device.vi.elapsed_time - device.ui.savestate.last_rewind_saved > 1.0 { - device.savestate.last_rewind_saved = device.vi.elapsed_time; + device.ui.savestate.last_rewind_saved = device.vi.elapsed_time; true } else { false }; - if device.savestate.save_state || save_rewind { - create_savestate(device, !device.savestate.save_state, None); - device.savestate.save_state = false; - } else if device.savestate.load_state || device.savestate.load_rewind { - load_savestate(device, device.savestate.load_rewind, None); - device.savestate.load_state = false; - device.savestate.load_rewind = false; + if device.ui.savestate.save_state || save_rewind { + create_savestate(device, !device.ui.savestate.save_state, None); + device.ui.savestate.save_state = false; + } else if device.ui.savestate.load_state || device.ui.savestate.load_rewind { + load_savestate(device, device.ui.savestate.load_rewind, None); + device.ui.savestate.load_state = false; + device.ui.savestate.load_rewind = false; } } @@ -133,7 +155,7 @@ pub fn create_savestate(device: &mut device::Device, rewind: bool, rewind_frame: let save_path = device.ui.storage.paths.savestate_file_path.clone(); let save_state_slot = device.ui.storage.save_state_slot; - let rewind_pool = device.savestate.rewind_pool.clone(); + let rewind_pool = device.ui.savestate.rewind_pool.clone(); tokio::spawn(async move { let mut error = false; @@ -215,6 +237,7 @@ pub fn load_savestate(device: &mut device::Device, rewind: bool, rewind_frame: O let now = std::time::Instant::now(); loop { if let Some(state) = device + .ui .savestate .rewind_pool .blocking_lock() @@ -227,7 +250,8 @@ pub fn load_savestate(device: &mut device::Device, rewind: bool, rewind_frame: O } std::thread::sleep(std::time::Duration::from_millis(1)); } - } else if let Some((_key, state)) = device.savestate.rewind_pool.blocking_lock().pop_last() + } else if let Some((_key, state)) = + device.ui.savestate.rewind_pool.blocking_lock().pop_last() { Some(state) } else { @@ -258,7 +282,7 @@ pub fn load_savestate(device: &mut device::Device, rewind: bool, rewind_frame: O ui::video::idle(); } - device.savestate.last_rewind_saved = state.device.vi.elapsed_time; + device.ui.savestate.last_rewind_saved = state.device.vi.elapsed_time; std::mem::swap(&mut device.rng, &mut state.device.rng); diff --git a/src/ui.rs b/src/ui.rs index 35c7ea47..1e9e4f8c 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -18,6 +18,8 @@ pub mod video; #[cfg(all(feature = "gui", not(target_os = "android")))] pub mod vru; +use crate::savestates; + pub const APP_ID: &str = "io.github.gopher64.gopher64"; pub fn install_default_crypto_provider() { @@ -148,6 +150,7 @@ pub struct Ui { pub video: Video, pub usb: Usb, pub speed_limiter: SpeedLimiter, + pub savestate: savestates::Savestate, } pub fn sdl_hints() { @@ -327,6 +330,7 @@ impl Ui { }, dirs, speed_limiter: SpeedLimiter::default(), + savestate: savestates::Savestate::default(), } } } diff --git a/src/ui/storage.rs b/src/ui/storage.rs index 637e305f..b73c840e 100644 --- a/src/ui/storage.rs +++ b/src/ui/storage.rs @@ -34,6 +34,7 @@ pub struct Save { #[derive(Default, Clone, serde::Serialize, serde::Deserialize)] pub struct RomSave { + #[serde(serialize_with = "crate::savestates::serialize_sorted_map")] pub data: rustc_hash::FxHashMap, pub write_pending: bool, } diff --git a/src/ui/video.rs b/src/ui/video.rs index a793bb85..b866a529 100644 --- a/src/ui/video.rs +++ b/src/ui/video.rs @@ -224,12 +224,12 @@ pub fn check_callback(device: &mut device::Device) -> (bool, bool) { device.cpu.running = callback.emu_running; if device.netplay.is_none() { if callback.save_state { - device.savestate.save_state = true; + device.ui.savestate.save_state = true; } else if callback.load_state { - device.savestate.load_state = true; + device.ui.savestate.load_state = true; } if callback.load_rewind && device.ui.config.emulation.rewind { - device.savestate.load_rewind = true; + device.ui.savestate.load_rewind = true; } if callback.reset_game { device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] |= device::cop0::COP0_CAUSE_IP4;