Skip to content

feat(frontend): wasm-winit unification, the full shell in the browser (T-81-006) - #51

Merged
doublegate merged 2 commits into
mainfrom
feat/v0.8.0-wasm-winit-unification
Jul 9, 2026
Merged

doublegate merged 2 commits into
mainfrom
feat/v0.8.0-wasm-winit-unification

Conversation

@doublegate

Copy link
Copy Markdown
Owner

Summary

Routes the wasm build through the SAME App/ApplicationHandler native uses (a new wasm_winit.rs entry point, ported from RustyNES's own — not invented), replacing the lightweight wasm-canvas MVP (T-81-005) as the crate's default wasm feature. The wgpu render pipeline and egui debugger overlay now work on the web exactly as they do natively.

  • app.rs is ONE ApplicationHandler<AppEvent> implementation shared by native and wasm32, with internal #[cfg(target_arch = "wasm32")] branches for:
    • The genuinely-async wgpu init (pollster::block_on cannot block on wasm32) — delivered back into the event loop as a new AppEvent::GfxReady through an EventLoopProxy.
    • Browser ROM loading (AppEvent::RomLoaded from the page's <input id="rom-input">), replacing rfd's native file dialog.
    • Audio (wasm_audio instead of cpal/AudioOutput, reusing the same DRC/resampler core wasm-canvas already extracted into audio_core.rs).
  • The window attaches to the existing <canvas id="snes-canvas"> (WindowAttributesExtWebSys::with_canvas) rather than a detached one, so the page's own CSS layout applies.
  • Gfx now probes navigator.gpu's mere presence to pick wgpu::Backends::BROWSER_WEBGPU or ::GL and commits to exactly one before ever touching the canvas — a <canvas> can only bind one context type for its whole lifetime, and a WebGPU create_surface call poisons it for a subsequent GL attempt regardless of whether request_adapter later succeeds, so a sequential try-then-fallback on the same element could never actually reach its own fallback.
  • The WebGL2 (Backend::Gl) path needed its own color-space fix: unlike WebGPU/native, its surface can't present to a real sRGB default framebuffer, so wgpu-hal adds an extra encode at present time that (combined with GL's own automatic sRGB write) breaks the sRGB round-trip and washes out the palette. Fixed by keeping the GL backend entirely in the UNORM domain.
  • wasm-winit is now the crate's default wasm feature; wasm-canvas (T-81-005) remains independently selectable (--no-default-features --features wasm-canvas) and fully functional — re-verified end-to-end with no regression after this change.

Verification

Verified with a real headless-browser load (Playwright/Chromium), not just an HTTP/compile check:

  • The WebGL2 fallback path renders correctly end-to-end — a full-page screenshot after loading a real committed test ROM (tests/roms/undisbeliever/inidisp_brightness_delay.sfc) shows the egui menu bar, the status bar (LoROM | Ntsc | 60.0 fps | ROM loaded), and the actual emulated framebuffer, not a blank canvas.
  • Note on methodology: getImageData-based pixel counting (the method T-81-005 used) reads back empty on a WebGL/WebGPU canvas whose drawing buffer isn't preserved across presents — page.screenshot(), which reads the browser's own compositor output, is what actually proved this renders.

Honest gap: the WebGPU path itself (wgpu::Backends::BROWSER_WEBGPU) could not be screenshotted in this sandbox — headless Chromium here exposes navigator.gpu but returns "no compatible wgpu adapter" for a real request, and several software-Vulkan launch-flag combinations didn't produce a working adapter. The WebGPU path shares the same Gfx::new_async core the verified GL path uses and its backend-selection/color-space reasoning is grounded in real prior hardware testing (documented in gfx.rs's own comments), but a live screenshot specifically on WebGPU is not claimed here — real-browser verification on a machine with actual WebGPU support is still owed as a follow-up.

Test plan

  • cargo test --workspace — 41 suites, all passing
  • cargo clippy --workspace --all-targets -- -D warnings — clean
  • cargo clippy --workspace --all-targets --features debug-hooks -- -D warnings — clean
  • cargo clippy -p rustysnes-frontend --target wasm32-unknown-unknown --no-default-features --features wasm-winit --lib -- -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
  • cargo build -p rustysnes-core --target thumbv7em-none-eabihf --no-default-features (no_std gate) — clean
  • Real trunk build --release (default features) succeeds, producing a 7.4 MB wasm binary (the full egui/wgpu stack, vs. the 447 KB wasm-canvas MVP)
  • Real headless-browser screenshot verification (WebGL2 path) — see above
  • wasm-canvas regression-tested after this change: real trunk build + real headless-browser load, same non-zero-pixel-data check T-81-005 used — no regression
  • Honest gap, not claimed as covered: WebGPU-specific path not screenshotted in this sandbox (see above)

🤖 Generated with Claude Code

… (T-81-006)

