Skip to content

v2.6.0 "Rollback Bridge"

Choose a tag to compare

@doublegate doublegate released this 02 Jul 20:17

Rusty2600 v2.6.0 "Rollback Bridge"

Third release of the RustyNES gap-closure arc — closes the browser WebRTC
netplay gap v2.3.0 deliberately deferred, and folds in a master
dependency-upgrade sweep consolidating 12 open Dependabot PRs into one
reviewed, tested pass.

Shipped through two PRs: #15 (browser WebRTC netplay transport) and #16 (the
dependency-upgrade sweep, merged first). Both went through this arc's
standing CI + automated review-bot adjudication process (GitHub Copilot,
Gemini Code Assist) before merging.

Browser WebRTC netplay transport (the headline gap)

Native UDP + STUN (v2.3.0) only ever reached native peers — browser-to-
browser netplay was impossible. This release closes that gap.

ADR 0008 (written first, before any implementation code, per this
release's own plan) answers the question v2.3.0's STUN work deliberately
deferred: can WebRTC's async surface stay contained to one-time connection
setup, without pulling an async runtime into this project's otherwise
100%-synchronous core? Yes — using the exact wasm_bindgen_futures:: spawn_local pattern v2.5.0's Gfx::new_async already proved in this
codebase. The per-frame send/poll hot path stays fully synchronous.

WebRtcSocket (rusty2600-netplay::webrtc, wasm32-only) implements
ggrs::NonBlockingSocket<SocketAddr> over an already-open RtcDataChannel,
mirroring the existing PunchedUdpSocket and reusing its exact wire format
— so RollbackSession::with_webrtc_socket (the one new public entry point)
needed zero changes to the session-driving code GGRS itself owns. Since this
project is deliberately 2-player-only, a fixed sentinel SocketAddr stands
in for "the one WebRTC peer" (never a real IP). WebRtcPeer handles the
one-time async connection-establishment dance (SDP offer/answer, ICE
gathering). A minimal manual/copy-paste SDP exchange (no signaling server,
per the ADR) is provided via a standalone rusty2600-netplay/web/ test
page.

Honest verification, stated plainly: the entire connection-establishment
code path — createOffer/createAnswer/acceptAnswer — was driven
end-to-end in a real Chromium instance across two independent
RTCPeerConnections (CDP-scripted). Every step succeeded with real data: a
real SDP offer generated, a real SDP answer generated in response,
set_remote_description accepting it without error. The data channel itself
never reached "open" in this sandbox — diagnosed to zero ICE candidates
being gathered on either side (ruled out mDNS candidate obfuscation as the
cause; a genuine sandbox/Chromium network restriction, not a bug in the Rust
code). A native (non-browser) WebRTC path stays explicitly deferred per the
ADR — browser-to-browser was always the primary, more valuable, and more
testable target.

PR review, for real

PR #15 was reviewed by GitHub Copilot and Gemini Code Assist — 12 findings,
all genuine, all fixed:

  • A real memory-safety bug (both bots independently found this): WebRtcSocket
    had no Drop impl, so a message arriving after the socket was dropped
    would have the browser invoke a JS reference into deallocated memory — a
    wasm-bindgen trap/panic, not a benign no-op. Fixed with impl Drop.
  • Two more leaked JS closures, both in the connection-establishment code:
    the ICE-gathering-completion handler and the joiner-side ondatachannel
    handler were both .forget()-ten unconditionally. Both are now held and
    cleared once their one-time job finishes.
  • A standalone test-harness page (web/index.html) wasn't actually testing
    the binary data-channel path production code depends on — it was sending
    plain strings. Now sends real Uint8Array bytes with binaryType = "arraybuffer", exercising the same path WebRtcSocket does.
  • Several doc-comment inaccuracies (a stale claim about a "JSON-serialized
    blob" return value that's actually a plain SDP string; a verification
    script's docstring that overstated what it proved) — all corrected.

Master dependency-upgrade sweep (PR #16)

Consolidated all 12 open Dependabot PRs into one reviewed, tested PR rather
than merging each in isolation:

Applied: egui/egui-wgpu/egui-winit 0.34.3 → 0.35.0, zip 2.4.2 →
8.6.0, mlua 0.10.5 → 0.11.6, uniffi 0.28.3 → 0.32.0 (Kotlin/Swift
bindings regenerated — 0.31 changed method checksums, making the old
bindings incompatible with the new Rust scaffolding), actions/checkout v4
→ v7, clap_complete 4.6.5 → 4.6.7.

Declined, each with a documented reason and a dependabot.yml ignore
rule
: bincode 1.3.3 → 3.0.0 (bincode is now unmaintained — RUSTSEC-2025-
0141 — and 3.0.0 is a deliberately broken placeholder release, not real
code; rusty2600-netplay needs byte-identical wire compatibility with
GGRS's own internal bincode 1.3.3 usage, so staying pinned is correct);
dtolnay/rust-toolchain@1.100 (Rust 1.100 does not exist yet — confirmed
via a direct install attempt against the real release server); wgpu/
naga 29 → 30 (egui-wgpu 0.35.0, the latest available release, still
requires wgpu 29.x internally — bumping would create two incompatible
wgpu versions in the dependency graph).

Also found and fixed a real, pre-existing clippy::large_stack_frames
failure in App::dispatch_actions under --features netplay — confirmed
present on main before any of these bumps, never caught before since this
project's CI only clippy-checks the default feature set.

Test count

333 tests passing on default features (337 with --features test-roms) —
unchanged from v2.5.0, since the new WebRTC netplay code is wasm32-only
and doesn't touch the native default test suite. Full CI green —
Linux/macOS/Windows, the perf regression gate, and the no_std gate.

What's next

v2.7.0 "True Colors" — the TIA object-ID mask sprite_pack's own doc
comment has flagged as the missing prerequisite since v1.4.0, plus the
HD-pack live rendering splice it's been waiting on ever since. See
to-dos/ROADMAP.md for the complete v2.4.0 -> v3.0.0 roadmap.