v2.6.0 "Rollback Bridge"
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 noDropimpl, 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 withimpl Drop. - Two more leaked JS closures, both in the connection-establishment code:
the ICE-gathering-completion handler and the joiner-sideondatachannel
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 realUint8Arraybytes withbinaryType = "arraybuffer", exercising the same pathWebRtcSocketdoes. - 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.