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
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:
parent
48598f45ca
commit
9c18fd09bb
140
src/netplay.rs
140
src/netplay.rs
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user