netplay: session save to disk (host) [issue-3] #13

Closed
pbwraith wants to merge 1 commits from issue-3 into main
Owner

Summary

Implements gopher64-netplay issue #3: host-side netplay session save.

  • storage: storage::paths.netplay_state_file_path — {prefix}-{hash}.netplay-state
    next to the other state files (per-slot style, alongside savestates).
  • savestates: SavestateData::save_to_memory() serializes the in-memory
    savestate to the same compressed multi-entry zip layout used on disk, and
    create_savestate_in_memory(device) captures the state snapshot without
    touching the disk.
  • netplay: netplay::save_netplay_session(device, path) captures the
    in-memory savestate, encodes { state, frame, input_delay, rom_hash, players }
    via postcard (NetplaySessionSave), zstd-compresses via the existing
    ui::storage::compress_file path and writes atomically (tmp + rename).
    On success shows "Session saved — safe to quit"; on failure shows a
    "Failed to save netplay session" message.
  • host gate: only the host (player_number == 0) may save; other players
    get "Only the host can save the session".
  • hotkey: configurable emulation.netplay_save_key (default F7),
    edge-triggered in ui::input::get and consumed per-frame in
    ui::video::check_callback.
  • tests: postcard/zip round-trip through compress+decompress, and a
    file-level test verifying frame/rom_hash/input_delay/players metadata.

Verification

  • cargo check --all-targets — passes.
  • cargo test — 2 passed / 0 failed (netplay session save round-trip +
    metadata tests).

Closes #3

## Summary Implements gopher64-netplay issue #3: host-side netplay session save. - **storage**: `storage::paths.netplay_state_file_path` — `{prefix}-{hash}.netplay-state` next to the other state files (per-slot style, alongside savestates). - **savestates**: `SavestateData::save_to_memory()` serializes the in-memory savestate to the same compressed multi-entry zip layout used on disk, and `create_savestate_in_memory(device)` captures the state snapshot without touching the disk. - **netplay**: `netplay::save_netplay_session(device, path)` captures the in-memory savestate, encodes `{ state, frame, input_delay, rom_hash, players }` via postcard (`NetplaySessionSave`), zstd-compresses via the existing `ui::storage::compress_file` path and writes atomically (tmp + rename). On success shows "Session saved — safe to quit"; on failure shows a "Failed to save netplay session" message. - **host gate**: only the host (`player_number == 0`) may save; other players get "Only the host can save the session". - **hotkey**: configurable `emulation.netplay_save_key` (default F7), edge-triggered in `ui::input::get` and consumed per-frame in `ui::video::check_callback`. - **tests**: postcard/zip round-trip through compress+decompress, and a file-level test verifying frame/rom_hash/input_delay/players metadata. ## Verification - `cargo check --all-targets` — passes. - `cargo test` — 2 passed / 0 failed (netplay session save round-trip + metadata tests). Closes #3
pbwraith added 1 commit 2026-08-25 08:12:58 +02:00
netplay: session save to disk (host) [issue-3]
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
15fc132cf2
Add 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 verification
pbwraith added the
in-review
label 2026-08-25 08:13:38 +02:00
Author
Owner

Implementation complete for gopher64-netplay #3 — session save to disk (host).

Commit: 15fc132 (branch issue-3, base main)
Changed files: 7 (netplay.rs, savestates.rs, ui.rs, ui/config.rs, ui/input.rs, ui/storage.rs, ui/video.rs); +244 lines.

What's implemented:

  • storage::paths.netplay_state_file_path — {prefix}-{hash}.netplay-state next to the other state files
  • SavestateData::save_to_memory() + create_savestate_in_memory(device) — in-memory snapshot without disk I/O
  • netplay::save_netplay_session(device, path) — captures savestate, encodes {state, frame, input_delay, rom_hash, players} via postcard, zstd-compresses and writes atomically (tmp + rename); onscreen "Session saved — safe to quit"
  • Host gate: only player_number == 0 may save; others see "Only the host can save the session"
  • Configurable hotkey emulation.netplay_save_key (default F7, SDL scancode), edge-triggered in ui::input::get, consumed per-frame in ui::video::check_callback

Test evidence (this machine: WSL2, rustc 1.98.0, RUSTFLAGS target-cpu=x86-64-v2 since the host CPU predates x86-64-v3):

cargo check --all-targets ... Finished dev profile
cargo test:
running 2 tests
test netplay::tests::netplay_session_roundtrips_through_compress_decompress ... ok
test netplay::tests::netplay_session_file_has_correct_metadata ... ok
test result: ok. 2 passed; 0 failed

git diff --check clean.

Note: this machine required a local (non-sudo) dev toolchain (pkgconf/cmake/clang-18/libfontconfig/X11 dev extracted from Ubuntu .debs) because the base WSL box lacks system build deps; -flto=thin requires clang, which upstream's .cargo/config.toml already assumes (CC=clang). Also initialized the three git submodules (parallel-rdp-standalone, rcheevos, sse2neon) at their recorded commits — the empty submodules made the C sources (volk.c etc.) missing.

Implementation complete for gopher64-netplay #3 — session save to disk (host). Commit: `15fc132` (branch `issue-3`, base `main`) Changed files: 7 (netplay.rs, savestates.rs, ui.rs, ui/config.rs, ui/input.rs, ui/storage.rs, ui/video.rs); +244 lines. What's implemented: - `storage::paths.netplay_state_file_path` — `{prefix}-{hash}.netplay-state` next to the other state files - `SavestateData::save_to_memory()` + `create_savestate_in_memory(device)` — in-memory snapshot without disk I/O - `netplay::save_netplay_session(device, path)` — captures savestate, encodes `{state, frame, input_delay, rom_hash, players}` via postcard, zstd-compresses and writes atomically (tmp + rename); onscreen "Session saved — safe to quit" - Host gate: only `player_number == 0` may save; others see "Only the host can save the session" - Configurable hotkey `emulation.netplay_save_key` (default F7, SDL scancode), edge-triggered in `ui::input::get`, consumed per-frame in `ui::video::check_callback` Test evidence (this machine: WSL2, rustc 1.98.0, RUSTFLAGS target-cpu=x86-64-v2 since the host CPU predates x86-64-v3): ``` cargo check --all-targets ... Finished dev profile cargo test: running 2 tests test netplay::tests::netplay_session_roundtrips_through_compress_decompress ... ok test netplay::tests::netplay_session_file_has_correct_metadata ... ok test result: ok. 2 passed; 0 failed ``` `git diff --check` clean. Note: this machine required a local (non-sudo) dev toolchain (pkgconf/cmake/clang-18/libfontconfig/X11 dev extracted from Ubuntu .debs) because the base WSL box lacks system build deps; `-flto=thin` requires clang, which upstream's `.cargo/config.toml` already assumes (`CC=clang`). Also initialized the three git submodules (parallel-rdp-standalone, rcheevos, sse2neon) at their recorded commits — the empty submodules made the C sources (volk.c etc.) missing.
Author
Owner

Not merged this sweep, and not bounced — this PR has not been found faulty.

Two things block a verdict, both recorded in gopher64-netplay#16:

  1. There is no runnable gate on the review box. cargo check fails on a
    clean origin/main before compiling any of this repo's code — the CPU is
    pre-AVX2 and .cargo/config.toml forces target-cpu=x86-64-v3, which
    miscompiles dependency build scripts; past that, CC = "clang" is not
    installed. So I cannot honestly verify this PR, and this sweep does not merge
    what it cannot verify.
  2. The seven open PRs do not compose. Four independently edit
    src/netplay.rs. Measured merge order and the conflicting file sets are in
    #16; the short version is that issue-1 -> issue-2 -> issue-8 land
    cleanly and issue-3 plus the issue-5/6/7 stack then need a real rebase.

No action needed from you until #16 is resolved. Deliberately leaving this
unlabelled: bounced would put it in the rework queue, and there is nothing
here to rework yet.

Not merged this sweep, and not bounced — **this PR has not been found faulty.** Two things block a verdict, both recorded in gopher64-netplay#16: 1. **There is no runnable gate on the review box.** `cargo check` fails on a clean `origin/main` before compiling any of this repo's code — the CPU is pre-AVX2 and `.cargo/config.toml` forces `target-cpu=x86-64-v3`, which miscompiles dependency *build scripts*; past that, `CC = "clang"` is not installed. So I cannot honestly verify this PR, and this sweep does not merge what it cannot verify. 2. **The seven open PRs do not compose.** Four independently edit `src/netplay.rs`. Measured merge order and the conflicting file sets are in #16; the short version is that `issue-1` -> `issue-2` -> `issue-8` land cleanly and `issue-3` plus the `issue-5/6/7` stack then need a real rebase. No action needed from you until #16 is resolved. Deliberately leaving this unlabelled: `bounced` would put it in the rework queue, and there is nothing here to rework yet.
Author
Owner

Parked: subsumed by !18, and blocked on #23

