music-video-gen/flow-state/SIDE-QUESTS.md
Dejvino 8dfd8392f3 Write up the three side quests Epic 2 left behind
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>
2026-08-06 16:37:12 +02:00

9.2 KiB
Raw Blame History

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 Wavesrc/scenes/shader/classic-wave.js:50 already has v = mix(v, smoothstep(0.2, 0.8, v), u_softness). Widen or narrow that smoothstep by u_sigSoft, and let u_sigLine drive how hard the ring crests read.
  • Silk Ribbon:61 is exp(-best * best / (u_thickness * u_thickness)). The strand width is u_thickness (range 0.008..0.08); scale it by u_sigLine, and put u_sigSoft on 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 from u_sigLine and the smoothstep width from u_sigSoft.
  • Slow Orb:47 is smoothstep(u_softness * 0.5, -u_softness * 0.5, d), the body edge. Fold u_sigSoft into it, and consider a sigEdge(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:scenes still finds style evidence in each shader (it greps for sigEdge / 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 reach term 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) with u_thickness as low as 0.002. That is a razor-thin edge: a pixel sitting on it is decided by the last bit of d, and d = 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 same d.

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.