Skip to main content

rlx_core/milk/
mod.rs

1//! The MilkDrop runtime: compiled EEL2 programs, the machine that executes them,
2//! and the driver that turns their output into warp-mesh parameters (ADR-0113).
3//!
4//! **No `.milk` text, no HLSL and no translator is anywhere in this module.**
5//! Conversion happens ahead of time in `milkconv`, which never ships; what
6//! reaches a binary is bytecode, a stack VM, and the driver below.
7//!
8//! # The execution model, which is MilkDrop's
9//!
10//! A bundle carries three programs over **one shared register file**:
11//!
12//! 1. `per_frame_init` runs **once**, when the preset loads. It is where a preset
13//!    seeds the `q` variables and its `megabuf`.
14//! 2. `per_frame` runs **once per frame**, after the host has written this frame's
15//!    audio and clock into the input registers. What it leaves in the output
16//!    registers is the whole-mesh transform, and what it leaves in `q1`–`q32` is
17//!    the bridge to the program below.
18//! 3. `per_vertex` runs **once per mesh vertex**, starting each time from the
19//!    register state `per_frame` left — so `q1` reads the same value at every
20//!    vertex, and a write inside the program does not leak from one vertex to the
21//!    next.
22//!
23//! That third property is why [`EelProgram::written_registers`] exists: the
24//! restore is over the registers the program can actually write, not over the
25//! whole file, which at thousands of vertices per frame is the difference between
26//! a memcpy that matters and one that does not.
27//!
28//! # Rates: MilkDrop is per frame, this engine is per second
29//!
30//! **The single most consequential translation in the conversion, and it is here
31//! rather than in the converter.** MilkDrop's `zoom`, `rot`, `dx`, `dy`, `warp`
32//! and `decay` are all *per rendered frame*: a preset written on a machine
33//! running 30 fps drifts at half the speed on one running 60. This engine's
34//! vocabulary is per second throughout (ADR-0019), which is what makes a look
35//! identical on any display.
36//!
37//! So the driver converts, per frame, using the frame's own measured `dt`:
38//! a factor becomes `v^fps` and a rate becomes `v * fps`. A preset authored
39//! against MilkDrop's nominal **30 fps** therefore moves at the speed its author
40//! saw, on any refresh — and a converted preset does not have to carry the
41//! assumption in its bytecode. [`NOMINAL_FPS`] is the frame rate the `fps`
42//! *variable* reports to the program, for the same reason: a preset that reads
43//! `fps` and divides by it is compensating for a cadence, and telling it the
44//! truth about a 144 Hz display would double-compensate.
45
46// Hot-path panic-denial pragma (Plan 0002 Phase 2, extended to core/src/milk by
47// Plan 0100 Phase 2). The driver runs per vertex per frame.
48#![deny(
49    clippy::unwrap_used,
50    clippy::expect_used,
51    clippy::indexing_slicing,
52    clippy::panic,
53    clippy::unreachable
54)]
55
56pub mod bytecode;
57pub mod outputs;
58pub mod shader;
59pub mod vm;
60
61use bytecode::{EelProgram, ProgramError};
62use outputs::{
63    FrameOutputs, FrameSlots, ShapeInstance, ShapeInstanceSlots, WavePoint, WavePointSlots,
64};
65use vm::{Budget, VmState};
66
67/// The frame rate the `fps` variable reports, and the cadence a per-frame rate is
68/// interpreted against.
69///
70/// MilkDrop's own nominal rate. A `.milk` preset's `zoom = 1.01` means "1 % per
71/// frame at about this rate", and its author tuned it by eye there — so this is
72/// the number that reproduces what they saw, not the display's actual refresh.
73/// See the module docs.
74pub const NOMINAL_FPS: f32 = 30.0;
75
76/// The nine per-vertex outputs, in the order
77/// [`warp_mesh::PER_VERTEX_PARAMS`](crate::render::scenes::warp_mesh::PER_VERTEX_PARAMS)
78/// declares them — the same roster, because they *are* the same roster. A
79/// converted preset and a hand-authored `[per_vertex]` table drive one scene.
80const OUTPUT_NAMES: [&str; 9] = ["zoom", "rot", "cx", "cy", "dx", "dy", "sx", "sy", "warp"];
81
82/// Whether output `i` is a **factor** (composed multiplicatively over time, so it
83/// converts as `v^fps`) or a **rate** (`v * fps`). Positional with
84/// [`OUTPUT_NAMES`].
85///
86/// `cx`/`cy` are neither: they are a *position*, not a motion, so they pass
87/// through untouched. `false` here with a `false` in [`OUTPUT_RATE`] means that.
88const OUTPUT_FACTOR: [bool; 9] = [true, false, false, false, false, false, true, true, false];
89/// Whether output `i` is a rate — see [`OUTPUT_FACTOR`].
90const OUTPUT_RATE: [bool; 9] = [false, true, false, false, true, true, false, false, true];
91
92/// How many `q` variables bridge the main program to a custom wave or shape.
93///
94/// MilkDrop's own count. The bridge is a **copy**, not a shared file: each
95/// element has its own register space (its own `t1`-`t8`, its own working
96/// variables), and what crosses is `q1`-`q32` after the main per-frame program
97/// has run. Copying is what keeps an element from writing back into the main
98/// program's state, which the reference also forbids.
99pub const Q_COUNT: usize = 32;
100
101/// A converted preset's compiled programs: what a bundle carries beyond an
102/// ordinary Ritmolux preset.
103///
104/// Cloned into the warp mesh's structural config at load and never touched again
105/// — the runtime state lives in [`MilkRuntime`], not here, so the same bundle can
106/// drive two scenes (the roster's and a `[layer]`'s) without them sharing a
107/// register file.
108#[derive(Debug, Clone, PartialEq)]
109pub struct MilkBundle {
110    /// Run once at preset load.
111    pub per_frame_init: EelProgram,
112    /// Run once per frame.
113    pub per_frame: EelProgram,
114    /// Run once per mesh vertex.
115    pub per_vertex: EelProgram,
116    /// Up to four custom waves — extra traces the preset draws with their own
117    /// per-point programs (Plan 0100 Phase 4). **47 % of the corpus enables at
118    /// least one**, which is why they are here rather than warned about.
119    pub waves: Vec<MilkElement>,
120    /// Up to four custom shapes — filled polygons with their own per-instance
121    /// programs. **63 % of the corpus enables at least one.**
122    pub shapes: Vec<MilkElement>,
123    /// The translated MilkDrop 2 `warp` shader, as a complete WGSL fragment
124    /// module (Plan 0100 Phase 6). `None` — most of MilkDrop 1.x, and any
125    /// MilkDrop 2 preset that never wrote one — takes the engine's built-in
126    /// decay path. Validated through naga at load ([`shader::validate_wgsl`]).
127    pub warp_wgsl: Option<String>,
128    /// The translated `comp` shader — replaces the built-in present remaps when
129    /// present. See [`warp_wgsl`](Self::warp_wgsl).
130    pub comp_wgsl: Option<String>,
131    /// The deepest `GetBlur`/`sampler_blur` level either shader reaches,
132    /// `0..=3`. Zero means the blur chain never runs for this preset.
133    pub blur_level: u8,
134    /// How many levels this bundle's **feedback field** quantizes to at the end
135    /// of the warp pass (ADR-0118), defaulting to [`DEFAULT_QUANTIZE_STEPS`].
136    ///
137    /// **The presence of a bundle is what turns this on**, which is the whole
138    /// per-bundle shape: the reference's 8-bit target truncates a `decay`-scaled
139    /// dim pixel to zero and this engine's `Rgba16Float` field does not, so an
140    /// imported preset wants the emulation and a native `warp_mesh` world — which
141    /// carries no bundle and so never reaches this field — does not.
142    ///
143    /// `0.0` is off. Negative selects ADR-0118's Alternative D (floor to zero at
144    /// one step, no ladder between). Both are reachable from `[milk]
145    /// quantize_steps`, so the look gate's A/B is a preset edit rather than a
146    /// re-convert.
147    pub quantize_steps: f32,
148}
149
150/// The 8-bit feedback target every MilkDrop preset was authored against
151/// (ADR-0118): 256 levels, 255 steps between black and white.
152pub const DEFAULT_QUANTIZE_STEPS: f32 = 255.0;
153
154/// What a custom element draws, which decides which of its programs run and how
155/// its outputs are read.
156#[derive(Debug, Clone, Copy, PartialEq, Eq)]
157pub enum ElementKind {
158    /// A custom **wave**: `count` points, each from one run of `per_point`,
159    /// stroked as a polyline or scattered as dots.
160    Wave,
161    /// A custom **shape**: `instances` filled polygons, each from one run of
162    /// `per_frame` with `instance` bound.
163    Shape,
164}
165
166/// One custom wave or shape: its three programs and the structural numbers that
167/// size its geometry.
168///
169/// The *look* numbers — position, colour, radius, alpha — are **not** here: they
170/// are outputs the element's own per-frame program leaves in named registers,
171/// seeded from the file's initial conditions by a prologue the converter emits.
172/// That is the same shape the main bundle takes, and it is what keeps this struct
173/// from being forty fields of `.milk` key.
174#[derive(Debug, Clone, PartialEq)]
175pub struct MilkElement {
176    /// Run once, at preset load.
177    pub init: EelProgram,
178    /// Run once per frame — and once per *instance* for a shape, with
179    /// `instance` bound.
180    pub per_frame: EelProgram,
181    /// Run once per point. Empty for a shape.
182    pub per_point: EelProgram,
183    /// Points (a wave) or sides (a shape).
184    pub count: u32,
185    /// How many copies a shape draws. Always `1` for a wave.
186    pub instances: u32,
187    /// Which it is.
188    pub kind: ElementKind,
189    /// Past `0.5`, a wave draws dots rather than a line.
190    pub use_dots: bool,
191    /// Past `0.5`, a wave's line or a shape's outline is drawn thick.
192    pub thick: bool,
193    /// Past `0.5`, a wave adds rather than blends. See [`ElementSpec::additive`]
194    /// for why a shape's flag is not here.
195    pub additive: bool,
196}
197
198/// The most points a custom wave may draw, and the most sides a shape may have.
199///
200/// MilkDrop's own limits are 512 points and 100 sides. A wave's points each cost
201/// one run of its per-point program on the render thread, so the bound matters
202/// for the same reason the mesh grid's does — and unlike the mesh it is not a
203/// tier capacity, because it is the *preset* that names it and a converted preset
204/// should draw the figure its author drew.
205pub const MAX_WAVE_POINTS: u32 = 512;
206/// See [`MAX_WAVE_POINTS`].
207pub const MAX_SHAPE_SIDES: u32 = 100;
208/// The most copies of one custom shape a preset may draw. MilkDrop's own limit.
209pub const MAX_SHAPE_INSTANCES: u32 = 1024;
210/// How many custom waves, and how many custom shapes, the `.milk` format
211/// declares. Exactly four of each — not a budget, the format's own shape.
212pub const MAX_ELEMENTS: usize = 4;
213
214/// What is wrong with a bundle, as a surfaced load error.
215#[derive(Debug, Clone, PartialEq)]
216pub enum BundleError {
217    /// One of the three programs did not decode.
218    Program {
219        /// Which section — `per_frame_init`, `per_frame` or `per_vertex`.
220        section: &'static str,
221        /// Why.
222        err: ProgramError,
223    },
224    /// The three programs declare different register rosters, so a `q1` written
225    /// by one would not be the `q1` the next reads.
226    RosterMismatch {
227        /// The section whose roster differs from `per_frame`'s.
228        section: &'static str,
229    },
230    /// More custom waves or shapes than the `.milk` format allows.
231    TooManyElements {
232        /// `"wave"` or `"shape"`.
233        which: &'static str,
234    },
235}
236
237impl std::fmt::Display for BundleError {
238    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
239        match self {
240            BundleError::Program { section, err } => write!(f, "[milk] {section}: {err}"),
241            BundleError::RosterMismatch { section } => write!(
242                f,
243                "[milk] {section} declares a different .regs roster from per_frame. \
244                 The three programs share one register file — that sharing IS the \
245                 q1..q32 bridge — so they must declare the same registers in the \
246                 same order. `milkconv` emits them that way; a hand-written bundle \
247                 has to as well."
248            ),
249            BundleError::TooManyElements { which } => write!(
250                f,
251                "[milk] more than {MAX_ELEMENTS} custom {which}s. The .milk format \
252                 declares exactly four of each, so a fifth is a bundle this \
253                 converter did not write."
254            ),
255        }
256    }
257}
258
259impl std::error::Error for BundleError {}
260
261impl MilkBundle {
262    /// Decode a bundle from the three assembly sections. An absent section is the
263    /// empty program, which runs nothing.
264    pub fn from_assembly(
265        per_frame_init: Option<&str>,
266        per_frame: Option<&str>,
267        per_vertex: Option<&str>,
268    ) -> Result<Self, BundleError> {
269        let decode = |section: &'static str, text: Option<&str>| match text {
270            None => Ok(EelProgram::empty()),
271            Some(text) => {
272                EelProgram::from_assembly(text).map_err(|err| BundleError::Program { section, err })
273            }
274        };
275        let bundle = Self {
276            per_frame_init: decode("per_frame_init", per_frame_init)?,
277            per_frame: decode("per_frame", per_frame)?,
278            per_vertex: decode("per_vertex", per_vertex)?,
279            waves: Vec::new(),
280            shapes: Vec::new(),
281            warp_wgsl: None,
282            comp_wgsl: None,
283            blur_level: 0,
284            quantize_steps: DEFAULT_QUANTIZE_STEPS,
285        };
286        // The shared register file is the bridge, so the rosters have to agree.
287        // An empty program declares nothing and is exempt.
288        for (section, program) in [
289            ("per_frame_init", &bundle.per_frame_init),
290            ("per_vertex", &bundle.per_vertex),
291        ] {
292            if program.register_count() > 0
293                && bundle.per_frame.register_count() > 0
294                && program.names() != bundle.per_frame.names()
295            {
296                return Err(BundleError::RosterMismatch { section });
297            }
298        }
299        Ok(bundle)
300    }
301
302    /// Attach one custom wave or shape, decoded from its own three assembly
303    /// sections.
304    ///
305    /// **Its own register file, not the bundle's** — an element's programs share
306    /// a scope with each other and with nothing else (see `ElementRuntime`), so
307    /// the roster check here is *within* the element and there is deliberately no
308    /// comparison against `per_frame`'s. Only `q1`-`q32` cross, by copy.
309    ///
310    /// Silently over-count is not an option: `count` and `instances` are what the
311    /// draw layer's buffers were sized from, so they are clamped to the format's
312    /// own limits by `ElementRuntime::spec` on the way out rather than trusted.
313    #[allow(clippy::too_many_arguments)]
314    pub fn push_element(
315        &mut self,
316        kind: ElementKind,
317        init: Option<&str>,
318        per_frame: Option<&str>,
319        per_point: Option<&str>,
320        count: u32,
321        instances: u32,
322        use_dots: bool,
323        thick: bool,
324        additive: bool,
325    ) -> Result<(), BundleError> {
326        let which = match kind {
327            ElementKind::Wave => "wave",
328            ElementKind::Shape => "shape",
329        };
330        let decode = |section: &'static str, text: Option<&str>| match text {
331            None => Ok(EelProgram::empty()),
332            Some(text) => {
333                EelProgram::from_assembly(text).map_err(|err| BundleError::Program { section, err })
334            }
335        };
336        let element = MilkElement {
337            init: decode("element init", init)?,
338            per_frame: decode("element per_frame", per_frame)?,
339            per_point: decode("element per_point", per_point)?,
340            count,
341            instances,
342            kind,
343            use_dots,
344            thick,
345            additive,
346        };
347        for (section, program) in [
348            ("element init", &element.init),
349            ("element per_point", &element.per_point),
350        ] {
351            if program.register_count() > 0
352                && element.per_frame.register_count() > 0
353                && program.names() != element.per_frame.names()
354            {
355                return Err(BundleError::RosterMismatch { section });
356            }
357        }
358        match kind {
359            ElementKind::Wave => self.waves.push(element),
360            ElementKind::Shape => self.shapes.push(element),
361        }
362        // The format's own ceiling of four each. A fifth would compile fine and
363        // then draw, which is not what the source preset asked for.
364        let full = match kind {
365            ElementKind::Wave => self.waves.len(),
366            ElementKind::Shape => self.shapes.len(),
367        };
368        if full > MAX_ELEMENTS {
369            return Err(BundleError::TooManyElements { which });
370        }
371        Ok(())
372    }
373
374    /// Whether any program in the bundle draws from the RNG.
375    pub fn uses_random(&self) -> bool {
376        let element = |e: &MilkElement| {
377            e.init.uses_random() || e.per_frame.uses_random() || e.per_point.uses_random()
378        };
379        self.per_frame_init.uses_random()
380            || self.per_frame.uses_random()
381            || self.per_vertex.uses_random()
382            || self.waves.iter().any(element)
383            || self.shapes.iter().any(element)
384    }
385
386    /// The roster the three programs share, for resolving indices once at load.
387    fn roster(&self) -> &[String] {
388        if self.per_frame.register_count() > 0 {
389            self.per_frame.names()
390        } else if self.per_vertex.register_count() > 0 {
391            self.per_vertex.names()
392        } else {
393            self.per_frame_init.names()
394        }
395    }
396}
397
398/// The register indices the host writes before a per-frame run.
399///
400/// Every field is an `Option`: a program that never names `treb` has no register
401/// for it, and writing one that does not exist is a no-op rather than an error.
402/// Resolved **once at load** — nothing per frame looks a name up.
403#[derive(Debug, Default, Clone, Copy)]
404struct FrameInputs {
405    bass: Option<u16>,
406    mid: Option<u16>,
407    treb: Option<u16>,
408    bass_att: Option<u16>,
409    mid_att: Option<u16>,
410    treb_att: Option<u16>,
411    time: Option<u16>,
412    frame: Option<u16>,
413    fps: Option<u16>,
414    progress: Option<u16>,
415    meshx: Option<u16>,
416    meshy: Option<u16>,
417    aspectx: Option<u16>,
418    aspecty: Option<u16>,
419}
420
421/// The register indices the host writes before each per-vertex run.
422#[derive(Debug, Default, Clone, Copy)]
423struct VertexInputs {
424    x: Option<u16>,
425    y: Option<u16>,
426    rad: Option<u16>,
427    ang: Option<u16>,
428}
429
430/// One loaded bundle's live state: the VM's arena and the resolved indices.
431///
432/// Built at preset load, borrowed mutably per frame, and **never resized while a
433/// preset renders** — the whole real-time claim.
434pub struct MilkRuntime {
435    bundle: MilkBundle,
436    state: VmState,
437    inputs: FrameInputs,
438    vertex_inputs: VertexInputs,
439    /// The nine per-vertex output registers, positionally with [`OUTPUT_NAMES`].
440    outputs: [Option<u16>; 9],
441    /// Every named per-frame output beyond the nine — the composite roster and
442    /// the whole draw layer (`outputs::FrameOutputs`).
443    frame_slots: FrameSlots,
444    /// The registers `q1`-`q32` live in, for the copy into each element.
445    q_slots: [Option<u16>; Q_COUNT],
446    /// One live state per custom wave, then one per custom shape.
447    waves: Vec<ElementRuntime>,
448    /// See [`waves`](Self::waves).
449    shapes: Vec<ElementRuntime>,
450    /// The render target's aspect as of the last [`run_frame`](Self::run_frame),
451    /// so [`run_vertex`](Self::run_vertex) can compute MilkDrop's `rad`/`ang`
452    /// without the caller having to hand it over per vertex.
453    aspect: f32,
454    /// The register values `per_frame` left, for the registers `per_vertex` can
455    /// write. Restored before each vertex (see the module docs).
456    snapshot: Vec<f32>,
457    /// Monotone frame counter, for the `frame` variable. Reset with the preset.
458    frame_index: u32,
459    /// Slow envelopes behind `bass_att`/`mid_att`/`treb_att`, which MilkDrop
460    /// supplies and this engine's analysis frame does not carry. One-pole on the
461    /// injected real `dt`, so they are frame-rate independent like everything
462    /// else here.
463    att: [f32; 3],
464    /// The MilkDrop-scaled band levels of the last [`run_frame`](Self::run_frame),
465    /// kept so the shader uniform reads exactly what the EEL program read.
466    last_bands: [f32; 3],
467    /// The preset's salt, kept for the two `rand_*` shader vectors.
468    salt: u32,
469    /// The shader-side `rand_frame` stream's state — a counter mixed per frame,
470    /// so a capture replaying the same frames sees the same randoms (ADR-0051).
471    shader_rand_state: u32,
472    /// This frame's `rand_frame` vector, advanced by `run_frame`.
473    shader_rand_frame: [f32; 4],
474}
475
476/// The time constant of the `*_att` envelopes, in seconds.
477///
478/// MilkDrop describes them as "an attenuated (smoothed) version" without naming a
479/// constant. Half a second is the value that behaves the way presets use them —
480/// as a slow floor under a percussive band, so `bass / bass_att` reads as "louder
481/// than it has been lately". Short enough to follow a build, long enough not to
482/// follow a kick.
483const ATT_TAU: f32 = 0.5;
484
485/// What MilkDrop's `bass` reads at an average level.
486///
487/// Its bands are normalized so ~1.0 is typical and a loud passage reaches 2–3.
488/// This engine's are `0..1` against their own recent peak (ADR-0049), where ~0.5
489/// is typical. Doubling puts a typical passage at MilkDrop's typical, which is
490/// what a preset's thresholds were tuned against.
491const BAND_SCALE: f32 = 2.0;
492
493impl MilkRuntime {
494    /// Build a runtime for `bundle`, resolving every index and running
495    /// `per_frame_init` once.
496    ///
497    /// `salt` is the preset's (ADR-0051): the pinned twin on every capture path
498    /// and the live one in the app, so a bundle using `rand()` is reproducible in
499    /// the harness and varied in the app.
500    pub fn new(bundle: MilkBundle, salt: u32) -> Self {
501        let roster = bundle.roster().to_vec();
502        let index = |name: &str| -> Option<u16> {
503            roster
504                .iter()
505                .position(|n| n == name)
506                .and_then(|i| u16::try_from(i).ok())
507        };
508        let inputs = FrameInputs {
509            bass: index("bass"),
510            mid: index("mid"),
511            treb: index("treb"),
512            bass_att: index("bass_att"),
513            mid_att: index("mid_att"),
514            treb_att: index("treb_att"),
515            time: index("time"),
516            frame: index("frame"),
517            fps: index("fps"),
518            progress: index("progress"),
519            meshx: index("meshx"),
520            meshy: index("meshy"),
521            aspectx: index("aspectx"),
522            aspecty: index("aspecty"),
523        };
524        let vertex_inputs = VertexInputs {
525            x: index("x"),
526            y: index("y"),
527            rad: index("rad"),
528            ang: index("ang"),
529        };
530        let outputs = std::array::from_fn(|i| OUTPUT_NAMES.get(i).and_then(|n| index(n)));
531        let frame_slots = FrameSlots::resolve(&index);
532        let q_slots = std::array::from_fn(|i| index(&format!("q{}", i + 1)));
533        let waves: Vec<ElementRuntime> = bundle
534            .waves
535            .iter()
536            .map(|e| ElementRuntime::new(e, salt))
537            .collect();
538        let shapes: Vec<ElementRuntime> = bundle
539            .shapes
540            .iter()
541            .map(|e| ElementRuntime::new(e, salt))
542            .collect();
543        let stack = bundle
544            .per_frame_init
545            .stack_depth()
546            .max(bundle.per_frame.stack_depth())
547            .max(bundle.per_vertex.stack_depth());
548        let mut state = VmState::new(roster.len(), stack, salt);
549        state.accommodate(&bundle.per_frame_init);
550        state.accommodate(&bundle.per_frame);
551        state.accommodate(&bundle.per_vertex);
552        let snapshot = vec![0.0; bundle.per_vertex.written_registers().len()];
553        let mut runtime = Self {
554            bundle,
555            state,
556            inputs,
557            vertex_inputs,
558            outputs,
559            frame_slots,
560            q_slots,
561            waves,
562            shapes,
563            aspect: 1.0,
564            snapshot,
565            frame_index: 0,
566            att: [0.0; 3],
567            last_bands: [0.0; 3],
568            salt,
569            shader_rand_state: 0,
570            shader_rand_frame: [0.0; 4],
571        };
572        runtime.reset();
573        runtime
574    }
575
576    /// Reset to the state a freshly-loaded preset is in: registers and arenas
577    /// zeroed, RNG back at its seed, frame counter at zero, `per_frame_init` run
578    /// once.
579    ///
580    /// **What makes a capture reproducible.** The harness rebuilds a preset from
581    /// the top, and everything the previous run left — a `megabuf` a program
582    /// filled, an RNG stream it advanced — has to go with it (NFR §6).
583    pub fn reset(&mut self) {
584        self.state.clear_registers();
585        self.state.clear_memory();
586        self.state.reset_rng();
587        self.frame_index = 0;
588        self.att = [0.0; 3];
589        self.last_bands = [0.0; 3];
590        self.shader_rand_state = 0;
591        self.shader_rand_frame = [0.0; 4];
592        vm::run(&self.bundle.per_frame_init, &mut self.state, Budget::INIT);
593        for element in self.waves.iter_mut().chain(self.shapes.iter_mut()) {
594            element.reset();
595        }
596    }
597
598    /// Whether this bundle has a per-vertex program at all. A bundle without one
599    /// drives the mesh from its per-frame outputs alone, which is a perfectly
600    /// good MilkDrop preset.
601    pub fn has_per_vertex(&self) -> bool {
602        !self.bundle.per_vertex.code().is_empty()
603    }
604
605    /// Run `per_frame` for this frame and return the whole-mesh outputs, already
606    /// converted from MilkDrop's per-frame rates to this engine's per-second ones
607    /// (module docs).
608    ///
609    /// Returns `(outputs, decay)`, positionally with `OUTPUT_NAMES`. `decay` is
610    /// `None` when the program never names it, so the scene keeps its own default
611    /// rather than being handed a zero.
612    pub fn run_frame(
613        &mut self,
614        frame: &crate::dsp::AnalysisFrame,
615        time: f32,
616        dt: f32,
617        mesh: (u32, u32),
618        aspect: f32,
619    ) -> ([f32; 9], FrameOutputs) {
620        self.aspect = if aspect.is_finite() && aspect > 0.0 {
621            aspect
622        } else {
623            1.0
624        };
625        // The `*_att` envelopes, on the injected real `dt`.
626        let alpha = if dt > 0.0 && dt.is_finite() {
627            1.0 - (-dt / ATT_TAU).exp()
628        } else {
629            0.0
630        };
631        for (slot, level) in self.att.iter_mut().zip([frame.bass, frame.mid, frame.treb]) {
632            *slot += alpha * (level * BAND_SCALE - *slot);
633        }
634
635        let set = |state: &mut VmState, slot: Option<u16>, value: f32| {
636            if let Some(index) = slot {
637                state.set(index, value);
638            }
639        };
640        self.last_bands = [
641            frame.bass * BAND_SCALE,
642            frame.mid * BAND_SCALE,
643            frame.treb * BAND_SCALE,
644        ];
645        // The shader-side `rand_frame`, mixed from the frame counter rather than
646        // drawn from a stream — replaying the same frame numbers replays the
647        // same randoms whatever else ran in between (NFR §6).
648        self.shader_rand_state = self.frame_index.wrapping_add(1);
649        self.shader_rand_frame = std::array::from_fn(|i| {
650            unit01(mix32(
651                self.salt.wrapping_add(
652                    self.shader_rand_state
653                        .wrapping_mul(4)
654                        .wrapping_add(i as u32),
655                ),
656            ))
657        });
658        set(&mut self.state, self.inputs.bass, frame.bass * BAND_SCALE);
659        set(&mut self.state, self.inputs.mid, frame.mid * BAND_SCALE);
660        set(&mut self.state, self.inputs.treb, frame.treb * BAND_SCALE);
661        set(&mut self.state, self.inputs.bass_att, self.att[0]);
662        set(&mut self.state, self.inputs.mid_att, self.att[1]);
663        set(&mut self.state, self.inputs.treb_att, self.att[2]);
664        set(&mut self.state, self.inputs.time, time);
665        set(&mut self.state, self.inputs.frame, self.frame_index as f32);
666        set(&mut self.state, self.inputs.fps, NOMINAL_FPS);
667        // `progress` is "how far through this preset's time slice", which this
668        // engine has no equivalent of — presets rotate on a transition rather
669        // than on a timer. Zero rather than absent: a preset reading it gets a
670        // defined value, and Phase 3's roster note says which of these are
671        // supplied rather than guessed.
672        set(&mut self.state, self.inputs.progress, 0.0);
673        set(&mut self.state, self.inputs.meshx, mesh.0 as f32);
674        set(&mut self.state, self.inputs.meshy, mesh.1 as f32);
675        // MilkDrop's aspect pair, in its own convention: the LONGER axis reads 1
676        // and the shorter one reads the ratio, which is what makes `x * aspectx`
677        // an isotropic coordinate.
678        let (ax, ay) = if aspect >= 1.0 {
679            (1.0, aspect)
680        } else {
681            (1.0 / aspect.max(1e-4), 1.0)
682        };
683        set(&mut self.state, self.inputs.aspectx, ax);
684        set(&mut self.state, self.inputs.aspecty, ay);
685
686        // The outputs start at the identity, so a program that writes only some
687        // of them leaves the rest still rather than at zero.
688        for (i, slot) in self.outputs.iter().enumerate() {
689            if let Some(index) = *slot {
690                self.state.set(index, identity_output(i));
691            }
692        }
693        self.frame_slots.seed(&mut self.state);
694
695        vm::run(&self.bundle.per_frame, &mut self.state, Budget::FRAME);
696        self.frame_index = self.frame_index.wrapping_add(1);
697
698        // Snapshot what `per_vertex` can write, so each vertex starts from here.
699        for (slot, index) in self
700            .snapshot
701            .iter_mut()
702            .zip(self.bundle.per_vertex.written_registers())
703        {
704            *slot = self.state.get(*index);
705        }
706
707        let raw: [f32; 9] = std::array::from_fn(|i| {
708            self.outputs
709                .get(i)
710                .and_then(|slot| *slot)
711                .map_or_else(|| identity_output(i), |index| self.state.get(index))
712        });
713        let frame_outputs = self.frame_slots.read(&self.state);
714
715        // **The q-bridge into the elements**, and it is a copy rather than a
716        // shared file (see `Q_COUNT`): each custom wave and shape has its own
717        // register space, and what crosses is `q1`-`q32` as the main per-frame
718        // program left them. Done here, once, rather than per point or per
719        // instance.
720        let mut q = [0.0f32; Q_COUNT];
721        for (slot, index) in q.iter_mut().zip(self.q_slots) {
722            if let Some(index) = index {
723                *slot = self.state.get(index);
724            }
725        }
726        for element in self.waves.iter_mut().chain(self.shapes.iter_mut()) {
727            element.begin_frame(&q, time, frame, self.att);
728        }
729
730        (convert_outputs(raw), frame_outputs)
731    }
732
733    /// How many custom waves this bundle carries.
734    pub fn wave_count(&self) -> usize {
735        self.waves.len()
736    }
737
738    /// The structural numbers of custom wave `index` — how many points it draws
739    /// and how it strokes them.
740    pub fn wave_spec(&self, index: usize) -> Option<ElementSpec> {
741        self.waves.get(index).map(ElementRuntime::spec)
742    }
743
744    /// The structural numbers of custom shape `index`.
745    pub fn shape_spec(&self, index: usize) -> Option<ElementSpec> {
746        self.shapes.get(index).map(ElementRuntime::spec)
747    }
748
749    /// How many custom shapes this bundle carries.
750    pub fn shape_count(&self) -> usize {
751        self.shapes.len()
752    }
753
754    /// Run custom wave `index`'s per-point program for the point at `sample`
755    /// (`0..1` along the wave) with `value1`/`value2` bound to the audio there,
756    /// and return where it put the point.
757    ///
758    /// `value1` and `value2` are MilkDrop's left and right channel samples. This
759    /// engine's analysis is **mono** by construction — the ring carries
760    /// interleaved PCM and the analyzer averages the channels before anything
761    /// else touches them (`dsp::Analyzer::push_interleaved`) — so the two are the
762    /// same number here. A preset that draws `value1` against `value2` as a
763    /// Lissajous figure therefore draws a diagonal line rather than a blob, which
764    /// is a real and stated fidelity loss rather than a bug.
765    pub fn run_wave_point(&mut self, index: usize, sample: f32, value: f32) -> Option<WavePoint> {
766        let element = self.waves.get_mut(index)?;
767        Some(element.run_point(sample, value))
768    }
769
770    /// Run custom wave `index`'s per-frame program, once, before its points.
771    pub fn run_wave_frame(&mut self, index: usize) -> Option<()> {
772        self.waves.get_mut(index)?.run_frame();
773        Some(())
774    }
775
776    /// Run custom shape `index`'s per-frame program for one instance and return
777    /// where it put that copy.
778    pub fn run_shape_instance(&mut self, index: usize, instance: u32) -> Option<ShapeInstance> {
779        let element = self.shapes.get_mut(index)?;
780        Some(element.run_instance(instance))
781    }
782
783    /// Run `per_vertex` for the vertex at uv `(x, y)` — `y = 0` at the **top**,
784    /// which is the reference's own convention — and return its nine outputs,
785    /// converted like the per-frame ones.
786    ///
787    /// `rad` and `ang` are computed here rather than taken, and deliberately:
788    /// **MilkDrop normalizes them differently from this engine's native
789    /// `[per_vertex]` vocabulary**, and a converted preset has to get MilkDrop's.
790    /// The reference takes `rad = |(x_ndc * aspectx, y_ndc * aspecty)|` with the
791    /// *longer* axis scaled to 1, so `rad` reaches `1.0` at the middle of the
792    /// left and right edges of a wide frame; the native `rad`
793    /// ([`warp_mesh::vertex_position`](crate::render::scenes::warp_mesh::vertex_position))
794    /// reaches `1.0` at the top and bottom instead. The two differ by a factor of
795    /// the aspect, which on a 16:9 display is 1.78 — enough that a preset written
796    /// as `zoom = 1 + rad * 0.1` would be most of a stop out. `ang` is the
797    /// reference's `atan2` in `-pi..pi`, not the native `0..tau`.
798    ///
799    /// Restores the per-frame register state first, so a write inside the program
800    /// does not leak into the next vertex — MilkDrop's semantics, and the reason
801    /// two adjacent vertices of an identical program give identical answers.
802    pub fn run_vertex(&mut self, x: f32, y: f32) -> [f32; 9] {
803        // Clip-space position, +y up, which is what the reference's `rad`/`ang`
804        // are taken from.
805        let nx = x * 2.0 - 1.0;
806        let ny = 1.0 - y * 2.0;
807        let (ax, ay) = if self.aspect >= 1.0 {
808            (1.0, 1.0 / self.aspect)
809        } else {
810            (self.aspect, 1.0)
811        };
812        let (px, py) = (nx * ax, ny * ay);
813        let rad = (px * px + py * py).sqrt();
814        let ang = py.atan2(px);
815        for (value, index) in self
816            .snapshot
817            .iter()
818            .zip(self.bundle.per_vertex.written_registers())
819        {
820            self.state.set(*index, *value);
821        }
822        if let Some(index) = self.vertex_inputs.x {
823            self.state.set(index, x);
824        }
825        if let Some(index) = self.vertex_inputs.y {
826            self.state.set(index, y);
827        }
828        if let Some(index) = self.vertex_inputs.rad {
829            self.state.set(index, rad);
830        }
831        if let Some(index) = self.vertex_inputs.ang {
832            self.state.set(index, ang);
833        }
834        vm::run(&self.bundle.per_vertex, &mut self.state, Budget::VERTEX);
835        let raw: [f32; 9] = std::array::from_fn(|i| {
836            self.outputs
837                .get(i)
838                .and_then(|slot| *slot)
839                .map_or_else(|| identity_output(i), |index| self.state.get(index))
840        });
841        convert_outputs(raw)
842    }
843
844    // --- the shader input surface (Plan 0100 Phase 6) ---
845    //
846    // Read after `run_frame` by the scene's uniform fill, so a converted shader
847    // sees the same frame the EEL programs saw.
848
849    /// `q1`..`q32` as the per-frame program left them.
850    pub fn q_values(&self) -> [f32; Q_COUNT] {
851        std::array::from_fn(|i| {
852            self.q_slots
853                .get(i)
854                .and_then(|slot| *slot)
855                .map_or(0.0, |index| self.state.get(index))
856        })
857    }
858
859    /// `bass, mid, treb, vol` then their attenuated four, MilkDrop-scaled.
860    /// `vol` is the mean of the three — this engine's analysis has no separate
861    /// loudness, and the mean behaves the way presets use `vol`.
862    pub fn shader_bands(&self) -> [f32; 8] {
863        let [b, m, t] = self.last_bands;
864        let [ba, ma, ta] = self.att;
865        let vol = (b + m + t) / 3.0;
866        let vol_att = (ba + ma + ta) / 3.0;
867        [b, m, t, vol, ba, ma, ta, vol_att]
868    }
869
870    /// This frame's `rand_frame` vector — four uniform randoms, fresh per frame,
871    /// a pure function of the salt and the frame index.
872    pub fn rand_frame(&self) -> [f32; 4] {
873        self.shader_rand_frame
874    }
875
876    /// The preset-lifetime `rand_preset` vector, fixed at load from the salt.
877    pub fn rand_preset(&self) -> [f32; 4] {
878        std::array::from_fn(|i| unit01(mix32(self.salt.wrapping_mul(0x9E37_79B9) ^ (i as u32))))
879    }
880
881    /// The frame counter, for the shader's `frame`.
882    pub fn frame_index(&self) -> u32 {
883        self.frame_index
884    }
885
886    /// The preset's salt, for shader inputs derived outside the runtime (the
887    /// `rot_*` matrices).
888    pub fn salt(&self) -> u32 {
889        self.salt
890    }
891}
892
893/// One round of the lowbias32 mixer — the CPU mirror of `gpu::HASH_WGSL`, here
894/// because `milk` sits below `render` and cannot reach the crate-private one.
895fn mix32(v: u32) -> u32 {
896    let mut h = v;
897    h ^= h >> 16;
898    h = h.wrapping_mul(0x7FEB_352D);
899    h ^= h >> 15;
900    h = h.wrapping_mul(0x846C_A68B);
901    h ^= h >> 16;
902    h
903}
904
905/// The top 24 bits as a unit fraction in `[0, 1)`.
906fn unit01(h: u32) -> f32 {
907    (h >> 8) as f32 / 16_777_216.0
908}
909
910/// The structural numbers a custom element's geometry is sized from — the parts
911/// of a [`MilkElement`] the draw layer needs and the VM does not.
912#[derive(Debug, Clone, Copy, PartialEq, Eq)]
913pub struct ElementSpec {
914    /// Points (a wave) or sides (a shape), already clamped to the format's own
915    /// limit.
916    pub count: u32,
917    /// How many copies a shape draws; `1` for a wave.
918    pub instances: u32,
919    /// Past `0.5` in the source, a wave draws dots.
920    pub use_dots: bool,
921    /// Past `0.5` in the source, the stroke is thick.
922    pub thick: bool,
923    /// Past `0.5` in the source, a wave **adds** rather than blends.
924    ///
925    /// A wave's flag is a file key (`wavecode_N_bAdditive`) and so is per element;
926    /// a *shape*'s is a register its own per-frame program may write, so that one
927    /// lives on [`ShapeInstance`] instead.
928    pub additive: bool,
929}
930
931/// One custom wave's or shape's live state (Plan 0100 Phase 4).
932///
933/// **Its own register file, its own arenas, its own RNG.** MilkDrop gives each
934/// element a separate variable scope — its own `t1`-`t8`, its own working
935/// variables — and only `q1`-`q32` cross from the main program, by copy. Sharing
936/// one file instead would let a shape's `t1` collide with a wave's, which the
937/// reference's own presets rely on not happening.
938struct ElementRuntime {
939    program: MilkElement,
940    state: VmState,
941    inputs: ElementInputs,
942    /// Where a wave's per-point outputs land.
943    point: WavePointSlots,
944    /// Where a shape's per-instance outputs land.
945    instance: ShapeInstanceSlots,
946    /// The registers `q1`-`q32` occupy **in this element's own file**, which the
947    /// bridge copies into.
948    q_slots: [Option<u16>; Q_COUNT],
949    /// The values a **shape's** per-frame program can write, saved before the
950    /// instance loop and restored before each instance — the same mechanism and
951    /// the same reason as the mesh's per-vertex snapshot.
952    ///
953    /// **Empty for a wave**, which is not an oversight: a wave's per-point
954    /// program walks its trace carrying state forward, and restoring between
955    /// points is what made *chasers 19 Portal*'s mirror inert. See
956    /// [`run_point`](Self::run_point).
957    snapshot: Vec<f32>,
958    /// The registers that snapshot covers — a shape's per-frame written set, and
959    /// nothing at all for a wave.
960    snapshot_of: Vec<u16>,
961}
962
963/// The read-only variables an element's programs are handed.
964#[derive(Debug, Default, Clone, Copy)]
965struct ElementInputs {
966    time: Option<u16>,
967    frame: Option<u16>,
968    fps: Option<u16>,
969    bass: Option<u16>,
970    mid: Option<u16>,
971    treb: Option<u16>,
972    bass_att: Option<u16>,
973    mid_att: Option<u16>,
974    treb_att: Option<u16>,
975    /// A wave's position along its own length, `0..1`.
976    sample: Option<u16>,
977    /// The audio at that position — MilkDrop's left and right channels, which
978    /// are the same number here (see `MilkRuntime::run_wave_point`).
979    value1: Option<u16>,
980    /// See [`value1`](Self::value1).
981    value2: Option<u16>,
982    /// Which copy of a shape this run is for, from `0`.
983    instance: Option<u16>,
984}
985
986impl ElementRuntime {
987    fn new(program: &MilkElement, salt: u32) -> Self {
988        let roster: Vec<String> = if program.per_frame.register_count() > 0 {
989            program.per_frame.names().to_vec()
990        } else if program.per_point.register_count() > 0 {
991            program.per_point.names().to_vec()
992        } else {
993            program.init.names().to_vec()
994        };
995        let index = |name: &str| -> Option<u16> {
996            roster
997                .iter()
998                .position(|n| n == name)
999                .and_then(|i| u16::try_from(i).ok())
1000        };
1001        let stack = program
1002            .init
1003            .stack_depth()
1004            .max(program.per_frame.stack_depth())
1005            .max(program.per_point.stack_depth());
1006        let mut state = VmState::new(roster.len(), stack, salt);
1007        state.accommodate(&program.init);
1008        state.accommodate(&program.per_frame);
1009        state.accommodate(&program.per_point);
1010        // **A shape's instances are independent; a wave's points are not.**
1011        //
1012        // A shape loops its per-FRAME program once per instance and each copy
1013        // starts from what the frame left, so its snapshot is that program's
1014        // written set. A wave loops its per-POINT program along its own length
1015        // and **carries state from one point to the next** — that is the
1016        // reference's semantics and the corpus is built on it, so a wave takes
1017        // no snapshot at all. See `run_point`.
1018        let snapshot_of = match program.kind {
1019            ElementKind::Wave => Vec::new(),
1020            ElementKind::Shape => program.per_frame.written_registers().to_vec(),
1021        };
1022        let mut runtime = Self {
1023            program: program.clone(),
1024            state,
1025            inputs: ElementInputs {
1026                time: index("time"),
1027                frame: index("frame"),
1028                fps: index("fps"),
1029                bass: index("bass"),
1030                mid: index("mid"),
1031                treb: index("treb"),
1032                bass_att: index("bass_att"),
1033                mid_att: index("mid_att"),
1034                treb_att: index("treb_att"),
1035                sample: index("sample"),
1036                value1: index("value1"),
1037                value2: index("value2"),
1038                instance: index("instance"),
1039            },
1040            point: WavePointSlots::resolve(&index),
1041            instance: ShapeInstanceSlots::resolve(&index),
1042            q_slots: std::array::from_fn(|i| index(&format!("q{}", i + 1))),
1043            snapshot: vec![0.0; snapshot_of.len()],
1044            snapshot_of,
1045        };
1046        runtime.reset();
1047        runtime
1048    }
1049
1050    /// The structural numbers, clamped to the format's own limits so a bundle
1051    /// cannot ask for geometry the buffers were not sized for.
1052    fn spec(&self) -> ElementSpec {
1053        let (max_count, max_instances) = match self.program.kind {
1054            ElementKind::Wave => (MAX_WAVE_POINTS, 1),
1055            ElementKind::Shape => (MAX_SHAPE_SIDES, MAX_SHAPE_INSTANCES),
1056        };
1057        ElementSpec {
1058            count: self.program.count.clamp(2, max_count),
1059            instances: self.program.instances.clamp(1, max_instances),
1060            use_dots: self.program.use_dots,
1061            thick: self.program.thick,
1062            additive: self.program.additive,
1063        }
1064    }
1065
1066    /// Back to the state a freshly-loaded preset is in.
1067    fn reset(&mut self) {
1068        self.state.clear_registers();
1069        self.state.clear_memory();
1070        self.state.reset_rng();
1071        vm::run(&self.program.init, &mut self.state, Budget::INIT);
1072    }
1073
1074    /// Copy the bridge in and bind this frame's inputs. Called once per frame,
1075    /// after the main per-frame program has run.
1076    fn begin_frame(
1077        &mut self,
1078        q: &[f32; Q_COUNT],
1079        time: f32,
1080        frame: &crate::dsp::AnalysisFrame,
1081        att: [f32; 3],
1082    ) {
1083        for (value, index) in q.iter().zip(self.q_slots) {
1084            if let Some(index) = index {
1085                self.state.set(index, *value);
1086            }
1087        }
1088        let mut set = |slot: Option<u16>, value: f32| {
1089            if let Some(index) = slot {
1090                self.state.set(index, value);
1091            }
1092        };
1093        set(self.inputs.time, time);
1094        set(self.inputs.fps, NOMINAL_FPS);
1095        set(self.inputs.bass, frame.bass * BAND_SCALE);
1096        set(self.inputs.mid, frame.mid * BAND_SCALE);
1097        set(self.inputs.treb, frame.treb * BAND_SCALE);
1098        set(self.inputs.bass_att, att[0]);
1099        set(self.inputs.mid_att, att[1]);
1100        set(self.inputs.treb_att, att[2]);
1101    }
1102
1103    /// A wave's per-frame program, run once before its points. Its outputs seed
1104    /// the per-point defaults, which is how a wave whose per-point code sets only
1105    /// `x`/`y` still gets its colour.
1106    fn run_frame(&mut self) {
1107        self.point.seed(&mut self.state);
1108        vm::run(&self.program.per_frame, &mut self.state, Budget::FRAME);
1109        self.take_snapshot();
1110    }
1111
1112    /// One point of a custom wave.
1113    ///
1114    /// **Nothing is restored between points, and that is the semantics rather
1115    /// than an omission** (Plan 0108 Phase 5). A wave's per-point program walks
1116    /// the trace carrying its own working variables forward, which is what lets
1117    /// a preset alternate, accumulate, or integrate along the wave. The idiom
1118    /// the corpus is full of is a two-state counter:
1119    ///
1120    /// ```text
1121    /// flip = flip + 1;
1122    /// flip = flip * below(flip, 2);      // 1, 0, 1, 0, ... down the trace
1123    /// yp   = (flip * 0.1 - 0.05) * sample;
1124    /// ```
1125    ///
1126    /// Reset the registers before each point and those three lines compute a
1127    /// **constant** — the mirrored pair collapses to a single trace and the
1128    /// preset's symmetry silently never happens, which is design-backlog 0107's
1129    /// *chasers 19 Portal* converting cleanly and rendering inert.
1130    ///
1131    /// Measured over the 10 347-file corpus, 2026-08-17: **6 347 files carry a
1132    /// custom-wave per-point program, and 3 368 of them (53 %) read a per-point
1133    /// variable before writing it with nothing in the file seeding it** — so
1134    /// only carry-over can supply a value. 612 of those use `flip` by name.
1135    ///
1136    /// This is the opposite of [`MilkRuntime::run_vertex`], which *does* restore,
1137    /// and the two are not inconsistent: the mesh's per-vertex program is a pure
1138    /// function of its vertex in the reference, and a wave's per-point program
1139    /// is a walk along a line.
1140    ///
1141    /// # The carry reaches past the end of the trace, too
1142    ///
1143    /// Nothing reseeds a working register at the **frame** boundary either
1144    /// (Plan 0108 Mode 4 review, 2026-08-17). [`run_frame`](Self::run_frame)
1145    /// seeds only the named wave-point *outputs* — `WavePointSlots`'s `x`, `y`,
1146    /// `r`, `g`, `b`, `a` — and a wave's `snapshot_of` is empty, so `flip`
1147    /// survives from the last point of one frame into the first point of the
1148    /// next. On an **even**-length trace the two-state counter returns to where
1149    /// it started and this is invisible; on an odd one the figure comes out
1150    /// inverted every other frame, which reads as an alternation at the
1151    /// **display's** refresh rate rather than at any authored one.
1152    ///
1153    /// That is believed faithful — the reference allocates a custom element's
1154    /// variable space once and only its `init` code reseeds it — but it is a
1155    /// claim about the reference and is **not verified against it**; Plan 0108's
1156    /// Phase 6 is where `foo_vis_milk2` answers it. It is pinned meanwhile by
1157    /// `milkconv/tests/draw_layer.rs`'s
1158    /// `a_waves_per_point_state_also_carries_across_the_frame_boundary`, so the
1159    /// behaviour cannot move without this comment moving with it.
1160    fn run_point(&mut self, sample: f32, value: f32) -> WavePoint {
1161        if let Some(index) = self.inputs.sample {
1162            self.state.set(index, sample);
1163        }
1164        if let Some(index) = self.inputs.value1 {
1165            self.state.set(index, value);
1166        }
1167        if let Some(index) = self.inputs.value2 {
1168            self.state.set(index, value);
1169        }
1170        vm::run(&self.program.per_point, &mut self.state, Budget::VERTEX);
1171        self.point.read(&self.state)
1172    }
1173
1174    /// One instance of a custom shape.
1175    fn run_instance(&mut self, instance: u32) -> ShapeInstance {
1176        self.restore_snapshot();
1177        self.instance.seed(&mut self.state);
1178        if let Some(index) = self.inputs.instance {
1179            self.state.set(index, instance as f32);
1180        }
1181        if let Some(index) = self.inputs.frame {
1182            self.state.set(index, instance as f32);
1183        }
1184        vm::run(&self.program.per_frame, &mut self.state, Budget::FRAME);
1185        self.instance.read(&self.state)
1186    }
1187
1188    fn take_snapshot(&mut self) {
1189        for (slot, index) in self.snapshot.iter_mut().zip(&self.snapshot_of) {
1190            *slot = self.state.get(*index);
1191        }
1192    }
1193
1194    fn restore_snapshot(&mut self) {
1195        for (value, index) in self.snapshot.iter().zip(&self.snapshot_of) {
1196            self.state.set(*index, *value);
1197        }
1198    }
1199}
1200
1201/// Output `i`'s identity value — what the register holds before a program runs,
1202/// so a program that never writes it leaves the past still.
1203fn identity_output(i: usize) -> f32 {
1204    match OUTPUT_NAMES.get(i) {
1205        Some(&"zoom") | Some(&"sx") | Some(&"sy") => 1.0,
1206        Some(&"cx") | Some(&"cy") => 0.5,
1207        _ => 0.0,
1208    }
1209}
1210
1211/// The widest a converted factor may get, and its reciprocal the narrowest.
1212///
1213/// Raising to [`NOMINAL_FPS`] is a thirtieth power, so it **overflows `f32` at a
1214/// per-frame factor of about 13** — and an overflow that fell back to `1.0` would
1215/// turn the most extreme zoom a preset can ask for into no zoom at all, which is
1216/// the opposite of what it says. Saturating instead keeps the direction: a
1217/// runaway zoom collapses the source window to a point, which is what a runaway
1218/// zoom looks like. Wide enough that no plausible preset reaches it (`1.05` per
1219/// frame, a brisk drift, is `4.3` per second).
1220const MAX_FACTOR: f32 = 1.0e30;
1221
1222/// A per-frame survival/scale factor as a per-second one, at [`NOMINAL_FPS`].
1223///
1224/// `v^fps`: thirty frames of `0.96` is `0.96^30` per second at the nominal rate.
1225/// Total on a non-finite or non-positive input, which a program can produce — a
1226/// factor at or below zero is not a factor, so it reads as the identity rather
1227/// than as a mirror.
1228fn per_second_factor(v: f32) -> f32 {
1229    if !v.is_finite() || v <= 0.0 {
1230        return 1.0;
1231    }
1232    let out = v.powf(NOMINAL_FPS);
1233    if out.is_finite() {
1234        out.clamp(1.0 / MAX_FACTOR, MAX_FACTOR)
1235    } else if v > 1.0 {
1236        MAX_FACTOR
1237    } else {
1238        1.0 / MAX_FACTOR
1239    }
1240}
1241
1242/// A per-frame **scale** as a per-second one, with its sign carried through.
1243///
1244/// [`per_second_factor`]'s "at or below zero is not a factor" is right for
1245/// `decay`, which is a survival fraction — but three of the nine per-vertex
1246/// outputs are *scales*, and a NEGATIVE scale is MilkDrop's standard mirror
1247/// idiom (363 corpus files, 3.5 %). Reading one as the identity deleted the
1248/// mirror here, before the mesh vertex stage ever saw the value. That stage's
1249/// own `max()` guard deleted it a second time; both halves are
1250/// design-backlog 0114, and the other half is `warp_mesh`'s `signed_rate`.
1251///
1252/// The magnitude converts exactly as an unsigned factor does, so a positive
1253/// input is bit-identical to [`per_second_factor`] and nothing shipping a
1254/// positive scale moves. Zero stays on the positive arm for the same reason it
1255/// does in the shader: it is not a mirror, and must not become one.
1256fn per_second_signed_factor(v: f32) -> f32 {
1257    if v.is_finite() && v < 0.0 {
1258        -per_second_factor(-v)
1259    } else {
1260        per_second_factor(v)
1261    }
1262}
1263
1264/// The nine raw MilkDrop outputs as this engine's per-second vocabulary.
1265fn convert_outputs(raw: [f32; 9]) -> [f32; 9] {
1266    std::array::from_fn(|i| {
1267        let v = raw.get(i).copied().unwrap_or(0.0);
1268        if OUTPUT_FACTOR.get(i).copied().unwrap_or(false) {
1269            per_second_signed_factor(v)
1270        } else if OUTPUT_RATE.get(i).copied().unwrap_or(false) {
1271            let out = v * NOMINAL_FPS;
1272            if out.is_finite() { out } else { 0.0 }
1273        } else if v.is_finite() {
1274            v
1275        } else {
1276            identity_output(i)
1277        }
1278    })
1279}
1280
1281#[cfg(test)]
1282mod tests;