All three were debts taken on knowingly, with the reason recorded at the time. Writing them down so that picking one up does not mean reconstructing why it exists — each gets the measurement that justifies it, a concrete approach naming the actual files and lines, the existing gate that says when it is done, and the wrong fix it must not reach for. 1. Four scenes (Classic Wave, Silk Ribbon, Kaleido Tunnel, Slow Orb) declare the style trait and honour it with `col += sigGrain(uv)` and nothing else, so they cannot decline the track's grain the way other scenes now can. Each already has an edge-weight or softness knob to route u_sigLine and u_sigSoft into. Parked at texture: 0.35, which keeps grain on the library's cleanest scenes purely to keep a gate green. 2. Curl Flow, Signal Decay and Circuit Bloom have no parameter that changes their structure — 1.31x, 1.21x and 1.10x against a 1.25x bar. They are statistically identical everywhere and at all times, which is what "it looks the same for five minutes" means technically. Needs structure, not motion; they already move too much. 3. Horizon Lines reports a 2/255 determinism delta when phase 7 runs alone and passes in a full run. Invisible as an image, but a gate whose verdict depends on preceding GPU load will eventually stay green through a real regression on the one property this whole project is built on. Prime suspects are named: 40 accumulated line terms, and a smoothstep edge as thin as 0.002 evaluated on a cancelling subtraction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
9.2 KiB
Side quests
Three follow-ups left behind by Epic 2. None of them blocks anything; all three are debts that were taken on knowingly, with the reason recorded at the time. They are written up here so that picking one up does not require reconstructing why it exists.
Each is independent. Each has an existing gate that will tell you when it is done.
| # | quest | size | what it unblocks |
|---|---|---|---|
| 1 | Four scenes express style only through grain |
small | lets them opt out of grain entirely |
| 2 | Three scenes have no structure a parameter can move | medium, per-scene | the open half of EPIC-2 §3.4 |
| 3 | Horizon Lines determinism flake | small, investigative | a gate that currently depends on GPU load |
1. Four scenes whose only art direction is grain
What is wrong
Classic Wave, Silk Ribbon, Kaleido Tunnel and Slow Orb all declare the style
personality trait. A scene that declares a trait is promising it visibly honours it — that is a
contract, not a hint, because the disqualification rule in Personality.js is the only thing
keeping off-design scenes out of a track.
These four honour it with exactly one line:
col += sigGrain(uv);
That is the whole of their art direction. So when the grain work (commit 14d1204) let a scene
decline the track's surface grain with texture: 0, these four could not take it: Phase 9's
"every declared trait visibly changes the scene that declares it" measured their style response
at exactly 0. They were parked at texture: 0.35 as a stopgap, which is the wrong shape of
fix — it keeps grain on scenes that would look better clean, purely so a gate stays green.
Why it matters
The user's original complaint was that grain was everywhere. Three of these four are the library's cleanest, softest scenes — a drifting orb, a silk ribbon, a ring wave. They are exactly the scenes that should be able to say "not on me".
The approach
Each already has a parameter that is an edge-weight or a softness knob. The work is routing
u_sigLine and u_sigSoft into it, so the track's hand shows in the line quality rather than
in the dirt. Concretely, per scene:
- Classic Wave —
src/scenes/shader/classic-wave.js:50already hasv = mix(v, smoothstep(0.2, 0.8, v), u_softness). Widen or narrow that smoothstep byu_sigSoft, and letu_sigLinedrive how hard the ring crests read. - Silk Ribbon —
:61isexp(-best * best / (u_thickness * u_thickness)). The strand width isu_thickness(range0.008..0.08); scale it byu_sigLine, and putu_sigSofton the falloff exponent so a soft-handed track gets a haze and a sharp one gets a filament. - Kaleido Tunnel — draws its grid with
smoothstep(0.42, 0.0, ringLines). That constant is a line weight with no name. Drive the threshold fromu_sigLineand the smoothstep width fromu_sigSoft. - Slow Orb —
:47issmoothstep(u_softness * 0.5, -u_softness * 0.5, d), the body edge. Foldu_sigSoftinto it, and consider asigEdge(d)rim so the shape reads in the track's hand rather than only in its own.
Then set texture: 0 on the ones that genuinely want to be clean — probably all four, but that
is a judgement call to make while looking at them, not now.
Done when
- Phase 9's trait check reports a non-zero style delta for all four with
texture: 0. npm run lint:scenesstill finds style evidence in each shader (it greps forsigEdge/sigGrain/sigFolded/u_sigLine/u_sigSoft/u_sigTexture/u_sigFold).- The Phase 11 grain checks are unaffected — this quest must not put grain back.
Do not
Do not solve this by dropping style from their traits arrays. That would pass every gate and
would shrink the library for every style-signature track, which is the opposite of the point.
2. Three scenes with no structure to move
What is wrong
EPIC-2 §3.4 gave scenes a slow axis: a declared parameter walked one way across the whole track, so the frame at four minutes is not the frame at thirty seconds. It works — Moiré Grid 3.93×, Gate Corridor 2.88×, Truchet Fold 1.40×, measured as axis-driven structural change against what the scene does on its own.
For three scenes it cannot work, because no parameter changes their structure:
| scene | best parameter | ratio |
|---|---|---|
curl-flow.js |
veins |
1.31× |
signal-decay.js |
traces |
1.21× |
circuit-bloom.js |
cells |
1.10× |
Sweeping any parameter through most of its range barely moves the ten-second time-averaged image. That is the technical statement of "it looks the same for five minutes": these scenes are statistically identical everywhere and at all times. They churn — every pixel moving, nothing developing — and the eye adapts in about two seconds.
Why it matters
Churn is the failure that survives every other fix. Cut rhythm, colour movement and framing all change what surrounds the image; none of them change the fact that the image itself has nothing to find after two seconds.
The approach
This is genuinely shader work and it is not mechanical — that assessment was wrong when EPIC-2 was first written and is corrected in §3.4. Each scene needs a persistent large-scale structure that a parameter can then move. Sketches, one per scene, to be argued with rather than followed:
- Curl Flow is a curl-noise field at one scale, so it looks the same everywhere by construction. Give it a low-frequency term that varies across the frame — a density gradient, a region where the flow stalls, a dominant current — and put the parameter that positions or scales that on the axis.
- Signal Decay already quantises to an era grid (
floor(u_barPhase * 8.0)), which is good rhythm and no structure: every trace is the same kind of trace. Consider making traces differ from one another — a hierarchy, one dominant channel, a slow reordering — so the arrangement can change over minutes. - Circuit Bloom is a uniform grid of cells with per-cell hashes. The
reachterm already gates growth by distance from the centre; that is the seed of a structure. Let the board grow, reroute or densify along a slow axis instead of being fully populated from frame one.
Then declare the parameter with slowAxis: true in the params block.
Done when
Phase 11's a declared slow axis actually changes the scene holds the scene at 1.25×, and
its companion check still reads ~1.00× with the axis disabled. Run:
http://localhost:5180/checks.html?phase=11&slow=1
Do not
Do not add slowAxis: true before the shader can back it up — the gate will fail, correctly.
And do not reach for more motion: these scenes already move too much. The missing thing is
structure, which is the opposite of motion.
3. The Horizon Lines determinism flake
What is wrong
Phase 7's every scene is deterministic renders each scene twice and requires a max channel
delta of ≤ 1/255. Horizon Lines reports 2, but only sometimes:
http://localhost:5180/checks.html?phase=7&slow=1 # FAILS — delta 2
http://localhost:5180/checks.html?slow=1 # PASSES — delta ≤ 1
Verified present on f050eaa, so it predates Epic 2. The difference is how much GPU work ran
before it: in a full suite, phases 0-6 warm the device first.
Why it matters
Not for the image — 2/255 is invisible, and PLAN.md §1 already accepts 1/255 as the floor the hardware offers. It matters because a gate whose verdict depends on preceding load will eventually stay green through a real determinism regression, and determinism is the single architectural constraint this whole project is built on. A flaky guard on the load-bearing property is worse than no guard, because it is trusted.
The approach
Two steps, in order.
First, find out why this scene and not the other 41. The suspects are in
src/scenes/shader/horizon-lines.js:
- The loop accumulates up to 40 line contributions into
col. Summing many small terms is where float ordering shows up, and no other scene in the library sums this many. float line = smoothstep(u_thickness * (0.5 + u_sigLine), 0.0, d)withu_thicknessas low as 0.002. That is a razor-thin edge: a pixel sitting on it is decided by the last bit ofd, andd = abs(p.y - y)is a subtraction of two nearby numbers — cancellation, right where the result is most sensitive.exp(-d * 26.0)on the samed.
If reformulating the line term stabilises it — accumulating in a saturated or normalised form, or widening the minimum edge so no pixel sits exactly on a discontinuity — that is the real fix and it costs nothing visually.
Only if it cannot be made stable, fix the check instead: either warm the GPU to a known state before measuring, or raise the tolerance with the reason written into the comment that already explains the 1-LSB rationale. Either is defensible; silently raising the number is not.
Done when
?phase=7&slow=1 passes in isolation, repeatedly, on a cold device.
Do not
Do not raise the tolerance as the first move. The whole value of this check is that a real bug scores in the tens or hundreds while the hardware floor is 1 — that separation is what makes it worth having, and widening it without understanding the cause spends it for nothing.