PURPOSE

Hosts workbench-emitted live-shader components in the shipping game. Compiles a fragment GLSL string and binds its declared uniforms per frame, rendering to an offscreen WebGL2 canvas that the main Canvas 2D pipeline composites via drawImage — the same pattern used by fire-engine.ts and nebula-engine.ts. Reserved for 1-3 hero effects (beams, chain lightning, cone fire) at roughly 0.3-0.5 ms per instance on iPhone 12-class mobile at half-res. Not for every projectile.

OWNS

  • LiveShaderRuntime class — one instance per registered live-shader component. Owns its own HTMLCanvasElement, WebGL2RenderingContext, compiled WebGLProgram, WebGLVertexArrayObject, WebGLBuffer, and a cache of WebGLUniformLocation lookups keyed by uniform name.
  • The shared vertex shader (SHARED_VS) — fullscreen triangle strip with a_pos attribute and v_uv passthrough varying, GLSL ES 3.00.
  • The shared quad vertex buffer (QUAD_VERTICES) — four-vertex triangle strip in clip space.
  • The module-level runtime registry (_runtimes Map keyed by component id) and the idempotency flag _registryLoaded.
  • The default scale factor constant DEFAULT_LIVE_SHADER_SCALE (0.5 — half-res offscreen target).
  • The local shader compile pipeline (compileProgram, compileShader) — throws on compile or link failure with id-tagged error messages.

READS FROM

  • /src/starship-survivors/data/vfx/live-shaders/manifest.json — fetched once at boot by initLiveShaderRuntimes(). Each LiveShaderManifestEntry provides componentId, fragGlsl, blendMode, plus metadata (name, category, uniformSchema, previewSampleInputs, vertGlsl, estCostMs, anchors, shippedAt).
  • Caller-supplied per-frame uniform values via setUniform / setUniforms.
  • Caller-supplied viewport dimensions via resize(screenW, screenH).
  • Caller-supplied time scalar via render(time).

PUSHES TO

  • Each runtime’s own offscreen HTMLCanvasElement — sized to screenW * scale by screenH * scale, cleared to transparent RGBA(0,0,0,0) every render() call. The canvas is exposed via getCanvas() so the caller can ctx.drawImage(canvas) it onto the main Canvas 2D surface.
  • WebGL2 uniforms on the bound program — u_time (float) and u_resolution (vec2) are always pushed by render(); arbitrary caller uniforms via setUniform use uniform1f for numbers and uniform2fv / uniform3fv / uniform4fv for arrays based on length.

DOES NOT

  • Does not composite onto the main canvas itself — the caller is responsible for the drawImage step.
  • Does not own the main game canvas or its 2D context.
  • Does not parse or validate GLSL beyond what the WebGL2 driver reports — compile and link errors are surfaced as thrown Error objects with id-tagged messages.
  • Does not pre-create uniform locations from the manifest’s uniformSchema — locations are looked up lazily on first setUniform call and cached.
  • Does not enforce the manifest’s declared uniform types — setUniform infers vector size from JS array length only.
  • Does not provide a Canvas 2D fallback path itself. If init() returns false (WebGL2 unavailable or shader compile failure), the caller must skip the live shader entirely.
  • Does not free or recreate the program on resize — only the offscreen canvas dimensions and the GL viewport are updated.
  • Does not load or compile custom vertex shaders — the manifest’s vertGlsl field is ignored; every runtime uses SHARED_VS.

Signals

  • init() returns booleanfalse when canvas.getContext('webgl2', ...) yields null or when compileProgram throws. On failure, a console.warn is emitted with the runtime id and the underlying error.
  • isReady() returns true only after init() has succeeded and neither the GL context nor the program has been destroyed.
  • initLiveShaderRuntimes() returns the count of successfully registered runtimes. Returns 0 on manifest fetch failure or non-ok HTTP response; returns the cached size on repeat calls (idempotent via _registryLoaded).
  • getLiveShaderRuntime(id) returns the runtime or null when the id is not registered.
  • getAllLiveShaderRuntimes() returns a fresh array snapshot of all registered runtimes.
  • destroy() deletes the program, VBO, and VAO via the GL context, then nulls the local references. Subsequent render() / setUniform calls become no-ops because they early-return when gl or program is null.

