Skip to content

feat(frontend): wasm-canvas MVP, the real browser demo (T-81-005) - #50

Merged
doublegate merged 5 commits into
mainfrom
feat/v0.8.0-wasm-canvas-mvp
Jul 9, 2026
Merged

doublegate merged 5 commits into
mainfrom
feat/v0.8.0-wasm-canvas-mvp

Conversation

@doublegate

Copy link
Copy Markdown
Owner

Summary

Replaces crates/rustysnes-frontend/src/wasm.rs's v0.1.0 scaffold stub (panic hook + one log line, never rendered anything) with a real, working canvas-2D frontend, ported from RustyNES's proven shape:

  • A CanvasRenderingContext2d.putImageData blit of the existing RGBA8 framebuffer.
  • A requestAnimationFrame loop paced by a new shared pacing::Pacer — extracted out of app.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.
  • Keyboard input via DOM keydown/keyup, reusing input::KeyBindings unchanged (its default binds are already KeyboardEvent.code strings).
  • ROM loading via <input type="file">.
  • A new wasm_audio.rs: AudioWorkletNode primary with a ScriptProcessorNode fallback (no SharedArrayBuffer, since GitHub Pages can't send COOP/COEP headers). The DRC/resampler core was extracted out of audio.rs into a new target-agnostic audio_core.rs specifically so this reuses the exact same logic native does, not a reimplementation — a new process_and_process_into_agree test proves the two output paths stay identical.

No wgpu/egui yet — that unification is wasm-winit/T-81-006, a separate follow-up. wasm-canvas is the crate's default wasm feature for now (documented in Cargo.toml), since there's no wasm-winit module 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, whose wasm32 arm is an empty fn 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 code wasm.rs contained. Confirmed directly: the built .wasm was only ~14 KB with zero emulator code linked in. Fixed to data-target-name="rustysnes_frontend", the same pattern RustyNES's own working index.html uses. pages.yml's RUSTFLAGS="-C target-feature=-reference-types" also had to be removed — it broke wasm-bindgen's externref table generation once the demo actually linked in real Closure-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 failed
  • cargo clippy --workspace --all-targets -- -D warnings — clean
  • cargo clippy -p rustysnes-frontend --target wasm32-unknown-unknown --no-default-features --features wasm-canvas --lib -- -D warnings — clean
  • cargo fmt --check — clean
  • RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps — clean
  • Real trunk build --release succeeds, producing a 446 KB wasm binary (vs. the previous ~14 KB stub)
  • Real headless-browser verification (Playwright/Chromium): loaded a committed test ROM (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
  • Honest gap, not claimed as covered: audio was verified to construct without throwing, but headless automation cannot conclusively prove audible output through the browser's real autoplay-gesture semantics — manual verification in a real browser is still owed as a follow-up

🤖 Generated with Claude Code

doublegate and others added 3 commits July 9, 2026 04:43
…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>
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>
Copilot AI review requested due to automatic review settings July 9, 2026 13:38

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread crates/rustysnes-frontend/src/wasm_audio.rs Outdated
Comment thread crates/rustysnes-frontend/src/wasm_audio.rs Outdated
Comment thread crates/rustysnes-frontend/src/wasm_audio.rs Outdated
Comment thread crates/rustysnes-frontend/src/wasm.rs

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-canvas frontend (requestAnimationFrame + putImageData blit), plus DOM keyboard + <input type="file"> ROM loading.
  • Add a wasm audio output path (AudioWorkletNode with ScriptProcessorNode fallback) and extract shared audio primitives into audio_core.rs.
  • Extract the wall-clock fixed-timestep pacer into pacing.rs, update Pages build plumbing (index.html trunk 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.

Comment thread crates/rustysnes-frontend/src/wasm.rs
Comment thread crates/rustysnes-frontend/src/audio_core.rs
Comment thread crates/rustysnes-frontend/src/wasm_audio.rs
doublegate and others added 2 commits July 9, 2026 10:14
- 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
@doublegate
doublegate merged commit 1f8f9ac into main Jul 9, 2026
5 checks passed
@doublegate
doublegate deleted the feat/v0.8.0-wasm-canvas-mvp branch July 9, 2026 14:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants