Skip to main content

rlx_core/render/scenes/warp_mesh/
draw.rs

1//! What MilkDrop draws between the warp and the composite (Plan 0100 Phase 4):
2//! the waveform, the custom waves and shapes, the two borders, and the
3//! motion-vector grid.
4//!
5//! # It is CPU geometry, deliberately
6//!
7//! Every figure here is a handful of hundreds of points produced by a program the
8//! preset wrote, so it is built on the render thread into two reused buffers and
9//! handed to the GPU as one line batch and one triangle batch. **Nothing here
10//! allocates per frame**: both buffers are sized once at preset load from the
11//! bundle's own counts, which are bounded by
12//! [`MAX_WAVE_POINTS`](crate::milk::MAX_WAVE_POINTS) and its siblings.
13//!
14//! # Two blend modes, and why the geometry is ordered
15//!
16//! MilkDrop chooses per element: `bAdditiveWaves` and a custom element's own
17//! `additive` pick between `dst = src + dst` and `dst = src*a + dst*(1-a)`. Both
18//! are honoured here, by **partitioning each buffer** — additive producers first,
19//! over-blended ones after — and handing the split index to the two-pipeline
20//! draw ([`LineRenderer::draw_split`](crate::render::scenes::lines::LineRenderer::draw_split)).
21//!
22//! Reading both as additive is what saturated the frame to flat colour inside
23//! half a second, and it is not a small error: an additive seam **sums** where
24//! alpha-over **replaces**, so N overlapping producers land at N rather than at
25//! ≤ 1. That bites hardest on the **28.5 % of the corpus that sets
26//! `fDecay >= 1.0`** (2 949 of 10 347, measured 2026-08-16), where the field is a
27//! perfect integrator and nothing brings the sum back down.
28//!
29//! The order within each half is the order MilkDrop draws in — waveform, custom
30//! waves, custom shapes, borders, motion vectors — because an over-blended stroke
31//! must land on top of what it covers.
32//!
33//! # The coordinate space, once
34//!
35//! MilkDrop places everything in **uv**: `0..1` across the frame with `y = 0` at
36//! the *top*. The shared line renderer takes **world** space: `y` in `-1..1`
37//! bottom-to-top, `x` in `-aspect..aspect` (its shader divides x by the aspect).
38//! [`uv_to_world`] is the one conversion, and every producer below goes through
39//! it — which is what makes a figure round on any display and is the ADR-0037
40//! rule applied to geometry rather than to a grid.
41//!
42//! # What is approximated, stated
43//!
44//! - **A dot is a very short segment with both caps extended.** `wave_usedots`
45//!   draws points; the line renderer draws quads between endpoints, and its
46//!   falloff runs across the stroke only — so a short segment is round *because*
47//!   both endpoint extensions push the quad past it by the half-width
48//!   (ADR-0158), not because it is short. Without them it is a sub-pixel dash;
49//!   see `dots`.
50//! - **`wave_mystery` means something different in every mode**, which is the
51//!   reference's own design rather than a simplification here.
52//! - **Mode 6 and 7's line does not drift.** Its angle is `wave_mystery` alone;
53//!   a `time * 0.05` term rotates it a full turn every ~126 s, which a Plan 0109
54//!   Phase 2 look gate rejected against *Blur Mix 3*'s horizontal reference
55//!   traces. Listed here because the reference's own mode 6 is documented only
56//!   as "a line", and "which line" is a reading of it — see the arm's comment.
57
58// Hot-path panic-denial pragma (Plan 0002 Phase 2, extended to scenes by Plan
59// 0003 Phase 0). This builds geometry every displayed frame.
60#![deny(
61    clippy::unwrap_used,
62    clippy::expect_used,
63    clippy::indexing_slicing,
64    clippy::panic,
65    clippy::unreachable
66)]
67
68use crate::dsp::WAVE_SAMPLES;
69use crate::milk::outputs::FrameOutputs;
70use crate::milk::{MAX_SHAPE_SIDES, MilkRuntime};
71use crate::render::scenes::lines::SegmentInstance;
72
73/// One vertex of a filled custom shape.
74#[repr(C)]
75#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
76pub struct ShapeVertex {
77    /// World-space position — see the module docs.
78    pub pos: [f32; 2],
79    /// Premultiplied RGB, already scaled by the instance's alpha.
80    pub color: [f32; 3],
81    /// Coverage, which the additive seam needs to equal the light's own
82    /// footprint (ADR-0056).
83    pub alpha: f32,
84}
85
86/// The two buffers a frame's draw layer fills, each **partitioned by blend
87/// mode**. Sized once at preset load.
88///
89/// Additive geometry occupies `..n_additive` and over-blended geometry the rest,
90/// which is what lets one buffer and one render pass serve two pipelines — see
91/// the module docs. Producers are appended through `push_segment`
92/// and `push_triangle` rather than to the vectors
93/// directly, so the invariant is maintained in one place.
94#[derive(Default)]
95pub struct DrawGeometry {
96    /// Every line: the waveform, custom waves, shape outlines, borders, motion
97    /// vectors.
98    pub segments: Vec<SegmentInstance>,
99    /// How many leading entries of [`segments`](Self::segments) blend additively.
100    pub segments_additive: usize,
101    /// Every filled shape's triangles, as a plain list.
102    pub triangles: Vec<ShapeVertex>,
103    /// How many leading entries of [`triangles`](Self::triangles) blend
104    /// additively. Always a multiple of 3 — a triangle's three vertices share one
105    /// blend mode.
106    pub triangles_additive: usize,
107}
108
109impl DrawGeometry {
110    /// Discard last frame's geometry without giving back its capacity — the
111    /// reuse that keeps the per-frame path allocation-free.
112    pub fn clear(&mut self) {
113        self.segments.clear();
114        self.segments_additive = 0;
115        self.triangles.clear();
116        self.triangles_additive = 0;
117    }
118
119    /// Whether anything was built this frame.
120    pub fn is_empty(&self) -> bool {
121        self.segments.is_empty() && self.triangles.is_empty()
122    }
123
124    /// Append one segment into its blend mode's half.
125    ///
126    /// An additive one is **inserted** at the partition rather than pushed, which
127    /// is `O(n)` in the over-blended tail. That is deliberate and it is cheap:
128    /// the whole layer is a few hundred segments of CPU geometry (see the module
129    /// docs), and the alternative — two vectors concatenated per frame — would
130    /// either allocate or need a third buffer. Neither is worth it at this size,
131    /// and this keeps the draw order MilkDrop's within each half.
132    fn push_segment(&mut self, segment: SegmentInstance, additive: bool) {
133        if additive {
134            self.segments.insert(self.segments_additive, segment);
135            self.segments_additive += 1;
136        } else {
137            self.segments.push(segment);
138        }
139    }
140
141    /// Append one triangle — three vertices, one blend mode. See
142    /// [`push_segment`](Self::push_segment) for why the additive case inserts.
143    fn push_triangle(&mut self, vertices: [ShapeVertex; 3], additive: bool) {
144        if additive {
145            for (offset, vertex) in vertices.into_iter().enumerate() {
146                self.triangles
147                    .insert(self.triangles_additive + offset, vertex);
148            }
149            self.triangles_additive += 3;
150        } else {
151            self.triangles.extend_from_slice(&vertices);
152        }
153    }
154}
155
156/// uv (`y` down, `0..1`) to the line renderer's world space (`y` up, `x` scaled
157/// by the aspect).
158///
159/// **The one conversion**, and the only place the aspect enters the draw layer.
160/// `aspect` is the **render target's** (ADR-0037).
161pub fn uv_to_world(x: f32, y: f32, aspect: f32) -> [f32; 2] {
162    [(x * 2.0 - 1.0) * aspect, 1.0 - y * 2.0]
163}
164
165/// The stroke half-width a `thick` flag selects, in NDC-y units.
166///
167/// MilkDrop draws a thick line as two or four passes offset by a pixel; here it
168/// is one stroke of twice the width, which is the same gesture through this
169/// engine's soft-falloff primitive.
170const THIN: f32 = 0.0025;
171/// See [`THIN`].
172const THICK: f32 = 0.006;
173
174/// How long a `wave_usedots` dot's segment is, in world units. Short enough that
175/// the falloff reads as round and long enough that the quad is not degenerate.
176const DOT_LENGTH: f32 = 0.0015;
177
178/// Build the whole draw layer for this frame.
179///
180/// `runtime` is `None` for a hand-authored `warp_mesh` preset, which draws no
181/// MilkDrop layer at all — so this whole file costs a native preset one branch.
182/// **`time` is read by exactly three of the eight `wave_mode` figures**, and by
183/// nothing else in this file: mode 0 turns at `0.2` rad/s (`milkdropfs.cpp`
184/// l.2886-2925), mode 1 at `2.3` (l.2942) and mode 5 at `0.3` (l.3085-3086).
185/// Every other figure is a pure function of the trace and the frame outputs, and
186/// that narrowed claim is what `draw_layer.rs` tests — by calling this twice at
187/// well-separated times for a mode with no time term, and by asserting each
188/// turning mode comes back after exactly one turn at its own rate.
189#[allow(clippy::too_many_arguments)]
190pub fn build(
191    geometry: &mut DrawGeometry,
192    runtime: Option<&mut MilkRuntime>,
193    out: &FrameOutputs,
194    pair: &[[f32; WAVE_SAMPLES]; 2],
195    time: f32,
196    dt: f32,
197    aspect: f32,
198    target_width: u32,
199) {
200    geometry.clear();
201    let Some(runtime) = runtime else {
202        return;
203    };
204    let exposure = Exposure::new(dt);
205    // The two traces, smoothed and scaled **once**, the way the source builds
206    // `fWaveL`/`fWaveR` before any figure reads them (`milkdropfs.cpp`
207    // l.933-942): a one-pole run along the trace at `wave_smoothing`, then
208    // `wave_scale`. Every mode and every custom wave indexes these.
209    let smooth = out.wave_smoothing.clamp(0.0, 0.99);
210    let scale = out.wave_scale;
211    let mut traces = [[0.0f32; WAVE_SAMPLES]; 2];
212    for (dst, src) in traces.iter_mut().zip(pair.iter()) {
213        let mut held = 0.0f32;
214        for (slot, raw) in dst.iter_mut().zip(src.iter()) {
215            held = held * smooth + raw * (1.0 - smooth);
216            *slot = held * scale;
217        }
218    }
219    let [left, right] = &traces;
220    waveform_figure(
221        geometry,
222        out,
223        left,
224        right,
225        time,
226        exposure,
227        aspect,
228        target_width,
229    );
230    custom_waves(geometry, runtime, left, right, exposure, aspect);
231    custom_shapes(geometry, runtime, exposure, aspect);
232    // The two borders and the motion-vector grid are **always** alpha-blended in
233    // the reference — neither has an additive flag to read — so they go to the
234    // over half unconditionally.
235    borders(geometry, out, exposure, aspect);
236    motion_vectors(geometry, out, exposure, aspect);
237}
238
239/// **What one frame of the draw layer is worth**, which depends on how the
240/// producer that drew it blends. Both cases are a rate rather than a constant,
241/// and for the same reason.
242///
243/// MilkDrop deposits once per rendered frame into a buffer that decays once per
244/// rendered frame, so the two are in step by construction. Here the field decays
245/// **per second** (`decay` is rate-converted like every other MilkDrop factor),
246/// which means a 60 Hz display would deposit twice the light per second that a
247/// 30 Hz one does into a buffer that fades at the same rate — and the picture
248/// would differ with the refresh, which ADR-0019 exists to prevent. `rate` is how
249/// many nominal frames of wall clock this frame is, and both branches of
250/// `scale` convert through it.
251#[derive(Clone, Copy)]
252pub struct Exposure {
253    /// `dt * NOMINAL_FPS`: how many nominal frames of wall clock this frame is.
254    rate: f32,
255}
256
257impl Exposure {
258    /// From this frame's `dt`, capped at **four** nominal frames: a long frame
259    /// deposits proportionally more light, but a stall cannot deposit an
260    /// unbounded amount in one go.
261    pub fn new(dt: f32) -> Self {
262        Self {
263            rate: (dt * crate::milk::NOMINAL_FPS).clamp(0.0, 4.0),
264        }
265    }
266
267    /// The producer's effective alpha this frame — its colour is premultiplied
268    /// by this, and it is the coverage the fragment writes.
269    ///
270    /// # The two conversions
271    ///
272    /// **Additive** light composes by *addition* across frames, so `n` frames
273    /// deposit `n * a` and the rate conversion is the plain product `a * rate`.
274    ///
275    /// **Alpha-over** composes by *repeated interpolation*: `n` frames of
276    /// `dst = src*a + dst*(1-a)` leave `1 - (1-a)^n` of the way travelled, so the
277    /// conversion is `1 - (1-a)^rate`. At `rate = 1` it is exactly `a`, which is
278    /// what makes 60 Hz the reference's own cadence rather than an approximation
279    /// of it; at `rate = 2` a 30 Hz frame travels as far as two 60 Hz ones, which
280    /// is the property ADR-0019 asks for.
281    ///
282    /// Note that the alpha-over branch is **bounded by 1** for every `rate`,
283    /// which is the whole reason this is worth two pipelines: the sum of N
284    /// over-blended producers is still ≤ 1, where N additive ones is N.
285    fn scale(self, alpha: f32, additive: bool) -> f32 {
286        let a = alpha.clamp(0.0, 1.0);
287        if additive {
288            a * self.rate
289        } else {
290            1.0 - (1.0 - a).powf(self.rate)
291        }
292    }
293}
294
295/// A producer's premultiplied colour and its coverage, both already scaled by
296/// [`Exposure::scale`].
297///
298/// The two are returned together because both seams need coverage to equal the
299/// light's own footprint: additively so a dim deposit does not occlude a lit
300/// backdrop (ADR-0056), and over-blended because the coverage *is* the blend's
301/// alpha.
302#[derive(Clone, Copy)]
303struct Light {
304    /// Premultiplied colour: the producer's RGB times its effective alpha.
305    rgb: [f32; 3],
306    /// **The alpha the fragment writes**, which is not the same number in the two
307    /// seams and that difference is ADR-0056's rule rather than an inconsistency.
308    ///
309    /// Over-blended, coverage *is* the blend's alpha — a `wave_a = 0.1` stroke
310    /// must replace a tenth of what is under it, so it is the effective alpha.
311    ///
312    /// Additive, it is **`1.0`**: "a dimmed stroke still covers its own
313    /// footprint", so brightness lives in [`rgb`](Self::rgb) and the geometry's
314    /// own falloff is the whole footprint. Passing the effective alpha here
315    /// instead would make a dim additive stroke *narrower* rather than darker,
316    /// and could exceed 1 at a long `dt`.
317    coverage: f32,
318}
319
320impl Light {
321    /// Whether this producer writes anything worth a draw call.
322    fn is_dark(&self) -> bool {
323        self.rgb.iter().all(|c| *c <= 0.0001)
324    }
325}
326
327/// One point of a stroked figure: where it is, and what it deposits there.
328type Point = ([f32; 2], Light);
329
330/// Colour and coverage for one producer. `wave_brighten` normalizes to the
331/// brightest channel first, which is what the reference's `bMaximizeWaveColor`
332/// does.
333fn light(
334    r: f32,
335    g: f32,
336    b: f32,
337    a: f32,
338    brighten: bool,
339    exposure: Exposure,
340    additive: bool,
341) -> Light {
342    let (mut r, mut g, mut b) = (r, g, b);
343    if brighten {
344        let peak = r.max(g).max(b);
345        if peak > 0.0001 {
346            let k = 1.0 / peak;
347            r *= k;
348            g *= k;
349            b *= k;
350        }
351    }
352    let a = exposure.scale(a, additive);
353    Light {
354        rgb: [r * a, g * a, b * a],
355        coverage: if additive { 1.0 } else { a },
356    }
357}
358
359/// Push a polyline, flagging the interior joins so the strokes meet cleanly
360/// (ADR-0041).
361fn polyline(
362    geometry: &mut DrawGeometry,
363    points: &[Point],
364    width: f32,
365    closed: bool,
366    additive: bool,
367) {
368    let n = points.len();
369    if n < 2 {
370        return;
371    }
372    let last = if closed { n } else { n - 1 };
373    for i in 0..last {
374        let Some((a, light)) = points.get(i) else {
375            continue;
376        };
377        let Some((b, _)) = points.get((i + 1) % n) else {
378            continue;
379        };
380        let ext_a = if closed || i > 0 { width } else { 0.0 };
381        let ext_b = if closed || i + 2 < n { width } else { 0.0 };
382        geometry.push_segment(
383            SegmentInstance {
384                a: *a,
385                b: *b,
386                color: light.rgb,
387                width,
388                alpha: light.coverage,
389                ext_a,
390                ext_b,
391            },
392            additive,
393        );
394    }
395}
396
397/// Emit one built trace the way the mode asked for it: separated marks when
398/// `wave_usedots` is set, a continuous stroke otherwise.
399///
400/// **This exists because there were four call sites and one of them forgot**
401/// (Plan 0108 Phase 4). `wave_mode 5` draws its figure in two passes and its
402/// first pass called [`polyline`] unconditionally, so a preset asking for dots
403/// got a continuous stroke above `wave_y` and beads below it. Nothing failed and
404/// nothing warned; the trace was simply half wrong. The defect was found by
405/// measurement — mode 5's dotted geometry held segments **13.6x longer than
406/// [`DOT_LENGTH`]** where every other mode's held none — and the repair is to
407/// leave one place where the choice is made.
408fn emit_trace(
409    geometry: &mut DrawGeometry,
410    points: &[Point],
411    width: f32,
412    closed: bool,
413    additive: bool,
414    use_dots: bool,
415) {
416    if use_dots {
417        dots(geometry, points, width, additive);
418    } else {
419        polyline(geometry, points, width, closed, additive);
420    }
421}
422
423/// Push each point as its own dot.
424///
425/// **Both ends are extended by a half-width, and that is what makes a dot a
426/// dot** (Plan 0108 Phase 4). The line renderer's falloff runs *across* the
427/// stroke only; the quad simply ends at each endpoint unless `ext_a`/`ext_b`
428/// push it past (ADR-0158). Without that extension a mark is a hard-edged
429/// [`DOT_LENGTH`] x `2 * width` rectangle — **3.3x wider than it is long**, a
430/// sub-pixel dash lying across the trace rather than the round dot this module's
431/// header describes. Measured at 1080p on one drawn frame, that cost 300 pixels
432/// above half brightness against a continuous stroke's 5 008, and at 320x180 it
433/// left **2**, which is design-backlog 0107's "the `wave_usedots` beads never
434/// appear".
435///
436/// With both ends extended the quad grows by the half-width at each cap, so the
437/// mark is `DOT_LENGTH + 2 * width` long against `2 * width` across — round
438/// enough that the falloff reads as a bead at any resolution, through the
439/// mechanism that already exists rather than a new constant.
440///
441/// The mark is also **centred** on its point rather than growing forward from
442/// it. Half of [`DOT_LENGTH`] is well under a pixel, so this moves nothing
443/// visible; it is here because a dot that is offset from the sample it stands
444/// for is wrong in a way nobody would ever see and everybody would inherit.
445fn dots(geometry: &mut DrawGeometry, points: &[Point], width: f32, additive: bool) {
446    for (p, light) in points {
447        geometry.push_segment(
448            SegmentInstance {
449                a: [p[0] - DOT_LENGTH * 0.5, p[1]],
450                b: [p[0] + DOT_LENGTH * 0.5, p[1]],
451                color: light.rgb,
452                width,
453                alpha: light.coverage,
454                // A cap, not a miter: this is the half-width that rounds the
455                // mark, and a zero-length segment has no interior angle to
456                // compute one from.
457                ext_a: width,
458                ext_b: width,
459            },
460            additive,
461        );
462    }
463}
464
465/// How many `wave_mode` figures there are — MilkDrop's own eight.
466pub const WAVE_MODES: u32 = 8;
467
468/// The factor between the source's sample term and what the host people run
469/// actually draws.
470///
471/// `foo_vis_milk2` 0.2.0.0, `wave_mode = 6`, a full-scale 200 Hz sine at
472/// `fWaveScale = 1`, captured by Plan 0127: the trace measured **0.316 frame
473/// heights peak to peak**, over a stroke whose own width read `0.0019`. The
474/// source's mode-6 coefficient is `0.25` clip along the line's normal, which is
475/// `0.125` frame heights per unit sample, so the factor is
476/// `((0.316 - 0.0019) / 2) / 0.125`.
477///
478/// **It multiplies the sample term and nothing else** — never a base radius, a
479/// separation or an extent (ADR-0199). It is measured on one mode and inferred
480/// for the other seven **and for a custom wave's `value1`/`value2`**
481/// (ADR-0223), on the argument that the host's gap is in the sample path every
482/// figure shares; the confirmation is a unit-scale mode-0 capture that
483/// Plan 0142's rig session owes.
484pub(crate) const HOST_SAMPLE_FACTOR: f32 = 1.256;
485
486/// A whole turn, **as the source writes it** (`milkdropfs.cpp` l.2886-2948):
487/// `6.28`, not `TAU`.
488///
489/// The difference is 0.05 % of a turn over the 239 steps modes 0 and 1 lay down,
490/// so the last point falls an eighth of a step short of the first. Mode 0's
491/// cosine blend is what closes that join, and writing `TAU` here would move every
492/// converted circle by that eighth of a step for no reason but tidiness.
493#[allow(
494    clippy::approx_constant,
495    reason = "the source writes 6.28 and the truncation is the behaviour, not a typo for TAU"
496)]
497const SOURCE_TURN: f32 = 6.28;
498
499/// The largest figure the source builds, in constructed points: modes 2, 3, 4
500/// and 5 at 480. [`smooth_wave`] turns `n` of them into `2n - 1`.
501const MAX_WAVE_POINTS: usize = 480;
502/// See [`MAX_WAVE_POINTS`].
503const MAX_SMOOTHED_POINTS: usize = 2 * MAX_WAVE_POINTS - 1;
504
505/// `CPlugin::DrawWave`'s `SmoothWave` (`milkdropfs.cpp` l.2549-2577), applied to
506/// every built-in figure once it is constructed (l.3319-3335) and to each side of
507/// mode 7's break separately.
508///
509/// It inserts one point between each consecutive pair, at
510/// `(-0.15 v[i-1] + 1.15 v[i] + 1.15 v[i+1] - 0.15 v[i+2]) / 2` with the indices
511/// clamped at the ends, so `n` points become `2n - 1` and **every original point
512/// keeps its position, at an even index**. That last property is what lets a test
513/// read the figure the mode built rather than the curve through it.
514///
515/// The interpolation is affine in the points, so running it in uv rather than in
516/// the line renderer's world space is the same arithmetic — `uv_to_world` is
517/// itself affine.
518fn smooth_wave(src: &[[f32; 2]], dst: &mut [[f32; 2]; MAX_SMOOTHED_POINTS]) -> usize {
519    if src.len() < 2 {
520        for (slot, point) in dst.iter_mut().zip(src) {
521            *slot = *point;
522        }
523        return src.len();
524    }
525    let at = |i: isize| -> [f32; 2] {
526        let clamped = i.clamp(0, src.len() as isize - 1) as usize;
527        src.get(clamped).copied().unwrap_or([0.0; 2])
528    };
529    let mut used = 0usize;
530    for i in 0..src.len() {
531        if let Some(slot) = dst.get_mut(used) {
532            *slot = at(i as isize);
533        }
534        used += 1;
535        if i + 1 == src.len() {
536            break;
537        }
538        let (a, b, c, d) = (
539            at(i as isize - 1),
540            at(i as isize),
541            at(i as isize + 1),
542            at(i as isize + 2),
543        );
544        if let Some(slot) = dst.get_mut(used) {
545            *slot = inserted_point(a, b, c, d);
546        }
547        used += 1;
548    }
549    used.min(MAX_SMOOTHED_POINTS)
550}
551
552/// The one point `SmoothWave` inserts between `b` and `c`, given the points
553/// either side of that pair — `a` before `b`, `d` after `c`, each clamped to the
554/// figure's ends by the caller.
555///
556/// **The kernel, in one place**, because two callers run it: the built-in
557/// figures through [`smooth_wave`] and a custom wave through [`smooth_points`].
558/// The four coefficients sum to `2`, so the `* 0.5` is the kernel's own
559/// normalization rather than an average of the middle pair.
560fn inserted_point(a: [f32; 2], b: [f32; 2], c: [f32; 2], d: [f32; 2]) -> [f32; 2] {
561    [
562        (-0.15 * a[0] + 1.15 * b[0] + 1.15 * c[0] - 0.15 * d[0]) * 0.5,
563        (-0.15 * a[1] + 1.15 * b[1] + 1.15 * c[1] - 0.15 * d[1]) * 0.5,
564    ]
565}
566
567/// [`smooth_wave`] over points that carry a light, which is the form a custom
568/// wave's per-point program produces (ADR-0223 — the source smooths a custom
569/// wave too, unless it draws dots).
570///
571/// The positions are the same insertion, so `n` points become `2n - 1` and every
572/// original keeps its place at an even index. **The inserted point takes the
573/// light of the point before it** rather than a blend of the pair: a custom
574/// wave's colour is its own program's output at a point the program ran at, and
575/// there is no run between two points to take a colour from.
576///
577/// Writes into `dst` rather than returning, so one buffer serves every wave of a
578/// frame.
579fn smooth_points(src: &[Point], dst: &mut Vec<Point>) {
580    dst.clear();
581    if src.len() < 2 {
582        dst.extend_from_slice(src);
583        return;
584    }
585    let at = |i: isize| -> Option<Point> {
586        let clamped = i.clamp(0, src.len() as isize - 1) as usize;
587        src.get(clamped).copied()
588    };
589    for i in 0..src.len() {
590        let i = i as isize;
591        let Some(b) = at(i) else {
592            continue;
593        };
594        dst.push(b);
595        if i + 1 == src.len() as isize {
596            break;
597        }
598        let (Some(a), Some(c), Some(d)) = (at(i - 1), at(i + 1), at(i + 2)) else {
599            continue;
600        };
601        dst.push((inserted_point(a.0, b.0, c.0, d.0), b.1));
602    }
603}
604
605/// The source's `wave_mystery` fold, `milkdropfs.cpp` l.2869-2877 — **modes 0, 1
606/// and 4 only**, which is why it is a function rather than applied to the output
607/// on the way in. Modes 6 and 7 read the value unfolded, and the rest ignore it.
608fn fold_mystery(m: f32) -> f32 {
609    if !m.is_finite() {
610        return 0.0;
611    }
612    m - 2.0 * ((m + 1.0) * 0.5).floor()
613}
614
615/// The built-in waveform: MilkDrop's eight `wave_mode` figures, each built the
616/// way `CPlugin::DrawWave` builds it (`milkdropfs.cpp` l.2765-3259).
617///
618/// # The two coordinate conventions, and which modes use which
619///
620/// The source works in a clip space where `-1..1` is the whole frame on **both**
621/// axes. Modes 0, 1, 2, 3 and 5 multiply their x offset by `m_fAspectY` and their
622/// y offset by `m_fAspectX` before placing it, which puts those figures in
623/// shorter-axis units and makes a circle round; modes 4, 6 and 7 apply no aspect
624/// term at all, so their extents are the frame's own. Both are reproduced here,
625/// and [`uv_to_world`] supplies the one conversion out (ADR-0037).
626///
627/// # What the eight figures are
628///
629/// Modes 2 and 3 are **the same geometry, line for line** (l.2950-2976 against
630/// l.2977-3004) and differ only in the alpha they draw at (l.2982-2991), which
631/// this file does not carry. That is the source's own design and not a gap here.
632///
633/// Modes 1, 2, 3, 5 and 7 read **both** channels of the analyzer's pair; modes 0
634/// and 6 read one.
635#[allow(clippy::too_many_arguments)]
636fn waveform_figure(
637    geometry: &mut DrawGeometry,
638    out: &FrameOutputs,
639    left: &[f32; WAVE_SAMPLES],
640    right: &[f32; WAVE_SAMPLES],
641    time: f32,
642    exposure: Exposure,
643    aspect: f32,
644    target_width: u32,
645) {
646    let additive = out.wave_additive >= 0.5;
647    let colour = light(
648        out.wave_r,
649        out.wave_g,
650        out.wave_b,
651        out.wave_a,
652        out.wave_brighten >= 0.5,
653        exposure,
654        additive,
655    );
656    if colour.is_dark() {
657        return;
658    }
659    let stroke = if out.wave_thick >= 0.5 { THICK } else { THIN };
660    // Read once and passed to every emit below: the mode that draws in two passes
661    // has to make the same choice in both, and reading the flag at each site is
662    // how one of them came to be a stroke where the other was beads.
663    let use_dots = out.wave_usedots >= 0.5;
664    let (cx, cy) = (out.wave_x, out.wave_y);
665    let mystery = out.wave_mystery;
666    let folded = fold_mystery(mystery);
667    let mode = (out.wave_mode.max(0.0) as u32) % WAVE_MODES;
668
669    // The sample term, with the host factor on it and nothing else (ADR-0199).
670    let l = |i: usize| left.get(i).copied().unwrap_or(0.0) * HOST_SAMPLE_FACTOR;
671    let r = |i: usize| right.get(i).copied().unwrap_or(0.0) * HOST_SAMPLE_FACTOR;
672
673    // Clip to uv. `centred` carries the aspect pair and the preset's centre;
674    // `plain` is the frame's own clip space, which modes 4, 6 and 7 place in.
675    let (ax, ay) = if aspect >= 1.0 {
676        (1.0, 1.0 / aspect)
677    } else {
678        (aspect, 1.0)
679    };
680    let centred = |px: f32, py: f32| [cx + 0.5 * px * ay, cy - 0.5 * py * ax];
681    let plain = |px: f32, py: f32| [0.5 + 0.5 * px, 0.5 - 0.5 * py];
682
683    let mut pts = [[0.0f32; 2]; MAX_WAVE_POINTS];
684    let mut n = 0usize;
685    let push = |p: [f32; 2], pts: &mut [[f32; 2]; MAX_WAVE_POINTS], n: &mut usize| {
686        if let Some(slot) = pts.get_mut(*n) {
687            *slot = p;
688            *n += 1;
689        }
690    };
691    // `min(480, texW/3)` and `min(240, texW/3)`: the source refuses to lay more
692    // points than a third of the frame's pixels across (l.3005-3065, l.3100-3244).
693    let thirds = (target_width / 3).max(2) as usize;
694
695    match mode {
696        // 0 — a circle of radius 0.5 clip, breathing with the right channel, and
697        // turning at 0.2 rad/s (l.2886-2925). The first 24 points cosine-blend to
698        // the sample 240 further along, which is what closes the loop without a
699        // step across the join.
700        0 => {
701            let count = 240usize;
702            for i in 0..count {
703                let ang = i as f32 / (count - 1) as f32 * SOURCE_TURN + time * 0.2;
704                let blend = 24usize;
705                let s = if i < blend {
706                    let mix = 0.5 - 0.5 * (i as f32 / blend as f32 * std::f32::consts::PI).cos();
707                    r(i + 360) * (1.0 - mix) + r(i + 120) * mix
708                } else {
709                    r(i + 120)
710                };
711                let rad = 0.5 + 0.4 * s + folded;
712                push(centred(rad * ang.cos(), rad * ang.sin()), &mut pts, &mut n);
713            }
714        }
715        // 1 — a polar figure: the right channel drives the radius and the left
716        // the angle, turning at 2.3 rad/s (l.2927-2948). Open, unlike mode 0.
717        1 => {
718            let count = 240usize;
719            for i in 0..count {
720                let rad = 0.53 + 0.43 * r(i) + folded;
721                let ang =
722                    i as f32 / (count - 1) as f32 * SOURCE_TURN + 1.57 * l(i + 32) + time * 2.3;
723                push(centred(rad * ang.cos(), rad * ang.sin()), &mut pts, &mut n);
724            }
725        }
726        // 2 and 3 — the x-y scope, right against left at a 32-sample offset
727        // (l.2950-3004). One construction: the two modes differ in alpha alone.
728        2 | 3 => {
729            let count = 480usize;
730            for i in 0..count {
731                push(centred(r(i), l(i + 32)), &mut pts, &mut n);
732            }
733        }
734        // 4 — a horizontal sweep whose y comes from the left channel and whose x
735        // is nudged by the right, then run through the source's momentum filter
736        // (l.3005-3065). `wave_mystery` sets how much of each point's step
737        // carries into the next.
738        4 => {
739            let count = 480usize.min(thirds);
740            let offset = (480 - count) / 2;
741            let w1 = 0.45 + 0.5 * (folded * 0.5 + 0.5);
742            let w2 = 1.0 - w1;
743            let mut dx = [0.0f32; MAX_WAVE_POINTS];
744            let mut dy = [0.0f32; MAX_WAVE_POINTS];
745            for i in 0..count {
746                if let (Some(sx), Some(sy)) = (dx.get_mut(i), dy.get_mut(i)) {
747                    *sx = 0.44 * r(i + 25 + offset);
748                    *sy = 0.47 * l(i + offset);
749                }
750            }
751            for series in [&mut dx, &mut dy] {
752                for i in 2..count {
753                    let (a, b) = (
754                        series.get(i - 1).copied().unwrap_or(0.0),
755                        series.get(i - 2).copied().unwrap_or(0.0),
756                    );
757                    if let Some(slot) = series.get_mut(i) {
758                        *slot = *slot * w2 + w1 * (2.0 * a - b);
759                    }
760                }
761            }
762            for i in 0..count {
763                let along = -1.0 + 2.0 * i as f32 / count.max(2) as f32;
764                let px = along + (cx * 2.0 - 1.0) + dx.get(i).copied().unwrap_or(0.0);
765                let py = (cy * 2.0 - 1.0) + dy.get(i).copied().unwrap_or(0.0);
766                push(plain(px, py), &mut pts, &mut n);
767            }
768        }
769        // 5 — the source's product figure, the two channels multiplied into each
770        // other and the whole thing turned at 0.3 rad/s (l.3067-3098).
771        5 => {
772            let count = 480usize;
773            let (s, c) = (time * 0.3).sin_cos();
774            for i in 0..count {
775                let x0 = r(i) * l(i + 32) + l(i) * r(i + 32);
776                let y0 = r(i) * r(i) - l(i + 32) * l(i + 32);
777                push(centred(x0 * c - y0 * s, x0 * s + y0 * c), &mut pts, &mut n);
778            }
779        }
780        // 6 and 7 — a line at an angle `wave_mystery` sets, run across the frame
781        // and clipped to just outside it, with the left channel displacing it
782        // along its own normal (l.3100-3244). Mode 7 draws the same line twice,
783        // one channel each, pushed apart by `wave_y` squared; the two sides are a
784        // break in the figure rather than one polyline, so they are emitted and
785        // smoothed separately.
786        //
787        // **`wave_mystery` is read unfolded here** and `wave_x` slides the line
788        // along its NORMAL rather than along itself, which is the source's own
789        // use of the two and not a transcription slip.
790        6 | 7 => {
791            let count = 240usize.min(thirds);
792            let ang = 1.57 * mystery;
793            let (sa, ca) = ang.sin_cos();
794            let dir = [ca, sa];
795            let nrm = [-sa, ca];
796            let slide = cx * 2.0 - 1.0;
797            let (t0, t1) = clip_to_frame(dir, [nrm[0] * slide, nrm[1] * slide]);
798            let sep = cy * cy;
799            let sides: &[(f32, bool)] = if mode == 7 {
800                &[(sep, true), (-sep, false)]
801            } else {
802                &[(0.0, true)]
803            };
804            let mut smoothed = [[0.0f32; 2]; MAX_SMOOTHED_POINTS];
805            for (offset, from_left) in sides {
806                n = 0;
807                for i in 0..count {
808                    let t = t0 + (t1 - t0) * i as f32 / (count - 1).max(1) as f32;
809                    let sample = if *from_left { l(i + 120) } else { r(i + 120) };
810                    let d = 0.25 * sample + offset;
811                    let px = dir[0] * t + nrm[0] * (slide + d);
812                    let py = dir[1] * t + nrm[1] * (slide + d);
813                    push(plain(px, py), &mut pts, &mut n);
814                }
815                let built = smooth_wave(pts.get(..n).unwrap_or(&[]), &mut smoothed);
816                emit_uv(
817                    geometry,
818                    smoothed.get(..built).unwrap_or(&[]),
819                    colour,
820                    stroke,
821                    false,
822                    additive,
823                    use_dots,
824                    aspect,
825                );
826            }
827            return;
828        }
829        _ => {}
830    }
831
832    let mut smoothed = [[0.0f32; 2]; MAX_SMOOTHED_POINTS];
833    let built = smooth_wave(pts.get(..n).unwrap_or(&[]), &mut smoothed);
834    emit_uv(
835        geometry,
836        smoothed.get(..built).unwrap_or(&[]),
837        colour,
838        stroke,
839        false,
840        additive,
841        use_dots,
842        aspect,
843    );
844}
845
846/// Where a line through `offset` in direction `dir` enters and leaves the source's
847/// `+/-1.1` clip box (`milkdropfs.cpp` l.3100-3244) — slightly outside the frame,
848/// so a rotated trace runs past the corners rather than stopping short of them.
849///
850/// Returns the parameter range along `dir`, clamped to the source's own
851/// `-3..3` extent when the line is axis-parallel and one axis never bounds it.
852fn clip_to_frame(dir: [f32; 2], offset: [f32; 2]) -> (f32, f32) {
853    const EDGE: f32 = 1.1;
854    const REACH: f32 = 3.0;
855    let (mut lo, mut hi) = (-REACH, REACH);
856    for axis in 0..2 {
857        let (d, o) = (
858            dir.get(axis).copied().unwrap_or(0.0),
859            offset.get(axis).copied().unwrap_or(0.0),
860        );
861        if d.abs() < 1e-6 {
862            continue;
863        }
864        let (a, b) = ((-EDGE - o) / d, (EDGE - o) / d);
865        lo = lo.max(a.min(b));
866        hi = hi.min(a.max(b));
867    }
868    if hi <= lo { (-REACH, REACH) } else { (lo, hi) }
869}
870
871/// Convert a figure built in uv into the line renderer's world space and emit it.
872#[allow(clippy::too_many_arguments)]
873fn emit_uv(
874    geometry: &mut DrawGeometry,
875    uv: &[[f32; 2]],
876    colour: Light,
877    width: f32,
878    closed: bool,
879    additive: bool,
880    use_dots: bool,
881    aspect: f32,
882) {
883    let mut points: Vec<Point> = Vec::with_capacity(uv.len());
884    for p in uv {
885        points.push((uv_to_world(p[0], p[1], aspect), colour));
886    }
887    emit_trace(geometry, &points, width, closed, additive, use_dots);
888}
889
890/// The preset's custom waves, each a polyline or a scatter from its own
891/// per-point program.
892///
893/// **A custom wave draws through the same figure contract as the eight built-in
894/// modes** (ADR-0223): its sample term carries [`HOST_SAMPLE_FACTOR`], and the
895/// figure its program placed passes through [`smooth_points`] unless the wave
896/// draws dots — which is the source's own exception (`milkdropfs.cpp` l.2722)
897/// rather than one taken here.
898fn custom_waves(
899    geometry: &mut DrawGeometry,
900    runtime: &mut MilkRuntime,
901    left: &[f32; WAVE_SAMPLES],
902    right: &[f32; WAVE_SAMPLES],
903    exposure: Exposure,
904    aspect: f32,
905) {
906    // Reused by every wave of this frame, in the shape the built-in figure's own
907    // smoothing buffer takes.
908    let mut smoothed: Vec<Point> = Vec::new();
909    for index in 0..runtime.wave_count() {
910        let Some(spec) = runtime.wave_spec(index) else {
911            continue;
912        };
913        if runtime.run_wave_frame(index).is_none() {
914            continue;
915        }
916        let count = spec.count.max(2) as usize;
917        let mut points: Vec<Point> = Vec::with_capacity(count);
918        for i in 0..count {
919            let t = i as f32 / (count - 1).max(1) as f32;
920            // The audio at this point along the wave — MilkDrop's `value1` and
921            // `value2`, which are its left and right channels and are two
922            // different numbers on a stereo stream (ADR-0199). A per-point
923            // program that plots one against the other draws the figure its
924            // author saw rather than a diagonal line.
925            // The sample term, with the host factor on it and nothing else —
926            // the same term the built-in figures read (ADR-0223).
927            let at = ((t * (WAVE_SAMPLES - 1) as f32) as usize).min(WAVE_SAMPLES - 1);
928            let value1 = left.get(at).copied().unwrap_or(0.0) * HOST_SAMPLE_FACTOR;
929            let value2 = right.get(at).copied().unwrap_or(0.0) * HOST_SAMPLE_FACTOR;
930            let Some(point) = runtime.run_wave_point(index, t, value1, value2) else {
931                break;
932            };
933            points.push((
934                uv_to_world(point.x, point.y, aspect),
935                light(
936                    point.r,
937                    point.g,
938                    point.b,
939                    point.a,
940                    false,
941                    exposure,
942                    spec.additive,
943                ),
944            ));
945        }
946        let width = if spec.thick { THICK } else { THIN };
947        if spec.use_dots {
948            dots(geometry, &points, width, spec.additive);
949        } else {
950            smooth_points(&points, &mut smoothed);
951            polyline(geometry, &smoothed, width, false, spec.additive);
952        }
953    }
954}
955
956/// The preset's custom shapes: filled polygons with an optional outline.
957fn custom_shapes(
958    geometry: &mut DrawGeometry,
959    runtime: &mut MilkRuntime,
960    exposure: Exposure,
961    aspect: f32,
962) {
963    for index in 0..runtime.shape_count() {
964        let Some(spec) = runtime.shape_spec(index) else {
965            continue;
966        };
967        for instance in 0..spec.instances {
968            let Some(shape) = runtime.run_shape_instance(index, instance) else {
969                break;
970            };
971            // Per **instance**, not per element: `additive` is one of the
972            // registers the shape's own per-frame program may write, so one
973            // shape's copies can blend differently from each other.
974            let additive = shape.additive >= 0.5;
975            let sides = (shape.sides.max(3.0) as u32).clamp(3, MAX_SHAPE_SIDES);
976            let centre = uv_to_world(shape.x, shape.y, aspect);
977            let inner = light(
978                shape.r, shape.g, shape.b, shape.a, false, exposure, additive,
979            );
980            let outer = light(
981                shape.r2, shape.g2, shape.b2, shape.a2, false, exposure, additive,
982            );
983            // The perimeter, in world space. `rad` is in frame-heights, so the
984            // aspect only enters through the centre — which is what keeps a
985            // shape round rather than stretched.
986            let point = |i: u32| -> [f32; 2] {
987                let t = shape.ang + i as f32 / sides as f32 * std::f32::consts::TAU;
988                [
989                    centre[0] + shape.rad * t.cos() * aspect,
990                    centre[1] + shape.rad * t.sin(),
991                ]
992            };
993            // A triangle fan, emitted as a plain list so the whole draw layer is
994            // one buffer, in the blend mode this instance asked for.
995            for i in 0..sides {
996                geometry.push_triangle(
997                    [
998                        ShapeVertex {
999                            pos: centre,
1000                            color: inner.rgb,
1001                            alpha: inner.coverage,
1002                        },
1003                        ShapeVertex {
1004                            pos: point(i),
1005                            color: outer.rgb,
1006                            alpha: outer.coverage,
1007                        },
1008                        ShapeVertex {
1009                            pos: point((i + 1) % sides),
1010                            color: outer.rgb,
1011                            alpha: outer.coverage,
1012                        },
1013                    ],
1014                    additive,
1015                );
1016            }
1017            // ...and the outline, through the same line batch as everything else.
1018            if shape.border_a > 0.0001 {
1019                let colour = light(
1020                    shape.border_r,
1021                    shape.border_g,
1022                    shape.border_b,
1023                    shape.border_a,
1024                    false,
1025                    exposure,
1026                    additive,
1027                );
1028                let outline: Vec<Point> = (0..sides).map(|i| (point(i), colour)).collect();
1029                let width = if shape.thick_outline >= 0.5 || spec.thick {
1030                    THICK
1031                } else {
1032                    THIN
1033                };
1034                polyline(geometry, &outline, width, true, additive);
1035            }
1036        }
1037    }
1038}
1039
1040/// The inner and outer borders: two rectangles inset from the frame edge.
1041fn borders(geometry: &mut DrawGeometry, out: &FrameOutputs, exposure: Exposure, aspect: f32) {
1042    for (size, r, g, b, a, inset) in [
1043        (out.ob_size, out.ob_r, out.ob_g, out.ob_b, out.ob_a, 0.0),
1044        (
1045            out.ib_size,
1046            out.ib_r,
1047            out.ib_g,
1048            out.ib_b,
1049            out.ib_a,
1050            out.ob_size,
1051        ),
1052    ] {
1053        if a <= 0.0001 || size <= 0.0 {
1054            continue;
1055        }
1056        let colour = light(r, g, b, a, false, exposure, false);
1057        // The rectangle sits at the middle of its own band, and the stroke is the
1058        // band's whole width — which is how a border of `size` reads as a band of
1059        // `size` rather than as a hairline.
1060        let half = (size * 0.5).clamp(0.0005, 0.4);
1061        let edge = inset + half;
1062        let corners = [
1063            (edge, edge),
1064            (1.0 - edge, edge),
1065            (1.0 - edge, 1.0 - edge),
1066            (edge, 1.0 - edge),
1067        ];
1068        let points: Vec<Point> = corners
1069            .iter()
1070            .map(|(u, v)| (uv_to_world(*u, *v, aspect), colour))
1071            .collect();
1072        polyline(geometry, &points, half * 2.0, true, false);
1073    }
1074}
1075
1076/// The motion-vector grid: a lattice of short strokes showing where the warp is
1077/// taking the frame.
1078///
1079/// MilkDrop samples its own warp mesh to draw these. Here the grid is drawn from
1080/// the same `mv_*` vocabulary at the positions the preset names, with `mv_l`
1081/// setting the length — the figure a preset asks for, without a second
1082/// evaluation of the per-vertex program per grid point.
1083fn motion_vectors(
1084    geometry: &mut DrawGeometry,
1085    out: &FrameOutputs,
1086    exposure: Exposure,
1087    aspect: f32,
1088) {
1089    if out.mv_a <= 0.0001 {
1090        return;
1091    }
1092    let nx = (out.mv_x.max(0.0) as u32).min(64);
1093    let ny = (out.mv_y.max(0.0) as u32).min(48);
1094    if nx == 0 || ny == 0 {
1095        return;
1096    }
1097    let colour = light(
1098        out.mv_r, out.mv_g, out.mv_b, out.mv_a, false, exposure, false,
1099    );
1100    let len = out.mv_l * 0.02;
1101    for iy in 0..ny {
1102        for ix in 0..nx {
1103            let u = (ix as f32 + 0.5 + out.mv_dx) / nx as f32;
1104            let v = (iy as f32 + 0.5 + out.mv_dy) / ny as f32;
1105            let a = uv_to_world(u, v, aspect);
1106            geometry.push_segment(
1107                SegmentInstance {
1108                    a,
1109                    b: [a[0] + len * aspect, a[1] + len],
1110                    color: colour.rgb,
1111                    width: THIN,
1112                    alpha: colour.coverage,
1113                    ext_a: 0.0,
1114                    ext_b: 0.0,
1115                },
1116                false,
1117            );
1118        }
1119    }
1120}