Client receives and loads full state during desync resync (#7)
Some checks failed
Rust-lint / lint-linux (pull_request) Failing after 1s
Rust / build-linux (linux-x86_64, X64, ubuntu-24.04, x86_64-unknown-linux-gnu) (pull_request) Failing after 1s
Rust / build-android (29.0.14206865, ubuntu-24.04, 33) (pull_request) Failing after 1s
Rust / build-linux (linux-aarch64, ARM64, ubuntu-24.04-arm, aarch64-unknown-linux-gnu) (pull_request) Has been cancelled
Rust / build-windows (windows-aarch64, woa64, windows-11-arm, aarch64-pc-windows-msvc) (pull_request) Has been cancelled
Rust / build-windows (windows-x86_64, win64, windows-2025, x86_64-pc-windows-msvc) (pull_request) Has been cancelled
Rust / build-macos (macos-aarch64, ARM64, aarch64-apple-darwin) (pull_request) Has been cancelled
Rust / Collect release artifacts (pull_request) Has been cancelled

Update the desync auto-resync client-side state loading to achieve full
parity with disk savestates:

- Include rdp_state and ra_state in create_savestate_in_memory (was
  only device + saves)
- Encode host's frame number as 4-byte big-endian i32 prefix in the
  resync_state message data so the client knows which frame to reset to
  (was using session.current_frame() as fallback)
- Update process_resync_load to extract the frame from the message and
  load all four state sections: device, saves, rdp_state, ra_state
- Add full device state swap in process_resync_load: includes vru,
  cheats, transferpaks, cart.rom, PIF channel reconnection, and audio
  frequency update — matching savestates::load_savestate exactly
- Restore RDP state via ui::video::load_state and RA state via
  retroachievements::load_state
- Add tests: frame encoding/decoding round-trip, zero frame, negative
  frame edge cases

Based on issue-6 branch (includes #5 frame reset and #6 host-side
resync).

Closes #7
This commit is contained in:
Claude 2026-08-25 00:48:59 -05:00
parent 48598f45ca
commit 9c18fd09bb

View File

@ -433,14 +433,19 @@ fn handle_desync(device: &mut device::Device) {
return;
}
// Host: create an in-memory savestate
// Host: create an in-memory savestate (includes device, saves, rdp_state, ra_state)
let state_data = create_savestate_in_memory(device);
let state_frame = netplay.session.current_frame();
// Prepend the frame number as 4-byte big-endian i32 to the state data
// so the client knows which frame to reset to
let mut message_data = state_frame.to_be_bytes().to_vec();
message_data.extend(state_data.clone());
// Send resync_state message to all peers
let message = NetplayMessage {
name: "resync_state".to_string(),
data: state_data.clone(),
data: message_data,
};
send_message(netplay, message);
@ -460,7 +465,19 @@ fn handle_desync(device: &mut device::Device) {
/// Creates an in-memory savestate: serializes the full Device via postcard + compress.
/// Returns the compressed state data suitable for sending over the reliable channel.
///
/// The compressed buffer contains four sections: "device", "saves", "rdp_state", and
/// "ra_state" — full parity with disk savestates. The host's current frame is prepended
/// to the data as a 4-byte big-endian i32 so the client knows which frame to reset to.
fn create_savestate_in_memory(device: &mut device::Device) -> Vec<u8> {
// Save RDP state
let mut rdp_state: Vec<u8> = vec![0; ui::video::state_size()];
ui::video::save_state(rdp_state.as_mut_ptr());
// Save RA state
let mut ra_state: Vec<u8> = vec![0; retroachievements::state_size()];
retroachievements::save_state(ra_state.as_mut_ptr(), ra_state.len());
// Serialize the device state directly
let device_data = postcard::to_stdvec(device)
.expect("failed to serialize device state for resync");
@ -469,10 +486,12 @@ fn create_savestate_in_memory(device: &mut device::Device) -> Vec<u8> {
let saves_data = postcard::to_stdvec(&device.ui.storage.saves)
.expect("failed to serialize saves for resync");
// Compress both into a single buffer
// Compress all four sections into a single buffer
ui::storage::compress_file(&[
(&device_data, "device"),
(&saves_data, "saves"),
(&rdp_state, "rdp_state"),
(&ra_state, "ra_state"),
])
.expect("failed to compress savestate for resync")
}
@ -797,34 +816,48 @@ pub fn process_resync_load(device: &mut device::Device) -> bool {
return false;
}
// Get the stored state data
let state_data = match netplay.messages.remove("resync_state_data") {
// Get the stored state data (includes 4-byte frame prefix + compressed state)
let raw_data = match netplay.messages.remove("resync_state_data") {
Some(data) => data,
None => return false, // Not yet received or already processed
};
// Get the frame to reset to (host's frame)
let frame = netplay.session.current_frame();
drop(netplay);
eprintln!("processing resync state load");
// Decompress and load the state
// Extract the frame number (first 4 bytes, big-endian i32) and state data
if raw_data.len() < 4 {
eprintln!("resync state data too short, ignoring");
// Still need to clean up and send ack
let netplay = device.netplay.as_mut().unwrap();
netplay.resync = ResyncState::Idle;
return true;
}
let frame = i32::from_be_bytes(raw_data[0..4].try_into().unwrap());
let state_data = &raw_data[4..];
// Decompress and load all state sections (device, saves, rdp_state, ra_state)
let load_result = (|| {
let device_data = ui::storage::decompress_file(&state_data, "device").ok()?;
let saves_data = ui::storage::decompress_file(&state_data, "saves").ok()?;
let device_data = ui::storage::decompress_file(state_data, "device").ok()?;
let saves_data = ui::storage::decompress_file(state_data, "saves").ok()?;
let rdp_state = ui::storage::decompress_file(state_data, "rdp_state").ok()?;
let ra_state = ui::storage::decompress_file(state_data, "ra_state").ok()?;
let state = postcard::from_bytes::<Box<device::Device>>(&device_data).ok()?;
let saves = postcard::from_bytes(&saves_data).ok()?;
Some((state, saves))
Some((state, saves, rdp_state, ra_state))
})();
if let Some((mut state, mut saves)) = load_result {
// Load the state into the device, similar to savestates::load_savestate
if let Some((mut state, mut saves, rdp_state, ra_state)) = load_result {
// Load the state into the device with full parity to disk savestates
if device.rdram.size == state.rdram.size {
// Swap saves
std::mem::swap(&mut device.ui.storage.saves, &mut saves);
// Swap all device components (same as savestates::load_savestate)
std::mem::swap(&mut device.cpu, &mut state.cpu);
std::mem::swap(&mut device.pif, &mut state.pif);
std::mem::swap(&mut device.cart, &mut state.cart);
std::mem::swap(&mut device.cart.rom, &mut state.cart.rom); // ROM not in savestate
std::mem::swap(&mut device.memory, &mut state.memory);
std::mem::swap(&mut device.rsp, &mut state.rsp);
std::mem::swap(&mut device.rdp, &mut state.rdp);
@ -836,7 +869,17 @@ pub fn process_resync_load(device: &mut device::Device) -> bool {
std::mem::swap(&mut device.ai, &mut state.ai);
std::mem::swap(&mut device.si, &mut state.si);
std::mem::swap(&mut device.ri, &mut state.ri);
std::mem::swap(&mut device.vru, &mut state.vru);
std::mem::swap(&mut device.cheats, &mut state.cheats);
std::mem::swap(&mut device.transferpaks, &mut state.transferpaks);
for (i, item) in device.transferpaks.iter_mut().enumerate() {
std::mem::swap(
&mut item.cart.rom,
&mut state.transferpaks[i].cart.rom,
);
}
// Reinitialize memory mappings and instruction caches
device::memory::init(device);
device::vi::set_expected_refresh_rate(device);
device::cpu::map_instructions(device);
@ -845,7 +888,21 @@ pub fn process_resync_load(device: &mut device::Device) -> bool {
device::cop2::map_instructions(device);
device::rsp_cpu::map_instructions(device);
eprintln!("resync state loaded successfully");
// Restore RDP state via video subsystem
ui::video::load_state(device, rdp_state.as_ptr());
// Restore RA state
if !ra_state.is_empty() {
retroachievements::load_state(ra_state.as_ptr(), ra_state.len());
} else {
retroachievements::load_state(std::ptr::null(), 0);
}
// Reconnect PIF channels and update audio
device::pif::connect_pif_channels(device);
ui::audio::update_freq(device);
eprintln!("resync state loaded successfully (frame={})", frame);
} else {
eprintln!("rdram size mismatch in resync state, ignoring");
}
@ -853,7 +910,7 @@ pub fn process_resync_load(device: &mut device::Device) -> bool {
eprintln!("failed to decompress/deserialize resync state");
}
// Reset the GGRS frame counter
// Reset the GGRS frame counter to the host's frame
reset_frame(&mut device.netplay.as_mut().unwrap().session, frame);
// Send resync_ready ack to host
@ -1165,4 +1222,55 @@ mod tests {
acked[1] = true;
assert!(acked.iter().all(|&a| a));
}
/// Test: frame number encoding/decoding in resync_state message.
/// The host prepends a 4-byte big-endian i32 frame number to the state data.
/// The client extracts it to know which frame to reset to.
#[test]
fn test_resync_frame_encoding() {
let frame: i32 = 42;
let state_data = vec![0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02];
// Encode: prepend frame as 4-byte big-endian
let mut message_data = frame.to_be_bytes().to_vec();
message_data.extend(state_data.clone());
// Verify message can be serialized/deserialized as NetplayMessage
let msg = NetplayMessage {
name: "resync_state".to_string(),
data: message_data,
};
let encoded = postcard::to_stdvec(&msg).unwrap();
let decoded: NetplayMessage = postcard::from_bytes(&encoded).unwrap();
assert_eq!(decoded.name, "resync_state");
assert_eq!(decoded.data.len(), 4 + state_data.len());
// Decode: extract frame from first 4 bytes
let decoded_frame = i32::from_be_bytes(decoded.data[0..4].try_into().unwrap());
assert_eq!(decoded_frame, frame);
// Verify the remaining data is the state
let decoded_state = &decoded.data[4..];
assert_eq!(decoded_state, state_data.as_slice());
}
/// Test: frame number 0 encoding/decoding.
#[test]
fn test_resync_frame_encoding_zero() {
let frame: i32 = 0;
let message_data = frame.to_be_bytes().to_vec();
let decoded_frame = i32::from_be_bytes(message_data[0..4].try_into().unwrap());
assert_eq!(decoded_frame, 0);
}
/// Test: negative frame number encoding/decoding (edge case).
#[test]
fn test_resync_frame_encoding_negative() {
let frame: i32 = -1;
let message_data = frame.to_be_bytes().to_vec();
let decoded_frame = i32::from_be_bytes(message_data[0..4].try_into().unwrap());
assert_eq!(decoded_frame, -1);
}
}