Three timescales now stack: per-frame reactivity, per-section seeded LFO
drift, and whole-song scene changes with lookahead. Layer instances are
cached per section and reused across crossfades — rebuilding them per frame
would recompile shaders every transition.
Crossfades run forward from a boundary: the outgoing scene holds while the
incoming one fades in over it.
Three real bugs, each found by a check that had to be rewritten first:
1. A pop exactly at every transition. buildSlope is discontinuous by
construction (~1 before a boundary, 0 after), and the outgoing layer is
still on screen when it flips — collapsing its lookahead ramp in one
frame. It now holds the slope it had entering the boundary.
2. FeatureTrack.at() returns a REUSED row object, and _boundarySlope()
called at() again mid-render, rewriting the features the layer was about
to read. Symptom: a frame correct on every repeat and wrong the first
time — invisible to fresh-vs-fresh comparison, and wrong in every export,
since export renders each frame exactly once. Now indexes the typed array
directly, with the aliasing hazard documented on at(), and a new check
covers the whole bug class.
3. Warm-up converged to 1%, leaving a visible 0.015 difference at heavy
feedback settings. Now targets 0.1%.
Two checks were themselves wrong and were rebuilt: a raw delta threshold
and an outlier-vs-local-median test both flag beat flashes as pops, and a
control window taken from a different scene reads an ordinary busy scene as
a 9x spike. The working formulation A/Bs each boundary against the interior
of the two scenes adjacent to it.
PLAN.md §6 corrected: boundary seeks are NOT exact for free. Layer state is
re-seeded there but the feedback buffer is global and carries across.
Clearing it at boundaries would buy exactness for a visible flash at every
transition; warm-up is the better trade and applies everywhere.
Gate 9/9.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Engine core: Timeline (fixed dt, audio-mastered in realtime), seeded Rng,
Renderer, Layer/ShaderLayer/SceneLayer, Compositor with blend modes,
feedback and post chain.
Shader scenes are compiled against a fixed uniform contract and define only
`vec4 scene(vec2 uv, vec2 p)`, so adding a scene costs a shader plus a
params block. Deep Nebula ported from party-stage as the first one.
Gate passes, 7/7 in checks.html:
- 300 frames rendered twice are bit-identical
- a fresh Engine reproduces the same frames
- simulated dropped frames change nothing (proves dt is fixed)
- seek matches sequential playback
- 320x180 vs 1280x720 agree within 0.010 (limit 0.06)
- seeded rng reproducible, forked streams independent
- compositor reset clears feedback history
Static gates: no wall-clock or unseeded randomness in deterministic
directories; scene schemas and shader sources agree in both directions.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>