Repository navigation
feat(frontend): wasm-winit unification, the full shell in the browser (T-81-006) - #51
Conversation
… (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>
|
Verification screenshot (WebGL2 fallback path, headless Chromium): egui menu bar + status bar ( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 winitAppand wires browser ROM loading viaEventLoopProxy<AppEvent>. - Refactor
app.rs,gfx.rs, andconfig.rsto 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-winitand 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. |
…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>
Summary
Routes the wasm build through the SAME
App/ApplicationHandlernative uses (a newwasm_winit.rsentry point, ported from RustyNES's own — not invented), replacing the lightweightwasm-canvasMVP (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.rsis ONEApplicationHandler<AppEvent>implementation shared by native andwasm32, with internal#[cfg(target_arch = "wasm32")]branches for:pollster::block_oncannot block onwasm32) — delivered back into the event loop as a newAppEvent::GfxReadythrough anEventLoopProxy.AppEvent::RomLoadedfrom the page's<input id="rom-input">), replacingrfd's native file dialog.wasm_audioinstead ofcpal/AudioOutput, reusing the same DRC/resampler corewasm-canvasalready extracted intoaudio_core.rs).<canvas id="snes-canvas">(WindowAttributesExtWebSys::with_canvas) rather than a detached one, so the page's own CSS layout applies.Gfxnow probesnavigator.gpu's mere presence to pickwgpu::Backends::BROWSER_WEBGPUor::GLand commits to exactly one before ever touching the canvas — a<canvas>can only bind one context type for its whole lifetime, and a WebGPUcreate_surfacecall poisons it for a subsequent GL attempt regardless of whetherrequest_adapterlater succeeds, so a sequential try-then-fallback on the same element could never actually reach its own fallback.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-winitis 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:
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.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 exposesnavigator.gpubut 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 sameGfx::new_asynccore the verified GL path uses and its backend-selection/color-space reasoning is grounded in real prior hardware testing (documented ingfx.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 passingcargo clippy --workspace --all-targets -- -D warnings— cleancargo clippy --workspace --all-targets --features debug-hooks -- -D warnings— cleancargo clippy -p rustysnes-frontend --target wasm32-unknown-unknown --no-default-features --features wasm-winit --lib -- -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— cleancargo build -p rustysnes-core --target thumbv7em-none-eabihf --no-default-features(no_std gate) — cleantrunk build --release(default features) succeeds, producing a 7.4 MB wasm binary (the full egui/wgpu stack, vs. the 447 KBwasm-canvasMVP)wasm-canvasregression-tested after this change: real trunk build + real headless-browser load, same non-zero-pixel-data check T-81-005 used — no regression🤖 Generated with Claude Code