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;