Phase 4: arc driver ("C" brain)
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>
This commit is contained in:
@@ -117,8 +117,13 @@ export class Compositor {
|
||||
this._buildTargets();
|
||||
}
|
||||
|
||||
/**
|
||||
* The compositor does NOT own its layers and never disposes them — the arc
|
||||
* driver caches Layer instances across sections and swaps them in and out
|
||||
* every crossfade, and disposing on removal would destroy shaders that are
|
||||
* about to be reused (and recompile them on the way back).
|
||||
*/
|
||||
setLayers(layers) {
|
||||
this.layers.forEach((l) => { if (!layers.includes(l)) l.dispose(); });
|
||||
this.layers = layers;
|
||||
return this;
|
||||
}
|
||||
@@ -261,7 +266,7 @@ export class Compositor {
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.layers.forEach((l) => l.dispose());
|
||||
this.layers = []; // owned elsewhere; see setLayers
|
||||
this.disposeTargets();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ export class Engine {
|
||||
this.compositor = new Compositor(this.renderer, { width, height });
|
||||
this.timeline = new Timeline({ fps, mode: REALTIME });
|
||||
this.featureProvider = null;
|
||||
this.ownedLayers = [];
|
||||
this.lastFrameRendered = -1;
|
||||
}
|
||||
|
||||
@@ -46,13 +47,19 @@ export class Engine {
|
||||
return this.featureProvider.at(frame) || NULL_FEATURES;
|
||||
}
|
||||
|
||||
/** Replace the stack. `specs` are { module, params, seed, opacity, blend }. */
|
||||
/**
|
||||
* Replace the stack from specs. `specs` are { module, params, seed, opacity,
|
||||
* blend }. Layers built this way are owned by the Engine and disposed with
|
||||
* it; layers supplied directly by the arc driver are owned by the driver.
|
||||
*/
|
||||
setLayerSpecs(specs) {
|
||||
this.ownedLayers.forEach((l) => l.dispose());
|
||||
const layers = specs.map((s) => {
|
||||
const layer = createLayer(s.module, s);
|
||||
if (s.palette) layer.setPalette(s.palette);
|
||||
return layer;
|
||||
});
|
||||
this.ownedLayers = layers;
|
||||
this.compositor.setLayers(layers);
|
||||
return layers;
|
||||
}
|
||||
@@ -119,6 +126,8 @@ export class Engine {
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.ownedLayers.forEach((l) => l.dispose());
|
||||
this.ownedLayers = [];
|
||||
this.compositor.dispose();
|
||||
this.renderer.dispose();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user