Skip to content

SH-054: the shooter runs in the browser (WASM + WebGPU)#38

Merged
proggeramlug merged 7 commits into
mainfrom
feat/sh-054-web-port
Jul 17, 2026
Merged

SH-054: the shooter runs in the browser (WASM + WebGPU)#38
proggeramlug merged 7 commits into
mainfrom
feat/sh-054-web-port

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

./tools/build-web.shdist/web → the full game in a WebGPU browser: menus, pointer-locked third-person combat, physics, water, forest, HUD — ~59 FPS in headless Edge, 0 console errors.

Stacked on #37 (base = feat/sh-052-living-soundscape); retarget to main after that merges. Requires engine PR Bloom-Engine/engine#118 and the Perry wasm-codegen fixes (local branch fix/wasm-new-array-sizedsix codegen divergences found by this port).

Game changes (native/iOS unchanged — verified)

  • Main loop → runGame(gameFrame): on native, runGame IS the old blocking while loop — a COMBATSHOT regression run produces identical combat numbers and exits cleanly at frame 260 through the closeWindow() calls that replaced bare breaks. On web the glue drives the same callback from rAF.
  • WEB platform const guards: process.argv, the boot splash/outro spin loops (synchronous frames never composite in a browser), fixed web render profile (SSGI/SSR off, scale 0.65).
  • stageModelsSync on web: Perry's parallelMap workers are separate wasm instances with no engine FFI.
  • terrain.wgsl: tri_normal samples with explicit gradients — browser WGSL uniformity analysis rejects implicit-derivative sampling in the layer-select branch. Identical mip math everywhere. @invariant on material clip positions.
  • Tooling: tools/build-web.sh (wasm-pack + perry --target web + splice + asset manifest for the glue's prefetch cache), tools/web-check.ts (headless-Edge boot check), tools/web-play-check.ts (drives PLAY, walks, fires), tools/web-anim-check.ts (walk-cycle capture — a skinned model on the wrong joint offset holds one pose forever).

Fixed since first review

The three the player noticed were all engine-side (see #118): the scene was ~a stop dark (non-sRGB canvas + a tonemapper that relies on hardware sRGB encode), trees rendered as torn opaque sheets (draw_model_rotated still on the pre-round-5 immediate path — no alpha cutout), and characters animated wrongly (draw_model never routed skinned models to the skinned cached draw, so every primitive read joint offset 0 — another model's matrices). A Perry bug compounded the trees: (a|r|g|b) >>> 0 returned the signed value, so every model tint crossed the FFI negative, Rust's saturating as u32 floored it to 0, and the canopies discarded themselves.

Known follow-ups

  • Em dash in the arena name renders as mojibake (string encoding in the FFI text path).
  • Boot is a synchronous ~15 s after the JS-side asset prefetch; loading-bar frames don't composite (browser semantics) — EN-032 async loading is the real fix.
  • Arena switching writes level.txt to localStorage but needs a page reload on web.
  • Web perf pass (currently the mobile-adjacent profile, unmeasured).

perry compile --target web + the engine's bloom_web.wasm, same
two-module architecture jump ships. tools/build-web.sh assembles
dist/web (engine pkg, spliced HTML, assets + prefetch manifest,
vendored JoltPhysics.js); tools/web-check.ts and web-play-check.ts
drive headless Edge for boot/gameplay verification.

Game changes, all guarded so native/iOS behave exactly as before:

- The main loop is runGame(gameFrame) now. On native runGame IS the
  old blocking while loop (verified: COMBATSHOT runs identical numbers
  and exits at frame 260); on web the JS glue drives the same callback
  from requestAnimationFrame, because a browser cannot host a blocking
  loop. Harness/quit exits that used to `break` call closeWindow().
- WEB platform const (getPlatform() === 7) in input.ts. Guards:
  process.argv (no argv in a page), the boot splash/outro spin loops
  (synchronous frames never composite in a browser), settings
  persistence stays as-is (the glue maps writeFile to localStorage).
- Web render profile: SSGI/SSR off, SSAO/shadows/bloom/TAA on,
  renderScale 0.65 - near the mobile profile until WebGPU perf is
  measured properly.
- stageModels -> stageModelsSync on web: Perry's parallelMap workers
  are separate WASM instances with no engine FFI, so worker-side
  staging reaches nothing.
- world-runtime computes its own WEB const: its module body runs at
  import time, and reading input.ts's export that early is an
  init-order ReferenceError (SH-025 lesson).
- terrain.wgsl tri_normal samples with explicit gradients
  (textureSampleGrad; dpdx/dpdy hoisted to uniform control flow) -
  browser WGSL uniformity analysis rejects textureSample inside the
  layer-select branch. Identical mip math, no visual change.

Requires the EN-063 engine branch (web FFI parity, scene-inputs
bind-group fold, glue/bridge fixes) and the perry wasm-codegen fixes
(new Array(n), namespace imports, re-export chains, ToInt32 bitwise,
per-consumer function resolution).
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@proggeramlug, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 22 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5abf44ba-d71f-48c2-9863-b45fa49a0158

📥 Commits

Reviewing files that changed from the base of the PR and between 48bf9da and fd0c603.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (19)
  • .gitignore
  • CLAUDE.md
  • assets/materials/building.wgsl
  • assets/materials/decal.wgsl
  • assets/materials/glass.wgsl
  • assets/materials/grass_instanced.wgsl
  • assets/materials/muzzle_flash.wgsl
  • assets/materials/particle.wgsl
  • assets/materials/terrain.wgsl
  • assets/materials/water.wgsl
  • package.json
  • src/boot.ts
  • src/input.ts
  • src/main.ts
  • src/world-runtime.ts
  • tools/build-web.sh
  • tools/web-anim-check.ts
  • tools/web-check.ts
  • tools/web-play-check.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/sh-054-web-port

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Ralph Kuepper and others added 6 commits July 17, 2026 02:26
Two causes: the synth wind was a FIXED bandpass x volume LFO (a static
spectrum is what the ear calls white noise), and SH-052 had dropped the
authored 40m window so the engine's long inverse-model tail spread that
hiss over the whole arena.

- gen-sfx wind v2: spectrum rides the gust (bough murmur in lulls ->
  leaf sizzle in gusts, brightness ~ gust^1.8, measured r=0.95), leaf
  flutter grains only in strong gusts, slow seeded wander, 24s loop.
  Seam verified (delta 0.23 vs body max 0.53), no clipping.
- updateWindAmbience: authored window restored on top of engine rolloff
  (full <27m, silent >45m). Engine keeps pan/absorption/close-range.

Skitter/crawl regenerated (shared seed stream, same contract as before).

Claude-Session: https://claude.ai/code/session_015wJLdkwm71jQCAqypaFeNE
Browser WGSL uniformity analysis (Tint enforces what naga waves through)
rejects implicit-derivative sampling inside tri_normal's per-fragment
layer-select branch. Take the world-position gradients in uniform control
flow and sample with textureSampleGrad — identical mip math, so no visual
change on any platform. @invariant on the material clip positions for the
same reason the engine's scene shader carries it: a position must compile
identically in every pipeline built from the shader.
…the 3D

Wind: two rebuilds could not make a spectral-only always-on synth bed stop
reading as white noise, so it is LEFT OUT - sfxWind=NO_SOUND, every wind
path early-returns as for a missing file. System stays live and dormant;
one line re-enables it when a real canopy recording lands (ASSET-TODO A3).
River/footsteps/creatures/reverb untouched.

--audiotest orbit|flyby|river: a deterministic way to HEAR the
spatialisation instead of inferring it from combat.
- orbit: a broadband voice circles the listener at 6m (pan + rear cue)
- flyby: it flies through you at ~14 m/s (doppler + front/back)
- river: free-walk, river-only, dist-to-bank on the HUD
Waves suppressed in all modes; orbit/flyby silence the river so the probe
is the single source; river mode skips the probe. Top-centre HUD readout.
Dormant unless --audiotest is passed. Verified: voice live, orbit angle
advancing, all modes + normal path boot clean.

Claude-Session: https://claude.ai/code/session_015wJLdkwm71jQCAqypaFeNE
feat: SH-052 - the living soundscape: river, wind loops, creature locomotion
@proggeramlug
proggeramlug changed the base branch from feat/sh-052-living-soundscape to main July 17, 2026 09:46
@proggeramlug
proggeramlug merged commit fab7f99 into main Jul 17, 2026
1 check passed
@proggeramlug
proggeramlug deleted the feat/sh-054-web-port branch July 17, 2026 23:04
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.

1 participant