← All decisions

Present via a native GPUCanvasContext under Deno, not TypeScript+Playwright, for maximum type safety

type-safetywebgputestingrendergame-loop

Context

Decision 0013 established a standing rule (user-set, explicitly the highest-priority rule in this project, above implementation convenience): use every tool's available type safety to the maximum extent possible. The user pointed out this had been misapplied to tools/browser_check.ts -- TypeScript-under-Deno driving Playwright is real type safety for hand-written logic, but Playwright itself is still a third-party npm dependency with escape hatches (TypeScript's own soundness gaps: `any`, assertions, structural typing), not this project's own, stricter Gleam type system, and not native to the Deno/wgpu stack this project already trusts (decision 0008). Asked for thorough research into alternatives -- naming Deno-native WebGPU, Puppeteer-core/CDP, and 'pure Deno / headless canvas' as starting points, not an exhaustive list -- before concluding TypeScript+Playwright was actually necessary.

Options considered

Puppeteer-core (CDP-native browser automation) — rejected
Same category and same fundamental issue as Playwright-core: a third-party npm library with its own bundled TypeScript types, still requires a real Chromium binary and the same headless-WebGPU flag discovery already done for Playwright. No meaningful type-safety or dependency advantage over what was already in place -- confirmed via direct comparison research, not assumed.
Hand-rolled Chrome DevTools Protocol client over a raw WebSocket, typed against the official `devtools-protocol` npm package — rejected
Reduces the runtime dependency to a types-only package, but shifts real risk onto hand-written protocol/session-lifecycle logic that Playwright already solved and battle-tested -- the message *shapes* being typed doesn't mean the *logic* driving them is correct. Still requires the same real Chromium binary and flag discovery. Not worth building given the next option removes the need for a real-browser dependency for the bulk of what needed verifying anyway.
Deno-native OffscreenCanvas + GPUCanvasContext ("webgpu" context), zero third-party dependency — chosen
Verified directly, not assumed: this project's Deno version (2.9.5, confirmed via the authoritative docs.deno.com Web Platform APIs page) supports OffscreenCanvas with a real "webgpu" context context ("2d"/"webgl"/"webgl2" are NOT supported, requestAnimationFrame/document/HTMLCanvasElement are NOT supported -- explicitly confirmed, not guessed). A minimal end-to-end prototype (configure a GPUCanvasContext, render a solid-color pass, read back via copyTextureToBuffer) worked correctly on the first real run in this exact sandbox. This uses only Gleam's own (sounder, no `any`-equivalent) type system plus Deno's first-party, already-trusted WebGPU stack -- strictly ahead of TypeScript+Playwright on the user's stated priority order.

Decision

glemy/render gained render_entities_to_canvas(entities, bounds, canvas) -- presents directly onto a real WebGPU canvas context (a real <canvas> in a browser, or a genuine OffscreenCanvas in tests; both share the same context surface, exposed to Gleam as an opaque Canvas type) instead of glemy/game's previous offscreen-texture-plus-Canvas2D-putImageData roundtrip. glemy/game.gleam split into tick_and_render (the real per-frame logic: spawn, physics, render/present -- now fully covered by `gleam test --target javascript` using a real OffscreenCanvas, no browser needed) and a thin requestAnimationFrame scheduling wrapper (loop/start) around it, which remains the one piece with no automated coverage, since Deno genuinely has no RAF -- confirmed, not assumed. tools/browser_check.ts's scope shrank to match: it no longer verifies rendering correctness (that's gleam test's job now) or reads canvas pixels at all (headless Chrome cannot present a WebGPU canvas to its compositor on Linux -- a real, documented upstream limitation discovered while adapting the script, gpuweb/gpuweb#1781, no known flag fixes it), instead directly counting real GPUCanvasContext.configure/GPUDevice.createBuffer calls to confirm the RAF loop runs and click-to-spawn grows the real entity count.

Verification

Prototyped the OffscreenCanvas+webgpu approach standalone before committing to the refactor: real adapter/device, real render pass, real copyTextureToBuffer readback, correct pixel value read back on the first attempt. render_test.gleam gained 4 new tests (including one proving render_entities_to_bytes and render_entities_to_canvas produce byte-identical output for the same scene, and one specifically exercising the WebGPU row-alignment-padding logic a canvas's arbitrary width needs that the old fixed-64-wide offscreen texture never had to). game_test.gleam gained 2 new tests covering tick_and_render end-to-end (physics + real rendering) with zero Playwright involved. Full suite: 84 Erlang / 114 JavaScript tests, all passing. tools/browser_check.ts, rewritten to count real WebGPU API activity instead of reading pixels, passes against the new architecture: requestAnimationFrame fires repeatedly, the real entity count starts at 3 (game.main()'s scene) and grows to 33+ after a simulated click-and-hold.

Consequences

Playwright remains a dependency (npm:playwright-core via Deno's npm interop, still no package.json) but now only for the genuinely browser-irreducible sliver -- RAF firing, real DOM wiring, real click coordinates -- not for anything gleam test can already prove. This also resolves decision 0010's deferred performance concern (§6 of development-plan.md: GPU-resident presentation without a CPU roundtrip) as a side effect of chasing type safety, not a separate optimization pass. Future browser-only WebGPU work in this project should default to checking whether Deno's OffscreenCanvas can cover it before reaching for real-browser automation at all -- that question wasn't asked before this decision and should have been.

References