Skip to main content

rlx_core/milk/
outputs.rs

1//! The named values a MilkDrop program hands back, as one table (Plan 0100
2//! Phase 4).
3//!
4//! # Why this is a macro
5//!
6//! There are forty-odd of them, and each needs the same four things: a field on a
7//! struct the scene reads, an EEL2 name, a default the host seeds before the
8//! program runs, and whether it is a per-frame *rate* that has to be converted to
9//! this engine's per-second vocabulary. Written by hand that is four parallel
10//! lists, and the failure mode of four parallel lists is a value that silently
11//! reads its neighbour — the exact defect `expr.rs`'s slot-base assertion exists
12//! to catch one axis of.
13//!
14//! So the table below is the single source, and the macro derives all four from
15//! it. Adding an output is one line, in one place, and it cannot be half-added.
16//!
17//! # What is not here
18//!
19//! The **nine warp outputs** (`zoom`, `rot`, `cx`, `cy`, `dx`, `dy`, `sx`, `sy`,
20//! `warp`) are not, and deliberately: they are also read **per vertex**, where
21//! these are per frame only, and they are positionally tied to
22//! [`warp_mesh::PER_VERTEX_PARAMS`](crate::render::scenes::warp_mesh::PER_VERTEX_PARAMS)
23//! in a way a struct of named fields would obscure rather than clarify.
24
25// Hot-path panic-denial pragma (Plan 0002 Phase 2; `core/src/milk` is scanned by
26// the hygiene guard). Read once per frame.
27#![deny(
28    clippy::unwrap_used,
29    clippy::expect_used,
30    clippy::indexing_slicing,
31    clippy::panic,
32    clippy::unreachable
33)]
34
35use super::vm::VmState;
36
37/// How a per-frame output converts to this engine's per-second vocabulary.
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum Rate {
40    /// Not a rate at all — a flag, a colour, a position, a count. Passes through.
41    Plain,
42    /// A factor composed multiplicatively every frame, so it becomes `v^fps`.
43    Factor,
44}
45
46/// Generate a named-output struct plus the three things a runtime needs to use
47/// it: the name roster, the index resolution, and the read-back.
48macro_rules! outputs {
49    (
50        $struct_name:ident, $slots_name:ident, $names:ident;
51        $($(#[$meta:meta])* $field:ident : $name:literal = $default:expr, $rate:ident;)*
52    ) => {
53        /// The values a program left in its output registers this frame, already
54        /// converted out of MilkDrop's per-frame vocabulary where that applies.
55        ///
56        /// [`Default`] is MilkDrop's own default for each, which is what the host
57        /// seeds before running a program — so an output the preset never writes
58        /// comes back as the reference's resting value rather than as zero.
59        ///
60        /// It is therefore **unconverted**, in the reference's vocabulary, and is
61        /// not interchangeable with a value `read` returns. Anything comparing
62        /// the two must convert first.
63        #[derive(Debug, Clone, Copy, PartialEq)]
64        pub struct $struct_name {
65            $($(#[$meta])* pub $field: f32,)*
66        }
67
68        impl Default for $struct_name {
69            fn default() -> Self {
70                Self { $($field: $default,)* }
71            }
72        }
73
74        /// Every field's EEL2 name, in field order. Read by the converter's
75        /// roster check, so a name here and a name there cannot drift.
76        pub const $names: &[&str] = &[$($name,)*];
77
78        /// The resolved register index of each field, or `None` for one the
79        /// program never mentions. Built **once at load**.
80        #[derive(Debug, Default, Clone)]
81        pub struct $slots_name {
82            $($field: Option<u16>,)*
83        }
84
85        impl $slots_name {
86            /// Resolve every field against a roster.
87            pub fn resolve(index: &impl Fn(&str) -> Option<u16>) -> Self {
88                Self { $($field: index($name),)* }
89            }
90
91            /// Seed every field's register with MilkDrop's default, before the
92            /// program runs.
93            pub fn seed(&self, state: &mut VmState) {
94                let d = $struct_name::default();
95                $(if let Some(i) = self.$field { state.set(i, d.$field); })*
96            }
97
98            /// Read every field back, converting the rates.
99            ///
100            /// **Both arms convert.** A default is stated in the reference's
101            /// vocabulary exactly as a program's own value is — [`Default`] is
102            /// MilkDrop's per-frame number, not this engine's per-second one — so
103            /// an unnamed output that skipped `convert` would leave a per-frame
104            /// factor in a field whose doc says "per second". That was
105            /// design-backlog 0121, and it silently ran an entire Plan 0109
106            /// experiment at `0.98`/s instead of `0.5455`/s.
107            pub fn read(&self, state: &VmState) -> $struct_name {
108                let d = $struct_name::default();
109                $struct_name {
110                    $($field: match self.$field {
111                        Some(i) => convert(state.get(i), Rate::$rate, d.$field),
112                        None => convert(d.$field, Rate::$rate, d.$field),
113                    },)*
114                }
115            }
116        }
117    };
118}
119
120outputs! {
121    FrameOutputs, FrameSlots, FRAME_OUTPUT_NAMES;
122
123    /// How much of the previous frame survives — **per second** here, from the
124    /// reference's per-frame factor.
125    decay: "decay" = 0.98, Factor;
126    /// Multiplies the light on the way out.
127    gamma: "gamma" = 1.0, Plain;
128    /// Past `0.5`, the past is toroidal.
129    wrap: "wrap" = 1.0, Plain;
130    /// Darkens a soft disc at the middle of the frame.
131    darken_center: "darken_center" = 0.0, Plain;
132    /// Past `0.5`, `sqrt` of the light.
133    brighten: "brighten" = 0.0, Plain;
134    /// Past `0.5`, the light squared.
135    darken: "darken" = 0.0, Plain;
136    /// Past `0.5`, `c * (1 - c) * 4`.
137    solarize: "solarize" = 0.0, Plain;
138    /// Past `0.5`, `1 - c`.
139    invert: "invert" = 0.0, Plain;
140
141    // --- the video echo (Plan 0109 Phase 3) ---
142    /// How much of a second sampled copy of the finished frame the composite
143    /// blends over the first. `0` is no echo at all, and is the identity.
144    echo_alpha: "echo_alpha" = 0.0, Plain;
145    /// How far that copy is zoomed, about the frame centre.
146    echo_zoom: "echo_zoom" = 1.0, Plain;
147    /// How it is flipped: `0` none, `1` x, `2` y, `3` both. Quantized where it
148    /// is read, not here — see `warp_mesh::echo_orientation`.
149    echo_orient: "echo_orient" = 0.0, Plain;
150
151    // --- the waveform ---
152    /// Which of the eight `wave_mode` figures the waveform draws.
153    wave_mode: "wave_mode" = 0.0, Plain;
154    /// The waveform's centre, in uv with `y = 0` at the top.
155    wave_x: "wave_x" = 0.5, Plain;
156    /// See [`wave_x`](Self::wave_x).
157    wave_y: "wave_y" = 0.5, Plain;
158    /// The waveform's colour.
159    wave_r: "wave_r" = 1.0, Plain;
160    /// See [`wave_r`](Self::wave_r).
161    wave_g: "wave_g" = 1.0, Plain;
162    /// See [`wave_r`](Self::wave_r).
163    wave_b: "wave_b" = 1.0, Plain;
164    /// The waveform's alpha. Under this engine's additive draw it reads as
165    /// intensity rather than as coverage — see the scene's draw layer.
166    wave_a: "wave_a" = 1.0, Plain;
167    /// How far the trace swings. MilkDrop's `fWaveScale`.
168    wave_scale: "wave_scale" = 1.0, Plain;
169    /// How much the trace is smoothed along its length, `0..1`.
170    wave_smoothing: "wave_smoothing" = 0.75, Plain;
171    /// The mode-dependent shape parameter, MilkDrop's `fWaveParam`. Means a
172    /// different thing in every `wave_mode`, which is the reference's own design.
173    wave_mystery: "wave_mystery" = 0.0, Plain;
174    /// Past `0.5`, the trace is drawn as dots rather than a line.
175    wave_usedots: "wave_usedots" = 0.0, Plain;
176    /// Past `0.5`, the trace is drawn thick.
177    wave_thick: "wave_thick" = 0.0, Plain;
178    /// Past `0.5`, the trace **adds** rather than blends, and this engine honours
179    /// the difference. The seam itself is additive by construction (ADR-0056), so
180    /// the alpha-blended case is matched at its steady state instead — see
181    /// [`warp_mesh::draw::Exposure`](crate::render::scenes::warp_mesh::draw::Exposure),
182    /// which is where reading both cases as additive saturated the frame.
183    wave_additive: "wave_additive" = 0.0, Plain;
184    /// Past `0.5`, the trace's colour is normalized to its brightest channel.
185    wave_brighten: "wave_brighten" = 1.0, Plain;
186
187    // --- the two borders ---
188    /// The outer border's thickness, as a fraction of the frame.
189    ob_size: "ob_size" = 0.01, Plain;
190    /// The outer border's colour.
191    ob_r: "ob_r" = 0.0, Plain;
192    /// See [`ob_r`](Self::ob_r).
193    ob_g: "ob_g" = 0.0, Plain;
194    /// See [`ob_r`](Self::ob_r).
195    ob_b: "ob_b" = 0.0, Plain;
196    /// The outer border's alpha, read as intensity.
197    ob_a: "ob_a" = 0.0, Plain;
198    /// The inner border's thickness.
199    ib_size: "ib_size" = 0.01, Plain;
200    /// The inner border's colour.
201    ib_r: "ib_r" = 0.25, Plain;
202    /// See [`ib_r`](Self::ib_r).
203    ib_g: "ib_g" = 0.25, Plain;
204    /// See [`ib_r`](Self::ib_r).
205    ib_b: "ib_b" = 0.25, Plain;
206    /// The inner border's alpha, read as intensity.
207    ib_a: "ib_a" = 0.0, Plain;
208
209    // --- the motion-vector grid ---
210    /// How many motion vectors across.
211    mv_x: "mv_x" = 12.0, Plain;
212    /// How many motion vectors down.
213    mv_y: "mv_y" = 9.0, Plain;
214    /// The grid's offset within a cell, `0..1`.
215    mv_dx: "mv_dx" = 0.0, Plain;
216    /// See [`mv_dx`](Self::mv_dx).
217    mv_dy: "mv_dy" = 0.0, Plain;
218    /// Each vector's length, as a multiple of the warp it samples.
219    mv_l: "mv_l" = 0.9, Plain;
220    /// The grid's colour.
221    mv_r: "mv_r" = 1.0, Plain;
222    /// See [`mv_r`](Self::mv_r).
223    mv_g: "mv_g" = 1.0, Plain;
224    /// See [`mv_r`](Self::mv_r).
225    mv_b: "mv_b" = 1.0, Plain;
226    /// The grid's alpha, read as intensity. `0` — the default — draws nothing.
227    mv_a: "mv_a" = 0.0, Plain;
228}
229
230outputs! {
231    WavePoint, WavePointSlots, WAVE_POINT_NAMES;
232
233    /// The point's position, in uv with `y = 0` at the top.
234    x: "x" = 0.5, Plain;
235    /// See [`x`](Self::x).
236    y: "y" = 0.5, Plain;
237    /// The point's colour, which a custom wave may vary along its length.
238    r: "r" = 1.0, Plain;
239    /// See [`r`](Self::r).
240    g: "g" = 1.0, Plain;
241    /// See [`r`](Self::r).
242    b: "b" = 1.0, Plain;
243    /// The point's alpha, read as intensity.
244    a: "a" = 1.0, Plain;
245}
246
247outputs! {
248    ShapeInstance, ShapeInstanceSlots, SHAPE_INSTANCE_NAMES;
249
250    /// The shape's centre, in uv with `y = 0` at the top.
251    x: "x" = 0.5, Plain;
252    /// See [`x`](Self::x).
253    y: "y" = 0.5, Plain;
254    /// The shape's radius, in frame-heights.
255    rad: "rad" = 0.1, Plain;
256    /// The shape's rotation, in radians.
257    ang: "ang" = 0.0, Plain;
258    /// The centre colour.
259    r: "r" = 1.0, Plain;
260    /// See [`r`](Self::r).
261    g: "g" = 0.0, Plain;
262    /// See [`r`](Self::r).
263    b: "b" = 0.0, Plain;
264    /// The centre alpha, read as intensity.
265    a: "a" = 1.0, Plain;
266    /// The edge colour, which the fill ramps toward.
267    r2: "r2" = 0.0, Plain;
268    /// See [`r2`](Self::r2).
269    g2: "g2" = 1.0, Plain;
270    /// See [`r2`](Self::r2).
271    b2: "b2" = 0.0, Plain;
272    /// The edge alpha.
273    a2: "a2" = 0.0, Plain;
274    /// The outline's colour.
275    border_r: "border_r" = 1.0, Plain;
276    /// See [`border_r`](Self::border_r).
277    border_g: "border_g" = 1.0, Plain;
278    /// See [`border_r`](Self::border_r).
279    border_b: "border_b" = 1.0, Plain;
280    /// The outline's alpha. `0` draws no outline.
281    border_a: "border_a" = 0.1, Plain;
282    /// Past `0.5`, the outline is drawn thick.
283    thick_outline: "thickoutline" = 0.0, Plain;
284    /// How many sides this instance has, overriding the structural count.
285    sides: "sides" = 4.0, Plain;
286    /// Past `0.5`, this instance **adds** rather than blends. Per instance rather
287    /// than per element, because a shape's per-frame program may write it — see
288    /// [`warp_mesh::draw::Exposure`](crate::render::scenes::warp_mesh::draw::Exposure).
289    additive: "additive" = 0.0, Plain;
290}
291
292/// One raw output as this engine's vocabulary.
293///
294/// A `Factor` is raised to [`NOMINAL_FPS`](super::NOMINAL_FPS) and saturated
295/// rather than allowed to overflow to the identity — see
296/// [`per_second_factor`](super::per_second_factor). Everything else passes
297/// through, with a non-finite value falling back to the reference's default so a
298/// single `NaN` cannot blank a frame.
299pub(super) fn convert(value: f32, rate: Rate, default: f32) -> f32 {
300    match rate {
301        Rate::Factor => super::per_second_factor(value),
302        Rate::Plain => {
303            if value.is_finite() {
304                value
305            } else {
306                default
307            }
308        }
309    }
310}