Route the wasm build through the SAME App/ApplicationHandler native
uses (a new wasm_winit.rs entry point, ported from RustyNES's own),
instead of the lightweight wasm-canvas MVP -- the wgpu render pipeline
and egui debugger overlay now work on the web exactly as they do
natively. app.rs is ONE ApplicationHandler<AppEvent> implementation
shared by native and wasm32, with internal cfg branches for the
genuinely-async wgpu init (delivered via a new AppEvent::GfxReady
through an EventLoopProxy), browser ROM loading (AppEvent::RomLoaded,
replacing rfd's native file dialog), and audio (wasm_audio instead of
cpal). Gfx now probes navigator.gpu's presence to choose WebGPU or
WebGL2 and commits to exactly one before touching the canvas, since a
canvas can only bind one context type for its lifetime; the WebGL2
fallback also needed its own color-space fix to avoid washing out the
palette (wgpu-hal's GL surface can't present to a real sRGB default
framebuffer). wasm-winit is now the crate's default wasm feature;
wasm-canvas (T-81-005) remains independently selectable and fully
functional, re-verified end-to-end with no regression.

Verified with a real headless-browser load: the WebGL2 path renders
correctly end-to-end, confirmed via a full-page screenshot showing the
egui shell and a real emulated framebuffer, not just an absence of
console errors. The WebGPU path itself could not be screenshotted in
this sandbox (no working adapter despite several software-Vulkan
flag attempts) and that gap is honestly tracked as a follow-up.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 9, 2026 15:36
@doublegate

Copy link
Copy Markdown
Owner Author

Verification screenshot (WebGL2 fallback path, headless Chromium): egui menu bar + status bar (LoROM | Ntsc | 60.0 fps | ROM loaded) + the actual emulated framebuffer for tests/roms/undisbeliever/inidisp_brightness_delay.sfc.

@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 unifies the native and WebAssembly frontends under a single ApplicationHandler implementation (wasm-winit unification, T-81-006). It shares the core winit, wgpu, and egui shell between desktop and browser environments, using target-conditional branches for asynchronous graphics initialization, audio, and ROM loading. Key adjustments include a pre-canvas backend probe to avoid canvas poisoning and color-space corrections for the WebGL2 fallback. Review feedback recommends adding a 32 MiB file size limit to prevent out-of-memory crashes when loading ROMs, and implementing a guard in resumed() to prevent concurrent graphics initializations on WebAssembly.

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_winit.rs
Comment thread crates/rustysnes-frontend/src/app.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

Unifies the browser (wasm32) frontend with the native winit/wgpu/egui shell so the web build runs the same App/ApplicationHandler<AppEvent> loop as desktop, replacing the previous wasm-canvas MVP as the default wasm feature.

Changes:

  • Add a new wasm_winit.rs #[wasm_bindgen(start)] entry point that boots the shared winit App and wires browser ROM loading via EventLoopProxy<AppEvent>.
  • Refactor app.rs, gfx.rs, and config.rs to support wasm-specific constraints (async wgpu init, browser ROM bytes, wasm audio, backend selection and WebGL2 color-space handling).
  • Flip default wasm feature to wasm-winit and update docs/checklists/changelog to reflect the new default and verification methodology.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
to-dos/phase-8-reach/sprint-1-instrumentation.md Marks acceptance criteria as completed and records verification details/gaps for T-81-006.
docs/frontend.md Documents the two wasm frontends (wasm-winit default and wasm-canvas fallback) and their behavioral differences.
crates/rustysnes-frontend/src/wasm_winit.rs New wasm entry point that spawns the shared winit app and forwards ROM bytes from the DOM file picker.
crates/rustysnes-frontend/src/main.rs Updates crate-level docs to reflect the dual wasm entry points and feature gating.
crates/rustysnes-frontend/src/lib.rs Exposes app for wasm use and adds the feature-gated wasm_winit module.
crates/rustysnes-frontend/src/gfx.rs Adds wasm-specific backend selection (WebGPU vs GL) and adjusts format selection for WebGL2 color correctness.
crates/rustysnes-frontend/src/config.rs Makes config path/load/save degrade cleanly on wasm (no filesystem) instead of being entirely native-only APIs.
crates/rustysnes-frontend/src/app.rs Converts to a single ApplicationHandler<AppEvent> for native+wasm; adds wasm async init + ROM/audio integration.
crates/rustysnes-frontend/Cargo.toml Switches default wasm feature to wasm-winit and enables web-sys Navigator for navigator.gpu probing.
CHANGELOG.md Adds an Unreleased entry describing the wasm-winit unification and verification notes.

Comment thread crates/rustysnes-frontend/src/wasm_winit.rs Outdated
…t error)

- wasm_winit.rs: reject a selected file over 32 MiB before reading it,
  guarding against an accidental non-ROM file selection triggering an
  unbounded Vec<u8> allocation on the wasm heap.
- app.rs: guard resumed() against firing again (e.g. a tab losing and
  regaining visibility) while an earlier resumed() call's async
  Gfx::new_async is still in flight -- without it, a second call would
  spawn a second concurrent Gfx init and window, racing the first.
- wasm_winit.rs: log FileReader.readAsArrayBuffer's error instead of
  discarding it, so a failed ROM load is visible in the console rather
  than silently doing nothing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@doublegate
doublegate merged commit 293149b into main Jul 9, 2026
5 checks passed
@doublegate
doublegate deleted the feat/v0.8.0-wasm-winit-unification branch July 9, 2026 16:53
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