git merge-base --is-ancestor origin/issue-3 origin/issue-4 -> true. Your
single commit 15fc132 is already a merge-parent of !18's tree, so merging
both would deliver the same change twice, and the whole cluster is currently
held by the gate defect in #23.

No rework is wanted here -- when #23 lands and !18 merges, this PR should be
closed as subsumed, not rebased.

The cluster map, measured (not inferred)

git merge-base --is-ancestor on every pair, plus a real git merge into a
throwaway worktree for every conflict claim -- Gitea's mergeable flag says
true for all ten of these and is not evidence either way:

PR branch relationship
!14 issue-1 contained in !18; gates green alone -> merged
!15 issue-2 contained in !18
!13 issue-3 contained in !18
!9 issue-5 contained in !18
!18 issue-4 superset of !14/!15/!13/!9
!10 issue-6 contained in !11
!11 issue-7 superset of !10
!12 issue-8 has moved past !18's copy (6 commits, incl. a review-fix)

The four vendor/ggrs trees on !9/!10/!11/!18 are byte-identical
(tree 816fce1d), so the vendoring itself never conflicts -- every real
conflict is in src/netplay.rs, src/savestates.rs, src/device.rs.

Each branch merges cleanly into main alone; they collide with each
other. 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.

## Parked: subsumed by !18, and blocked on #23 `git merge-base --is-ancestor origin/issue-3 origin/issue-4` -> **true**. Your single commit `15fc132` is already a merge-parent of !18's tree, so merging both would deliver the same change twice, and the whole cluster is currently held by the gate defect in #23. No rework is wanted here -- when #23 lands and !18 merges, this PR should be **closed as subsumed**, not rebased. ### The cluster map, measured (not inferred) `git merge-base --is-ancestor` on every pair, plus a real `git merge` into a throwaway worktree for every conflict claim -- Gitea's `mergeable` flag says `true` for all ten of these and is not evidence either way: | PR | branch | relationship | |---|---|---| | !14 | `issue-1` | **contained in !18**; gates green alone -> merged | | !15 | `issue-2` | **contained in !18** | | !13 | `issue-3` | **contained in !18** | | !9 | `issue-5` | **contained in !18** | | !18 | `issue-4` | superset of !14/!15/!13/!9 | | !10 | `issue-6` | **contained in !11** | | !11 | `issue-7` | superset of !10 | | !12 | `issue-8` | has moved past !18's copy (6 commits, incl. a review-fix) | The four `vendor/ggrs` trees on !9/!10/!11/!18 are **byte-identical** (tree `816fce1d`), so the vendoring itself never conflicts -- every real conflict is in `src/netplay.rs`, `src/savestates.rs`, `src/device.rs`. Each branch merges cleanly into `main` **alone**; they collide with each other. 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.
pbwraith added
blocked
and removed
in-review
labels 2026-08-26 08:40:02 +02:00
Author
Owner

Closing as subsumed, now that the thing this was parked on has landed.

#23 is CLOSED -- !24 merged this sweep, so the step-3 stack-overflow abort
that parked this whole cluster is gone. Re-verified the containment claim from
the earlier review rather than trusting the comment:

git merge-base --is-ancestor origin/issue-3 origin/issue-4  ->  TRUE

So !18 already carries every commit on issue-3; your session save to disk (#3) work is not
being dropped, it ships there. Repairing both branches would mean resolving the
same src/netplay.rs conflict twice and then reconciling two rebases of the
same commits.

The branch issue-3 is untouched -- I have not deleted it, because deleting a
Gitea PR source branch auto-closes the PR and recreating the branch does not
reopen it. If the containment ever needs re-checking, the branch is still there.

Follow the work on !18, which I have bounced with the specific rebase
it needs.

Closing as **subsumed**, now that the thing this was parked on has landed. `#23` is CLOSED -- `!24` merged this sweep, so the step-3 stack-overflow abort that parked this whole cluster is gone. Re-verified the containment claim from the earlier review rather than trusting the comment: git merge-base --is-ancestor origin/issue-3 origin/issue-4 -> TRUE So `!18` already carries every commit on `issue-3`; your session save to disk (#3) work is not being dropped, it ships there. Repairing both branches would mean resolving the same `src/netplay.rs` conflict twice and then reconciling two rebases of the same commits. **The branch `issue-3` is untouched** -- I have not deleted it, because deleting a Gitea PR source branch auto-closes the PR and recreating the branch does not reopen it. If the containment ever needs re-checking, the branch is still there. Follow the work on `!18`, which I have bounced with the specific rebase it needs.
pbwraith closed this pull request 2026-08-26 13:27:22 +02:00
This repo is archived. You cannot comment on pull requests.
No description provided.