Phase 7: grow the scene library to 16

Ten new scenes, with 'minimal' first: the family was empty, so intros and
breakdowns fell through to flow/organic and every track opened at full
density. Quiet sections now land on a restful family 48/48 times across 24
seeds, 28 of them minimal.

New: Horizon Lines, Spectrum Sculpture, Slow Orb (minimal); Curl Flow
(flow); Plasma Bloom, Metaballs (organic); Kaleido Tunnel, Moiré Grid
(geometric); Ridge Terrain (structural); Scan Tear (glitch).

Four real bugs, three of which the existing gates could not have caught:

1. SHADER PROGRAMS LINK ASYNCHRONOUSLY. three.js uses
   KHR_parallel_shader_compile, so draws against an unlinked program render
   wrong. The heaviest scene had its first TEN frames differ from every
   later render of the same frames. Preview hides this entirely; export
   renders each frame once, so those frames would ship broken. Added
   Engine.prime() — WebGLRenderer.compile() plus a discarded warm frame —
   and the exporter now primes before encoding. Rendering a throwaway frame
   and reading it back is NOT sufficient; measured, it left 3-5 frames wrong.

2. Moiré Grid declared a param on u_width, which the shader contract already
   uses for stereo width. GLSL redefinition, and the only symptom was a
   black frame. Lint now rejects any param uniform colliding with the
   contract.

3. Spectrum Sculpture strobed at 4 flashes/s. Two causes: rotation measured
   in turns meant bar-crossing frequency was bars x rate (82 bars put a
   slow-looking 0.12 turns/s at 10 Hz), and hard band-tier boundaries made
   every bar switch band simultaneously. Rotation is now in segment units so
   the rate IS the crossing frequency, bands interpolate, and the range is
   capped where the flash meter measures zero.

4. Particle Field was being chosen as a primary background despite being
   mostly empty by design. Scenes now declare role: 'accent'; those are
   never primary and are judged on variance rather than luminance.

Three checks were themselves wrong and were rebuilt: mean-distance metrics
unfairly fail sparse scenes for being tasteful rather than static, so
"animates" and "no duplicates" now use max channel delta.

PLAN.md §1 gains two refinements: programs must be primed before the first
frame, and even same-machine the heaviest shaders vary by one LSB under
differing GPU load — so the per-scene criterion is max delta <= 1 rather
than an identical hash. A real bug scores in the tens there.

Full suite 67/67 across all seven phases. Worst 4K frame 3.8ms,
worst flash rate 0/s, worst determinism delta 1/255. Adds README.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Dejvino
2026-08-05 18:40:56 +02:00
co-authored by Claude Opus 5
parent 8a94a3f3a5
commit 9d15c3cf49
24 changed files with 1234 additions and 10 deletions
+20
View File
@@ -85,6 +85,24 @@ unrealistic, and chasing it would be wasted effort. The cross-machine guarantee
*perceptual*: frames must match within a small diff threshold. The validation checks in §11
are written to that distinction, so the acceptance criteria are actually achievable.
Two refinements, both found by building it:
**Programs must be primed before the first frame.** Shader programs link asynchronously
(`KHR_parallel_shader_compile`), and a draw issued against an unlinked program produces
wrong output. Measured on the heaviest scene, the first *ten* frames rendered differently
from every later render of the same frames. Preview hides this completely — the frames go
past and the next pass is right — but an export renders each frame exactly once, so those
frames would ship broken. `Engine.prime()` compiles every program and discards a warm frame,
and the exporter calls it before encoding anything. Rendering a throwaway frame and reading
it back is *not* sufficient; `WebGLRenderer.compile()` is.
**Even same-machine, the heaviest shaders vary by one LSB.** With priming in place most
scenes reproduce byte-for-byte, but a few come back with scattered pixels differing by 1/255
— floating-point variance under differing GPU load. That is below any perceptual threshold
and is not something the hardware offers to fix, so the per-scene criterion is a max
channel delta of ≤ 1 rather than an identical hash. A real determinism bug scores in the
tens or hundreds on that metric, so the check keeps its teeth.
---
## 2. Audio analysis
@@ -317,6 +335,8 @@ difference between "seems fine" and "verified".
| **Schema lint** | Parses each scene's shader for `uniform` declarations and cross-checks against its `params` block, both directions. Catches typos and orphans; the thing that keeps a 30-scene library maintainable. |
| **Param range sweep** | Renders each param at several points across its declared range, asserts no NaN, no all-black, no all-white frames. |
| **Perf HUD** | Frame time, GPU time per layer, frame index, active section, live param values. |
| **Flash-rate meter** | Counts light-dark cycles per second against the WCAG 2.3.1 / Harding ceiling of three. Added during Phase 5 and not in the original plan — this generates beat-reactive video for publication, an unsupervised generator finds unsafe states on its own, and nobody watches every frame of every export. It caught a scene running at 7-8 flashes/s at *every* output resolution. |
| **First-render check** | Renders a frame in a fresh engine and compares against the same frame rendered later. Catches anything that is correct on repeat but wrong the first time — invisible to fresh-vs-fresh comparison, and wrong in every export, which renders each frame exactly once. Caught two separate bugs (a reused feature row, and unlinked shader programs). |
### The track battery