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` — finite and positive
626        // by the renderer entry's substitution (ADR-0191), so not re-checked.
627        let alpha = 1.0 - (-dt / ATT_TAU).exp();
628        for (slot, level) in self.att.iter_mut().zip([frame.bass, frame.mid, frame.treb]) {
629            *slot += alpha * (level * BAND_SCALE - *slot);
630        }
631
632        let set = |state: &mut VmState, slot: Option<u16>, value: f32| {
633            if let Some(index) = slot {
634                state.set(index, value);
635            }
636        };
637        self.last_bands = [
638            frame.bass * BAND_SCALE,
639            frame.mid * BAND_SCALE,
640            frame.treb * BAND_SCALE,
641        ];
642        // The shader-side `rand_frame`, mixed from the frame counter rather than
643        // drawn from a stream — replaying the same frame numbers replays the
644        // same randoms whatever else ran in between (NFR §6).
645        self.shader_rand_state = self.frame_index.wrapping_add(1);
646        self.shader_rand_frame = std::array::from_fn(|i| {
647            unit01(mix32(
648                self.salt.wrapping_add(
649                    self.shader_rand_state
650                        .wrapping_mul(4)
651                        .wrapping_add(i as u32),
652                ),
653            ))
654        });
655        set(&mut self.state, self.inputs.bass, frame.bass * BAND_SCALE);
656        set(&mut self.state, self.inputs.mid, frame.mid * BAND_SCALE);
657        set(&mut self.state, self.inputs.treb, frame.treb * BAND_SCALE);
658        set(&mut self.state, self.inputs.bass_att, self.att[0]);
659        set(&mut self.state, self.inputs.mid_att, self.att[1]);
660        set(&mut self.state, self.inputs.treb_att, self.att[2]);
661        set(&mut self.state, self.inputs.time, time);
662        set(&mut self.state, self.inputs.frame, self.frame_index as f32);
663        set(&mut self.state, self.inputs.fps, NOMINAL_FPS);
664        // `progress` is "how far through this preset's time slice", which this
665        // engine has no equivalent of — presets rotate on a transition rather
666        // than on a timer. Zero rather than absent: a preset reading it gets a
667        // defined value, and Phase 3's roster note says which of these are
668        // supplied rather than guessed.
669        set(&mut self.state, self.inputs.progress, 0.0);
670        set(&mut self.state, self.inputs.meshx, mesh.0 as f32);
671        set(&mut self.state, self.inputs.meshy, mesh.1 as f32);
672        // MilkDrop's aspect pair, in its own convention: the LONGER axis reads 1
673        // and the shorter one reads the ratio, which is what makes `x * aspectx`
674        // an isotropic coordinate.
675        let (ax, ay) = if aspect >= 1.0 {
676            (1.0, aspect)
677        } else {
678            (1.0 / aspect.max(1e-4), 1.0)
679        };
680        set(&mut self.state, self.inputs.aspectx, ax);
681        set(&mut self.state, self.inputs.aspecty, ay);
682
683        // The outputs start at the identity, so a program that writes only some
684        // of them leaves the rest still rather than at zero.
685        for (i, slot) in self.outputs.iter().enumerate() {
686            if let Some(index) = *slot {
687                self.state.set(index, identity_output(i));
688            }
689        }
690        self.frame_slots.seed(&mut self.state);
691
692        vm::run(&self.bundle.per_frame, &mut self.state, Budget::FRAME);
693        self.frame_index = self.frame_index.wrapping_add(1);
694
695        // Snapshot what `per_vertex` can write, so each vertex starts from here.
696        for (slot, index) in self
697            .snapshot
698            .iter_mut()
699            .zip(self.bundle.per_vertex.written_registers())
700        {
701            *slot = self.state.get(*index);
702        }
703
704        let raw: [f32; 9] = std::array::from_fn(|i| {
705            self.outputs
706                .get(i)
707                .and_then(|slot| *slot)
708                .map_or_else(|| identity_output(i), |index| self.state.get(index))
709        });
710        let frame_outputs = self.frame_slots.read(&self.state);
711
712        // **The q-bridge into the elements**, and it is a copy rather than a
713        // shared file (see `Q_COUNT`): each custom wave and shape has its own
714        // register space, and what crosses is `q1`-`q32` as the main per-frame
715        // program left them. Done here, once, rather than per point or per
716        // instance.
717        let mut q = [0.0f32; Q_COUNT];
718        for (slot, index) in q.iter_mut().zip(self.q_slots) {
719            if let Some(index) = index {
720                *slot = self.state.get(index);
721            }
722        }
723        for element in self.waves.iter_mut().chain(self.shapes.iter_mut()) {
724            element.begin_frame(&q, time, frame, self.att);
725        }
726
727        (convert_outputs(raw), frame_outputs)
728    }
729
730    /// How many custom waves this bundle carries.
731    pub fn wave_count(&self) -> usize {
732        self.waves.len()
733    }
734
735    /// The structural numbers of custom wave `index` — how many points it draws
736    /// and how it strokes them.
737    pub fn wave_spec(&self, index: usize) -> Option<ElementSpec> {
738        self.waves.get(index).map(ElementRuntime::spec)
739    }
740
741    /// The structural numbers of custom shape `index`.
742    pub fn shape_spec(&self, index: usize) -> Option<ElementSpec> {
743        self.shapes.get(index).map(ElementRuntime::spec)
744    }
745
746    /// How many custom shapes this bundle carries.
747    pub fn shape_count(&self) -> usize {
748        self.shapes.len()
749    }
750
751    /// Run custom wave `index`'s per-point program for the point at `sample`
752    /// (`0..1` along the wave) with `value1`/`value2` bound to the audio there,
753    /// and return where it put the point.
754    ///
755    /// `left` and `right` are MilkDrop's `value1` and `value2` — channels 0 and 1
756    /// of the analyzer's levelled pair (`AnalysisFrame::waveform_pair`,
757    /// ADR-0199). They are two genuinely different numbers on a stereo stream, so
758    /// a preset that plots one against the other draws the figure its author saw
759    /// rather than the diagonal line a mono stand-in gives; on a one-channel
760    /// stream they are equal, because the pair fills both slots from channel 0.
761    pub fn run_wave_point(
762        &mut self,
763        index: usize,
764        sample: f32,
765        left: f32,
766        right: f32,
767    ) -> Option<WavePoint> {
768        let element = self.waves.get_mut(index)?;
769        Some(element.run_point(sample, left, right))
770    }
771
772    /// Run custom wave `index`'s per-frame program, once, before its points.
773    pub fn run_wave_frame(&mut self, index: usize) -> Option<()> {
774        self.waves.get_mut(index)?.run_frame();
775        Some(())
776    }
777
778    /// Run custom shape `index`'s per-frame program for one instance and return
779    /// where it put that copy.
780    pub fn run_shape_instance(&mut self, index: usize, instance: u32) -> Option<ShapeInstance> {
781        let element = self.shapes.get_mut(index)?;
782        Some(element.run_instance(instance))
783    }
784
785    /// Run `per_vertex` for the vertex at uv `(x, y)` — `y = 0` at the **top**,
786    /// which is the reference's own convention — and return its nine outputs,
787    /// converted like the per-frame ones.
788    ///
789    /// `x` and `y` are aspect-corrected here rather than passed through, and
790    /// `rad` and `ang` are computed rather than taken — all three deliberately:
791    /// **MilkDrop normalizes the whole per-vertex input set differently from this
792    /// engine's native `[per_vertex]` vocabulary**, and a converted preset has to
793    /// get MilkDrop's.
794    /// The reference takes `rad = |(x_ndc * aspectx, y_ndc * aspecty)|` with the
795    /// *longer* axis scaled to 1, so `rad` reaches `1.0` at the middle of the
796    /// left and right edges of a wide frame; the native `rad`
797    /// ([`warp_mesh::vertex_position`](crate::render::scenes::warp_mesh::vertex_position))
798    /// reaches `1.0` at the top and bottom instead. The two differ by a factor of
799    /// the aspect, which on a 16:9 display is 1.78 — enough that a preset written
800    /// as `zoom = 1 + rad * 0.1` would be most of a stop out. `ang` is the
801    /// reference's `atan2` in `-pi..pi`, not the native `0..tau`.
802    ///
803    /// `x` and `y` are the same correction applied to the uv itself: the
804    /// reference hands the program `x * 0.5 * aspectx + 0.5` and
805    /// `y * -0.5 * aspecty + 0.5` off the clip position, so on a 16:9 frame `x`
806    /// still spans `0..1` and `y` spans only `0.5 +/- 0.28125` — the shorter axis
807    /// is compressed, and the pair is the frame's shape rather than its extent.
808    /// The native vocabulary's `vertex_position` spans `0..1` on both axes, so a
809    /// converted preset reading `y` through that would drive its stretch or its
810    /// translation 1.78 times too far. The same corrected space is where the warp
811    /// chain's middle stages run (ADR-0212), which is what keeps the program's
812    /// inputs and the transform they feed in one set of units.
813    ///
814    /// Restores the per-frame register state first, so a write inside the program
815    /// does not leak into the next vertex — MilkDrop's semantics, and the reason
816    /// two adjacent vertices of an identical program give identical answers.
817    pub fn run_vertex(&mut self, x: f32, y: f32) -> [f32; 9] {
818        // Clip-space position, +y up, which is what the reference's `rad`/`ang`
819        // are taken from.
820        let nx = x * 2.0 - 1.0;
821        let ny = 1.0 - y * 2.0;
822        let (ax, ay) = if self.aspect >= 1.0 {
823            (1.0, 1.0 / self.aspect)
824        } else {
825            (self.aspect, 1.0)
826        };
827        let (px, py) = (nx * ax, ny * ay);
828        let rad = (px * px + py * py).sqrt();
829        let ang = py.atan2(px);
830        // Back to a 0..1-centred uv from the corrected clip pair, y down —
831        // `px * 0.5 + 0.5` and `-py * 0.5 + 0.5` are the reference's own two
832        // expressions with the `aspect` factor already in `px`/`py`.
833        let (sx, sy) = (px * 0.5 + 0.5, 0.5 - py * 0.5);
834        for (value, index) in self
835            .snapshot
836            .iter()
837            .zip(self.bundle.per_vertex.written_registers())
838        {
839            self.state.set(*index, *value);
840        }
841        if let Some(index) = self.vertex_inputs.x {
842            self.state.set(index, sx);
843        }
844        if let Some(index) = self.vertex_inputs.y {
845            self.state.set(index, sy);
846        }
847        if let Some(index) = self.vertex_inputs.rad {
848            self.state.set(index, rad);
849        }
850        if let Some(index) = self.vertex_inputs.ang {
851            self.state.set(index, ang);
852        }
853        vm::run(&self.bundle.per_vertex, &mut self.state, Budget::VERTEX);
854        let raw: [f32; 9] = std::array::from_fn(|i| {
855            self.outputs
856                .get(i)
857                .and_then(|slot| *slot)
858                .map_or_else(|| identity_output(i), |index| self.state.get(index))
859        });
860        convert_outputs(raw)
861    }
862
863    // --- the shader input surface (Plan 0100 Phase 6) ---
864    //
865    // Read after `run_frame` by the scene's uniform fill, so a converted shader
866    // sees the same frame the EEL programs saw.
867
868    /// `q1`..`q32` as the per-frame program left them.
869    pub fn q_values(&self) -> [f32; Q_COUNT] {
870        std::array::from_fn(|i| {
871            self.q_slots
872                .get(i)
873                .and_then(|slot| *slot)
874                .map_or(0.0, |index| self.state.get(index))
875        })
876    }
877
878    /// `bass, mid, treb, vol` then their attenuated four, MilkDrop-scaled.
879    /// `vol` is the mean of the three — this engine's analysis has no separate
880    /// loudness, and the mean behaves the way presets use `vol`.
881    pub fn shader_bands(&self) -> [f32; 8] {
882        let [b, m, t] = self.last_bands;
883        let [ba, ma, ta] = self.att;
884        let vol = (b + m + t) / 3.0;
885        let vol_att = (ba + ma + ta) / 3.0;
886        [b, m, t, vol, ba, ma, ta, vol_att]
887    }
888
889    /// This frame's `rand_frame` vector — four uniform randoms, fresh per frame,
890    /// a pure function of the salt and the frame index.
891    pub fn rand_frame(&self) -> [f32; 4] {
892        self.shader_rand_frame
893    }
894
895    /// The preset-lifetime `rand_preset` vector, fixed at load from the salt.
896    pub fn rand_preset(&self) -> [f32; 4] {
897        std::array::from_fn(|i| unit01(mix32(self.salt.wrapping_mul(0x9E37_79B9) ^ (i as u32))))
898    }
899
900    /// The frame counter, for the shader's `frame`.
901    pub fn frame_index(&self) -> u32 {
902        self.frame_index
903    }
904
905    /// The preset's salt, for shader inputs derived outside the runtime (the
906    /// `rot_*` matrices).
907    pub fn salt(&self) -> u32 {
908        self.salt
909    }
910}
911
912/// One round of the lowbias32 mixer — the CPU mirror of `gpu::HASH_WGSL`, here
913/// because `milk` sits below `render` and cannot reach the crate-private one.
914fn mix32(v: u32) -> u32 {
915    let mut h = v;
916    h ^= h >> 16;
917    h = h.wrapping_mul(0x7FEB_352D);
918    h ^= h >> 15;
919    h = h.wrapping_mul(0x846C_A68B);
920    h ^= h >> 16;
921    h
922}
923
924/// The top 24 bits as a unit fraction in `[0, 1)`.
925fn unit01(h: u32) -> f32 {
926    (h >> 8) as f32 / 16_777_216.0
927}
928
929/// The structural numbers a custom element's geometry is sized from — the parts
930/// of a [`MilkElement`] the draw layer needs and the VM does not.
931#[derive(Debug, Clone, Copy, PartialEq, Eq)]
932pub struct ElementSpec {
933    /// Points (a wave) or sides (a shape), already clamped to the format's own
934    /// limit.
935    pub count: u32,
936    /// How many copies a shape draws; `1` for a wave.
937    pub instances: u32,
938    /// Past `0.5` in the source, a wave draws dots.
939    pub use_dots: bool,
940    /// Past `0.5` in the source, the stroke is thick.
941    pub thick: bool,
942    /// Past `0.5` in the source, a wave **adds** rather than blends.
943    ///
944    /// A wave's flag is a file key (`wavecode_N_bAdditive`) and so is per element;
945    /// a *shape*'s is a register its own per-frame program may write, so that one
946    /// lives on [`ShapeInstance`] instead.
947    pub additive: bool,
948}
949
950/// One custom wave's or shape's live state (Plan 0100 Phase 4).
951///
952/// **Its own register file, its own arenas, its own RNG.** MilkDrop gives each
953/// element a separate variable scope — its own `t1`-`t8`, its own working
954/// variables — and only `q1`-`q32` cross from the main program, by copy. Sharing
955/// one file instead would let a shape's `t1` collide with a wave's, which the
956/// reference's own presets rely on not happening.
957struct ElementRuntime {
958    program: MilkElement,
959    state: VmState,
960    inputs: ElementInputs,
961    /// Where a wave's per-point outputs land.
962    point: WavePointSlots,
963    /// Where a shape's per-instance outputs land.
964    instance: ShapeInstanceSlots,
965    /// The registers `q1`-`q32` occupy **in this element's own file**, which the
966    /// bridge copies into.
967    q_slots: [Option<u16>; Q_COUNT],
968    /// The values a **shape's** per-frame program can write, saved before the
969    /// instance loop and restored before each instance — the same mechanism and
970    /// the same reason as the mesh's per-vertex snapshot.
971    ///
972    /// **Empty for a wave**, which is not an oversight: a wave's per-point
973    /// program walks its trace carrying state forward, and restoring between
974    /// points is what made *chasers 19 Portal*'s mirror inert. See
975    /// [`run_point`](Self::run_point).
976    snapshot: Vec<f32>,
977    /// The registers that snapshot covers — a shape's per-frame written set, and
978    /// nothing at all for a wave.
979    snapshot_of: Vec<u16>,
980}
981
982/// The read-only variables an element's programs are handed.
983#[derive(Debug, Default, Clone, Copy)]
984struct ElementInputs {
985    time: Option<u16>,
986    frame: Option<u16>,
987    fps: Option<u16>,
988    bass: Option<u16>,
989    mid: Option<u16>,
990    treb: Option<u16>,
991    bass_att: Option<u16>,
992    mid_att: Option<u16>,
993    treb_att: Option<u16>,
994    /// A wave's position along its own length, `0..1`.
995    sample: Option<u16>,
996    /// The audio at that position — MilkDrop's left and right channels, which
997    /// are the same number here (see `MilkRuntime::run_wave_point`).
998    value1: Option<u16>,
999    /// See [`value1`](Self::value1).
1000    value2: Option<u16>,
1001    /// Which copy of a shape this run is for, from `0`.
1002    instance: Option<u16>,
1003}
1004
1005impl ElementRuntime {
1006    fn new(program: &MilkElement, salt: u32) -> Self {
1007        let roster: Vec<String> = if program.per_frame.register_count() > 0 {
1008            program.per_frame.names().to_vec()
1009        } else if program.per_point.register_count() > 0 {
1010            program.per_point.names().to_vec()
1011        } else {
1012            program.init.names().to_vec()
1013        };
1014        let index = |name: &str| -> Option<u16> {
1015            roster
1016                .iter()
1017                .position(|n| n == name)
1018                .and_then(|i| u16::try_from(i).ok())
1019        };
1020        let stack = program
1021            .init
1022            .stack_depth()
1023            .max(program.per_frame.stack_depth())
1024            .max(program.per_point.stack_depth());
1025        let mut state = VmState::new(roster.len(), stack, salt);
1026        state.accommodate(&program.init);
1027        state.accommodate(&program.per_frame);
1028        state.accommodate(&program.per_point);
1029        // **A shape's instances are independent; a wave's points are not.**
1030        //
1031        // A shape loops its per-FRAME program once per instance and each copy
1032        // starts from what the frame left, so its snapshot is that program's
1033        // written set. A wave loops its per-POINT program along its own length
1034        // and **carries state from one point to the next** — that is the
1035        // reference's semantics and the corpus is built on it, so a wave takes
1036        // no snapshot at all. See `run_point`.
1037        let snapshot_of = match program.kind {
1038            ElementKind::Wave => Vec::new(),
1039            ElementKind::Shape => program.per_frame.written_registers().to_vec(),
1040        };
1041        let mut runtime = Self {
1042            program: program.clone(),
1043            state,
1044            inputs: ElementInputs {
1045                time: index("time"),
1046                frame: index("frame"),
1047                fps: index("fps"),
1048                bass: index("bass"),
1049                mid: index("mid"),
1050                treb: index("treb"),
1051                bass_att: index("bass_att"),
1052                mid_att: index("mid_att"),
1053                treb_att: index("treb_att"),
1054                sample: index("sample"),
1055                value1: index("value1"),
1056                value2: index("value2"),
1057                instance: index("instance"),
1058            },
1059            point: WavePointSlots::resolve(&index),
1060            instance: ShapeInstanceSlots::resolve(&index),
1061            q_slots: std::array::from_fn(|i| index(&format!("q{}", i + 1))),
1062            snapshot: vec![0.0; snapshot_of.len()],
1063            snapshot_of,
1064        };
1065        runtime.reset();
1066        runtime
1067    }
1068
1069    /// The structural numbers, clamped to the format's own limits so a bundle
1070    /// cannot ask for geometry the buffers were not sized for.
1071    fn spec(&self) -> ElementSpec {
1072        let (max_count, max_instances) = match self.program.kind {
1073            ElementKind::Wave => (MAX_WAVE_POINTS, 1),
1074            ElementKind::Shape => (MAX_SHAPE_SIDES, MAX_SHAPE_INSTANCES),
1075        };
1076        ElementSpec {
1077            count: self.program.count.clamp(2, max_count),
1078            instances: self.program.instances.clamp(1, max_instances),
1079            use_dots: self.program.use_dots,
1080            thick: self.program.thick,
1081            additive: self.program.additive,
1082        }
1083    }
1084
1085    /// Back to the state a freshly-loaded preset is in.
1086    fn reset(&mut self) {
1087        self.state.clear_registers();
1088        self.state.clear_memory();
1089        self.state.reset_rng();
1090        vm::run(&self.program.init, &mut self.state, Budget::INIT);
1091    }
1092
1093    /// Copy the bridge in and bind this frame's inputs. Called once per frame,
1094    /// after the main per-frame program has run.
1095    fn begin_frame(
1096        &mut self,
1097        q: &[f32; Q_COUNT],
1098        time: f32,
1099        frame: &crate::dsp::AnalysisFrame,
1100        att: [f32; 3],
1101    ) {
1102        for (value, index) in q.iter().zip(self.q_slots) {
1103            if let Some(index) = index {
1104                self.state.set(index, *value);
1105            }
1106        }
1107        let mut set = |slot: Option<u16>, value: f32| {
1108            if let Some(index) = slot {
1109                self.state.set(index, value);
1110            }
1111        };
1112        set(self.inputs.time, time);
1113        set(self.inputs.fps, NOMINAL_FPS);
1114        set(self.inputs.bass, frame.bass * BAND_SCALE);
1115        set(self.inputs.mid, frame.mid * BAND_SCALE);
1116        set(self.inputs.treb, frame.treb * BAND_SCALE);
1117        set(self.inputs.bass_att, att[0]);
1118        set(self.inputs.mid_att, att[1]);
1119        set(self.inputs.treb_att, att[2]);
1120    }
1121
1122    /// A wave's per-frame program, run once before its points. Its outputs seed
1123    /// the per-point defaults, which is how a wave whose per-point code sets only
1124    /// `x`/`y` still gets its colour.
1125    fn run_frame(&mut self) {
1126        self.point.seed(&mut self.state);
1127        vm::run(&self.program.per_frame, &mut self.state, Budget::FRAME);
1128        self.take_snapshot();
1129    }
1130
1131    /// One point of a custom wave.
1132    ///
1133    /// **Nothing is restored between points, and that is the semantics rather
1134    /// than an omission** (Plan 0108 Phase 5). A wave's per-point program walks
1135    /// the trace carrying its own working variables forward, which is what lets
1136    /// a preset alternate, accumulate, or integrate along the wave. The idiom
1137    /// the corpus is full of is a two-state counter:
1138    ///
1139    /// ```text
1140    /// flip = flip + 1;
1141    /// flip = flip * below(flip, 2);      // 1, 0, 1, 0, ... down the trace
1142    /// yp   = (flip * 0.1 - 0.05) * sample;
1143    /// ```
1144    ///
1145    /// Reset the registers before each point and those three lines compute a
1146    /// **constant** — the mirrored pair collapses to a single trace and the
1147    /// preset's symmetry silently never happens, which is design-backlog 0107's
1148    /// *chasers 19 Portal* converting cleanly and rendering inert.
1149    ///
1150    /// Measured over the 10 347-file corpus, 2026-08-17: **6 347 files carry a
1151    /// custom-wave per-point program, and 3 368 of them (53 %) read a per-point
1152    /// variable before writing it with nothing in the file seeding it** — so
1153    /// only carry-over can supply a value. 612 of those use `flip` by name.
1154    ///
1155    /// This is the opposite of [`MilkRuntime::run_vertex`], which *does* restore,
1156    /// and the two are not inconsistent: the mesh's per-vertex program is a pure
1157    /// function of its vertex in the reference, and a wave's per-point program
1158    /// is a walk along a line.
1159    ///
1160    /// # The carry reaches past the end of the trace, too
1161    ///
1162    /// Nothing reseeds a working register at the **frame** boundary either
1163    /// (Plan 0108 Mode 4 review, 2026-08-17). [`run_frame`](Self::run_frame)
1164    /// seeds only the named wave-point *outputs* — `WavePointSlots`'s `x`, `y`,
1165    /// `r`, `g`, `b`, `a` — and a wave's `snapshot_of` is empty, so `flip`
1166    /// survives from the last point of one frame into the first point of the
1167    /// next. On an **even**-length trace the two-state counter returns to where
1168    /// it started and this is invisible; on an odd one the figure comes out
1169    /// inverted every other frame, which reads as an alternation at the
1170    /// **display's** refresh rate rather than at any authored one.
1171    ///
1172    /// That is believed faithful — the reference allocates a custom element's
1173    /// variable space once and only its `init` code reseeds it — but it is a
1174    /// claim about the reference and is **not verified against it**; Plan 0108's
1175    /// Phase 6 is where `foo_vis_milk2` answers it. It is pinned meanwhile by
1176    /// `milkconv/tests/draw_layer.rs`'s
1177    /// `a_waves_per_point_state_also_carries_across_the_frame_boundary`, so the
1178    /// behaviour cannot move without this comment moving with it.
1179    fn run_point(&mut self, sample: f32, left: f32, right: f32) -> WavePoint {
1180        if let Some(index) = self.inputs.sample {
1181            self.state.set(index, sample);
1182        }
1183        if let Some(index) = self.inputs.value1 {
1184            self.state.set(index, left);
1185        }
1186        if let Some(index) = self.inputs.value2 {
1187            self.state.set(index, right);
1188        }
1189        vm::run(&self.program.per_point, &mut self.state, Budget::VERTEX);
1190        self.point.read(&self.state)
1191    }
1192
1193    /// One instance of a custom shape.
1194    fn run_instance(&mut self, instance: u32) -> ShapeInstance {
1195        self.restore_snapshot();
1196        self.instance.seed(&mut self.state);
1197        if let Some(index) = self.inputs.instance {
1198            self.state.set(index, instance as f32);
1199        }
1200        if let Some(index) = self.inputs.frame {
1201            self.state.set(index, instance as f32);
1202        }
1203        vm::run(&self.program.per_frame, &mut self.state, Budget::FRAME);
1204        self.instance.read(&self.state)
1205    }
1206
1207    fn take_snapshot(&mut self) {
1208        for (slot, index) in self.snapshot.iter_mut().zip(&self.snapshot_of) {
1209            *slot = self.state.get(*index);
1210        }
1211    }
1212
1213    fn restore_snapshot(&mut self) {
1214        for (value, index) in self.snapshot.iter().zip(&self.snapshot_of) {
1215            self.state.set(*index, *value);
1216        }
1217    }
1218}
1219
1220/// Output `i`'s identity value — what the register holds before a program runs,
1221/// so a program that never writes it leaves the past still.
1222fn identity_output(i: usize) -> f32 {
1223    match OUTPUT_NAMES.get(i) {
1224        Some(&"zoom") | Some(&"sx") | Some(&"sy") => 1.0,
1225        Some(&"cx") | Some(&"cy") => 0.5,
1226        _ => 0.0,
1227    }
1228}
1229
1230/// The widest a converted factor may get, and its reciprocal the narrowest.
1231///
1232/// Raising to [`NOMINAL_FPS`] is a thirtieth power, so it **overflows `f32` at a
1233/// per-frame factor of about 13** — and an overflow that fell back to `1.0` would
1234/// turn the most extreme zoom a preset can ask for into no zoom at all, which is
1235/// the opposite of what it says. Saturating instead keeps the direction: a
1236/// runaway zoom collapses the source window to a point, which is what a runaway
1237/// zoom looks like. Wide enough that no plausible preset reaches it (`1.05` per
1238/// frame, a brisk drift, is `4.3` per second).
1239const MAX_FACTOR: f32 = 1.0e30;
1240
1241/// A per-frame survival/scale factor as a per-second one, at [`NOMINAL_FPS`].
1242///
1243/// `v^fps`: thirty frames of `0.96` is `0.96^30` per second at the nominal rate.
1244/// Total on a non-finite or non-positive input, which a program can produce — a
1245/// factor at or below zero is not a factor, so it reads as the identity rather
1246/// than as a mirror.
1247fn per_second_factor(v: f32) -> f32 {
1248    if !v.is_finite() || v <= 0.0 {
1249        return 1.0;
1250    }
1251    let out = v.powf(NOMINAL_FPS);
1252    if out.is_finite() {
1253        out.clamp(1.0 / MAX_FACTOR, MAX_FACTOR)
1254    } else if v > 1.0 {
1255        MAX_FACTOR
1256    } else {
1257        1.0 / MAX_FACTOR
1258    }
1259}
1260
1261/// A per-frame **scale** as a per-second one, with its sign carried through.
1262///
1263/// [`per_second_factor`]'s "at or below zero is not a factor" is right for
1264/// `decay`, which is a survival fraction — but three of the nine per-vertex
1265/// outputs are *scales*, and a NEGATIVE scale is MilkDrop's standard mirror
1266/// idiom (363 corpus files, 3.5 %). Reading one as the identity deleted the
1267/// mirror here, before the mesh vertex stage ever saw the value. That stage's
1268/// own `max()` guard deleted it a second time; both halves are
1269/// design-backlog 0114, and the other half is `warp_mesh`'s `signed_rate`.
1270///
1271/// The magnitude converts exactly as an unsigned factor does, so a positive
1272/// input is bit-identical to [`per_second_factor`] and nothing shipping a
1273/// positive scale moves. Zero stays on the positive arm for the same reason it
1274/// does in the shader: it is not a mirror, and must not become one.
1275fn per_second_signed_factor(v: f32) -> f32 {
1276    if v.is_finite() && v < 0.0 {
1277        -per_second_factor(-v)
1278    } else {
1279        per_second_factor(v)
1280    }
1281}
1282
1283/// The nine raw MilkDrop outputs as this engine's per-second vocabulary.
1284fn convert_outputs(raw: [f32; 9]) -> [f32; 9] {
1285    std::array::from_fn(|i| {
1286        let v = raw.get(i).copied().unwrap_or(0.0);
1287        if OUTPUT_FACTOR.get(i).copied().unwrap_or(false) {
1288            per_second_signed_factor(v)
1289        } else if OUTPUT_RATE.get(i).copied().unwrap_or(false) {
1290            let out = v * NOMINAL_FPS;
1291            if out.is_finite() { out } else { 0.0 }
1292        } else if v.is_finite() {
1293            v
1294        } else {
1295            identity_output(i)
1296        }
1297    })
1298}
1299
1300#[cfg(test)]
1301mod tests;