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 nothing here, and that is the contract** (Plan 0109
183/// Phase 2): every figure this layer builds is a pure function of the trace and
184/// the frame outputs. The parameter stays in the signature because the scene has
185/// the value and because time-independence is a claim worth being able to *test*
186/// — `draw_layer.rs` calls this twice at well-separated times and compares the
187/// geometry. A future mode that legitimately animates would rename it back, and
188/// would owe that test a reason.
189pub fn build(
190    geometry: &mut DrawGeometry,
191    runtime: Option<&mut MilkRuntime>,
192    out: &FrameOutputs,
193    waveform: &[f32; WAVE_SAMPLES],
194    _time: f32,
195    dt: f32,
196    aspect: f32,
197) {
198    geometry.clear();
199    let Some(runtime) = runtime else {
200        return;
201    };
202    let exposure = Exposure::new(dt);
203    waveform_figure(geometry, out, waveform, exposure, aspect);
204    custom_waves(geometry, runtime, waveform, exposure, aspect);
205    custom_shapes(geometry, runtime, exposure, aspect);
206    // The two borders and the motion-vector grid are **always** alpha-blended in
207    // the reference — neither has an additive flag to read — so they go to the
208    // over half unconditionally.
209    borders(geometry, out, exposure, aspect);
210    motion_vectors(geometry, out, exposure, aspect);
211}
212
213/// **What one frame of the draw layer is worth**, which depends on how the
214/// producer that drew it blends. Both cases are a rate rather than a constant,
215/// and for the same reason.
216///
217/// MilkDrop deposits once per rendered frame into a buffer that decays once per
218/// rendered frame, so the two are in step by construction. Here the field decays
219/// **per second** (`decay` is rate-converted like every other MilkDrop factor),
220/// which means a 60 Hz display would deposit twice the light per second that a
221/// 30 Hz one does into a buffer that fades at the same rate — and the picture
222/// would differ with the refresh, which ADR-0019 exists to prevent. `rate` is how
223/// many nominal frames of wall clock this frame is, and both branches of
224/// `scale` convert through it.
225#[derive(Clone, Copy)]
226pub struct Exposure {
227    /// `dt * NOMINAL_FPS`: how many nominal frames of wall clock this frame is.
228    rate: f32,
229}
230
231impl Exposure {
232    /// From this frame's `dt`, capped at **four** nominal frames: a long frame
233    /// deposits proportionally more light, but a stall cannot deposit an
234    /// unbounded amount in one go.
235    pub fn new(dt: f32) -> Self {
236        Self {
237            rate: (dt * crate::milk::NOMINAL_FPS).clamp(0.0, 4.0),
238        }
239    }
240
241    /// The producer's effective alpha this frame — its colour is premultiplied
242    /// by this, and it is the coverage the fragment writes.
243    ///
244    /// # The two conversions
245    ///
246    /// **Additive** light composes by *addition* across frames, so `n` frames
247    /// deposit `n * a` and the rate conversion is the plain product `a * rate`.
248    ///
249    /// **Alpha-over** composes by *repeated interpolation*: `n` frames of
250    /// `dst = src*a + dst*(1-a)` leave `1 - (1-a)^n` of the way travelled, so the
251    /// conversion is `1 - (1-a)^rate`. At `rate = 1` it is exactly `a`, which is
252    /// what makes 60 Hz the reference's own cadence rather than an approximation
253    /// of it; at `rate = 2` a 30 Hz frame travels as far as two 60 Hz ones, which
254    /// is the property ADR-0019 asks for.
255    ///
256    /// Note that the alpha-over branch is **bounded by 1** for every `rate`,
257    /// which is the whole reason this is worth two pipelines: the sum of N
258    /// over-blended producers is still ≤ 1, where N additive ones is N.
259    fn scale(self, alpha: f32, additive: bool) -> f32 {
260        let a = alpha.clamp(0.0, 1.0);
261        if additive {
262            a * self.rate
263        } else {
264            1.0 - (1.0 - a).powf(self.rate)
265        }
266    }
267}
268
269/// A producer's premultiplied colour and its coverage, both already scaled by
270/// [`Exposure::scale`].
271///
272/// The two are returned together because both seams need coverage to equal the
273/// light's own footprint: additively so a dim deposit does not occlude a lit
274/// backdrop (ADR-0056), and over-blended because the coverage *is* the blend's
275/// alpha.
276#[derive(Clone, Copy)]
277struct Light {
278    /// Premultiplied colour: the producer's RGB times its effective alpha.
279    rgb: [f32; 3],
280    /// **The alpha the fragment writes**, which is not the same number in the two
281    /// seams and that difference is ADR-0056's rule rather than an inconsistency.
282    ///
283    /// Over-blended, coverage *is* the blend's alpha — a `wave_a = 0.1` stroke
284    /// must replace a tenth of what is under it, so it is the effective alpha.
285    ///
286    /// Additive, it is **`1.0`**: "a dimmed stroke still covers its own
287    /// footprint", so brightness lives in [`rgb`](Self::rgb) and the geometry's
288    /// own falloff is the whole footprint. Passing the effective alpha here
289    /// instead would make a dim additive stroke *narrower* rather than darker,
290    /// and could exceed 1 at a long `dt`.
291    coverage: f32,
292}
293
294impl Light {
295    /// Whether this producer writes anything worth a draw call.
296    fn is_dark(&self) -> bool {
297        self.rgb.iter().all(|c| *c <= 0.0001)
298    }
299}
300
301/// One point of a stroked figure: where it is, and what it deposits there.
302type Point = ([f32; 2], Light);
303
304/// Colour and coverage for one producer. `wave_brighten` normalizes to the
305/// brightest channel first, which is what the reference's `bMaximizeWaveColor`
306/// does.
307fn light(
308    r: f32,
309    g: f32,
310    b: f32,
311    a: f32,
312    brighten: bool,
313    exposure: Exposure,
314    additive: bool,
315) -> Light {
316    let (mut r, mut g, mut b) = (r, g, b);
317    if brighten {
318        let peak = r.max(g).max(b);
319        if peak > 0.0001 {
320            let k = 1.0 / peak;
321            r *= k;
322            g *= k;
323            b *= k;
324        }
325    }
326    let a = exposure.scale(a, additive);
327    Light {
328        rgb: [r * a, g * a, b * a],
329        coverage: if additive { 1.0 } else { a },
330    }
331}
332
333/// Push a polyline, flagging the interior joins so the strokes meet cleanly
334/// (ADR-0041).
335fn polyline(
336    geometry: &mut DrawGeometry,
337    points: &[Point],
338    width: f32,
339    closed: bool,
340    additive: bool,
341) {
342    let n = points.len();
343    if n < 2 {
344        return;
345    }
346    let last = if closed { n } else { n - 1 };
347    for i in 0..last {
348        let Some((a, light)) = points.get(i) else {
349            continue;
350        };
351        let Some((b, _)) = points.get((i + 1) % n) else {
352            continue;
353        };
354        let ext_a = if closed || i > 0 { width } else { 0.0 };
355        let ext_b = if closed || i + 2 < n { width } else { 0.0 };
356        geometry.push_segment(
357            SegmentInstance {
358                a: *a,
359                b: *b,
360                color: light.rgb,
361                width,
362                alpha: light.coverage,
363                ext_a,
364                ext_b,
365            },
366            additive,
367        );
368    }
369}
370
371/// Emit one built trace the way the mode asked for it: separated marks when
372/// `wave_usedots` is set, a continuous stroke otherwise.
373///
374/// **This exists because there were four call sites and one of them forgot**
375/// (Plan 0108 Phase 4). `wave_mode 5` draws its figure in two passes and its
376/// first pass called [`polyline`] unconditionally, so a preset asking for dots
377/// got a continuous stroke above `wave_y` and beads below it. Nothing failed and
378/// nothing warned; the trace was simply half wrong. The defect was found by
379/// measurement — mode 5's dotted geometry held segments **13.6x longer than
380/// [`DOT_LENGTH`]** where every other mode's held none — and the repair is to
381/// leave one place where the choice is made.
382fn emit_trace(
383    geometry: &mut DrawGeometry,
384    points: &[Point],
385    width: f32,
386    closed: bool,
387    additive: bool,
388    use_dots: bool,
389) {
390    if use_dots {
391        dots(geometry, points, width, additive);
392    } else {
393        polyline(geometry, points, width, closed, additive);
394    }
395}
396
397/// Push each point as its own dot.
398///
399/// **Both ends are extended by a half-width, and that is what makes a dot a
400/// dot** (Plan 0108 Phase 4). The line renderer's falloff runs *across* the
401/// stroke only; the quad simply ends at each endpoint unless `ext_a`/`ext_b`
402/// push it past (ADR-0158). Without that extension a mark is a hard-edged
403/// [`DOT_LENGTH`] x `2 * width` rectangle — **3.3x wider than it is long**, a
404/// sub-pixel dash lying across the trace rather than the round dot this module's
405/// header describes. Measured at 1080p on one drawn frame, that cost 300 pixels
406/// above half brightness against a continuous stroke's 5 008, and at 320x180 it
407/// left **2**, which is design-backlog 0107's "the `wave_usedots` beads never
408/// appear".
409///
410/// With both ends extended the quad grows by the half-width at each cap, so the
411/// mark is `DOT_LENGTH + 2 * width` long against `2 * width` across — round
412/// enough that the falloff reads as a bead at any resolution, through the
413/// mechanism that already exists rather than a new constant.
414///
415/// The mark is also **centred** on its point rather than growing forward from
416/// it. Half of [`DOT_LENGTH`] is well under a pixel, so this moves nothing
417/// visible; it is here because a dot that is offset from the sample it stands
418/// for is wrong in a way nobody would ever see and everybody would inherit.
419fn dots(geometry: &mut DrawGeometry, points: &[Point], width: f32, additive: bool) {
420    for (p, light) in points {
421        geometry.push_segment(
422            SegmentInstance {
423                a: [p[0] - DOT_LENGTH * 0.5, p[1]],
424                b: [p[0] + DOT_LENGTH * 0.5, p[1]],
425                color: light.rgb,
426                width,
427                alpha: light.coverage,
428                // A cap, not a miter: this is the half-width that rounds the
429                // mark, and a zero-length segment has no interior angle to
430                // compute one from.
431                ext_a: width,
432                ext_b: width,
433            },
434            additive,
435        );
436    }
437}
438
439/// How many `wave_mode` figures there are — MilkDrop's own eight.
440pub const WAVE_MODES: u32 = 8;
441
442/// The built-in waveform: MilkDrop's eight `wave_mode` figures over the audio
443/// trace.
444///
445/// Each mode is the reference's own construction, and the eight are **pairwise
446/// distinct figures** — a circle, a pair of rings, a scope, a Lissajous, a
447/// mirrored pair, an angled line and its double — which is what Phase 4's
448/// done-when asks and what
449/// [`every_wave_mode_builds_a_different_figure`](super::tests) holds them to.
450///
451/// Two pairs had to be separated to get there, and both for the same reason: the
452/// reference tells 0 from 1, and 6 from 7, using the **second audio channel**,
453/// and this engine's analysis is mono by construction. Where the reference would
454/// draw two diverging traces, this draws the one trace at the separation the
455/// reference's own parameters name — the same figure with the channel difference
456/// removed rather than an invented eighth mode. See each arm.
457fn waveform_figure(
458    geometry: &mut DrawGeometry,
459    out: &FrameOutputs,
460    waveform: &[f32; WAVE_SAMPLES],
461    exposure: Exposure,
462    aspect: f32,
463) {
464    let additive = out.wave_additive >= 0.5;
465    let colour = light(
466        out.wave_r,
467        out.wave_g,
468        out.wave_b,
469        out.wave_a,
470        out.wave_brighten >= 0.5,
471        exposure,
472        additive,
473    );
474    if colour.is_dark() {
475        return;
476    }
477    let width = if out.wave_thick >= 0.5 { THICK } else { THIN };
478    // Read once and passed to every [`emit_trace`] below: the modes that draw in
479    // two passes have to make the same choice in both, and reading the flag at
480    // each site is how one of them came to be a stroke where the other was beads.
481    let use_dots = out.wave_usedots >= 0.5;
482    let scale = out.wave_scale;
483    let mystery = out.wave_mystery;
484    let (cx, cy) = (out.wave_x, out.wave_y);
485    // `wave_smoothing` is a running average along the trace, exactly as the
486    // reference's `fWaveSmoothing` is: 0 is the raw samples and 1 is a straight
487    // line.
488    let smooth = out.wave_smoothing.clamp(0.0, 0.99);
489
490    // How many points this mode draws, and the sampled trace it draws them from.
491    let mode = (out.wave_mode.max(0.0) as u32) % WAVE_MODES;
492    let count: usize = match mode {
493        // The two "spectrum"-ish modes draw a coarser figure, as the reference
494        // does — a 512-point circle at 480 px is denser than the frame.
495        3 | 4 => 128,
496        _ => 256,
497    };
498
499    let mut trace = [0.0f32; 256];
500    let mut held = 0.0f32;
501    for (i, slot) in trace.iter_mut().enumerate().take(count) {
502        let source = waveform
503            .get(i * WAVE_SAMPLES / count.max(1))
504            .copied()
505            .unwrap_or(0.0);
506        held = held * smooth + source * (1.0 - smooth);
507        *slot = held * scale;
508    }
509    let sample = |i: usize| trace.get(i).copied().unwrap_or(0.0);
510
511    let mut points: [Point; 256] = [([0.0; 2], colour); 256];
512    let mut used = 0usize;
513    let mut closed = false;
514    let push = |uv: (f32, f32), points: &mut [Point; 256], used: &mut usize| {
515        if let Some(slot) = points.get_mut(*used) {
516            slot.0 = uv_to_world(uv.0, uv.1, aspect);
517            *used += 1;
518        }
519    };
520
521    match mode {
522        // 0 — a circle whose radius breathes with the trace. MilkDrop's first
523        // mode and the one most presets use.
524        0 => {
525            closed = true;
526            let base = 0.2 + 0.1 * mystery;
527            for i in 0..count {
528                let t = i as f32 / count as f32 * std::f32::consts::TAU;
529                let r = base + sample(i) * 0.1;
530                push(
531                    (cx + r * t.cos() / aspect.max(0.1), cy + r * t.sin()),
532                    &mut points,
533                    &mut used,
534                );
535            }
536        }
537        // 1 — the reference's **second** circular mode, which draws the left and
538        // right channels as two rings whose separation is `wave_mystery`. This
539        // engine's analysis is mono (see `MilkRuntime::run_wave_point`), so the
540        // two rings carry the same trace and only the separation tells them
541        // apart — which is the reference's own figure with the channel
542        // difference removed, and is what keeps mode 1 from being mode 0.
543        1 => {
544            closed = true;
545            let base = 0.2 + 0.1 * mystery;
546            let separation = 0.04 + 0.06 * mystery.abs();
547            for ring in [separation, -separation] {
548                used = 0;
549                for i in 0..count {
550                    let t = i as f32 / count as f32 * std::f32::consts::TAU;
551                    let r = base + ring + sample(i) * 0.1;
552                    push(
553                        (cx + r * t.cos() / aspect.max(0.1), cy + r * t.sin()),
554                        &mut points,
555                        &mut used,
556                    );
557                }
558                // The outer ring closes here; the inner one falls through to the
559                // shared emit below, so both are stroked exactly once.
560                if ring > 0.0 {
561                    let built = points.get(..used).unwrap_or(&[]);
562                    emit_trace(geometry, built, width, true, additive, use_dots);
563                }
564            }
565        }
566        // 2 — a horizontal line across the frame, the classic scope.
567        2 => {
568            for i in 0..count {
569                let u = i as f32 / (count - 1).max(1) as f32;
570                push((u, cy + sample(i) * 0.15), &mut points, &mut used);
571            }
572        }
573        // 3 — the same line, vertical.
574        3 => {
575            for i in 0..count {
576                let v = i as f32 / (count - 1).max(1) as f32;
577                push((cx + sample(i) * 0.15, v), &mut points, &mut used);
578            }
579        }
580        // 4 — a Lissajous-style figure: the trace against itself, offset. In
581        // MilkDrop this is the left channel against the right; this engine's
582        // analysis is mono, so the offset stands in for the channel difference
583        // and the figure is a leaning loop rather than a blob (see
584        // `MilkRuntime::run_wave_point`).
585        4 => {
586            let lag = 8usize;
587            for i in 0..count {
588                push(
589                    (
590                        cx + sample(i) * 0.2 / aspect.max(0.1),
591                        cy + sample((i + lag) % count) * 0.2,
592                    ),
593                    &mut points,
594                    &mut used,
595                );
596            }
597        }
598        // 5 — a double horizontal line, mirrored about `wave_y`.
599        5 => {
600            for i in 0..count {
601                let u = i as f32 / (count - 1).max(1) as f32;
602                let s = sample(i).abs() * 0.15;
603                push((u, cy + s), &mut points, &mut used);
604            }
605            emit_trace(
606                geometry,
607                points.get(..used).unwrap_or(&[]),
608                width,
609                false,
610                additive,
611                use_dots,
612            );
613            used = 0;
614            for i in 0..count {
615                let u = i as f32 / (count - 1).max(1) as f32;
616                let s = sample(i).abs() * 0.15;
617                push((u, cy - s), &mut points, &mut used);
618            }
619        }
620        // 6 — a line at an angle set by `wave_mystery`, which is what the
621        // reference uses it for here, and 7 — the reference's **double** line, the
622        // same figure offset to both sides along its own normal. That is exactly
623        // the relationship mode 5 has to mode 2, so the pair is consistent with
624        // the pair above it rather than being two names for one figure.
625        6 | 7 => {
626            // **No `time` term here, deliberately** (Plan 0109 Phase 2,
627            // design-backlog 0115). A `time * 0.05` addend turns the figure a
628            // full turn every ~126 s, so a trace authored horizontal would be
629            // horizontal only at the instants
630            // `mystery * PI + time * 0.05` happened to be a multiple of `pi`.
631            // Plan 0108 Phase 4 named it a suspect and deliberately left it in,
632            // because removing it moves every mode-6 and mode-7 preset and
633            // because whether the reference's line drifts is a question about
634            // the reference. Plan 0108 Phase 6 asked it: *Blur Mix 3*'s traces
635            // stay horizontal in `foo_vis_milk2` and drew one steep diagonal
636            // here. So the angle is what the sentence above always said it was —
637            // `wave_mystery` alone — and this file is now a pure function of the
638            // trace and the outputs, with no use of `time` anywhere in it.
639            let angle = mystery * std::f32::consts::PI;
640            let (s, c) = angle.sin_cos();
641            let offsets: &[f32] = if mode == 7 { &[0.03, -0.03] } else { &[0.0] };
642            for (index, offset) in offsets.iter().enumerate() {
643                used = 0;
644                for i in 0..count {
645                    let t = i as f32 / (count - 1).max(1) as f32 - 0.5;
646                    let n = sample(i) * 0.15 + offset;
647                    // Built in uv and stretched by the target on the way out, so
648                    // `t` spans the frame's **width**: `uv_to_world` is the only
649                    // aspect term, exactly as this module's one-conversion rule
650                    // says. Dividing x by the aspect here would cancel that
651                    // multiply and normalize the trace to the frame's *height*
652                    // instead, which is 56 % of the width at 16:9 and full width
653                    // only on a square target (design-backlog 0122).
654                    //
655                    // A rotated trace therefore picks up the target's shape in
656                    // its amplitude — a uv-space construction is stretched, which
657                    // is what the reference does and not a defect to correct. At
658                    // `wave_mystery = 0` the amplitude is pure y and aspect-free.
659                    push(
660                        (cx + t * c - n * s, cy + t * s + n * c),
661                        &mut points,
662                        &mut used,
663                    );
664                }
665                // All but the last pass emit here; the last falls through to the
666                // shared emit below.
667                if index + 1 < offsets.len() {
668                    let built = points.get(..used).unwrap_or(&[]);
669                    emit_trace(geometry, built, width, false, additive, use_dots);
670                }
671            }
672        }
673        _ => {}
674    }
675
676    let built = points.get(..used).unwrap_or(&[]);
677    emit_trace(geometry, built, width, closed, additive, use_dots);
678}
679
680/// The preset's custom waves, each a polyline or a scatter from its own
681/// per-point program.
682fn custom_waves(
683    geometry: &mut DrawGeometry,
684    runtime: &mut MilkRuntime,
685    waveform: &[f32; WAVE_SAMPLES],
686    exposure: Exposure,
687    aspect: f32,
688) {
689    for index in 0..runtime.wave_count() {
690        let Some(spec) = runtime.wave_spec(index) else {
691            continue;
692        };
693        if runtime.run_wave_frame(index).is_none() {
694            continue;
695        }
696        let count = spec.count.max(2) as usize;
697        let mut points: Vec<Point> = Vec::with_capacity(count);
698        for i in 0..count {
699            let t = i as f32 / (count - 1).max(1) as f32;
700            // The audio at this point along the wave — MilkDrop's `value1`.
701            let value = waveform
702                .get(((t * (WAVE_SAMPLES - 1) as f32) as usize).min(WAVE_SAMPLES - 1))
703                .copied()
704                .unwrap_or(0.0);
705            let Some(point) = runtime.run_wave_point(index, t, value) else {
706                break;
707            };
708            points.push((
709                uv_to_world(point.x, point.y, aspect),
710                light(
711                    point.r,
712                    point.g,
713                    point.b,
714                    point.a,
715                    false,
716                    exposure,
717                    spec.additive,
718                ),
719            ));
720        }
721        let width = if spec.thick { THICK } else { THIN };
722        if spec.use_dots {
723            dots(geometry, &points, width, spec.additive);
724        } else {
725            polyline(geometry, &points, width, false, spec.additive);
726        }
727    }
728}
729
730/// The preset's custom shapes: filled polygons with an optional outline.
731fn custom_shapes(
732    geometry: &mut DrawGeometry,
733    runtime: &mut MilkRuntime,
734    exposure: Exposure,
735    aspect: f32,
736) {
737    for index in 0..runtime.shape_count() {
738        let Some(spec) = runtime.shape_spec(index) else {
739            continue;
740        };
741        for instance in 0..spec.instances {
742            let Some(shape) = runtime.run_shape_instance(index, instance) else {
743                break;
744            };
745            // Per **instance**, not per element: `additive` is one of the
746            // registers the shape's own per-frame program may write, so one
747            // shape's copies can blend differently from each other.
748            let additive = shape.additive >= 0.5;
749            let sides = (shape.sides.max(3.0) as u32).clamp(3, MAX_SHAPE_SIDES);
750            let centre = uv_to_world(shape.x, shape.y, aspect);
751            let inner = light(
752                shape.r, shape.g, shape.b, shape.a, false, exposure, additive,
753            );
754            let outer = light(
755                shape.r2, shape.g2, shape.b2, shape.a2, false, exposure, additive,
756            );
757            // The perimeter, in world space. `rad` is in frame-heights, so the
758            // aspect only enters through the centre — which is what keeps a
759            // shape round rather than stretched.
760            let point = |i: u32| -> [f32; 2] {
761                let t = shape.ang + i as f32 / sides as f32 * std::f32::consts::TAU;
762                [
763                    centre[0] + shape.rad * t.cos() * aspect,
764                    centre[1] + shape.rad * t.sin(),
765                ]
766            };
767            // A triangle fan, emitted as a plain list so the whole draw layer is
768            // one buffer, in the blend mode this instance asked for.
769            for i in 0..sides {
770                geometry.push_triangle(
771                    [
772                        ShapeVertex {
773                            pos: centre,
774                            color: inner.rgb,
775                            alpha: inner.coverage,
776                        },
777                        ShapeVertex {
778                            pos: point(i),
779                            color: outer.rgb,
780                            alpha: outer.coverage,
781                        },
782                        ShapeVertex {
783                            pos: point((i + 1) % sides),
784                            color: outer.rgb,
785                            alpha: outer.coverage,
786                        },
787                    ],
788                    additive,
789                );
790            }
791            // ...and the outline, through the same line batch as everything else.
792            if shape.border_a > 0.0001 {
793                let colour = light(
794                    shape.border_r,
795                    shape.border_g,
796                    shape.border_b,
797                    shape.border_a,
798                    false,
799                    exposure,
800                    additive,
801                );
802                let outline: Vec<Point> = (0..sides).map(|i| (point(i), colour)).collect();
803                let width = if shape.thick_outline >= 0.5 || spec.thick {
804                    THICK
805                } else {
806                    THIN
807                };
808                polyline(geometry, &outline, width, true, additive);
809            }
810        }
811    }
812}
813
814/// The inner and outer borders: two rectangles inset from the frame edge.
815fn borders(geometry: &mut DrawGeometry, out: &FrameOutputs, exposure: Exposure, aspect: f32) {
816    for (size, r, g, b, a, inset) in [
817        (out.ob_size, out.ob_r, out.ob_g, out.ob_b, out.ob_a, 0.0),
818        (
819            out.ib_size,
820            out.ib_r,
821            out.ib_g,
822            out.ib_b,
823            out.ib_a,
824            out.ob_size,
825        ),
826    ] {
827        if a <= 0.0001 || size <= 0.0 {
828            continue;
829        }
830        let colour = light(r, g, b, a, false, exposure, false);
831        // The rectangle sits at the middle of its own band, and the stroke is the
832        // band's whole width — which is how a border of `size` reads as a band of
833        // `size` rather than as a hairline.
834        let half = (size * 0.5).clamp(0.0005, 0.4);
835        let edge = inset + half;
836        let corners = [
837            (edge, edge),
838            (1.0 - edge, edge),
839            (1.0 - edge, 1.0 - edge),
840            (edge, 1.0 - edge),
841        ];
842        let points: Vec<Point> = corners
843            .iter()
844            .map(|(u, v)| (uv_to_world(*u, *v, aspect), colour))
845            .collect();
846        polyline(geometry, &points, half * 2.0, true, false);
847    }
848}
849
850/// The motion-vector grid: a lattice of short strokes showing where the warp is
851/// taking the frame.
852///
853/// MilkDrop samples its own warp mesh to draw these. Here the grid is drawn from
854/// the same `mv_*` vocabulary at the positions the preset names, with `mv_l`
855/// setting the length — the figure a preset asks for, without a second
856/// evaluation of the per-vertex program per grid point.
857fn motion_vectors(
858    geometry: &mut DrawGeometry,
859    out: &FrameOutputs,
860    exposure: Exposure,
861    aspect: f32,
862) {
863    if out.mv_a <= 0.0001 {
864        return;
865    }
866    let nx = (out.mv_x.max(0.0) as u32).min(64);
867    let ny = (out.mv_y.max(0.0) as u32).min(48);
868    if nx == 0 || ny == 0 {
869        return;
870    }
871    let colour = light(
872        out.mv_r, out.mv_g, out.mv_b, out.mv_a, false, exposure, false,
873    );
874    let len = out.mv_l * 0.02;
875    for iy in 0..ny {
876        for ix in 0..nx {
877            let u = (ix as f32 + 0.5 + out.mv_dx) / nx as f32;
878            let v = (iy as f32 + 0.5 + out.mv_dy) / ny as f32;
879            let a = uv_to_world(u, v, aspect);
880            geometry.push_segment(
881                SegmentInstance {
882                    a,
883                    b: [a[0] + len * aspect, a[1] + len],
884                    color: colour.rgb,
885                    width: THIN,
886                    alpha: colour.coverage,
887                    ext_a: 0.0,
888                    ext_b: 0.0,
889                },
890                false,
891            );
892        }
893    }
894}