← All decisions

Per-entity render color is tier-driven, single source of truth in tier.color, sent to the shader as a vec4 uniform

merge-gamerenderingwebgputier

Context

Phase 3 of the approved merge-puzzler plan: every entity was rendering as a hardcoded solid red circle (render_ffi.mjs's fragment shader returned a literal vec4<f32>(1.0, 0.0, 0.0, 1.0)), with no way to tell tiers apart visually -- essential once entities of six different tiers coexist on screen simultaneously.

Options considered

Duplicate a tier -> color lookup table directly in render_ffi.mjs (JS), keyed off each entity's own .tier field (already present on every entity object reaching the FFI layer) — rejected
Would violate this project's standing FFI-minimization rule (decision 0016): a tier/color mapping is ordinary data, not irreducible WebGPU/browser bridging, and duplicating it in JS creates a second, untyped, un-gleam-test-covered place for tier colors to drift out of sync with glemy/pe/tier.gleam's tier.color, the single already-tested source of truth (added in Phase 2).
Compute each entity's color in Gleam via tier.color(e.tier), pass the resulting list into the FFI layer positionally alongside entities, and have the shader read it as a per-draw-call uniform — chosen
Keeps tier.color as the only place tier -> color is decided, fully gleam test-covered (glemy/pe/tier_test.gleam) independent of any GPU machinery. render.gleam's public API (render_entities_to_bytes/render_entities_to_canvas) stays unchanged; the color list is threaded through a private *_raw layer exactly like render_entities_to_canvas_raw already threads padding metadata.

Decision

render.gleam gained a private tier_colors(entities) -> List(#(Float, Float, Float)) helper (list.map over tier.color(e.tier)), computed once per render call and passed as a new positional parameter into both render_entities_to_bytes_raw and render_entities_to_canvas_raw (renderEntitiesToBytes/renderEntitiesToCanvasRaw in render_ffi.mjs). render_ffi.mjs gained a 5th uniform binding (binding 4, FRAGMENT-stage visibility) of type vec4<f32> (not vec3 -- WGSL's uniform address-space alignment requires vec3<f32> to be sized/aligned as 16 bytes anyway, so vec4 sidesteps a real padding footgun for no extra cost, alpha always 1.0). drawEntities zips entities with their same-length-by-construction colors array by index, building one color uniform buffer per entity alongside the existing center/radius buffers. fs_main now returns the color uniform directly instead of the hardcoded red literal.

Verification

140 JavaScript tests passing (up from 139 pre-Phase-3), fully additive: one new dedicated test (render_entities_to_bytes_uses_each_entitys_own_tier_color_test) renders two entities of two different tiers (0 and 3, both exact 0/255 RGB byte values so there's no float-to-byte rounding ambiguity) in a single draw call, positioned far enough apart not to touch, and asserts the correct, distinct color lands at each entity's own known texture pixel -- real GPU execution via Deno's native WebGPU (OffscreenCanvas), not mocked. Every pre-existing pixel-red assertion across render_test.gleam and game_test.gleam still passes unchanged, since tier 0's color is pinned to exactly (1.0, 0.0, 0.0) (established in decision 0017/0019's tier_test.gleam). Manual eyeball verification via the dev server was not performed: this project's headless-browser harness (tools/browser_check.ts) already documents, from its own prior investigation, that WebGPU canvas presentation pixel readback does not work under headless Chromium on Linux (upstream limitation, https://github.com/gpuweb/gpuweb/issues/1781) -- the new real-GPU-readback test above is a strictly stronger verification of exact byte-level color correctness than an eyeball check would have been.

Consequences

Any future entity property that needs to reach the shader per-entity (not just color) now has a proven pattern to follow: compute it in Gleam from typed entity/tier data, thread it through the *_raw FFI layer as a new positional list parameter, zip it with entities in drawEntities. tier.color remains the only place to edit to retune any tier's visual appearance.

References