netplay: session resume — reload saved state and broadcast to peers (#4) #18
No reviewers
Labels
No Label
blocked
bounced
in-progress
in-review
manual-only
needs-guidance
netplay
p1
p2
p3
rust
No Milestone
No project
No Assignees
1 Participants
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: pbwraith/gopher64-netplay#18
Loading…
Reference in New Issue
Block a user
No description provided.
Delete Branch "issue-4"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #4 — Session resume: reload saved state and broadcast to peers (host + join)
Implements the full resume flow on the netplay state-transfer stack. This
branch stacks the dependency PRs per the repo convention (merge order:
#14 issue-1 → #15 issue-2 → #13 issue-3 → #12 issue-8 → #9 issue-5 → this PR):
netplay_state/netplay_state_ready+ chunked codecsave_to_memory/load_savestate_from_data.netplay-stateenvelope (encode/decode_netplay_session)netplay::reset_frame)Host path:
.netplay-statefile picker (
select_netplay_state, mirrorsselect_rom). The pickedpath marks the session as a resume, advertised to joining peers through
the session
resumefeature.run_game, before the frame loop),netplay::resume_sessionruns:loads the embedded savestate into its own device, resets the GGRS
frame counter to the saved frame, broadcasts the file bytes verbatim
via
send_netplay_state, then waits for every peer'snetplay_state_readyack (30 s timeout — on timeout the session isdisconnected rather than started desynced);
netplay_state, decode the same envelope, load thesavestate from memory, reset the GGRS frame counter, and ack with
netplay_state_ready.resume_frame= saved frame, so the frame loopresumes at the SAVED frame, not 0.
resumes".
Fixes carried by this stack (found while verifying this branch):
wait_for_ready_allcounts acks per decoded batch instead of removingthem from the by-name
messagesmap: several peers can ack within asingle channel poll and the map would collapse them into one entry,
causing a spurious 30 s timeout in 3+-player sessions.
savestates.rs: add the missingcrate::netplayimport for thereset_framehook merged from #5 (that branch's tree does not compilestandalone).
Tests (
cargo test --lib, full lib suite, all green):resume_state_reaches_peer_with_same_frame_metadata— mock 2-peerflow: host's in-memory savestate travels through the chunked
encode/decode envelope, the peer loads it, both sides agree on frame
metadata (frame 12_345) and the peer's GGRS counter resumes at the
saved frame.
resume_host_loads_state_from_disk_and_resets_frame— host path witha real on-disk
.netplay-statefile; device state and GGRS framecounter match the saved session.
ack_batch_counts_every_ack— two acks arriving in one poll are bothcounted.
wait_for_ready_all_counts_acks_and_times_out— missing acks time outinstead of hanging.
resume_session_skips_non_resume_sessions— regular netplay isuntouched.
Limitations (documented honestly):
real clients per lane policy); the mocked 2-peer flow covers the
protocol + state-load mechanics.
.cargo/config.toml,build.rsx86-64flags, pkg-config path) are intentionally NOT part of this PR.
git diff --checkclean on the issue-#4 commits (one pre-existingtrailing-whitespace line in vendored
vendor/ggrsfrom #5).After a netplay savestate load, the GGRS session's internal frame counter must be reset to match the savestate's frame, or desync detection fires immediately on resume. Changes: - Patch vendored ggrs 0.13.0 to expose set_current_frame(frame) on P2PSession and SyncTestSession, with internal SyncLayer and InputQueue reset methods to clear stale saved states, checksum history, and input queues - Add [patch.crates-io] ggrs = { path = 'vendor/ggrs' } in Cargo.toml - Add netplay::reset_frame(session, frame) wrapper function - Record current GGRS frame in Savestate.netplay_frame during create_savestate when in netplay context - Call reset_frame after load_savestate in netplay context - Add tests: advance 120 frames, reset to 60, verify current_frame()==60 and clean resume with matching inputs; reset to 0; idempotent reset Closes #5Add host-side netplay session save: - storage::paths.netplay_state_file_path next to other state files - savestates::create_savestate_in_memory + SavestateData::save_to_memory (in-memory capture, same zip layout as on-disk savestates) - netplay::save_netplay_session(device, path): postcard-encodes {state, frame, input_delay, rom_hash, players}, zstd-compresses and writes atomically (tmp + rename); onscreen 'Session saved — safe to quit' - only the host (player_number 0) may save; others get an onscreen notice - configurable hotkey (emulation.netplay_save_key, default F7), edge triggered in ui::input::get and consumed per-frame in check_callback - unit tests: encode/decode round-trip + file metadata verificationParked on #23: gate step 3 aborts, and it is not your bug
Gating
main+!21+!22+!18:Eleven of your tests pass first; the twelfth builds two
Devices(
src/netplay.rs:1353and:1382) and exceeds the 2 MiB libtest threadstack. At
RUST_MIN_STACK=8388608that exact test passes unchanged, so thisis a stack-budget problem, not a logic one. Full numbers in #23.
Because a stack overflow SIGABRTs the whole binary, this also hides whatever
would have run after it -- so a green step 3 here is not available until #23
lands, and I could not assess the rest of your test suite.
Blocked on #23, which I filed with the full measurement. Not
bounced: there is nothing for this branch to repair -- the fix belongs inscripts/gate.sh/ a test helper, and doing it inside this PR would collide with #23's branch.You are the one to land after #23. !18 already contains !14/!15/!13/!9 as
merge commits, so it delivers #1-#5 in one coherent tree; the others should be
closed as subsumed rather than merged. !14 is already on
main, which yourbranch contains, so that merge will be trivial.
The cluster map, measured (not inferred)
git merge-base --is-ancestoron every pair, plus a realgit mergeinto athrowaway worktree for every conflict claim -- Gitea's
mergeableflag saystruefor all ten of these and is not evidence either way:issue-1issue-2issue-3issue-5issue-4issue-6issue-7issue-8The four
vendor/ggrstrees on !9/!10/!11/!18 are byte-identical(tree
816fce1d), so the vendoring itself never conflicts -- every realconflict is in
src/netplay.rs,src/savestates.rs,src/device.rs.Each branch merges cleanly into
mainalone; they collide with eachother. So the order is forced regardless of this issue: one lands, the rest
rebase.
Suggested resolution once #23 lands
Land !18 (it delivers #1/#2/#3/#4/#5 in one self-consistent tree), then
close !15/!13/!9 as subsumed rather than merging them -- their commits are
already ancestors of
issue-4. Then rebase !11 (which carries !10) and !12.Unblocked, and re-bounced with the specific thing left to do.
blockedcleared,
bouncedapplied -- a PR carrying neither label is invisible to theworker lane AND suppresses its own issue as a candidate, so this is the label
that makes the rework reachable.
The block is genuinely gone
#23is CLOSED.!24merged this sweep and I verified the gate now executesDevice-constructing tests instead of aborting:
So Session resume, carrying #5 (frame reset) and #3 (session save) can finally be gated on its merits.
What is actually left: a rebase, measured not guessed
Re-measured with a real
git merge origin/mainin the warm clone (NOT themergeableflag, which is unreliable in both directions):Second thing, in the SAME pass
Your Device-constructing test bodies should route through the helper
!24added, not rely on the gate's env var:
scripts/gate.shnow exportsRUST_MIN_STACK=8388608, which immunises theGATE -- but a bare
cargo test --libstill gives each test thread the 2 MiBlibtest default, and a stack overflow SIGABRTs the whole binary rather than
failing one test. I verified
with_big_stackcarries a full Device round tripat
RUST_MIN_STACK=2097152, so wrapping the body makes the test independent ofhow it is invoked. Do both in one pass -- a rebase now and a wrapper later is
two conflict resolutions on the same lines.
Note on the target
mainis moving during this sweep (!24landed;!15is in the gate as Iwrite). Rebase onto whatever
origin/mainis when you pick this up andre-measure the conflict set -- do not work from the file list above as if it
were fixed.
Ordering note -- land !9 first, and one conflict hunk is a trap
Not a new bounce; this PR keeps its existing
bouncedfinding. Re-measured againstcurrent
main(c857fea) this sweep rather than trusting the last verdict:Adding what I learned reviewing the cluster as a whole:
1. Order.
issue-5is a strict ancestor of this branch(
git merge-base --is-ancestor origin/issue-5 origin/issue-4-> TRUE), and all threeopen PRs currently conflict on overlapping
src/files. I have bounced !9 witha full resolution recipe and it should land first; rebasing this branch afterwards
means merging over content it already contains, which is much cheaper than
resolving the same hunks here independently. Wait for !9 unless you are told
otherwise.
2. The
src/savestates.rshunk is not additive. If you hit it, do not"keep both sides": your side is the savestate-load success path,
main's sideis
show_savestate_load_failure(device, rewind);from#2's refactor of thefailure path. git aligned two unrelated regions. Keeping both splices the
netplay
reset_framecall into the failure branch. Takemain's structure andre-insert the netplay block into the success branch only. Full detail on !9.
3.
src/netplay.rsnow has a#[cfg(test)] mod testsonmain. A secondmod testsin the same file will not compile -- fold into the existing one.