Status: read-only audit of commit ff0ad952980f5083afd21c3d3758208a7a093d72, recorded 2026-08-25.
This is the durable index for the current netplay research. It distinguishes what the release actually ships from experimental rollback groundwork and from proposed work. No production source was changed as part of the audit.
The executive verdict below remains the historical result for audited commit
ff0ad952. It must not be read as a description of the current
codex/rollback-netplay worktree. At implementation commit 6518db52 on
2026-08-25, the branch has a normal-launcher-selectable
Experimental rollback path: an exact mode/fingerprint connect extension,
Ready-gated start, bounded SI input prediction and repair, broad Dolphin
checkpoints including guest-visible memory-card protocol/content state,
restore/replay, fault-latched output quarantine, and mandatory confirmed-state
comparison. The exact current
anchors are ModernGekko/vendor/dolphin/Source/Core/Core/NetPlay/NetPlayConnectProtocol.h:17-95,
ModernGekko/vendor/dolphin/Source/Core/Core/NetPlay/NetPlayServer.cpp:1057-1110,1812-1850,
ModernGekko/vendor/dolphin/Source/Core/Core/NetPlay/NetPlayClientRollback.cpp:198-317,
and
ModernGekko/vendor/dolphin/Source/Core/Core/NetPlay/LiveRollbackOutputGate.cpp:212-338.
A production-path two-process run retained at
/tmp/ringout-live-rollback.final-correction.OHN0EzDz negotiated generation 1 with two SI
samples of base delay and an eight-frame horizon. Each peer captured a
frame-one checkpoint: 48,213,190 host bytes and 48,213,199 guest bytes (45.98
MiB each). The host's fault schedule impaired the direction in which the host
changed input; the guest corrected and
committed restore_frame=25, replay_through_frame=27, first_batch=21, then
restore_frame=137, replay_through_frame=137, first_batch=133. Both peers
produced identical confirmed logical-state rows at every 60-frame checkpoint
from 60 through 660; the complete files share SHA-256
4969a73790008801a05a27522d2508a7ffceb32d181a55be1b2f5d14caec9795.
Runtime, module, and DOL SHA-256 values were respectively
3a7e27d7420ac9ae49eca997e0301a972f3a3799997dcff9a2920f098b1351ee,
e01d1fc7f14d41cf170fb5b036e5c754cb3062b8e5421f147258b627e2931d48,
and 0ad25684426e6e04ee92a1d7919eec08d8d1528af8513472c44dd2eb20ea7ac5.
That post-fix run passed the ordinary production gate and closes the three
previously recorded memory-card, teardown-output, and corrected-frontier fault
blockers for this tested path. A clean production route retained at
/tmp/ringout-live-rollback.final-clean.wPXzfTPg produced 673 host / 674 guest
physical trace rows and 11 matching confirmed states. An isolated one-frame-
horizon run retained at /tmp/ringout-live-rollback.final-horizon.QGQ1Cu8O
stalled after a bounded 64-action drop schedule, resumed, and finished with 641
equal physical rows and 10 matching confirmed states. An isolated
report-only fault at logical frame 60 made both peers report DESYNC and stop
(/tmp/ringout-live-rollback.final-digest.iay4Pxu8). The fixed-delay final
regression retained at /tmp/ringout-fixed-delay-final-3a7e produced 2,958
byte-identical rows
with trim SHA-256
bd76b76faa049e7e9e9dee0a3bf1ae3be9173b9ea15ed2f532204b5596fa3cb3.
Public prerelease v1.2.1-ell.11 now contains this path. Its Windows and
AppImage workflows passed their complete build, package-policy, Wine/AppImage,
and clean-host smoke gates at source commit
a24a0cd5e0372d724a053992c11b56c1a554b087. This supersedes the package/tag
gap recorded above, but no retained physical/cross-machine run validates the
release.
The later branch snapshot adds an Online Room beta through Dolphin's hosted eight-character-code rendezvous, while retaining Direct IP under Advanced. The transport remains unauthenticated, unencrypted direct UDP for mutually trusted friends; there is no relay or IP privacy. The hosted rendezvous exchange passes, but a complete independently routed two-machine gameplay run remains a release gate. Authenticated ICE/TURN rooms remain proposed. See the implementation handoff and live harness for current evidence.
The same later branch snapshot makes the desktop launcher the only visible
player entry point: the obsolete Direct-IP Host/Join/Scan rows are no longer in
the in-game System tab. A persisted detailed-diagnostics toggle enables
Dolphin's NETPLAY trace; the launcher captures it in Logs/RingOut.log, rotates
one RingOut.previous.log, exposes a Copy log path action, and warns that logs
can contain endpoint and local-machine information. This player-UX and
diagnostics checkpoint is commit
6c89d6d4c801003fbe8cad34b1d8939998921750, recorded 2026-08-25.
- Lobby architecture and UX: entry points, LAN discovery, direct connection, player mapping, start synchronization, and lobby defects.
- Protocol, security, and reliability: packet flow, trust model, parser and save-transfer risks, compatibility, desync detection, and fail-closed remediation.
- Rollback and test roadmap: current fixed-delay algorithm, rollback experiments and measurements, test evidence, performance constraints, and a staged hybrid rollback design.
- Rollback implementation handoff: the later branch-local live implementation, safety policy, evidence, and blockers.
- Live rollback test harness: exact real-game commands and confirmed logical-state comparison.
RingOut does not ship rollback netcode. It ships symmetric, fixed-delay, deterministic lockstep using Dolphin's stock NetPlay protocol.
The implementation explicitly sets NETPLAY_NETWORK_MODE to fixeddelay and disables host-input authority so every peer consumes the same delayed inputs (ModernGekko/tools/netplay_session.cpp:813-824, :912-920). Missing remote input blocks the emulator (ModernGekko/vendor/dolphin/Source/Core/Core/NetPlay/NetPlayClientInput.cpp:105-115); it is never predicted, corrected, or replayed.
The README is internally inconsistent:
README.md:33-35correctly describes fixed-delay play.README.md:127incorrectly advertises rollback.
Practical ratings for the audited revision:
| Area | Rating | Meaning |
|---|---|---|
| Trusted wired LAN or private VPN | 7/10 | Deterministic and usable, with good synchronization work |
| Lobby UX | 6/10 | Functional roster and discovery, but incomplete lifecycle handling |
| Internet/WAN responsiveness | 3/10 | Fixed delay, reliable-channel stalls, no adaptation or relay |
| Security against untrusted peers | 2/10 | Treat rooms as trusted-friend sessions until P0 hardening lands |
| Shipping rollback implementation | 0/10 | Absent; only offline research hooks exist |
local controller poll
|
+--> local N-sample FIFO ---------------------+
| |
+--> reliable ENet --> host server --> peers |
v
each emulator consumes the next sample for every mapped pad
|
missing sample --> wait; guest time does not advance
The host is also an ordinary local client connected through loopback. The server validates controller ownership before relaying PadData (NetPlayServer.cpp:682-729). This avoids a host-only input path and prevents one normal client from claiming another player's controller.
All ordinary NetPlay packets use reliable ENet delivery (ModernGekko/vendor/dolphin/Source/Core/Common/ENet.cpp:40-63). Gameplay input has no frame or sequence number. A lost packet therefore delays later input behind retransmission instead of allowing prediction or redundant recovery.
The buffer setting is measured in queued SI input samples, not video frames. Dolphin updates input at a variable SI rate, typically 120 Hz (Core/HW/SI/SI.cpp:551-557). The lobby labels the value as 60 Hz frames and computes milliseconds from 60 Hz (netplay_session.cpp:703-714), so that display is not authoritative. auto does not adapt: it leaves the fixed server default at five samples (NetPlayServer.cpp:167-173).
- Host and guests share the same client path.
- The server validates controller-port ownership.
- Pads are assigned deterministically by player ID, host first (
netplay_session.cpp:248-295). - Host settings, initial RTC, SRAM/save data, and enabled AR/Gecko codes are synchronized before boot.
- Strict settings synchronization is enabled and CPU/VI clocks plus emulation speed are pinned (
netplay_session.cpp:813-840). - A deterministic GPU mode is used with dual-core emulation, with a single-core diagnostic fallback.
- Background input is forced for netplay, avoiding silent neutral input when a peer loses window focus (
netplay_session.cpp:842-853). - Netplay overlay input is neutralized before it reaches the game (
NetPlayClientInput.cpp:189-217). - Save/load/reset/speed/rebinding/cheat mutation is locked during netplay.
- Historical manual tests reached 6,470 synchronized frames and an 8,635-frame VS match with input from both peers (
07de096e,e467c92a).
These are substantial deterministic-lockstep results. They are not evidence of rollback.
Several packet parsers repeat while !packet.endOfPacket() but do not test the packet-valid flag after extraction (NetPlayServer.cpp:691-713; NetPlayClient.cpp:674-694). SFML marks a failed extraction invalid without necessarily advancing the read cursor (Externals/SFML/SFML/src/SFML/Network/Packet.cpp:86-95, :572-578). A truncated record can therefore wedge the network thread.
RingOut always enables host save synchronization. A host-provided GCI filename is concatenated without basename/path validation (NetPlayClientSaves.cpp:111-143), allowing writes outside the intended card folder. The shared decompressor also trusts a peer-provided u64 allocation size and uses non-bounds-checking LZO decompression without exact output accounting (NetPlayCommon.cpp:269-310). These paths need fail-closed parsing, strict caps, safe decompression, and path containment before sessions with untrusted hosts are encouraged.
The protocol is not authenticated or encrypted. There is no room secret, and the host begins listening before its local client reserves player ID 1. LAN discovery is plaintext and spoofable. Any client can request StopGame or forward the power-button event (NetPlayServer.cpp:888-933).
ModernGekko/tools/netplay_compatibility.cpp:67-96 already computes a useful fingerprint containing the RingOut revision, disc ID and DOL SHA, module/CPU ABI versions, CPUState size, module ranges, and chunk hashes. Only ModernGekko/tests/netplay_protocol_test.cpp:191-198 calls it.
The live handshake checks the SCM revision (NetPlayClient.cpp:241-250, NetPlayServer.cpp:443-464) and later compares the main.dol sync identifier. A swapped, damaged, or differently generated recomp module can still appear ready. The existing fingerprint should be sent and rejected before the peer joins the room.
Every 60 frames, a client sends GetFakeTimeBase() (NetPlayClient.cpp:1623-1640). The server compares those values (NetPlayServer.cpp:936-977). Two peers can retain equal clocks while RAM or another deterministic subsystem has diverged.
The manual scripts optionally hash guest RAM every frame because of this exact limitation (.github/scripts/netplay-local.sh:89-93). A production-friendly state digest should be keyed by emulated frame and exchanged periodically, with bounded history and a useful diagnostic on mismatch.
- There is no Ready protocol.
SameGameis displayed asready, and an interactive host can start with only itself (netplay_session.cpp:721-733). - Headless start uses a fixed 500 ms sleep instead of a pad-map acknowledgement (
netplay_session.cpp:1018-1028). NetPlayClient::GetPlayers()returns raw pointers into a mutable map after releasing its lock (NetPlayClient.cpp:1159-1169); the network thread can erase one while the lobby renders it.- All connection rejections are flattened to
HostUnavailable, leaving detailed launcher error cases unreachable (netplay_session.cpp:935-943). - Lobby connection loss and user cancellation both become successful exit code 0 (
netplay_session.cpp:953-963). - The no-window path claims to auto-start, but returns without requesting a start (
netplay_session.cpp:601-605). - The server accepts more peers than the four mapped controller slots and has no explicit spectator model.
RingOut disables traversal and the public index, and constructs NetPlayServer without UPnP forwarding (netplay_session.cpp:823-824, :872-884). LAN scan is custom UDP broadcast; outside one broadcast domain, users need a manually supplied hostname/IP, UDP forwarding, or a VPN.
QWave/DSCP omission in the Windows package is not the architectural blocker. Traffic marking may help congested networks, but it cannot supply missing prediction, sequence numbers, recovery, authentication, or NAT traversal.
There are two entry paths:
- The desktop launcher starts the runner with host/join arguments.
- The in-game System menu writes
netplay-request.ini, safely stops the offline runtime, and lets the runner delete and consume the request before rebuilding a pre-boot netplay session (RecompMenu.cpp:1714-1737;moderngekko_run.cpp:397-478).
A host binds the selected direct UDP port, normally 2626, and connects its own local client. While the interactive lobby is open it broadcasts RINGOUT1 <port> <nickname> once per second on UDP 2627 (netplay_session.cpp:519-595). The in-game scanner listens for 2.5 seconds and adopts the sender IP and advertised port (RecompMenu.cpp:881-977).
The lobby displays names, ping, assigned pad port, and game comparison. Starting synchronizes saves/codes/settings before delivering boot data. A mapped guest disconnect disables the game for everyone; the peer timeout is 30 seconds. There is no reconnect, resynchronization, host migration, password, kick UI, chat UI, or public matchmaking service.
The repository contains useful offline feasibility tooling, not live rollback:
State::SaveToBufferandLoadFromBufferexpose synchronous in-memory state (Core/State.h:128-136).RecompDeterminismcan save at one frame, run forward, restore, replay identical scripted input, and compare guest RAM (Core/RecompDeterminism.cpp:462-518).- Snapshot skip flags can experimentally omit VMEM, MEM1 padding, video, ARAM, and rollback-only JIT invalidation (
Core/State.h:100-125).
Historical measurements:
| Commit | Result |
|---|---|
619767f0 |
Full state 106.57 MiB; warm save 21.17 ms; restore 38.18 ms; 60-frame replay matched |
4a877ca5 |
State breakdown: 32 MiB MEM1 arena, 32 MiB unused fake VMEM, 26.23 MiB video, 16.01 MiB ARAM |
2e93f53e |
Narrowed state 24.34 MiB; save 3.06 ms; restore 21.81 ms; replay matched |
1f16b92a |
With rollback-only JIT-clear skip: save 2.95 ms; restore 6.66 ms; replay matched |
The final result is encouraging, but its proof was a 60-frame menu window. It does not establish that omitting video and ARAM is safe during a live match or streaming audio. At 24.34 MiB, an eight-frame uncompressed state ring is about 195 MiB. Saving every frame costs roughly 3 ms, and a misprediction adds restore plus re-simulation. Deck builds currently have little obvious catch-up headroom.
True rollback therefore remains a separate emulator-core project: frame-numbered inputs, prediction, confirmed frames, a snapshot or delta ring, state-hash exchange, restore and fast re-simulation, render suppression, audio reconciliation, side-effect control, and cross-platform/network-impairment tests.
The current moderngekko_netplay_protocol_test passed for the audited commit when run with localhost networking available. It verifies:
- invalid-host failure,
- two local clients and roster exchange,
- pad-buffer message round trip,
- pad mapping,
- disconnecting a peer that sends input for a port it does not own.
It explicitly does not boot the core or assert that remote pad data reaches gameplay (netplay_protocol_test.cpp:260-264).
The manual scripts provide stronger observational evidence, including full guest-RAM hashes and a two-input VS route, but they currently print failure rather than returning nonzero (netplay-local.sh:170-193; netplay-match.sh:163-175). Workflows do not invoke them. Linux and Deck run the CTest suite; Windows cross-builds configure tests off and the Wine smoke test only loads the runtime far enough to print --help before exercising synthetic recompilation.
Not yet proven for this exact release:
- two physical Windows machines,
- a Windows/Linux or Windows/Deck live session,
- induced latency, loss, duplication, and reordering,
- NAT/firewall behavior,
- three- or four-player sessions,
- disconnect/reconnect behavior under load,
- hostile or fuzzed protocol inputs,
- narrowed rollback snapshots during a complete match with audio.
- Safety: fail-closed packet decoding; sanitize and contain save paths; cap all peer-controlled sizes; use safe decompression with exact output accounting.
- Identity: reserve the local host identity, add a random room token, authenticate the handshake, and restrict host-only operations.
- Compatibility: put the existing game/module fingerprint into the live handshake and return a precise rejection reason.
- Lobby correctness: value-based player snapshots, typed cancel/error outcomes, expected-player count, Ready state, four-slot/spectator policy, and explicit mapping/start acknowledgements.
- Current netcode: frame-stamp inputs, isolate the input channel, send redundant recent samples, measure SI rate/RTT/jitter, and make delay selection genuinely adaptive.
- Detection: exchange periodic frame-keyed deterministic state hashes and retain bounded diagnostic history.
- Test gates: make manual scripts fail hard; add packet fuzzing, sanitizers, synthetic boot/input integration, Windows execution, and nightly
netemimpairment tests. - Connectivity: add invite strings plus authenticated traversal/relay or, at minimum, UPnP and explicit forwarding diagnostics.
- Rollback prototype: validate narrowed snapshots in match/audio, measure catch-up speed on every target, then implement a bounded hybrid delay-plus-rollback mode while keeping fixed delay as a fallback.
Run the protocol test from a configured Linux build with localhost networking enabled:
./build-appimage/moderngekko_netplay_protocol_testInspect the historical rollback measurements:
git show -s --format=fuller 619767f0 4a877ca5 2e93f53e 1f16b92a
git show -s --format=fuller 07de096e e467c92aWith a prepared package at dist/RingOut-1.0-deck, the existing manual harnesses are:
HASH=1 .github/scripts/netplay-local.sh /tmp/netplay-local 60 2626
.github/scripts/netplay-match.sh /tmp/netplay-match 60 2640These commands are diagnostic until the scripts are changed to return nonzero on every failed assertion.