1#![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
28macro_rules! patterns {
33 ($($variant:ident $name:literal $on:literal ($pa:literal, $pb:literal);)*) => {
34 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
36 #[allow(missing_docs)]
37 pub enum Md3Pattern {
38 $($variant,)*
39 }
40
41 impl Md3Pattern {
42 pub const ALL: [Md3Pattern; 30] = [$(Md3Pattern::$variant,)*];
44
45 pub fn name(self) -> &'static str {
47 match self {
48 $(Md3Pattern::$variant => $name,)*
49 }
50 }
51
52 pub fn default_enabled(self) -> bool {
54 match self {
55 $(Md3Pattern::$variant => $on,)*
56 }
57 }
58
59 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 pub fn index(self) -> usize {
106 self as usize
107 }
108
109 pub fn from_index(i: usize) -> Option<Md3Pattern> {
111 Self::ALL.get(i).copied()
112 }
113
114 pub fn from_name(name: &str) -> Option<Md3Pattern> {
116 Self::ALL.iter().copied().find(|p| p.name() == name)
117 }
118
119 pub fn uses_direction(self) -> bool {
121 matches!(
122 self,
123 Md3Pattern::Cercle | Md3Pattern::Triangle | Md3Pattern::Corner | Md3Pattern::Patches
124 )
125 }
126}
127
128pub fn default_enabled_patterns() -> Vec<Md3Pattern> {
130 Md3Pattern::ALL
131 .into_iter()
132 .filter(|p| p.default_enabled())
133 .collect()
134}
135
136#[derive(Clone, Copy, Debug, PartialEq)]
138pub struct Md3PatternInstance {
139 pub pattern: Md3Pattern,
141 pub direction: i32,
143 pub r: [f32; 5],
145 pub progress: f32,
147}
148
149pub 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 let band = r1 * 0.14 + 0.02 + r2 * 0.34;
180
181 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 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 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 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 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 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 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 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 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 [0.0, 0.0]
381 } else {
382 [(band + 1.0) * ib, t * ib - ib]
383 }
384 });
385 }
386 }
387}
388
389pub trait Rand15 {
396 fn rand15(&mut self) -> u32;
398}
399
400impl Rand15 for StreamRng {
401 fn rand15(&mut self) -> u32 {
403 self.next32() >> 17
404 }
405}
406
407fn frand7380(rng: &mut impl Rand15) -> f32 {
409 (rng.rand15() % 7381) as f32 / 7380.0
410}
411
412#[derive(Clone, Copy, Debug, Default, PartialEq)]
414pub struct Md3Locks {
415 pub pattern: Option<Md3Pattern>,
417 pub progress: Option<f32>,
419 pub direction: Option<i32>,
421}
422
423pub 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
429pub 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
482pub(super) enum ProgressMode {
483 Normal,
485 SoftInFirst,
488 SoftInSecond,
490 Frozen,
492 SoftOut,
494}
495
496#[derive(Clone, Copy)]
498pub(super) enum LaunchPattern {
499 Md2,
501 Md3Random,
503 Md3(Option<Md3PatternInstance>),
505}
506
507#[derive(Clone, Copy)]
509pub(super) struct BlendLaunch {
510 pub mode: ProgressMode,
511 pub target: f32,
513 pub pattern: LaunchPattern,
514 pub no_borders: bool,
516 pub sprite: bool,
519}
520
521impl BlendLaunch {
522 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
538struct Pending {
540 bytes: Vec<u8>,
541 name: String,
542 seconds: f64,
543}
544
545pub(super) struct DoubleState {
547 name: String,
548 milk2: Milk2,
549 inst: Option<Md3PatternInstance>,
551 pending: Option<Pending>,
553}
554
555#[derive(Clone, Copy, Debug, PartialEq, Eq)]
557pub enum DoubleMode {
558 SoftInPreset1,
560 SoftInPreset2,
562 Steady,
564 SoftOut,
566}
567
568#[derive(Clone, Debug, PartialEq)]
570pub struct DoublePresetInfo {
571 pub name: String,
573 pub pattern: String,
575 pub known_pattern: Option<Md3Pattern>,
577 pub progress: f32,
579 pub direction: i32,
581 pub random: [f32; 5],
583 pub name1: Option<String>,
585 pub name2: Option<String>,
587 pub has_sprite: bool,
589 pub mode: DoubleMode,
591 pub blend_progress: f32,
593}
594
595pub(super) enum Md3Dispatch {
597 Done(Result<PresetReport, EngineError>),
599 Plain(Transition),
601}
602
603impl super::Engine {
604 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 pub fn set_md3_transition_patterns(&mut self, patterns: &[Md3Pattern]) {
631 self.cfg.md3_transition_patterns = patterns.to_vec();
632 }
633
634 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 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 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 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 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 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 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 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 _ => 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 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 m.start = now - m.target * m.duration;
825 }
826 m.mode = ProgressMode::SoftOut;
827 Ok(report)
828 }
829
830 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 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 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 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 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 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 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 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 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 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 let _ = self.load_preset(&p.bytes, &p.name, t);
976 }
977 true
978 }
979 _ => false,
980 }
981 }
982
983 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 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 const PICK: [usize; 4] = [0, 3, 7, 14];
1012
1013 #[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 #[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 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 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 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 let sq = fill(Md3Pattern::Horizontal, 1, R, 4, 4);
1201 assert!(sq.iter().all(|e| e[0] == 6.0));
1202 }
1203
1204 #[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 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 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 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 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 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 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 let z = md3_random_instance(&mut Msvc(3), &[], false, false, &Md3Locks::default());
1313 assert_eq!(z.pattern, Md3Pattern::Zoom);
1314 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 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 #[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 #[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 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 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 #[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 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}