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}