ADR-0004 — Stateless GPU particle animation for the black hole
Status: Accepted (documents the existing decision)
Context
The background renders ~300k particles at 60 fps on every page, sharing the main thread with scroll handling, Vue hydration, and the artifacts themselves. The persistent runtime (ADR-0002) also wants scene state to survive navigation trivially.
Problem
Animate hundreds of thousands of particles without per-frame CPU cost or large mutable GPU state, in a way that survives navigation and (someday) context loss.
Decision
Particles are pure functions of their seed position and time. The only mutable
inputs are uniforms (uTime, uRadius). The vertex shader (bh.vert.glsl) derives
rotation, infall (cyclic via mod), disk flattening, and point size per frame from the
static position attribute. No attribute is ever written after construction; the CPU’s
per-frame work is one uniform update and one draw call.
Alternatives considered
- CPU simulation + buffer upload: (classic
points.geometry.attributes.positionmutation) — 300k × 3 floats uploaded per frame; main-thread cost and memory churn; rejected. - GPGPU ping-pong (render-target simulation,
GPUComputationRenderer): enables history-dependent behavior (turbulence, collisions) but adds FBO state, doubles memory, complicates context restore, and nothing in the current visual needs history. Rejected for the background; right tool for a future compute-heavy artifact. - Transform feedback / WebGPU compute: same conclusion as GPGPU, plus platform constraints (see ADR-0008).
Tradeoffs
- (+) ~Zero CPU per frame; scene “state” is just elapsed time → persistence and (future) context restore are trivial (re-render at t).
- (+) Deterministic: any moment is reproducible from (seed, t) — enables offline hi-res prints (future-ideas) and perfect resume.
- (−) No history-dependent physics: particles can’t react to each other or accumulate responses. Pointer interaction must be expressed as an instantaneous field in the shader (fine for gravity-swirl effects), not as forces integrated over time.
- (−) All complexity budget lives in one increasingly dense vertex shader; needs comments and a config panel to stay tunable (performance.md R5).
Consequences
- Graphics roadmap items must be formulated as stateless functions (lensing, doppler, pointer field all qualify). Anything requiring integration (trails, turbulence memory) is out of scope for the background or becomes a separate GPGPU artifact.
- Quality tiers can use
setDrawRangebecause particle identity doesn’t matter (rendering.md R5).
Future evolution
- Per-particle
aSeedattribute (graphics.md R1) stays within the stateless contract (written once at construction). - If a history-dependent background is ever truly wanted, that’s a new ADR superseding this one — with WebGPU compute as the presumptive implementation.