Repository navigation
feat(frontend): wasm-canvas MVP, the real browser demo (T-81-005) - #50
Conversation
…ation" The user found RustySNES's live wasm demo renders blank by comparing against RustyNES's own working deployment. Root cause: wasm.rs has been an explicitly-labeled scaffold stub since v0.1.0 -- it installs a panic hook, logs one message, and returns, never building the actual app. Every prior "wasm demo is live" CHANGELOG claim checked only HTTP-level liveness (200 status, correct content-types), never that the app actually renders. A research pass into RustyNES's own wasm.rs/wasm_winit.rs confirmed a concrete two-stage port path RustySNES's own Cargo.toml feature split (wasm-winit default / wasm-canvas) already anticipated: a canvas-2D MVP first (fast, no wgpu/egui dependency), then winit/wgpu unification through the same App native already uses. Also confirmed a real gap, not just plumbing: app.rs/audio.rs are currently excluded from the wasm32 build entirely. Updates to-dos/VERSION-PLAN.md's v0.8.0 section, to-dos/phase-8-reach/ overview.md (goal, exit criteria, scope, risks), and sprint-1-instrumentation.md (two new tickets, T-81-005/T-81-006) with the concrete port plan, sourced from RustyNES's actual code, not invented from scratch. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
# Conflicts: # CHANGELOG.md
Replace wasm.rs's v0.1.0 scaffold stub with a working canvas-2D frontend ported from RustyNES's proven shape: putImageData blit of the RGBA8 framebuffer, a requestAnimationFrame loop paced by a new shared pacing::Pacer (extracted from app.rs, now used by both native and wasm), keyboard input via KeyBindings, and ROM loading via a file input. Audio is a new wasm_audio.rs: AudioWorkletNode primary with a ScriptProcessorNode fallback, reusing the native DRC/resampler core verbatim via a new target-agnostic audio_core.rs. wasm-canvas is the crate's default wasm feature for now, since there is no wasm-winit module yet (T-81-006). Verifying this with a real headless-browser load (not just an HTTP check) surfaced a second, deeper, pre-existing bug: index.html's trunk directive built the [[bin]] instead of the [lib] cdylib, so the #[wasm_bindgen(start)] entry point was dead-code-eliminated regardless of what wasm.rs contained -- confirmed by the built .wasm being only ~14 KB. Fixed to data-target-name, matching RustyNES's own working config. pages.yml's RUSTFLAGS reference-types override also had to go; it silently broke wasm-bindgen's externref table once real Closure- based code was actually linked in. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request implements the wasm-canvas MVP frontend for RustySNES, replacing the previous scaffold stub with a working canvas-2D renderer, keyboard input, ROM loading, and a Web Audio output path (supporting AudioWorklet with a ScriptProcessorNode fallback). It also resolves a critical bug in web/index.html where the empty binary was being built instead of the library, which caused the live demo to render a blank page. The review feedback highlights several memory and performance optimization opportunities in the WebAssembly implementation, specifically recommending the revocation of the worklet blob URL to prevent resource leaks, and the use of thread-local buffers to eliminate heap allocations in the audio rendering hot paths and avoid closure leaks during ROM loading.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
There was a problem hiding this comment.
Pull request overview
This PR turns the RustySNES web build from a non-rendering wasm stub into a working wasm-canvas browser demo, while extracting shared pacing/audio primitives so native and wasm reuse the same core logic.
Changes:
- Implement a real
wasm-canvasfrontend (requestAnimationFrame+putImageDatablit), plus DOM keyboard +<input type="file">ROM loading. - Add a wasm audio output path (
AudioWorkletNodewithScriptProcessorNodefallback) and extract shared audio primitives intoaudio_core.rs. - Extract the wall-clock fixed-timestep pacer into
pacing.rs, update Pages build plumbing (index.htmltrunk target + workflow flags), and document the new wasm plan in to-dos/CHANGELOG.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| to-dos/VERSION-PLAN.md | Expands v0.8.0 scope to include the real wasm frontend and details the two-stage plan. |
| to-dos/phase-8-reach/sprint-1-instrumentation.md | Adds tickets T-81-005/T-81-006 and acceptance criteria (including headless render verification). |
| to-dos/phase-8-reach/overview.md | Updates phase scope/exit criteria/risk list to include verified wasm frontend work. |
| crates/rustysnes-frontend/web/index.html | Fixes trunk target selection to build the cdylib entrypoint; adds canvas + ROM input UI. |
| crates/rustysnes-frontend/src/wasm.rs | Replaces wasm scaffold with a working canvas-2D frontend loop + input + ROM loading. |
| crates/rustysnes-frontend/src/wasm_audio.rs | New wasm audio backend with worklet primary and script-processor fallback. |
| crates/rustysnes-frontend/src/pacing.rs | New shared pacer module (native + wasm) with unit tests. |
| crates/rustysnes-frontend/src/lib.rs | Wires in new modules and feature/target gating for wasm entrypoints + audio. |
| crates/rustysnes-frontend/src/audio.rs | Refactors native audio module to use shared audio_core primitives. |
| crates/rustysnes-frontend/src/audio_core.rs | New shared audio ring/resampler/DRC servo plus tests. |
| crates/rustysnes-frontend/src/app.rs | Switches native app pacing to the extracted pacing::Pacer. |
| crates/rustysnes-frontend/Cargo.toml | Makes wasm-canvas the default feature for now; expands web-sys feature list. |
| CHANGELOG.md | Documents the new real wasm demo and the verification/root-cause findings. |
| .github/workflows/pages.yml | Removes the -reference-types RUSTFLAGS override and explains why. |
- wasm_audio.rs: revoke the worklet blob URL once add_module settles, instead of leaking it for the page's lifetime. - wasm_audio.rs: reuse scratch buffers in both the Worklet push_samples path and the ScriptProcessorNode onaudioprocess callback, instead of allocating a fresh Vec on every call (~60 Hz / ~15 Hz hot paths). - wasm.rs: reuse a single FileReader + Closure across ROM loads instead of leaking one Closure per load. - audio_core.rs: fix a test comment overclaiming "byte-identical" when the assertion is actually an epsilon comparison. Region hardcoding to Ntsc (flagged by Copilot) is left as-is: it matches native app.rs's existing behavior exactly (region is a manual Settings selection requiring restart, not cart-auto-detected for pacing purposes in either frontend) -- not a wasm-canvas regression. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…as-mvp # Conflicts: # CHANGELOG.md # to-dos/VERSION-PLAN.md # to-dos/phase-8-reach/sprint-1-instrumentation.md
Summary
Replaces
crates/rustysnes-frontend/src/wasm.rs'sv0.1.0scaffold stub (panic hook + one log line, never rendered anything) with a real, working canvas-2D frontend, ported from RustyNES's proven shape:CanvasRenderingContext2d.putImageDatablit of the existing RGBA8 framebuffer.requestAnimationFrameloop paced by a new sharedpacing::Pacer— extracted out ofapp.rs(native's wall-clock fixed-timestep pacer) into its own module so both native and the new wasm-canvas path use the SAME logic; without it a 144 Hz display would run emulation 2.4x too fast.keydown/keyup, reusinginput::KeyBindingsunchanged (its default binds are alreadyKeyboardEvent.codestrings).<input type="file">.wasm_audio.rs:AudioWorkletNodeprimary with aScriptProcessorNodefallback (noSharedArrayBuffer, since GitHub Pages can't send COOP/COEP headers). The DRC/resampler core was extracted out ofaudio.rsinto a new target-agnosticaudio_core.rsspecifically so this reuses the exact same logic native does, not a reimplementation — a newprocess_and_process_into_agreetest proves the two output paths stay identical.No
wgpu/eguiyet — that unification iswasm-winit/T-81-006, a separate follow-up.wasm-canvasis the crate's default wasm feature for now (documented inCargo.toml), since there's nowasm-winitmodule yet for that feature to select.A second, deeper bug found while verifying this
Verifying with a real headless-browser load (Playwright/Chromium — not just an HTTP-status check, exactly the gap that let the original stub ship unnoticed since
v0.1.0) surfaced a separate, pre-existing bug:web/index.html's trunk directive (data-bin="rustysnes" data-type="main") built the[[bin]](main.rs, whosewasm32arm is an emptyfn main() {}that never references the lib), not the[lib]cdylib — so the real#[wasm_bindgen(start)]entry point got dead-code-eliminated entirely, regardless of what codewasm.rscontained. Confirmed directly: the built.wasmwas only ~14 KB with zero emulator code linked in. Fixed todata-target-name="rustysnes_frontend", the same pattern RustyNES's own workingindex.htmluses.pages.yml'sRUSTFLAGS="-C target-feature=-reference-types"also had to be removed — it broke wasm-bindgen's externref table generation once the demo actually linked in realClosure-based code; it had been a silent no-op until now since there was no real code for it to break.Depends on #48
Branched on top of
docs/fold-wasm-frontend-into-v0.8.0(#48), so this PR's diff will include that branch's commits until #48 merges — at which point this PR's diff shrinks to just the wasm-canvas work.Test plan
cargo test --workspace— 331 passed, 0 failedcargo clippy --workspace --all-targets -- -D warnings— cleancargo clippy -p rustysnes-frontend --target wasm32-unknown-unknown --no-default-features --features wasm-canvas --lib -- -D warnings— cleancargo fmt --check— cleanRUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps— cleantrunk build --releasesucceeds, producing a 446 KB wasm binary (vs. the previous ~14 KB stub)tests/roms/undisbeliever/inidisp_brightness_delay.sfc) through the live#rom-input; confirmed a<canvas>element exists and received non-zero pixel data (28672/57344 non-black pixels), zero console errors,"RustySNES wasm-canvas: ready"+"RustySNES: ROM loaded"logged🤖 Generated with Claude Code