Skip to content

Commit 37102eb

Browse files
doublegateclaude
andauthored
docs(plan): fold the real wasm frontend build into v0.8.0 "Instrumentation" (#48)
* docs(plan): fold the real wasm frontend build into v0.8.0 "Instrumentation" 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> * fix(changelog): remove debugger-overlay entry accidentally added here This entry belongs to PR #49 (a separate, still-unmerged branch) and was mistakenly duplicated into this branch's CHANGELOG while resolving a merge conflict against main -- this PR has nothing to do with the debugger overlay. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * docs: fix bot-flagged issues on PR #48 (split code spans, T-81-005 audio gap) - Rejoin three inline code spans that were split across a line wrap (CommonMark renders the newline as a literal space, corrupting the path/identifier), in CHANGELOG.md, VERSION-PLAN.md, and sprint-1-instrumentation.md. - T-81-005's description text never mentioned audio even though its acceptance criteria required it; added it to the description so the two agree. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 32f34db commit 37102eb

4 files changed

Lines changed: 160 additions & 9 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
1010
## [Unreleased]
1111

12+
### Changed
13+
14+
- **Folded the real wasm frontend build into `v0.8.0 "Instrumentation"`'s scope, per explicit
15+
direction.** The user compared RustySNES's live Pages demo against RustyNES's working one and
16+
found it renders a blank page — root-caused to `crates/rustysnes-frontend/src/wasm.rs` being
17+
an explicitly-labeled scaffold stub since `v0.1.0` (installs a panic hook, logs one message,
18+
returns — never builds the app or creates a canvas). Every prior "wasm demo is live"
19+
verification (`v0.1.0`-`v0.6.0`) checked only HTTP-level liveness, never that the app actually
20+
renders. Scoped as two stages ported from RustyNES's own proven shape (`wasm.rs`/
21+
`wasm_winit.rs`, confirmed by reading the source directly): a `wasm-canvas` MVP first (canvas-2D
22+
blit, no `wgpu`/`egui`, ships a real working demo fast), then `wasm-winit` unification (routes
23+
wasm through the same `App` native uses — requires un-gating `app.rs`/`audio.rs` from their
24+
current `wasm32` exclusion, a real architectural gap, not just plumbing).
25+
`to-dos/VERSION-PLAN.md`'s `v0.8.0` section, `to-dos/phase-8-reach/overview.md`, and
26+
`sprint-1-instrumentation.md` (two new tickets, T-81-005/T-81-006) updated accordingly.
27+
1228
### Fixed
1329

1430
- **`crates/rustysnes-frontend/web/index.html`: added the missing link to `/api/` on the live

‎to-dos/VERSION-PLAN.md‎

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -402,13 +402,15 @@ established discipline:
402402
opportunistic posture already established across four prior releases; don't gate a rung on
403403
finding ROMs that may never become available.
404404

405-
### v0.8.0 "Instrumentation" — debugger, scripting/TAS, cheats
405+
### v0.8.0 "Instrumentation" — debugger, scripting/TAS, cheats, the real wasm frontend
406406

407407
One coherent theme: tooling that inspects and manipulates emulator state, all sharing the same
408-
memory-watch/introspection substrate. This is the first rung of the breadth pass — RustySNES's
409-
own crate manifest already anticipated it (`crates/rustysnes-frontend/Cargo.toml`'s `debug-hooks`/
410-
`scripting` flags, present since the frontend's first cut, "mirroring the RustyNES feature
411-
surface so the chip-implementation phase can switch them on without restructuring the manifest").
408+
memory-watch/introspection substrate — plus the wasm frontend build, folded in here per explicit
409+
direction after the live demo page was found rendering blank (see below). This is the first rung
410+
of the breadth pass — RustySNES's own crate manifest already anticipated the tooling half
411+
(`crates/rustysnes-frontend/Cargo.toml`'s `debug-hooks`/`scripting` flags, present since the
412+
frontend's first cut, "mirroring the RustyNES feature surface so the chip-implementation phase
413+
can switch them on without restructuring the manifest").
412414

413415
- **Debugger overlay** — fills in `ui_shell.rs`'s already-wired 65C816/PPU/APU/Cart panels
414416
(currently `"TODO(impl-phase)"` placeholders) behind the existing `debug-hooks` flag; the
@@ -426,6 +428,43 @@ surface so the chip-implementation phase can switch them on without restructurin
426428
fundamentally memory-watch/poke tooling, the same substrate as the debugger's memory panel —
427429
grouped here rather than with netplay/RetroAchievements for that reason. Adds a new `cheats`
428430
feature flag matching the existing naming convention.
431+
- **The real wasm frontend** — `crates/rustysnes-frontend/src/wasm.rs` has been an explicitly-
432+
labeled scaffold stub since `v0.1.0` (installs a panic hook, logs one message, returns — never
433+
builds the `App`, creates a wgpu canvas surface, or drives a render loop), making the live
434+
`https://doublegate.github.io/RustySNES/` demo a blank page despite every prior "wasm demo is
435+
live" CHANGELOG claim (`v0.1.0`-`v0.6.0`) being true only at the HTTP level (200 status,
436+
correct content-types) — found live by direct comparison against RustyNES's own working wasm
437+
deployment, not by CI (`pages.yml` only asserts the build/deploy steps succeed, never that the
438+
resulting app actually renders). `rustysnes-frontend/Cargo.toml`'s `wasm-winit` (default) /
439+
`wasm-canvas` feature split already anticipated exactly the two-stage build RustyNES itself
440+
used to reach a working wasm frontend (confirmed by reading RustyNES's `wasm.rs`/`wasm_winit.rs`
441+
source directly, not inferred): a `wasm-canvas` MVP first (a `CanvasRenderingContext2d`
442+
`putImageData` blit of the RGBA8 framebuffer + a `requestAnimationFrame` loop + keyboard via DOM
443+
events + ROM load via `<input type="file">` — no `wgpu`/`egui`, ~500 lines in RustyNES's
444+
`wasm.rs`), then `wasm-winit` unification as a larger follow-up (routes wasm through the *same*
445+
`App`/`ApplicationHandler` native already uses, via
446+
`winit::platform::web::EventLoopExtWebSys::spawn_app` + an `EventLoopProxy` delivering
447+
`RomLoaded`/`GfxReady`-style events in from JS — RustyNES's own `app.rs` states "the `ApplicationHandler` impl serves both
448+
native and wasm32," proving this reuse is architecturally sound, not aspirational). **Real,
449+
confirmed gap, not just plumbing:** RustySNES's `app.rs` and `audio.rs` are currently
450+
`#[cfg(not(target_arch = "wasm32"))]` — entirely excluded from the wasm build today, not merely
451+
unused; `gfx.rs` has zero `wasm32`/`web_sys`/canvas references yet. Un-gating and adapting them
452+
(swapping `cpal` for Web Audio behind a conditional path, gating out native-only deps like
453+
`gilrs`/`directories`) is the same real work RustyNES needed for its own `wasm_winit.rs`
454+
follow-up, not a small addition. Minimal wasm-canvas-MVP subsystem list, RustyNES file
455+
(line count) → RustySNES port target: canvas-2D entry (`wasm.rs`, 538) → replaces the current
456+
stub; audio (`wasm_audio.rs`, 456) → AudioWorklet primary + ScriptProcessorNode fallback, no
457+
`SharedArrayBuffer` since GitHub Pages can't send COOP/COEP headers, reuses the same DRC/
458+
resampler logic already in native `audio.rs`; gamepad (`wasm_gamepad.rs`, 64) → trivial,
459+
`navigator.getGamepads()` polled from JS + one bridge fn; file I/O (`wasm_io.rs`, 345) → the
460+
`<input type="file">` + generic save-file-with-fallback helper. **Explicitly out of scope for
461+
this rung** (RustyNES's own Phase-8-equivalent reach features, needed only once RustySNES's
462+
native equivalents exist, not as a wasm-specific prerequisite): `wasm_idb.rs` (IndexedDB
463+
save-state persistence — native save-states already work, can defer), `wasm_save_states.rs`,
464+
`wasm_touch.rs` (mobile touch overlay), `wasm_lobby.rs`/`wasm_netplay.rs` (netplay lobby UI —
465+
belongs with `v0.9.0`'s netplay work instead), `wasm_script.rs`/`wasm_cheevos.rs` (wire once
466+
this rung's own native scripting/`v0.9.0`'s cheevos land), `wasm_share.rs` (settings
467+
share-links).
429468
- **Recurring gate, starting here:** a byte-identical-with-all-flags-off CI check (every new
430469
flag landed on this ladder must leave the default build unchanged) — re-verify after `v0.9.0`
431470
and `v1.0.0` too. Use explicit feature combos in CI, never `--all-features`.

‎to-dos/phase-8-reach/overview.md‎

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ RustyNES front-loaded this exact breadth into its own v1.0.0, and matching that
1313
stay genuinely post-`v1.0.0` Reach — polish items with no accuracy-adjacency, matching how
1414
RustyNES itself treated them.
1515

16+
**Folded in after `v0.7.0`, per explicit direction:** the real wasm frontend build.
17+
`crates/rustysnes-frontend/src/wasm.rs` has been a scaffold stub since `v0.1.0` — it never
18+
builds the actual app, so the live Pages demo renders blank despite every prior "wasm demo is
19+
live" claim being true only at the HTTP level. Found live by direct comparison against
20+
RustyNES's own working wasm deployment, not by CI. Ported from RustyNES's proven two-stage
21+
shape (`wasm-canvas` MVP, then `wasm-winit` unification) — see Sprint 1 below.
22+
1623
## Exit criteria
1724

1825
- [ ] Debugger overlay (65C816/PPU/APU/Cart panels, including SA-1/Super FX coprocessor state
@@ -25,6 +32,11 @@ RustyNES itself treated them.
2532
full-snapshot design is fast enough for a real rollback window.
2633
- [ ] RetroAchievements (opt-in, native FFI, the `RustySNES/<ver> rcheevos/<ver>` User-Agent
2734
pattern).
35+
- [ ] The real wasm frontend: a `wasm-canvas` MVP (canvas-2D blit, `requestAnimationFrame`,
36+
keyboard, ROM load) landed first for a fast working demo, then `wasm-winit` unification
37+
(the same `App` native uses, via `EventLoopExtWebSys::spawn_app`) — both ported from
38+
RustyNES's proven shape, verified by a real headless-browser render check, not just an
39+
HTTP status check.
2840
- [ ] Every feature off by default; with all off, builds are byte-identical (a CI gate proves
2941
it, re-verified after each sprint below).
3042
- [ ] All sprints complete; `v1.0.0` cut prerequisites met (`to-dos/VERSION-PLAN.md`'s v1.0.0
@@ -35,8 +47,14 @@ RustyNES itself treated them.
3547
In-scope:
3648

3749
- The five breadth feature families above, each default-off and byte-identical-when-off.
50+
- The real wasm frontend build (two stages — `wasm-canvas` MVP, then `wasm-winit`
51+
unification), which requires un-gating `app.rs`/`audio.rs` from their current
52+
`#[cfg(not(target_arch = "wasm32"))]` exclusion — a real, confirmed architectural gap, not
53+
just plumbing (see Sprint 1's T-81-005/T-81-006 for the full breakdown).
3854
- The `Board: Send` fix `emu-thread` needs (a prerequisite for `v1.0.0`'s dedicated emulation
3955
thread, tracked here since it's discovered/fixed alongside this phase's instrumentation work).
56+
Unrelated to the wasm-winit unification above — `emu-thread` is a native-only, single-player
57+
dedicated-thread feature; wasm's own event loop is a separate concern.
4058

4159
Out-of-scope (post-`v1.0.0` Reach, `to-dos/VERSION-PLAN.md`):
4260

@@ -47,8 +65,8 @@ Out-of-scope (post-`v1.0.0` Reach, `to-dos/VERSION-PLAN.md`):
4765

4866
## Sprints
4967

50-
- [Sprint 1 — Instrumentation](sprint-1-instrumentation.md) — debugger, scripting/TAS, cheats.
51-
Maps to `v0.8.0 "Instrumentation"`.
68+
- [Sprint 1 — Instrumentation](sprint-1-instrumentation.md) — debugger, scripting/TAS, cheats,
69+
the real wasm frontend (two stages). Maps to `v0.8.0 "Instrumentation"`.
5270
- [Sprint 2 — Community](sprint-2-community.md) — rollback netplay, RetroAchievements. Maps to
5371
`v0.9.0 "Community"`.
5472

@@ -81,6 +99,12 @@ Phase 5 (the frontend + the exercised determinism contract); Phases 1–4 featur
8199
frontend-orchestrated resimulation model. Mitigate by keeping the two mutually exclusive by
82100
session type (a netplay session uses its own rollback-aware loop, never the generic
83101
`emu-thread` pacer) rather than trying to unify them.
102+
- **The wasm frontend shipping unverified again** — the root cause of the original stub going
103+
unnoticed since `v0.1.0` was that every prior check (`pages.yml`, this project's own CHANGELOG
104+
claims) only asserted the build/deploy pipeline succeeds, never that the resulting page
105+
actually renders. Mitigate by requiring a real headless-browser render check (Playwright/
106+
Chromium: assert a canvas exists and receives non-zero pixel data) as an explicit acceptance
107+
criterion for both T-81-005 and T-81-006, not just a build-success check.
84108

85109
## Reference docs
86110

‎to-dos/phase-8-reach/sprint-1-instrumentation.md‎

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
**Phase:** Phase 8 — Instrumentation + Community
44
**Sprint goal:** the debugger overlay, Lua scripting + TAS movie API, and cheat-code support
5-
ship behind default-off feature flags, each byte-identical with the feature off.
6-
**Estimated duration:** 3 weeks
5+
ship behind default-off feature flags, each byte-identical with the feature off; the wasm
6+
frontend goes from a scaffold stub to a genuinely working demo.
7+
**Estimated duration:** 3 weeks (native tooling) + 2-3 weeks (wasm frontend, two stages)
78
**Release mapping:** `v0.8.0 "Instrumentation"` (`to-dos/VERSION-PLAN.md`)
89

910
## Tickets
@@ -94,8 +95,79 @@ feature combo (never `--all-features`).
9495

9596
---
9697

98+
### T-81-005 — The real wasm frontend, stage 1: `wasm-canvas` MVP
99+
100+
**Description:** replace `crates/rustysnes-frontend/src/wasm.rs`'s scaffold stub (panic hook +
101+
one log line, never renders anything) with a working canvas-2D MVP behind the existing
102+
`wasm-canvas` flag, porting RustyNES's proven shape
103+
(`../RustyNES/crates/rustynes-frontend/src/wasm.rs`, not inventing a new approach): a
104+
`CanvasRenderingContext2d.putImageData` blit of the existing RGBA8 framebuffer
105+
(`emu.rs::framebuffer()` already produces this — no PPU/core changes needed), a
106+
`requestAnimationFrame` loop, keyboard input via DOM `keydown`/`keyup` events, ROM loading via
107+
`<input type="file">`, and audio via `AudioWorklet`/`ScriptProcessorNode` (ported from RustyNES's
108+
`wasm_audio.rs`, reusing the native DRC/resampler core — see the acceptance criteria below). No
109+
`wgpu`/`egui` — this stage proves a real, visible, playable demo exists fast, without needing
110+
`app.rs`/`audio.rs` un-gated for wasm32 yet. Ships to the live Pages deployment as soon as it
111+
lands, closing the actual user-facing gap (a blank demo page) even before stage 2 (T-81-006) is
112+
ready.
113+
114+
**Acceptance criteria:**
115+
116+
- [ ] The live demo shows a real picture and accepts keyboard input for a loaded ROM.
117+
- [ ] Audio plays via `AudioWorklet` (primary) with a `ScriptProcessorNode` fallback — no
118+
`SharedArrayBuffer` (GitHub Pages can't send COOP/COEP headers), reusing the same DRC/
119+
resampler logic `audio.rs` already has for native.
120+
- [ ] Verified with a real headless-browser load (e.g. Playwright/Chromium), not just an HTTP
121+
200 check — assert a canvas element exists and receives non-zero pixel data after loading
122+
a ROM. `pages.yml`'s existing verification only checks the build/deploy steps succeed;
123+
this is the gap that let the stub ship unnoticed since `v0.1.0`.
124+
- [ ] `pages.yml` updated if the trunk build target/feature selection needs to change.
125+
126+
**Dependencies:** none beyond what already exists (`emu.rs::framebuffer()`, existing keyboard/
127+
ROM-load native code as the porting reference)
128+
**Reference:** `../RustyNES/crates/rustynes-frontend/src/wasm.rs` (538 lines, the port source);
129+
`docs/frontend.md` §wasm
130+
**Estimated complexity:** L
131+
132+
---
133+
134+
### T-81-006 — The real wasm frontend, stage 2: `wasm-winit` unification
135+
136+
**Description:** route the wasm build through the *same* `App`/`ApplicationHandler` native
137+
already uses, replacing the stage-1 canvas-2D path as the default (`wasm-winit` is already the
138+
default feature in `Cargo.toml`), via `winit::platform::web::EventLoopExtWebSys::spawn_app` +
139+
an `EventLoopProxy` delivering `RomLoaded`/`GfxReady`-style events in from JS — the exact shape
140+
RustyNES's `wasm_winit.rs` (254 lines) already proves works, ported not invented. Requires
141+
un-gating `app.rs` and `audio.rs` from their current `#[cfg(not(target_arch = "wasm32"))]`
142+
exclusion and adapting them for wasm32 (swap `cpal` for Web Audio behind a conditional path,
143+
gate out native-only deps — `gilrs`, `directories`, direct `std::fs`). This is the real work;
144+
stage 1 (T-81-005) deliberately avoids needing it so a working demo ships sooner.
145+
146+
**Acceptance criteria:**
147+
148+
- [ ] The same `App`/egui UI (menu bar, Settings, debugger panels once T-81-001 lands) renders
149+
in the browser via wgpu, not just the stage-1 canvas blit.
150+
- [ ] `app.rs`/`audio.rs` compile and run for both native and `wasm32` targets from the same
151+
source, matching RustyNES's own "`ApplicationHandler` impl serves both native and wasm32"
152+
pattern.
153+
- [ ] Verified with the same real headless-browser check as T-81-005, re-run against the
154+
winit/wgpu path.
155+
- [ ] With `wasm-canvas` (not `wasm-winit`) selected instead, the stage-1 MVP still builds and
156+
works — the two paths stay independently functional, matching the manifest's existing
157+
"exactly one is compiled" comment.
158+
159+
**Dependencies:** T-81-005 (ships the interim working demo first); `Board: Send` is NOT required
160+
here (that's `v1.0.0`'s native `emu-thread` prerequisite, a separate, unrelated flag)
161+
**Reference:** `../RustyNES/crates/rustynes-frontend/src/wasm_winit.rs` (254 lines, the port
162+
source); `../RustyNES/crates/rustynes-frontend/src/app.rs`'s `ApplicationHandler` doc comment
163+
**Estimated complexity:** L
164+
165+
---
166+
97167
## Sprint review checklist
98168

99169
- [ ] All tickets checked off or explicitly deferred (with reason).
100170
- [ ] Every instrumentation feature is off by default + byte-identical when off.
171+
- [ ] The live wasm demo actually renders and is playable, verified by a real headless-browser
172+
check, not just an HTTP status check.
101173
- [ ] CHANGELOG.md updated.

0 commit comments

Comments
 (0)