← All decisions

Present the game runner's frames via Canvas2D putImageData, not a WebGPU canvas context

renderinggame-loopwebgpucanvas

Context

Building glemy/game.gleam, the requestAnimationFrame runner tying io->pe->render together (development-plan.md, section 4), required deciding how the pixels glemy/render already produces actually reach a visible <canvas> element. The plan's original section 6 anticipated cs/render eventually handing the runner a GPU-resident buffer to present directly via a WebGPU-configured canvas context (context.configure, getCurrentTexture, format negotiation). That path is real but untested anywhere in this project so far, and this sandbox's headless Chromium has no GPU adapter at all (confirmed separately, see decision 0011), making it impossible for an agent in this environment to visually verify a new, unproven WebGPU-canvas code path before committing it.

Options considered

WebGPU canvas context: context.configure() + present render's output as a native swapchain texture — rejected
The eventually-correct, most performant path (per section 6), but a genuinely new, unverified surface (context format negotiation, presentation timing) with no way for an agent in this sandbox to visually confirm it works, and no existing tested code to build on -- committing it now would mean shipping unverified browser-only code on top of already-unverified browser-only code.
Canvas2D putImageData, blitting render_entities_to_bytes's existing RGBA byte array — chosen
Reuses a rendering path that is already fully tested (test/glemy/render_test.gleam, real GPU execution under Deno) end to end up to the byte array; the only new code is a single, extremely well-established browser API call (ImageData/putImageData, verified against MDN) with a hard length precondition (width*height*4) that the existing tests already guarantee is met. Zero new WebGPU surface area.

Decision

glemy/game_ffi.mjs's drawToCanvas blits render_entities_to_bytes's RGBA byte array onto a plain 2D canvas (canvas.getContext('2d').putImageData). No WebGPU canvas context is configured. glemy/render is unchanged; it still only produces an offscreen-texture readback.

Verification

Confirmed the ImageData(Uint8ClampedArray, width, height) constructor's exact contract (data type, argument order, width*height*4 length requirement) against MDN before writing the FFI. The full module/FFI import graph (index.html -> game.mjs -> game_ffi.mjs -> render.mjs -> ...) was loaded in a real (headless) Chromium via Playwright against a live deno file-server: zero uncaught exceptions, zero failed resource loads, canvas correctly sized from render.texture_width/height. Actual pixel output was not visually confirmed (no GPU adapter available in this sandbox, see decision 0011) -- that remains a manual step for a real GPU-capable browser.

Consequences

Every frame still does a full GPU-to-CPU-to-GPU(canvas) roundtrip -- fine for a small (64x64) offscreen texture and a simple demo, but the documented future optimization (section 6: GPU-resident STORAGE|VERTEX buffers shared between cs and render, presented without a CPU roundtrip) remains open and is now more clearly motivated: it would also mean switching this file from Canvas2D to a WebGPU canvas context. Revisit only once frame-rate/latency is an actual, measured problem, not preemptively.

References