Entry points

  • LiveShaderRuntime class constructor — (id, fragSource, blendMode, scale = DEFAULT_LIVE_SHADER_SCALE). Creates the canvas element but does not touch WebGL until init() is called.
  • LiveShaderRuntime.init() — boots the GL context (alpha: true, premultipliedAlpha: true, no antialias/depth/stencil), compiles and links the program, sets up the VAO/VBO with the shared quad, configures the blend func (ONE, ONE for additive; ONE, ONE_MINUS_SRC_ALPHA for alpha).
  • LiveShaderRuntime.resize(screenW, screenH) — must be called before render whenever the viewport changes.
  • LiveShaderRuntime.setUniform(name, value) / setUniforms(values) — bulk setter wraps the single setter.
  • LiveShaderRuntime.render(time) — clears, sets u_time + u_resolution, draws a 4-vertex triangle strip.
  • LiveShaderRuntime.getCanvas() — handle for ctx.drawImage compositing.
  • LiveShaderRuntime.destroy() — releases GL resources.
  • initLiveShaderRuntimes() — module-level async boot entry. Idempotent.
  • getLiveShaderRuntime(id) / getAllLiveShaderRuntimes() — module-level registry accessors.
  • Exports: LiveShaderUniformDecl (interface), DEFAULT_LIVE_SHADER_SCALE (const), LiveShaderRuntime (class), initLiveShaderRuntimes, getLiveShaderRuntime, getAllLiveShaderRuntimes.

Pattern notes

  • Mirrors fire-engine.ts / nebula-engine.ts: offscreen GL canvas + per-frame ctx.drawImage composite onto the main Canvas 2D surface. The main pipeline stays Canvas 2D; live shaders are scoped composite layers.
  • Half-res offscreen target (scale = 0.5) is the documented sweet spot for soft effects — beams, fire, chain lightning. The scale is per-runtime and constructor-injected, so individual hero effects can override it.
  • Fragment-only authoring: the manifest carries a vertGlsl field but the runtime always uses the shared fullscreen-quad VS. Authors only ship the fragment shader.
  • Shared uniform contract with the workbench preview surface: u_time (float, seconds) and u_resolution (vec2, canvas pixels) are guaranteed every frame. All other uniforms are author-defined and pushed by the caller.
  • Uniform location cache uses undefined vs. null to distinguish “not yet looked up” from “looked up and program does not declare it” — once null is cached, subsequent sets early-return cheaply.
  • Vector type is inferred from JS array length, not the manifest’s uniformSchema. Length 2/3/4 map directly; anything else falls through to uniform3fv as a default, which silently mismatches GLSL types — authors are expected to send the right shape.
  • Blend mode is fixed at construction. Additive uses (ONE, ONE) for HDR-style glow accumulation; alpha uses (ONE, ONE_MINUS_SRC_ALPHA) assuming premultiplied alpha (matches the premultipliedAlpha: true context attribute).
  • Registry is module-scoped singleton state with a fetch-once boot pattern. The manifestUrl is a hard-coded absolute path (/src/starship-survivors/data/vfx/live-shaders/manifest.json) — assumes a dev/Vite-style URL layout served from the repo root.
  • Manifest fetch failures (network error, non-OK status, malformed JSON) are swallowed and return 0 — there is no telemetry hook for “live shaders failed to load.” The caller path is expected to fall back automatically because no runtimes get registered.
  • Per-runtime init() failures log via console.warn (not telemetry). Failed runtimes are simply skipped and never added to the registry — getLiveShaderRuntime(id) will then return null for them, and callers must treat that as the fallback signal.
  • destroy() is provided but the boot path never calls it. Runtimes are assumed to live for the full session.
  • Spec reference: 2026-04-20 §10 B (lifecycle), live-shaders data dir as the authored source. Companion authoring directory is /src/starship-survivors/data/vfx/live-shaders/ (includes dynamic_cone_fire.live.ts).