Skip to main content

lactea/engine/
md3_blend.rs

1//! MilkDrop 3 blend patterns and double-preset playback (`Compat::MilkDrop3`, `docs/md3.md` "Double presets").
2//!
3//! * the 30 MD3 blend patterns: [`Md3Pattern`], [`Md3PatternInstance`] (the `.milk2` header's
4//!   `blending_pattern` / `blending_direction` / `random_1..5` / `blending_progress`) and the pure generator
5//!   [`md3_fill_vertinfo`] that turns an instance into the `(a, c)` mesh `mix2 = clamp(a * progress + c, 0, 1)` of
6//!   MD2's per-vertex blend (`md2_blend.rs`). All randomness is drawn up front ([`md3_random_instance`]), so a
7//!   generator is a pure function of `(instance, grid, aspect)`;
8//! * `.milk2` playback on top of MD2's soft-cut machinery: the progress modes, soft in / out, hard cuts and the
9//!   frozen blend ([`ProgressMode`], `impl Engine`).
10//!
11//! The float literals of the generators (`6.28`, `1.41421`, `3.14159274`, ...) are MD3's own and stay as written.
12#![allow(
13    clippy::approx_constant,
14    clippy::excessive_precision,
15    clippy::manual_clamp
16)]
17
18use super::blend_patterns::{PlasmaGrid, Span, band_entry, extent, gen_plasma};
19use super::md2_blend::{randomize_blend_pattern, start_blend_from};
20use super::{LastFrame, Slot};
21use crate::rng::StreamRng;
22use crate::{EngineError, PresetReport, Transition};
23use lactea_preset::milk2::{
24    Milk2, Milk2Error, Milk2Header, is_double_preset, is_milk2_path, parse_milk2,
25};
26use lactea_preset::{Flavor, Preset};
27
28// ---------------------------------------------------------------------------------------------
29// the 30 patterns
30// ---------------------------------------------------------------------------------------------
31
32macro_rules! patterns {
33    ($($variant:ident $name:literal $on:literal ($pa:literal, $pb:literal);)*) => {
34        /// The 30 MD3 blend patterns, in MD3's order (`index()` is the discriminant).
35        #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
36        #[allow(missing_docs)]
37        pub enum Md3Pattern {
38            $($variant,)*
39        }
40
41        impl Md3Pattern {
42            /// All patterns in index order.
43            pub const ALL: [Md3Pattern; 30] = [$(Md3Pattern::$variant,)*];
44
45            /// The pattern name as `blending_pattern=` and the `[transitions]` settings write it.
46            pub fn name(self) -> &'static str {
47                match self {
48                    $(Md3Pattern::$variant => $name,)*
49                }
50            }
51
52            /// Enabled for random soft cuts when `settings.ini` has no `[transitions]` entry for it.
53            pub fn default_enabled(self) -> bool {
54                match self {
55                    $(Md3Pattern::$variant => $on,)*
56                }
57            }
58
59            /// `(A, B)` of the draw of `blending_progress` in double-preset mode: the progress is
60            /// `round((u * A + B) * 100) / 100` for a uniform `u`, i.e. in `[B, B + A]`.
61            pub fn progress_range(self) -> (f32, f32) {
62                match self {
63                    $(Md3Pattern::$variant => ($pa, $pb),)*
64                }
65            }
66        }
67    };
68}
69
70patterns! {
71    Zoom "zoom" true (0.7, 0.2);
72    Side "side" true (0.3, 0.3);
73    Plasma "plasma" true (0.4, 0.2);
74    Plasma2 "plasma2" true (0.5, 0.1);
75    Plasma3 "plasma3" true (0.3, 0.1);
76    Cercle "cercle" true (0.3, 0.3);
77    Square "square" false (0.3, 0.5);
78    Snail "snail" true (0.3, 0.3);
79    Snail2 "snail2" false (0.3, 0.3);
80    Snail3 "snail3" false (0.3, 0.3);
81    Triangle "triangle" true (0.4, 0.2);
82    Donuts "donuts" false (0.3, 0.3);
83    Corner "corner" true (0.4, 0.4);
84    Patches "patches" true (0.4, 0.4);
85    Checkerboard "checkerboard" false (0.4, 0.2);
86    Bubbles "bubbles" false (0.4, 0.2);
87    Stars "stars" false (0.5, 0.1);
88    Stars2 "stars2" false (0.5, 0.2);
89    Clock "clock" true (0.5, 0.1);
90    Nuclear "nuclear" false (0.5, 0.1);
91    Arrow "arrow" true (0.3, 0.15);
92    Cisor "cisor" false (0.4, 0.2);
93    Wave "wave" false (0.4, 0.2);
94    Curtain "curtain" true (0.2, 0.1);
95    Vertical "vertical" false (0.3, 0.3);
96    Horizontal "horizontal" false (0.3, 0.3);
97    LinesVertical "linesvertical" false (0.4, 0.2);
98    LinesHorizontal "lineshorizontal" false (0.4, 0.2);
99    Cross "cross" false (0.4, 0.5);
100    Cross2 "cross2" false (0.25, 0.7);
101}
102
103impl Md3Pattern {
104    /// `0..30`: the pattern's index in MD3's name table.
105    pub fn index(self) -> usize {
106        self as usize
107    }
108
109    /// The pattern with this index.
110    pub fn from_index(i: usize) -> Option<Md3Pattern> {
111        Self::ALL.get(i).copied()
112    }
113
114    /// The pattern called `name` (exact, case-sensitive, like MD3's name match).
115    pub fn from_name(name: &str) -> Option<Md3Pattern> {
116        Self::ALL.iter().copied().find(|p| p.name() == name)
117    }
118
119    /// Whether the pattern reads `blending_direction` (only `-1` is special): cercle, triangle, corner, patches.
120    pub fn uses_direction(self) -> bool {
121        matches!(
122            self,
123            Md3Pattern::Cercle | Md3Pattern::Triangle | Md3Pattern::Corner | Md3Pattern::Patches
124        )
125    }
126}
127
128/// The 13 patterns random soft cuts may pick when `settings.ini` has no `[transitions]` section.
129pub fn default_enabled_patterns() -> Vec<Md3Pattern> {
130    Md3Pattern::ALL
131        .into_iter()
132        .filter(|p| p.default_enabled())
133        .collect()
134}
135
136/// Everything a generator needs: the values of the `.milk2` header keys.
137#[derive(Clone, Copy, Debug, PartialEq)]
138pub struct Md3PatternInstance {
139    /// `blending_pattern`.
140    pub pattern: Md3Pattern,
141    /// `blending_direction`: only `-1` is special.
142    pub direction: i32,
143    /// `random_1` .. `random_5`.
144    pub r: [f32; 5],
145    /// `blending_progress`: where a frozen blend stands (1.0 for ordinary soft cuts); used raw.
146    pub progress: f32,
147}
148
149// ---------------------------------------------------------------------------------------------
150// the generators (pure)
151// ---------------------------------------------------------------------------------------------
152
153/// Fill the `(gx + 1) * (gy + 1)` mesh `vi` (`vi[y * (gx + 1) + x] = (a, c)`) with the pattern. `aspect` is
154/// `(m_fAspectX, m_fAspectY)`. Pure: the same instance gives the same mesh, nothing is read from `vi`.
155///
156/// `horizontal` fills the mesh with transposed loops and a running index (MD3's own quirk, which scrambles the
157/// pattern on grids that are not square); `plasma3` seeds its min / max with `0.5 * h(0, 0)`; `cross2` zeroes
158/// the vertices that never blend.
159pub fn md3_fill_vertinfo(
160    inst: &Md3PatternInstance,
161    gx: usize,
162    gy: usize,
163    aspect: (f32, f32),
164    vi: &mut [[f32; 2]],
165) {
166    assert_eq!(
167        vi.len(),
168        (gx + 1) * (gy + 1),
169        "vertinfo has one entry per mesh vertex"
170    );
171    let (ax, ay) = aspect;
172    let (gxf, gyf) = (gx as f32, gy as f32);
173    let [r1, r2, r3, r4, r5] = inst.r;
174    let flip = inst.direction == -1;
175    let sqrt2 = 2.0f32.sqrt();
176    let w = gx + 1;
177
178    // the common band `B` of most patterns
179    let band = r1 * 0.14 + 0.02 + r2 * 0.34;
180
181    // one entry per vertex from `f(X, Y)` (`X = x / gx`, `Y = y / gy`)
182    let each = |vi: &mut [[f32; 2]], f: &dyn Fn(f32, f32) -> [f32; 2]| {
183        for y in 0..=gy {
184            for x in 0..=gx {
185                vi[y * w + x] = f(x as f32 / gxf, y as f32 / gyf);
186            }
187        }
188    };
189    // `emit(t, k)`: `a = (B + k) / B`, `c = t / B - 1 / B`
190    let banded = |vi: &mut [[f32; 2]], band: f32, k: f32, t: &dyn Fn(f32, f32) -> f32| {
191        let ib = 1.0 / band;
192        each(vi, &|x, y| band_entry(ib, band + k, t(x, y)));
193    };
194    // `(dx, dy)` of the centred patterns
195    let centred = |x: f32, y: f32| ((x - 0.5) * ax, (y - 0.5) * ay);
196    let radius = |dx: f32, dy: f32| (dx * dx + dy * dy).sqrt();
197    let flipped = |t: f32| if flip { 1.0 - t } else { t };
198    // `side`, `vertical` and `horizontal`: the projection on a direction
199    let wipe = |u: f32, v: f32, vx: f32, vy: f32| {
200        (((u - 0.5) * vx + (v - 0.5) * vy + 0.5) - 0.5) / sqrt2 + 0.5
201    };
202
203    match inst.pattern {
204        Md3Pattern::Zoom => vi.fill([1.0, 0.0]),
205        Md3Pattern::Side => {
206            let ang = r1 * 6.28;
207            let (vx, vy) = (ang.cos(), ang.sin());
208            let b = 0.1 + 0.2 * r2;
209            let ib = 1.0 / b;
210            each(vi, &|x, y| {
211                band_entry(ib, 1.0 + b, wipe(x * ax, y * ay, vx, vy))
212            });
213        }
214        Md3Pattern::Plasma | Md3Pattern::Plasma2 | Md3Pattern::Plasma3 => {
215            let (b, dt) = match inst.pattern {
216                Md3Pattern::Plasma => (0.12 + 0.13 * r1, 0.25),
217                Md3Pattern::Plasma2 => (0.25 + 0.15 * r1, 0.25),
218                _ => (0.25 + 0.15 * r1, 0.75),
219            };
220            let ib = 1.0 / b;
221            let mut h = vec![0.0f32; w * (gy + 1)];
222            h[0] = r2;
223            h[gx] = r3;
224            h[gy * w] = r4;
225            h[gy * w + gx] = r5;
226            let r = inst.r;
227            gen_plasma(
228                &mut h,
229                |v| v,
230                PlasmaGrid { gx, aspect },
231                &mut |k| r[k],
232                Span {
233                    x0: 0,
234                    x1: gx,
235                    y0: 0,
236                    y1: gy,
237                },
238                dt,
239            );
240            // `plasma3` seeds both extremes with half of h(0, 0)
241            let seed = if inst.pattern == Md3Pattern::Plasma3 {
242                0.5 * h[0]
243            } else {
244                h[0]
245            };
246            let (mn, mx) = extent(seed, h.iter().copied());
247            for (v, out) in h.iter().zip(vi.iter_mut()) {
248                let t = if inst.pattern == Md3Pattern::Plasma {
249                    (v - mn) * (1.0 / (mx - mn))
250                } else {
251                    ((v - mn) * 2.0 / (mx - mn)).sin()
252                };
253                let [_, c] = band_entry(ib, 0.0, t);
254                *out = [(1.0 + b) / b, c];
255            }
256        }
257        Md3Pattern::Cercle => banded(vi, band, 1.0, &|x, y| {
258            let (dx, dy) = centred(x, y);
259            flipped(radius(dx, dy) * 1.41421)
260        }),
261        Md3Pattern::Square => banded(vi, band, 1.0, &|x, y| {
262            let (dx, dy) = centred(x, y);
263            (dx.abs() * 1.41421).max(dy.abs() * 1.41421)
264        }),
265        Md3Pattern::Snail | Md3Pattern::Snail2 | Md3Pattern::Snail3 => {
266            let pattern = inst.pattern;
267            banded(vi, band, 2.0, &|x, y| {
268                let (dx, dy) = centred(x, y);
269                let (rho, th) = (radius(dx, dy), dy.atan2(dx));
270                match pattern {
271                    Md3Pattern::Snail => (th + 10.0 * rho - 0.2).cos() * rho,
272                    Md3Pattern::Snail2 => (th + 25.0 * rho - 0.2).cos() * rho,
273                    _ => (th + 50.0 * rho + 0.5).sin() * rho,
274                }
275            })
276        }
277        Md3Pattern::Triangle => banded(vi, band, 1.0, &|x, y| {
278            let (dx, dy) = centred(x, y);
279            flipped(dx.abs() + dy.abs())
280        }),
281        Md3Pattern::Donuts => banded(vi, band, 3.0, &|x, y| {
282            let (dx, dy) = centred(x, y);
283            (10.0 * radius(dx, dy) + 90.0).sin()
284        }),
285        Md3Pattern::Corner => banded(vi, band, 1.0, &|x, y| {
286            let dx = ((x - 0.5) + 0.25) * ax;
287            let dy = ((y - 0.5) + 0.35) * ay;
288            let t = (radius(dx, dy) * 1.41421).min(1.0).max(0.0);
289            flipped(t)
290        }),
291        Md3Pattern::Patches => banded(vi, band, 1.0, &|x, y| {
292            let (dx, dy) = centred(x, y);
293            flipped(((10.0 * dy).cos() + (10.0 * dx).sin()) * 0.25 + 0.5)
294        }),
295        Md3Pattern::Checkerboard => banded(vi, band, 3.0, &|x, y| {
296            // the origin is the (1, 0) corner, not the centre
297            let xs = (x - 1.0) * ax * 10.0 + 90.0;
298            let ys = (y - 0.0) * ay * 10.0 + 90.0;
299            xs.sin() * ys.cos()
300        }),
301        Md3Pattern::Bubbles => each(vi, &|x, y| {
302            [
303                7.0,
304                2.0 * ((x - 10.0 * r1) * ax * 10.0 + 0.5 * r3).sin()
305                    * (0.5 * r4 + (y - 0.5) * ay * 10.0).cos()
306                    - 2.0,
307            ]
308        }),
309        Md3Pattern::Stars | Md3Pattern::Stars2 | Md3Pattern::Clock | Md3Pattern::Nuclear => {
310            let pattern = inst.pattern;
311            banded(vi, band, 3.0, &|x, y| {
312                let (dx, dy) = centred(x, y);
313                let th = dy.atan2(dx);
314                match pattern {
315                    Md3Pattern::Stars => (5.0 * th + 90.0).sin(),
316                    Md3Pattern::Stars2 => (10.0 * th + 90.0).sin(),
317                    Md3Pattern::Clock => ((th + th) * 0.5).sin(),
318                    _ => ((th + th) * 1.5).sin(),
319                }
320            })
321        }
322        Md3Pattern::Arrow => banded(vi, band, 1.0, &|x, y| {
323            let (dx, dy) = centred(x, y);
324            // |dx|, |dy| <= 0.5 * aspect stay below 1 for any window the engine uses
325            let lim = |v: f32| v.clamp(-0.9999, 0.9999);
326            let (ax_, ay_) = (lim(dx).atanh(), lim(dy).atanh());
327            ((4.0 * ay_).cos() + ax_).sin() * 0.5 + 0.5
328        }),
329        Md3Pattern::Cisor => banded(vi, band, 3.0, &|x, y| {
330            let th = ((y - 0.0) * ay).atan2((x - 1.0) * ax);
331            (5.0 * th + 90.0).sin()
332        }),
333        Md3Pattern::Wave => banded(vi, band, 3.0, &|x, y| {
334            let (dx, dy) = ((x - 1.0) * ax, (y - 0.0) * ay);
335            (10.0 * radius(dx, dy) + 90.0).sin()
336        }),
337        Md3Pattern::Curtain => {
338            // constants, unlike `side`
339            let (vx, vy) = (6.28f32.cos(), 6.28f32.sin());
340            let ten_thirds = 3.33333325f32;
341            let a = f32::from_bits(0x408a_aaaa);
342            each(vi, &|x, y| {
343                let s = ((x * ax - 0.5) * vx + (y * ay - 0.5) * vy + 0.5) * 3.14159274;
344                let h = s.sin() * 0.5 + 0.5;
345                [a, h * ten_thirds - ten_thirds]
346            });
347        }
348        Md3Pattern::Vertical => {
349            let (vx, vy) = (6.28f32.cos(), 6.28f32.sin());
350            each(vi, &|x, y| [6.0, 5.0 * wipe(x * ax, y * ay, vx, vy) - 5.0]);
351        }
352        Md3Pattern::Horizontal => {
353            // the same value as `vertical`, but the loops are transposed (outer `x`, inner `y`) and the entry is
354            // written at a running index: row-major reads of a non-square grid see scrambled values
355            let (vx, vy) = (6.28f32.cos(), 6.28f32.sin());
356            let mut k = 0;
357            for i in 0..=gx {
358                for j in 0..=gy {
359                    let (u, v) = (i as f32 / gxf * ax, j as f32 / gyf * ay);
360                    let t = ((((v - 0.5) * vy + (u - 0.5) * vx + 0.5) - 0.5) / sqrt2) + 0.5;
361                    vi[k] = [6.0, 5.0 * t - 5.0];
362                    k += 1;
363                }
364            }
365        }
366        Md3Pattern::LinesVertical => each(vi, &|x, _| {
367            [7.0, 2.0 * ((x - 0.5) * ax * r1 * 60.0).sin() - 2.0]
368        }),
369        Md3Pattern::LinesHorizontal => each(vi, &|_, y| {
370            [7.0, 2.0 * (r1 * 60.0 * (y - 0.5) * ay).sin() - 2.0]
371        }),
372        Md3Pattern::Cross | Md3Pattern::Cross2 => {
373            let cut = inst.pattern == Md3Pattern::Cross2;
374            let ib = 1.0 / band;
375            each(vi, &|x, y| {
376                let (dx, dy) = centred(x, y);
377                let t = (dx.abs() + dx.abs()).min(dy.abs() + dy.abs());
378                if cut && t >= 0.3 {
379                    // never blends, for any progress
380                    [0.0, 0.0]
381                } else {
382                    [(band + 1.0) * ib, t * ib - ib]
383                }
384            });
385        }
386    }
387}
388
389// ---------------------------------------------------------------------------------------------
390// the random block
391// ---------------------------------------------------------------------------------------------
392
393/// A source of `rand()` values (`0..=32767`, MSVC's range): the engine's seeded stream, or an exact MSVC `rand()`
394/// in tests.
395pub trait Rand15 {
396    /// The next `rand()`.
397    fn rand15(&mut self) -> u32;
398}
399
400impl Rand15 for StreamRng {
401    /// The top 15 bits of the stream's next 32-bit draw.
402    fn rand15(&mut self) -> u32 {
403        self.next32() >> 17
404    }
405}
406
407/// `FRAND` of MD3: `(rand() % 7381) / 7380`.
408fn frand7380(rng: &mut impl Rand15) -> f32 {
409    (rng.rand15() % 7381) as f32 / 7380.0
410}
411
412/// The user locks of the random block (`forcePattern`, `forceProgress`, `dirLock`), all off by default.
413#[derive(Clone, Copy, Debug, Default, PartialEq)]
414pub struct Md3Locks {
415    /// `forcePattern >= 0`: use this pattern instead of the random one.
416    pub pattern: Option<Md3Pattern>,
417    /// `forceProgress > 0`: use this progress instead of the drawn one.
418    pub progress: Option<f32>,
419    /// `dirLock != 0`: use this direction (no draw is consumed).
420    pub direction: Option<i32>,
421}
422
423/// The progress of a double preset drawn for `pattern`: `round((rand() / 32767 * A + B) * 100) / 100`.
424pub fn md3_draw_progress(pattern: Md3Pattern, rng: &mut impl Rand15) -> f32 {
425    let (a, b) = pattern.progress_range();
426    (((rng.rand15() as f32 / 32767.0) * a + b) * 100.0).round() / 100.0
427}
428
429/// The random block of `RandomizeBlendPattern`, once per preset change: draws, in this order, the pattern (a uniform
430/// element of `enabled`, `{zoom}` if empty), the direction (`+-1`, unless locked), `r1..r5` and, in double-preset
431/// mode, the pattern again if `auto_hardcut` (one of the first six patterns, ignoring `enabled`) and the progress.
432/// Ordinary soft cuts have `progress = 1`.
433pub fn md3_random_instance(
434    rng: &mut impl Rand15,
435    enabled: &[Md3Pattern],
436    double_preset: bool,
437    auto_hardcut: bool,
438    locks: &Md3Locks,
439) -> Md3PatternInstance {
440    let zoom = [Md3Pattern::Zoom];
441    let list = if enabled.is_empty() {
442        &zoom[..]
443    } else {
444        enabled
445    };
446    let mut pattern = list[rng.rand15() as usize % list.len()];
447    let direction = match locks.direction {
448        Some(d) => d,
449        None => 2 * (rng.rand15() & 1) as i32 - 1,
450    };
451    let mut r = [0.0f32; 5];
452    for v in &mut r {
453        *v = frand7380(rng);
454    }
455    if double_preset && auto_hardcut {
456        pattern = Md3Pattern::ALL[rng.rand15() as usize % 6];
457    }
458    if let Some(p) = locks.pattern {
459        pattern = p;
460    }
461    let mut progress = 1.0;
462    if double_preset {
463        progress = md3_draw_progress(pattern, rng);
464    }
465    if let Some(p) = locks.progress.filter(|&p| p > 0.0) {
466        progress = p;
467    }
468    Md3PatternInstance {
469        pattern,
470        direction,
471        r,
472        progress,
473    }
474}
475
476// ---------------------------------------------------------------------------------------------
477// progress modes and how a blend is launched
478// ---------------------------------------------------------------------------------------------
479
480/// MD3's `progressMode` of the running blend (`m_pState->m_fBlendProgress` source).
481#[derive(Clone, Copy, Debug, PartialEq, Eq)]
482pub(super) enum ProgressMode {
483    /// 0: an ordinary soft cut, `(now - start) / duration`, over once it exceeds 1.
484    Normal,
485    /// 3: first leg of a soft transition *into* a double preset: the running preset blends to PRESET1 with the
486    /// double preset's own pattern; when the ramp passes 1 PRESET2 is loaded on top of PRESET1.
487    SoftInFirst,
488    /// 4: second leg: PRESET1 to PRESET1 + PRESET2, the ramp is capped at `blending_progress` and held there.
489    SoftInSecond,
490    /// 2: the steady double preset: progress is `blending_progress` for as long as it is on screen.
491    Frozen,
492    /// 5: leaving a double preset: the ramp continues from `blending_progress` to 1, then the next preset loads.
493    SoftOut,
494}
495
496/// Where the `(a, c)` mesh of a blend comes from.
497#[derive(Clone, Copy)]
498pub(super) enum LaunchPattern {
499    /// BeatDrop's 19 patterns, drawn from the engine's seeded stream (`RandomizeBlendPattern`).
500    Md2,
501    /// An ordinary MD3 soft cut: a pattern drawn from the enabled list.
502    Md3Random,
503    /// A `.milk2` header. `None` is a name no generator knows: `m_vertinfo` stays as it was.
504    Md3(Option<Md3PatternInstance>),
505}
506
507/// How a blend starts ([`Engine::load_preset_blend`]).
508#[derive(Clone, Copy)]
509pub(super) struct BlendLaunch {
510    pub mode: ProgressMode,
511    /// `blending_progress`: the value `Frozen` holds and `SoftInSecond` rises to.
512    pub target: f32,
513    pub pattern: LaunchPattern,
514    /// A `.milk2` is loaded: no borders.
515    pub no_borders: bool,
516    /// The preset this blend loads honours its sprite block: ordinary presets always, a `.milk2` only for PRESET2 and
517    /// only with the header's `sprite=1`.
518    pub sprite: bool,
519}
520
521impl BlendLaunch {
522    /// A plain soft cut: MD3's patterns under `CompatRules::md3_blend_patterns`, else MD2's.
523    pub fn ordinary(md3_patterns: bool) -> Self {
524        Self {
525            mode: ProgressMode::Normal,
526            target: 1.0,
527            pattern: if md3_patterns {
528                LaunchPattern::Md3Random
529            } else {
530                LaunchPattern::Md2
531            },
532            no_borders: false,
533            sprite: true,
534        }
535    }
536}
537
538/// The next preset asked for while a double preset was on screen (`savedPath` / `savedBlendTime`).
539struct Pending {
540    bytes: Vec<u8>,
541    name: String,
542    seconds: f64,
543}
544
545/// The double preset on screen.
546pub(super) struct DoubleState {
547    name: String,
548    milk2: Milk2,
549    /// `None`: the file's pattern name is not one of the 30.
550    inst: Option<Md3PatternInstance>,
551    /// Set while leaving (`progressMode` 5).
552    pending: Option<Pending>,
553}
554
555/// Where a double preset is in its life ([`DoublePresetInfo::mode`]).
556#[derive(Clone, Copy, Debug, PartialEq, Eq)]
557pub enum DoubleMode {
558    /// A soft transition into the double preset, first leg: the running preset blends to PRESET1.
559    SoftInPreset1,
560    /// Second leg: PRESET2 is wiped in over PRESET1 up to `blending_progress`.
561    SoftInPreset2,
562    /// The steady frozen blend.
563    Steady,
564    /// Leaving: the blend runs on to PRESET2 alone before the next preset loads.
565    SoftOut,
566}
567
568/// What the host can show about the double preset on screen ([`crate::Engine::double_preset_info`]).
569#[derive(Clone, Debug, PartialEq)]
570pub struct DoublePresetInfo {
571    /// Name given to [`crate::Engine::load_preset`].
572    pub name: String,
573    /// `blending_pattern` as the file says it.
574    pub pattern: String,
575    /// The pattern, `None` if its name is not one of the 30 (the previous mesh is kept).
576    pub known_pattern: Option<Md3Pattern>,
577    /// `blending_progress`: where the blend is held.
578    pub progress: f32,
579    /// `blending_direction`.
580    pub direction: i32,
581    /// `random_1` .. `random_5`.
582    pub random: [f32; 5],
583    /// Names of the two halves (`NAME=` lines), `None` when the half has none.
584    pub name1: Option<String>,
585    /// See `name1`.
586    pub name2: Option<String>,
587    /// The file carries a sprite block (belongs to PRESET2).
588    pub has_sprite: bool,
589    /// The phase of the double preset.
590    pub mode: DoubleMode,
591    /// The blend's current progress (`m_fBlendProgress`).
592    pub blend_progress: f32,
593}
594
595/// What the double-preset layer decided about a load request.
596pub(super) enum Md3Dispatch {
597    /// Handled (or refused).
598    Done(Result<PresetReport, EngineError>),
599    /// An ordinary load with this transition.
600    Plain(Transition),
601}
602
603impl super::Engine {
604    /// The double preset on screen, `None` for ordinary presets. Only `Compat::MilkDrop3` loads `.milk2` files.
605    pub fn double_preset_info(&self) -> Option<DoublePresetInfo> {
606        let (d, m) = (self.double.as_ref()?, self.md2.as_ref()?);
607        let mode = match m.mode {
608            ProgressMode::SoftInFirst => DoubleMode::SoftInPreset1,
609            ProgressMode::SoftInSecond if m.progress < m.target => DoubleMode::SoftInPreset2,
610            ProgressMode::SoftOut => DoubleMode::SoftOut,
611            _ => DoubleMode::Steady,
612        };
613        Some(DoublePresetInfo {
614            name: d.name.clone(),
615            pattern: d.milk2.pattern.clone(),
616            known_pattern: d.inst.map(|i| i.pattern),
617            progress: d.milk2.progress,
618            direction: d.milk2.direction,
619            random: d.milk2.random,
620            name1: d.milk2.name1.clone(),
621            name2: d.milk2.name2.clone(),
622            has_sprite: d.milk2.has_sprite,
623            mode,
624            blend_progress: m.progress,
625        })
626    }
627
628    /// The patterns random soft cuts of `Compat::MilkDrop3` may pick (`[transitions]` of MD3's settings); an empty
629    /// list means `zoom` only.
630    pub fn set_md3_transition_patterns(&mut self, patterns: &[Md3Pattern]) {
631        self.cfg.md3_transition_patterns = patterns.to_vec();
632    }
633
634    /// `RandomizeBlendPattern`: fill `m_vertinfo` for a blend that starts now and return the pattern kind.
635    pub(super) fn fill_blend_pattern(&mut self, launch: &LaunchPattern, aspect: (f32, f32)) -> u32 {
636        let (gx, gy) = (self.cfg.mesh[0], self.cfg.mesh[1]);
637        let n = ((gx + 1) * (gy + 1)) as usize;
638        if self.md2_vertinfo.len() != n {
639            // `m_vertinfo` initial values: a = 1, c = 0
640            self.md2_vertinfo = vec![[1.0, 0.0]; n];
641        }
642        let inst = match *launch {
643            LaunchPattern::Md2 => {
644                return randomize_blend_pattern(
645                    &mut self.md2_vertinfo,
646                    [gx, gy],
647                    aspect,
648                    self.cfg.fps as f32,
649                    &mut self.md2_pattern_rng,
650                    &mut self.md2_clocks,
651                    self.md2_forced_pattern,
652                );
653            }
654            LaunchPattern::Md3Random => {
655                let locks = Md3Locks {
656                    pattern: self
657                        .md2_forced_pattern
658                        .and_then(|k| Md3Pattern::from_index(k as usize)),
659                    ..Default::default()
660                };
661                let inst = md3_random_instance(
662                    &mut self.md3_pattern_rng,
663                    &self.cfg.md3_transition_patterns,
664                    false,
665                    false,
666                    &locks,
667                );
668                // a soft cut overwrites the globals a later `.milk2` without those keys falls back on
669                self.md3_carry = Milk2Header {
670                    pattern: inst.pattern.name().to_owned(),
671                    progress: inst.progress,
672                    direction: inst.direction,
673                    random: inst.r,
674                };
675                inst
676            }
677            LaunchPattern::Md3(Some(inst)) => inst,
678            LaunchPattern::Md3(None) => return 0,
679        };
680        md3_fill_vertinfo(
681            &inst,
682            gx as usize,
683            gy as usize,
684            aspect,
685            &mut self.md2_vertinfo,
686        );
687        inst.pattern.index() as u32
688    }
689
690    /// A resize changes the aspect the pattern was generated for: a frozen double preset regenerates it from its
691    /// header values (no re-roll).
692    pub(super) fn md3_refresh_pattern(&mut self) {
693        let (Some(d), Some(m), Some(act)) = (
694            self.double.as_ref(),
695            self.md2.as_mut(),
696            self.active.as_ref(),
697        ) else {
698            return;
699        };
700        if let Some(inst) = d.inst {
701            let geom = act.renderer.mesh_geometry();
702            let (gx, gy) = (self.cfg.mesh[0] as usize, self.cfg.mesh[1] as usize);
703            md3_fill_vertinfo(
704                &inst,
705                gx,
706                gy,
707                (geom.aspect_x(), geom.aspect_y()),
708                &mut m.pattern,
709            );
710        }
711    }
712
713    /// The mesh size changed from `old`: the blend in progress keeps its pattern on the new grid (a double preset
714    /// regenerates it from its header values, any other blend resamples the old mesh to the nearest vertex), so no
715    /// later frame or resize sees a pattern of the wrong length.
716    pub(super) fn md2_follow_mesh(&mut self, old: [u32; 2]) {
717        let new = self.cfg.mesh;
718        let Some(m) = self.md2.as_mut() else {
719            return;
720        };
721        if old == new {
722            return;
723        }
724        let (ow, oh) = (old[0] as usize + 1, old[1] as usize + 1);
725        let (nw, nh) = (new[0] as usize + 1, new[1] as usize + 1);
726        let src = std::mem::take(&mut m.pattern);
727        m.pattern = if src.len() == ow * oh {
728            let at = |i: usize, n: usize, o: usize| (i * (o - 1) + (n - 1) / 2) / (n - 1);
729            (0..nh)
730                .flat_map(|y| (0..nw).map(move |x| (x, y)))
731                .map(|(x, y)| src[at(y, nh, oh) * ow + at(x, nw, ow)])
732                .collect()
733        } else {
734            vec![[1.0, 0.0]; nw * nh]
735        };
736        self.md3_refresh_pattern();
737    }
738
739    /// `LoadPreset` for `Compat::MilkDrop3` and `.milk2`: recognises double presets, enters them (hard load or soft
740    /// transition in) and defers the next preset while one is on screen (the soft-out of `progressMode` 5). Other
741    /// requests continue as ordinary loads.
742    pub(super) fn load_preset_md3(
743        &mut self,
744        bytes: &[u8],
745        name: &str,
746        transition: Transition,
747    ) -> Md3Dispatch {
748        if !self.cfg.compat.rules().md3_double_preset {
749            if is_milk2_path(name) && is_double_preset(bytes) {
750                return Md3Dispatch::Done(Err(EngineError::Unsupported(
751                    "a .milk2 double preset needs Compat::MilkDrop3".to_owned(),
752                )));
753            }
754            return Md3Dispatch::Plain(transition);
755        }
756        // a name that says `.milk` is a plain preset; otherwise (also for names without extension) the marker scan
757        // decides, like MD3 does for `.milk2` files, and a failed scan means a plain preset
758        let milk2 = if name.ends_with(".milk") {
759            None
760        } else {
761            match parse_milk2(bytes, &self.md3_carry) {
762                Ok(m) => Some(m),
763                Err(Milk2Error::NotDouble) => None,
764                Err(e) => return Md3Dispatch::Done(Err(e.into())),
765            }
766        };
767        if self.double.is_none() {
768            return match milk2 {
769                None => Md3Dispatch::Plain(transition),
770                Some(m) => Md3Dispatch::Done(self.enter_double(m, name, transition)),
771            };
772        }
773        // leaving a double preset: `BlendMilk2 = 0` makes it a hard cut
774        let transition = if self.cfg.blend_milk2 {
775            transition
776        } else {
777            Transition::Cut
778        };
779        match transition {
780            Transition::Soft { seconds } if seconds > 0.0 => {
781                Md3Dispatch::Done(self.defer_leaving(bytes, name, seconds, milk2.as_ref()))
782            }
783            // the hard cut: `commit` drops the double preset once the new one is in
784            _ => match milk2 {
785                None => Md3Dispatch::Plain(Transition::Cut),
786                Some(m) => Md3Dispatch::Done(self.enter_double(m, name, Transition::Cut)),
787            },
788        }
789    }
790
791    /// `LoadRandomPreset(T > 0)` while a double preset is on screen: remember the request, start the soft-out ramp
792    /// (`progressMode` 5) and load it when the ramp ends. During the first soft-in leg the ramp just continues.
793    /// A request while already leaving is ignored, like MD3. The report is the request's parsed header only: the
794    /// preset itself is built when the ramp ends (a failure then goes to the switch-failed callback).
795    fn defer_leaving(
796        &mut self,
797        bytes: &[u8],
798        name: &str,
799        seconds: f64,
800        milk2: Option<&Milk2>,
801    ) -> Result<PresetReport, EngineError> {
802        let probe = milk2.map_or(bytes, |m| m.preset1.as_slice());
803        let parsed = Preset::parse_with(probe, Flavor::Md2)?;
804        let report = PresetReport {
805            name: name.to_owned(),
806            preset_version: parsed.milkdrop_preset_version,
807            compat: self.cfg.compat,
808            ..Default::default()
809        };
810        let now = self.clock.now as f32;
811        let (Some(d), Some(m)) = (self.double.as_mut(), self.md2.as_mut()) else {
812            return Ok(report);
813        };
814        if d.pending.is_some() {
815            return Ok(report);
816        }
817        d.pending = Some(Pending {
818            bytes: bytes.to_vec(),
819            name: name.to_owned(),
820            seconds,
821        });
822        if m.mode != ProgressMode::SoftInFirst {
823            // the next frame's progress is the target, even if the second leg had not reached it yet
824            m.start = now - m.target * m.duration;
825        }
826        m.mode = ProgressMode::SoftOut;
827        Ok(report)
828    }
829
830    /// Load a `.milk2`: the hard load (nothing running, `BlendMilk2` off, a cut) or the soft transition in.
831    fn enter_double(
832        &mut self,
833        milk2: Milk2,
834        name: &str,
835        transition: Transition,
836    ) -> Result<PresetReport, EngineError> {
837        let inst = Md3Pattern::from_name(&milk2.pattern).map(|pattern| Md3PatternInstance {
838            pattern,
839            direction: milk2.direction,
840            r: milk2.random,
841            progress: milk2.progress,
842        });
843        // PRESET2 is only built when the first leg ends: refuse an unusable one now
844        Preset::parse_with(&milk2.preset2, Flavor::Md2)?;
845        let mut launch = BlendLaunch {
846            mode: ProgressMode::Frozen,
847            target: milk2.progress,
848            pattern: LaunchPattern::Md3(inst),
849            no_borders: true,
850            // leg A loads PRESET1, which never carries the sprite; the hard load builds PRESET2 with it
851            sprite: false,
852        };
853        let user = self.cfg.blend_time_user;
854        let soft = matches!(transition, Transition::Soft { seconds } if seconds > 0.0)
855            && self.active.is_some()
856            && self.cfg.blend_milk2
857            && user > 0.0;
858        let report = if soft {
859            // the duration of both legs is `BlendTimeUser`, the request's time only has to be positive
860            launch.mode = ProgressMode::SoftInFirst;
861            self.load_preset_blend(&milk2.preset1, name, user, launch)?
862        } else {
863            self.load_double_hard(&milk2, name, launch)?
864        };
865        self.md3_carry = milk2.header();
866        self.double = Some(DoubleState {
867            name: name.to_owned(),
868            milk2,
869            inst,
870            pending: None,
871        });
872        Ok(report)
873    }
874
875    /// The hard load: PRESET1 and PRESET2 are built into one renderer (PRESET1 as `m_pOldState`), the pattern is
876    /// generated and the frozen blend is on screen from the first frame. MD3 shows PRESET1 alone for about seven
877    /// frames first (its staged load); this port does not.
878    fn load_double_hard(
879        &mut self,
880        milk2: &Milk2,
881        name: &str,
882        launch: BlendLaunch,
883    ) -> Result<PresetReport, EngineError> {
884        let mut renderer = self.acquire_renderer();
885        renderer.reset_images();
886        let (mut old, rep1) = match self.build_loaded(&milk2.preset1, name, &mut renderer, false) {
887            Ok(v) => v,
888            Err(e) => {
889                self.spare = Some(renderer);
890                return Err(e);
891            }
892        };
893        let prev = renderer.take_shader_state();
894        let (mut new, mut report) =
895            match self.build_loaded(&milk2.preset2, name, &mut renderer, milk2.has_sprite) {
896                Ok(v) => v,
897                Err(e) => {
898                    self.spare = Some(renderer);
899                    return Err(e);
900                }
901            };
902        let t = self.clock.now as f32;
903        let geom = renderer.mesh_geometry();
904        let aspect = (geom.aspect_x(), geom.aspect_y());
905        let kind = self.fill_blend_pattern(&launch.pattern, aspect);
906        // `m_fLoadingPresetBlendTime := 1.0` for the base values' slide
907        let statics = start_blend_from(&mut new, &mut old, t, 1.0);
908        renderer.set_old_shader_state(Some(prev));
909        self.commit(
910            Slot {
911                renderer,
912                loaded: new,
913                last: LastFrame::default(),
914            },
915            Transition::Cut,
916        );
917        self.md2 =
918            Some(self.new_blend_state(old, LastFrame::default(), statics, (t, 1.0), kind, &launch));
919        report.warnings = rep1
920            .warnings
921            .into_iter()
922            .map(|w| format!("PRESET1: {w}"))
923            .chain(report.warnings.into_iter().map(|w| format!("PRESET2: {w}")))
924            .collect();
925        report.shader_fallbacks.extend(rep1.shader_fallbacks);
926        report.unresolved_textures.extend(rep1.unresolved_textures);
927        report.resolved_textures.extend(rep1.resolved_textures);
928        Ok(report)
929    }
930
931    /// The events of `progressMode` 3 and 5 after the frame's progress update; `true` when the blend state was
932    /// replaced (the caller refreshes its preset progress).
933    pub(super) fn double_tick(&mut self) -> bool {
934        let (mode, progress) = match (&self.double, &self.md2) {
935            (Some(_), Some(m)) => (m.mode, m.progress),
936            _ => return false,
937        };
938        match mode {
939            // leg A ended: PRESET2 is loaded on top of PRESET1, progress restarts at 0
940            ProgressMode::SoftInFirst if progress > 1.0 => {
941                let d = self.double.as_ref().expect("checked above");
942                let (bytes, name, inst) = (d.milk2.preset2.clone(), d.name.clone(), d.inst);
943                let launch = BlendLaunch {
944                    mode: ProgressMode::SoftInSecond,
945                    target: d.milk2.progress,
946                    pattern: LaunchPattern::Md3(inst),
947                    no_borders: true,
948                    sprite: d.milk2.has_sprite,
949                };
950                let user = self.cfg.blend_time_user;
951                if let Err(e) = self.load_preset_blend(&bytes, &name, user, launch) {
952                    // PRESET1 stays alone on screen
953                    log::error!("preset '{name}' failed to load: {e}");
954                    self.on_switch_failed
955                        .fire(&crate::callbacks::SwitchFailure {
956                            name,
957                            message: e.to_string(),
958                        });
959                    self.end_double();
960                }
961                true
962            }
963            // the ramp reached 1: PRESET2 is on screen alone, the saved request loads (`savedBlendTime`, 0 for
964            // `BlendMilk2 = 0`)
965            ProgressMode::SoftOut if progress >= 1.0 => {
966                let pending = self.double.as_mut().and_then(|d| d.pending.take());
967                self.end_double();
968                if let Some(p) = pending {
969                    let t = if self.cfg.blend_milk2 && p.seconds > 0.0 {
970                        Transition::Soft { seconds: p.seconds }
971                    } else {
972                        Transition::Cut
973                    };
974                    // a failure reaches the switch-failed callback; the engine stays on PRESET2
975                    let _ = self.load_preset(&p.bytes, &p.name, t);
976                }
977                true
978            }
979            _ => false,
980        }
981    }
982
983    /// The double preset is over and the (new) state stays alone: the blend ends, old shaders are released.
984    fn end_double(&mut self) {
985        self.double = None;
986        self.md2 = None;
987        if let Some(a) = self.active.as_mut() {
988            a.renderer.set_old_shader_state(None);
989        }
990    }
991}
992
993#[cfg(test)]
994mod tests {
995    use super::*;
996
997    /// MSVC UCRT `rand()` after `srand(seed)`.
998    struct Msvc(u32);
999    impl Rand15 for Msvc {
1000        fn rand15(&mut self) -> u32 {
1001            self.0 = self.0.wrapping_mul(214013).wrapping_add(2531011);
1002            (self.0 >> 16) & 0x7fff
1003        }
1004    }
1005
1006    const GX: usize = 4;
1007    const GY: usize = 2;
1008    const ASPECT: (f32, f32) = (1.0, 0.5625);
1009    const R: [f32; 5] = [0.25, 0.5, 0.75, 0.125, 0.875];
1010    /// Vertices the table lists: v0 = (0, 0), v3 = (3, 0), v7 = (2, 1), v14 = (4, 2).
1011    const PICK: [usize; 4] = [0, 3, 7, 14];
1012
1013    /// `(pattern index, direction, [(a, c) at v0, v3, v7, v14])` of the reference table (4x2 mesh, aspect
1014    /// `(1, 0.5625)`, `r = [0.25, 0.5, 0.75, 0.125, 0.875]`).
1015    #[rustfmt::skip]
1016    #[allow(clippy::type_complexity)]
1017    const TABLE: &[(usize, i32, [(f32, f32); 4])] = &[
1018        (0, 1, [(1.0000, 0.0000), (1.0000, 0.0000), (1.0000, 0.0000), (1.0000, 0.0000)]),
1019        (1, 1, [(6.0000, -4.2692), (6.0000, -4.2671), (6.0000, -3.2734), (6.0000, -2.2776)]),
1020        (2, 1, [(7.5574, -3.2787), (7.5574, -0.5464), (7.5574, -1.3832), (7.5574, 0.0000)]),
1021        (3, 1, [(4.4783, -0.5514), (4.4783, -0.1192), (4.4783, -0.0001), (4.4783, -0.3155)]),
1022        (4, 1, [(4.4783, -0.3137), (4.4783, -0.3155), (4.4783, -0.2387), (4.4783, -0.0357)]),
1023        (5, 1, [(5.4444, -0.8387), (5.4444, -2.0793), (5.4444, -4.4444), (5.4444, -0.8387)]),
1024        (5, -1, [(5.4444, -3.6058), (5.4444, -2.3652), (5.4444, 0.0000), (5.4444, -3.6058)]),
1025        (6, 1, [(5.4444, -1.3018), (5.4444, -2.6767), (5.4444, -4.4444), (5.4444, -1.3018)]),
1026        (7, 1, [(9.8889, -6.9246), (9.8889, -5.9697), (9.8889, -4.4444), (9.8889, -1.9643)]),
1027        (8, 1, [(9.8889, -3.1841), (9.8889, -5.2600), (9.8889, -4.4444), (9.8889, -5.7048)]),
1028        (9, 1, [(9.8889, -1.9231), (9.8889, -5.0628), (9.8889, -4.4444), (9.8889, -6.9658)]),
1029        (10, 1, [(5.4444, -0.9722), (5.4444, -2.0833), (5.4444, -4.4444), (5.4444, -0.9722)]),
1030        (10, -1, [(5.4444, -3.4722), (5.4444, -2.3611), (5.4444, 0.0000), (5.4444, -3.4722)]),
1031        (11, 1, [(14.3333, -0.0149), (14.3333, -6.5156), (14.3333, -0.4711), (14.3333, -0.0149)]),
1032        (12, 1, [(5.4444, -2.7860), (5.4444, -1.2573), (5.4444, -2.4444), (5.4444, 0.0000)]),
1033        (12, -1, [(5.4444, -1.6584), (5.4444, -3.1871), (5.4444, -2.0001), (5.4444, -4.4444)]),
1034        (13, 1, [(5.4444, -2.2082), (5.4444, -2.6087), (5.4444, -1.1111), (5.4444, -4.3392)]),
1035        (13, -1, [(5.4444, -2.2362), (5.4444, -1.8357), (5.4444, -3.3333), (5.4444, -0.1053)]),
1036        (14, 1, [(14.3333, -2.4652), (14.3333, -3.5522), (14.3333, -4.5502), (14.3333, -3.6801)]),
1037        (15, 1, [(7.0000, -2.8988), (7.0000, -3.8268), (7.0000, -3.3973), (7.0000, -0.2957)]),
1038        (16, 1, [(14.3333, -0.0294), (14.3333, -8.0757), (14.3333, -0.4711), (14.3333, -8.8595)]),
1039        (17, 1, [(14.3333, -1.0300), (14.3333, -4.9895), (14.3333, -0.4711), (14.3333, -1.0300)]),
1040        (18, 1, [(14.3333, -6.6234), (14.3333, -7.7663), (14.3333, -4.4444), (14.3333, -2.2655)]),
1041        (19, 1, [(14.3333, -8.8864), (14.3333, -6.9874), (14.3333, -4.4444), (14.3333, -0.0025)]),
1042        (20, 1, [(5.4444, -2.5465), (5.4444, -0.8628), (5.4444, -0.3523), (5.4444, -0.4118)]),
1043        (21, 1, [(14.3333, -8.4178), (14.3333, -8.4178), (14.3333, -2.2109), (14.3333, -6.4359)]),
1044        (22, 1, [(14.3333, -6.6950), (14.3333, -8.8195), (14.3333, -0.0149), (14.3333, -0.0830)]),
1045        (23, 1, [(4.3333, -1.6583), (4.3333, -0.4941), (4.3333, -0.0000), (4.3333, -1.6656)]),
1046        (24, 1, [(6.0000, -4.2621), (6.0000, -1.6105), (6.0000, -2.4975), (6.0000, -0.7329)]),
1047        (25, 1, [(6.0000, -4.2621), (6.0000, -3.3782), (6.0000, -2.4975), (6.0000, -0.7329)]),
1048        (26, 1, [(7.0000, -3.8760), (7.0000, -3.1431), (7.0000, -2.0000), (7.0000, -0.1240)]),
1049        (27, 1, [(7.0000, -0.2388), (7.0000, -0.2388), (7.0000, -2.0000), (7.0000, -3.7612)]),
1050        (28, 1, [(5.4444, -1.9444), (5.4444, -2.2222), (5.4444, -4.4444), (5.4444, -1.9444)]),
1051        (29, 1, [(0.0000, 0.0000), (0.0000, 0.0000), (5.4444, -4.4444), (0.0000, 0.0000)]),
1052    ];
1053
1054    fn fill(
1055        pattern: Md3Pattern,
1056        direction: i32,
1057        r: [f32; 5],
1058        gx: usize,
1059        gy: usize,
1060    ) -> Vec<[f32; 2]> {
1061        let mut vi = vec![[f32::NAN; 2]; (gx + 1) * (gy + 1)];
1062        md3_fill_vertinfo(
1063            &Md3PatternInstance {
1064                pattern,
1065                direction,
1066                r,
1067                progress: 0.5,
1068            },
1069            gx,
1070            gy,
1071            ASPECT,
1072            &mut vi,
1073        );
1074        vi
1075    }
1076
1077    #[test]
1078    fn names_indices_and_defaults() {
1079        assert_eq!(Md3Pattern::ALL.len(), 30);
1080        for (i, p) in Md3Pattern::ALL.iter().enumerate() {
1081            assert_eq!(p.index(), i);
1082            assert_eq!(Md3Pattern::from_index(i), Some(*p));
1083            assert_eq!(Md3Pattern::from_name(p.name()), Some(*p));
1084        }
1085        assert_eq!(Md3Pattern::from_index(30), None);
1086        assert_eq!(
1087            Md3Pattern::from_name("Zoom"),
1088            None,
1089            "names are case-sensitive"
1090        );
1091        assert_eq!(Md3Pattern::from_name("cercle "), None);
1092        assert_eq!(Md3Pattern::Wave.index(), 22);
1093        assert_eq!(Md3Pattern::Cross2.name(), "cross2");
1094        let on: Vec<&str> = default_enabled_patterns()
1095            .iter()
1096            .map(|p| p.name())
1097            .collect();
1098        assert_eq!(
1099            on,
1100            [
1101                "zoom", "side", "plasma", "plasma2", "plasma3", "cercle", "snail", "triangle",
1102                "corner", "patches", "clock", "arrow", "curtain"
1103            ]
1104        );
1105        let dir: Vec<&str> = Md3Pattern::ALL
1106            .iter()
1107            .filter(|p| p.uses_direction())
1108            .map(|p| p.name())
1109            .collect();
1110        assert_eq!(dir, ["cercle", "triangle", "corner", "patches"]);
1111    }
1112
1113    /// The reference table of every pattern (both directions where the direction matters), 1e-3.
1114    #[test]
1115    fn all_thirty_patterns_match_the_reference_table() {
1116        let mut seen = [false; 30];
1117        for (idx, dir, cells) in TABLE {
1118            let p = Md3Pattern::from_index(*idx).unwrap();
1119            seen[*idx] = true;
1120            let vi = fill(p, *dir, R, GX, GY);
1121            for (k, (a, c)) in PICK.iter().zip(cells) {
1122                let [va, vc] = vi[*k];
1123                assert!(
1124                    (va - a).abs() <= 1e-3 && (vc - c).abs() <= 1e-3,
1125                    "{} dir {dir} v{k}: got ({va:.4}, {vc:.4}), want ({a:.4}, {c:.4})",
1126                    p.name()
1127                );
1128            }
1129        }
1130        assert!(seen.iter().all(|&s| s), "every pattern has a row");
1131    }
1132
1133    #[test]
1134    fn generators_are_pure() {
1135        // the old contents of the mesh never matter, only the instance does
1136        for p in Md3Pattern::ALL {
1137            let a = fill(p, -1, [0.3, 0.6, 0.2, 0.9, 0.4], 8, 6);
1138            let mut b = vec![[123.0, -77.0]; 9 * 7];
1139            md3_fill_vertinfo(
1140                &Md3PatternInstance {
1141                    pattern: p,
1142                    direction: -1,
1143                    r: [0.3, 0.6, 0.2, 0.9, 0.4],
1144                    progress: 0.1,
1145                },
1146                8,
1147                6,
1148                ASPECT,
1149                &mut b,
1150            );
1151            assert_eq!(a, b, "{}", p.name());
1152            assert!(
1153                a.iter().all(|v| v[0].is_finite() && v[1].is_finite()),
1154                "{} is finite",
1155                p.name()
1156            );
1157        }
1158    }
1159
1160    #[test]
1161    fn direction_matters_for_four_patterns_only() {
1162        for p in Md3Pattern::ALL {
1163            let (up, down, other) = (
1164                fill(p, 1, R, GX, GY),
1165                fill(p, -1, R, GX, GY),
1166                fill(p, 7, R, GX, GY),
1167            );
1168            assert_eq!(up, other, "{}: only -1 is special", p.name());
1169            assert_eq!(p.uses_direction(), up != down, "{}", p.name());
1170        }
1171        // `t -> 1 - t`: the centre vertex of cercle is the first region where the second preset appears
1172        let c = fill(Md3Pattern::Cercle, -1, R, GX, GY)[7];
1173        assert!(
1174            c[1].abs() < 1e-6,
1175            "centre: c = 0 with direction -1, got {}",
1176            c[1]
1177        );
1178    }
1179
1180    #[test]
1181    fn horizontal_fills_the_mesh_transposed() {
1182        // grid 4x2: entry k belongs to the transposed loop nest (outer i in 0..=4, inner j in 0..=2, k = i*3 + j)
1183        let h = fill(Md3Pattern::Horizontal, 1, R, GX, GY);
1184        let v = fill(Md3Pattern::Vertical, 1, R, GX, GY);
1185        assert_eq!(h[0], v[0]);
1186        assert_ne!(h[3], v[3], "k = 3 is (i = 1, j = 0), not (x = 3, y = 0)");
1187        let (vx, vy) = (6.28f32.cos(), 6.28f32.sin());
1188        for i in 0..=GX {
1189            for j in 0..=GY {
1190                let (u, w) = (
1191                    i as f32 / GX as f32 * ASPECT.0,
1192                    j as f32 / GY as f32 * ASPECT.1,
1193                );
1194                let t = (((w - 0.5) * vy + (u - 0.5) * vx + 0.5) - 0.5) / 2.0f32.sqrt() + 0.5;
1195                let got = h[i * (GY + 1) + j];
1196                assert!((got[1] - (5.0 * t - 5.0)).abs() < 1e-5 && got[0] == 6.0);
1197            }
1198        }
1199        // on a square grid the transposition is a plain row/column swap of the same value function
1200        let sq = fill(Md3Pattern::Horizontal, 1, R, 4, 4);
1201        assert!(sq.iter().all(|e| e[0] == 6.0));
1202    }
1203
1204    /// With every random 0.5 the displacement vanishes and the plasma field is flat (`h = 0.5`). `plasma2` then has
1205    /// `mn == mx` (0 / 0); `plasma3` seeds both extremes with `0.5 * h(0, 0) = 0.25`, so its range is 0.25..0.5
1206    /// and every vertex gets `sin(2)`.
1207    #[test]
1208    fn plasma3_seeds_min_and_max_with_half_the_corner() {
1209        let r = [0.5; 5];
1210        let b = 0.25 + 0.15 * r[0];
1211        let ib = 1.0 / b;
1212        let want = 2.0f32.sin() * ib - ib;
1213        for e in fill(Md3Pattern::Plasma3, 1, r, GX, GY) {
1214            assert!((e[1] - want).abs() < 1e-4, "{} vs {want}", e[1]);
1215        }
1216        for e in fill(Md3Pattern::Plasma2, 1, r, GX, GY) {
1217            assert!(e[1].is_nan(), "plasma2 has no seed offset: 0 / 0");
1218        }
1219    }
1220
1221    #[test]
1222    fn cross2_zeroes_the_vertices_that_never_blend() {
1223        let v = fill(Md3Pattern::Cross2, 1, R, GX, GY);
1224        assert_eq!(v[0], [0.0, 0.0]);
1225        assert!((v[7][0] - 5.4444).abs() < 1e-3 && (v[7][1] + 4.4444).abs() < 1e-3);
1226        // zeroed vertices keep mix 0 at any progress
1227        for p in [0.0f32, 0.5, 1.0, 3.0] {
1228            assert_eq!((v[0][0] * p + v[0][1]).clamp(0.0, 1.0), 0.0);
1229        }
1230    }
1231
1232    #[test]
1233    fn mix_ramp_of_the_wipe_patterns_covers_zero_to_one() {
1234        // `c <= 0` and `a + c >= 1`: mix is 0 at progress 0 and 1 at progress 1 for every vertex of the patterns
1235        // that fully blend
1236        for p in Md3Pattern::ALL {
1237            if matches!(p, Md3Pattern::Cross2 | Md3Pattern::Horizontal) {
1238                continue;
1239            }
1240            for e in fill(p, 1, R, 16, 12) {
1241                let (m0, m1) = ((e[1]).clamp(0.0, 1.0), (e[0] + e[1]).clamp(0.0, 1.0));
1242                assert!(
1243                    m0 <= 0.0 + 1e-6 || p == Md3Pattern::Zoom,
1244                    "{} start {m0}",
1245                    p.name()
1246                );
1247                assert!(m1 >= 1.0 - 1e-3, "{} end {m1}", p.name());
1248            }
1249        }
1250    }
1251
1252    #[test]
1253    fn random_block_follows_msvc_rand() {
1254        // MSVC srand(1): rand() = 41, 18467, 6334, 26500, 19169, 15724, 11478, 29358, ...
1255        let mut rng = Msvc(1);
1256        assert_eq!(
1257            [rng.rand15(), rng.rand15(), rng.rand15()],
1258            [41, 18467, 6334]
1259        );
1260        let enabled = default_enabled_patterns();
1261        let inst = md3_random_instance(&mut Msvc(1), &enabled, false, false, &Md3Locks::default());
1262        assert_eq!(inst.pattern, Md3Pattern::Plasma, "41 % 13 = 2");
1263        assert_eq!(inst.direction, 1, "18467 is odd");
1264        let want = [0.85827, 0.59038, 0.59715, 0.13035, 0.55515];
1265        for (g, w) in inst.r.iter().zip(want) {
1266            assert!((g - w).abs() < 1e-5, "{g} vs {w}");
1267        }
1268        assert_eq!(inst.progress, 1.0);
1269        // double-preset mode draws one more value: 29358 -> 0.56
1270        let d = md3_random_instance(&mut Msvc(1), &enabled, true, false, &Md3Locks::default());
1271        assert_eq!(d.pattern, Md3Pattern::Plasma);
1272        assert_eq!(d.progress, 0.56);
1273        // and the mesh of that instance: a = 5.3183 everywhere, mix at P = 0.56
1274        let mut vi = vec![[0.0; 2]; 15];
1275        md3_fill_vertinfo(&d, GX, GY, ASPECT, &mut vi);
1276        assert!(vi.iter().all(|e| (e[0] - 5.3183).abs() < 1e-3));
1277        let c = [-0.3912, -0.0247, -0.0123, 0.0];
1278        for (k, w) in c.iter().enumerate() {
1279            assert!((vi[k][1] - w).abs() < 1e-3, "c[{k}] = {} vs {w}", vi[k][1]);
1280        }
1281    }
1282
1283    #[test]
1284    fn random_block_locks_and_lists() {
1285        let enabled = [Md3Pattern::Square, Md3Pattern::Wave];
1286        let locks = Md3Locks {
1287            pattern: Some(Md3Pattern::Cross),
1288            progress: Some(0.33),
1289            direction: Some(-1),
1290        };
1291        let inst = md3_random_instance(&mut Msvc(5), &enabled, true, false, &locks);
1292        assert_eq!(inst.pattern, Md3Pattern::Cross);
1293        assert_eq!(inst.direction, -1);
1294        assert_eq!(inst.progress, 0.33);
1295        // a locked direction consumes no draw: the five randoms are the next five values
1296        let mut a = Msvc(9);
1297        let locked = md3_random_instance(
1298            &mut a,
1299            &enabled,
1300            false,
1301            false,
1302            &Md3Locks {
1303                direction: Some(1),
1304                ..Default::default()
1305            },
1306        );
1307        let mut b = Msvc(9);
1308        let _ = b.rand15();
1309        let want: Vec<f32> = (0..5).map(|_| frand7380(&mut b)).collect();
1310        assert_eq!(locked.r.to_vec(), want);
1311        // empty list = {zoom}; the draw is still consumed
1312        let z = md3_random_instance(&mut Msvc(3), &[], false, false, &Md3Locks::default());
1313        assert_eq!(z.pattern, Md3Pattern::Zoom);
1314        // auto hard cut restricts double-preset patterns to the first six
1315        for seed in 0..64 {
1316            let h =
1317                md3_random_instance(&mut Msvc(seed), &enabled, true, true, &Md3Locks::default());
1318            assert!(h.pattern.index() < 6, "{}", h.pattern.name());
1319        }
1320        // progress draws stay in the documented range of every pattern
1321        for p in Md3Pattern::ALL {
1322            let (a, b) = p.progress_range();
1323            for seed in 0..32 {
1324                let v = md3_draw_progress(p, &mut Msvc(seed));
1325                assert!(v >= b - 0.006 && v <= b + a + 0.006, "{} {v}", p.name());
1326            }
1327        }
1328    }
1329
1330    #[test]
1331    fn stream_rng_is_a_rand15_source() {
1332        let mut r = StreamRng::new(1, "md3_test");
1333        let mut s = StreamRng::new(1, "md3_test");
1334        for _ in 0..100 {
1335            let v = r.rand15();
1336            assert!(v <= 0x7fff);
1337            assert_eq!(v, s.next32() >> 17);
1338        }
1339    }
1340
1341    /// Every import resets the global `FFTAttack` / `FFTDecay` to 0.5 / 0.7 and then reads its own keys, so after a
1342    /// double preset is loaded the globals are PRESET2's (PRESET1 is imported first), also when PRESET2 has no keys.
1343    #[test]
1344    fn double_preset_leaves_preset2s_fft_params() {
1345        use crate::{Compat, Engine, EngineConfig, Transition};
1346        let Some((device, queue)) = lactea_testutil::gpu() else {
1347            eprintln!("skipping: no wgpu adapter");
1348            return;
1349        };
1350        let half = |keys: &str| {
1351            format!(
1352                "MILKDROP_PRESET_VERSION=201\nPSVERSION=2\nPSVERSION_WARP=0\nPSVERSION_COMP=0\n[preset00]\n{keys}fDecay=1\n"
1353            )
1354        };
1355        let file = |p1: &str, p2: &str| {
1356            format!(
1357                "Created with MILKDROP 3.37 on 05/01/2026 at 12:34:56\nwww.example.org\n\nblending_pattern=zoom\nblending_progress=0.5\nblending_direction=1\nrandom_1=0.1\nrandom_2=0.2\nrandom_3=0.3\nrandom_4=0.4\nrandom_5=0.5\n[PRESET1_BEGIN]\nNAME=a\n{p1}[PRESET1_END]\n\n[PRESET2_BEGIN]\nNAME=b\n{p2}[PRESET2_END]\n"
1358            )
1359        };
1360        let mut e = Engine::new(
1361            &device,
1362            &queue,
1363            EngineConfig {
1364                width: 32,
1365                height: 24,
1366                mesh: [8, 8],
1367                compat: Compat::MilkDrop3,
1368                ..Default::default()
1369            },
1370        )
1371        .unwrap();
1372        let p1 = half("FFTAttack=0.9\nFFTDecay=0.2\n");
1373        let p2 = half("FFTAttack=0.3\nFFTDecay=0.4\n");
1374        e.load_preset(file(&p1, &p2).as_bytes(), "pair.milk2", Transition::Cut)
1375            .unwrap();
1376        assert_eq!(e.md3.fft_params(), (0.3, 0.4), "PRESET2's keys win");
1377        e.load_preset(
1378            file(&p1, &half("")).as_bytes(),
1379            "pair2.milk2",
1380            Transition::Cut,
1381        )
1382        .unwrap();
1383        assert_eq!(
1384            e.md3.fft_params(),
1385            (0.5, 0.7),
1386            "PRESET2 without keys resets them, PRESET1's are gone"
1387        );
1388    }
1389
1390    /// Shader `time` counts from the start of each state's own preset: a hard load builds both halves at once (the
1391    /// same start); a soft transition starts PRESET1 with the request and PRESET2 when the first leg ends.
1392    #[test]
1393    fn halves_of_a_double_preset_have_their_own_shader_start_times() {
1394        use crate::{Compat, Engine, EngineConfig, FrameClock, Transition};
1395        let Some((device, queue)) = lactea_testutil::gpu() else {
1396            eprintln!("skipping: no wgpu adapter");
1397            return;
1398        };
1399        let tex = device.create_texture(&wgpu::TextureDescriptor {
1400            label: None,
1401            size: wgpu::Extent3d {
1402                width: 32,
1403                height: 24,
1404                depth_or_array_layers: 1,
1405            },
1406            mip_level_count: 1,
1407            sample_count: 1,
1408            dimension: wgpu::TextureDimension::D2,
1409            format: wgpu::TextureFormat::Rgba8Unorm,
1410            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1411            view_formats: &[],
1412        });
1413        let view = tex.create_view(&Default::default());
1414        let half = "MILKDROP_PRESET_VERSION=201\nPSVERSION=2\nPSVERSION_WARP=0\nPSVERSION_COMP=0\n[preset00]\nfDecay=1\n";
1415        let file = format!(
1416            "Created with MILKDROP 3.37 on 05/01/2026 at 12:34:56\nwww.example.org\n\nblending_pattern=zoom\nblending_progress=0.5\nblending_direction=1\nrandom_1=0.1\nrandom_2=0.2\nrandom_3=0.3\nrandom_4=0.4\nrandom_5=0.5\n[PRESET1_BEGIN]\nNAME=a\n{half}[PRESET1_END]\n\n[PRESET2_BEGIN]\nNAME=b\n{half}[PRESET2_END]\n"
1417        );
1418        let mut e = Engine::new(
1419            &device,
1420            &queue,
1421            EngineConfig {
1422                width: 32,
1423                height: 24,
1424                mesh: [8, 8],
1425                compat: Compat::MilkDrop3,
1426                blend_time_user: 1.0,
1427                ..Default::default()
1428            },
1429        )
1430        .unwrap();
1431        let mut frame = 0u32;
1432        let mut run = |e: &mut Engine, until: f64| {
1433            while f64::from(frame) / 60.0 <= until {
1434                let mut enc = device.create_command_encoder(&Default::default());
1435                e.render(
1436                    &mut enc,
1437                    &view,
1438                    FrameClock {
1439                        time: f64::from(frame) / 60.0,
1440                        frame,
1441                    },
1442                );
1443                queue.submit(Some(enc.finish()));
1444                frame += 1;
1445            }
1446        };
1447        e.load_preset(half.as_bytes(), "plain", Transition::Cut)
1448            .unwrap();
1449        run(&mut e, 2.0);
1450        // hard load at t = 2: both halves start there
1451        e.load_preset(file.as_bytes(), "pair.milk2", Transition::Cut)
1452            .unwrap();
1453        let (new, old) = (
1454            e.active.as_ref().unwrap().loaded.start_time,
1455            e.md2.as_ref().unwrap().old.start_time,
1456        );
1457        assert_eq!(new, old);
1458        assert!((new - 2.0).abs() < 0.05, "{new}");
1459        // soft transition at t = 4 from a plain preset (a soft request while a `.milk2` is on screen is deferred):
1460        // PRESET1 starts now, PRESET2 when the 1 s leg ends
1461        e.load_preset(half.as_bytes(), "plain", Transition::Cut)
1462            .unwrap();
1463        run(&mut e, 4.0);
1464        e.load_preset(
1465            file.as_bytes(),
1466            "pair2.milk2",
1467            Transition::Soft { seconds: 2.7 },
1468        )
1469        .unwrap();
1470        let p1_start = e.active.as_ref().unwrap().loaded.start_time;
1471        assert!((p1_start - 4.0).abs() < 0.05, "{p1_start}");
1472        run(&mut e, 5.5);
1473        let new = e.active.as_ref().unwrap().loaded.start_time;
1474        let old = e.md2.as_ref().unwrap().old.start_time;
1475        assert_eq!(old, p1_start, "PRESET1 keeps its own start");
1476        assert!((new - 5.0).abs() < 0.05, "PRESET2 starts with leg B: {new}");
1477    }
1478
1479    /// The `_mxd3q*` block reads `q64` of the state MD3 treats as current (`active`, `m_pState`) right after that
1480    /// state's own per-frame code, in the same frame. `q64` is the first detector's minimum delay: a preset that
1481    /// writes `q64 = 1` suppresses the start-up beats (latch L0 keeps counting from the first frame), otherwise the
1482    /// transient resets L0 on frame 4. In a double preset the new state is PRESET2, not the old PRESET1.
1483    #[test]
1484    fn the_mxd3q_block_reads_the_current_states_q64_after_its_per_frame_code() {
1485        use crate::{Compat, Engine, EngineConfig, FrameClock, Transition};
1486        let Some((device, queue)) = lactea_testutil::gpu() else {
1487            eprintln!("skipping: no wgpu adapter");
1488            return;
1489        };
1490        let tex = device.create_texture(&wgpu::TextureDescriptor {
1491            label: None,
1492            size: wgpu::Extent3d {
1493                width: 32,
1494                height: 24,
1495                depth_or_array_layers: 1,
1496            },
1497            mip_level_count: 1,
1498            sample_count: 1,
1499            dimension: wgpu::TextureDimension::D2,
1500            format: wgpu::TextureFormat::Rgba8Unorm,
1501            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1502            view_formats: &[],
1503        });
1504        let view = tex.create_view(&Default::default());
1505        // `qsa.w` (latch L0) after 10 silent frames at 60 fps, the first at `elapsed = 1/60`
1506        let latch_after_ten_frames = |preset: &str, name: &str| {
1507            let mut e = Engine::new(
1508                &device,
1509                &queue,
1510                EngineConfig {
1511                    width: 32,
1512                    height: 24,
1513                    mesh: [8, 8],
1514                    compat: Compat::MilkDrop3,
1515                    ..Default::default()
1516                },
1517            )
1518            .unwrap();
1519            e.load_preset(preset.as_bytes(), name, Transition::Cut)
1520                .unwrap();
1521            for frame in 0..10u32 {
1522                let mut enc = device.create_command_encoder(&Default::default());
1523                e.render(
1524                    &mut enc,
1525                    &view,
1526                    FrameClock {
1527                        time: f64::from(frame + 1) / 60.0,
1528                        frame,
1529                    },
1530                );
1531                queue.submit(Some(enc.finish()));
1532            }
1533            e.md3.last_mxd3q().qsa[3]
1534        };
1535        let half = |code: &str| {
1536            format!(
1537                "MILKDROP_PRESET_VERSION=201\nPSVERSION=2\nPSVERSION_WARP=0\nPSVERSION_COMP=0\n[preset00]\nfDecay=1\nper_frame_1={code}\n"
1538            )
1539        };
1540        let double = |p1: &str, p2: &str| {
1541            format!(
1542                "Created with MILKDROP 3.37 on 05/01/2026 at 12:34:56\nwww.example.org\n\nblending_pattern=zoom\nblending_progress=0.5\nblending_direction=1\nrandom_1=0.1\nrandom_2=0.2\nrandom_3=0.3\nrandom_4=0.4\nrandom_5=0.5\n[PRESET1_BEGIN]\nNAME=a\n{p1}[PRESET1_END]\n\n[PRESET2_BEGIN]\nNAME=b\n{p2}[PRESET2_END]\n"
1543            )
1544        };
1545        let (held, free) = (10.0 / 60.0, 6.0 / 60.0);
1546        let near = |got: f32, want: f32| assert!((got - want).abs() < 1e-5, "{got} != {want}");
1547        near(latch_after_ten_frames(&half("q64 = 1;"), "held"), held);
1548        near(latch_after_ten_frames(&half("q1 = 1;"), "free"), free);
1549        let (sets, silent) = (half("q64 = 1;"), half("q1 = 1;"));
1550        near(
1551            latch_after_ten_frames(&double(&silent, &sets), "held.milk2"),
1552            held,
1553        );
1554        near(
1555            latch_after_ten_frames(&double(&sets, &silent), "free.milk2"),
1556            free,
1557        );
1558    }
1559}