onedrop_engine/engine/
state_sync.rs

1//! Mirror a slot's evaluator context into its `RenderState`.
2//!
3//! After the per-frame equations have run, every variable they wrote
4//! (motion params, wave params, decay, gamma, echo, q1..q32, feedback
5//! flags, borders, motion vectors) is read back here and packed into the
6//! slot's `RenderState`. Read-only against the evaluator; every fallback
7//! default matches the MD2 spec so a preset that omits a variable behaves
8//! as MilkDrop would.
9//!
10//! Takes `&mut PresetSlot` so it can run against either the primary slot
11//! (`current`) or the outgoing one (`fading_out`) during a transition.
12
13use super::preset_slot::PresetSlot;
14use onedrop_renderer::{
15    BorderParams, FeedbackParams, MotionParams, MotionVectorParams, WaveParams,
16};
17
18pub(super) fn sync_render_state(slot: &mut PresetSlot) {
19    let ctx = slot.evaluator.context();
20
21    // Update motion parameters
22    slot.state.motion = MotionParams {
23        zoom: ctx.get_var("zoom").unwrap_or(1.0) as f32,
24        rot: ctx.get_var("rot").unwrap_or(0.0) as f32,
25        cx: ctx.get_var("cx").unwrap_or(0.5) as f32,
26        cy: ctx.get_var("cy").unwrap_or(0.5) as f32,
27        dx: ctx.get_var("dx").unwrap_or(0.0) as f32,
28        dy: ctx.get_var("dy").unwrap_or(0.0) as f32,
29        warp: ctx.get_var("warp").unwrap_or(0.0) as f32,
30        sx: ctx.get_var("sx").unwrap_or(1.0) as f32,
31        sy: ctx.get_var("sy").unwrap_or(1.0) as f32,
32    };
33
34    // Update wave parameters (full MD2 cluster).
35    let read_flag = |name: &str| ctx.get_var(name).unwrap_or(0.0).abs() > 0.5;
36    slot.state.wave = WaveParams {
37        r: ctx.get_var("wave_r").unwrap_or(1.0) as f32,
38        g: ctx.get_var("wave_g").unwrap_or(1.0) as f32,
39        b: ctx.get_var("wave_b").unwrap_or(1.0) as f32,
40        a: ctx.get_var("wave_a").unwrap_or(1.0) as f32,
41        x: ctx.get_var("wave_x").unwrap_or(0.5) as f32,
42        y: ctx.get_var("wave_y").unwrap_or(0.5) as f32,
43        mode: ctx.get_var("wave_mode").unwrap_or(0.0) as i32,
44        scale: ctx.get_var("wave_scale").unwrap_or(1.0) as f32,
45        param: ctx.get_var("wave_param").unwrap_or(0.0) as f32,
46        smoothing: ctx.get_var("wave_smoothing").unwrap_or(0.0) as f32,
47        thick: read_flag("wave_thick"),
48        dots: read_flag("wave_dots"),
49        additive: read_flag("additivewave"),
50        maximize_color: read_flag("wave_brighten"),
51        mod_alpha_by_volume: read_flag("modwavealphabyvolume"),
52        mod_alpha_start: ctx.get_var("modwavealphastart").unwrap_or(0.75) as f32,
53        mod_alpha_end: ctx.get_var("modwavealphaend").unwrap_or(0.95) as f32,
54        // `split_lr` is a player-side option, not a per-preset
55        // variable. The caller (engine) sets it on the state after
56        // this sync — default `false` so unconfigured engines stay
57        // mono.
58        split_lr: false,
59    };
60
61    // Frame-buffer decay (used by the warp pass fragment shader).
62    slot.state.decay = ctx.get_var("decay").unwrap_or(0.98) as f32;
63
64    // Display-time brightness multiplier (used by the comp pass).
65    slot.state.gamma_adj = ctx.get_var("gamma").unwrap_or(2.0) as f32;
66
67    // Echo blend. `echo_zoom` defaults to 1.0 (passthrough);
68    // `echo_alpha = 0` skips the echo branch in the comp pass entirely.
69    slot.state.echo_zoom = ctx.get_var("echo_zoom").unwrap_or(1.0) as f32;
70    slot.state.echo_alpha = ctx.get_var("echo_alpha").unwrap_or(0.0) as f32;
71    slot.state.echo_orient = ctx
72        .get_var("echo_orient")
73        .or_else(|| ctx.get_var("video_echo_orientation"))
74        .unwrap_or(0.0)
75        .clamp(0.0, 3.0) as u32;
76
77    // Anaglyph stereo split.
78    slot.state.red_blue_stereo = ctx.get_var("red_blue_stereo").unwrap_or(0.0).abs() > 0.5;
79
80    // Mirror q1..q32 into RenderState so the comp pass exposes them as
81    // `uniforms.q[0..7]` to user shaders. Skip the `format!("qN")` +
82    // trait-`get` route: q1..q32 live in MilkContext's array-backed
83    // q_vars, indexable directly.
84    for (i, slot_q) in slot.state.q_snapshot.iter_mut().enumerate() {
85        *slot_q = ctx.q_get_idx(i) as f32;
86    }
87
88    // Feedback filter toggles. Per-frame eqs can flip these (rare but
89    // legal in MD2), so we re-read them every frame instead of caching at
90    // load time.
91    let read_flag = |name: &str| ctx.get_var(name).unwrap_or(0.0).abs() > 0.5;
92    slot.state.feedback = FeedbackParams {
93        wrap: read_flag("wrap"),
94        darken_center: read_flag("darken_center"),
95        invert: read_flag("invert"),
96        brighten: read_flag("brighten"),
97        darken: read_flag("darken"),
98        solarize: read_flag("solarize"),
99    };
100
101    // Borders + motion vectors.
102    let read_f32 =
103        |name: &str, fallback: f32| ctx.get_var(name).map(|v| v as f32).unwrap_or(fallback);
104    slot.state.borders = BorderParams {
105        outer_size: read_f32("ob_size", 0.0),
106        outer_color: [
107            read_f32("ob_r", 0.0),
108            read_f32("ob_g", 0.0),
109            read_f32("ob_b", 0.0),
110            read_f32("ob_a", 0.0),
111        ],
112        inner_size: read_f32("ib_size", 0.0),
113        inner_color: [
114            read_f32("ib_r", 0.0),
115            read_f32("ib_g", 0.0),
116            read_f32("ib_b", 0.0),
117            read_f32("ib_a", 0.0),
118        ],
119    };
120    slot.state.motion_vectors = MotionVectorParams {
121        grid_x: read_f32("nMotionVectorsX", 12.0).max(0.0).round() as u32,
122        grid_y: read_f32("nMotionVectorsY", 9.0).max(0.0).round() as u32,
123        dx: read_f32("mv_dx", 0.0),
124        dy: read_f32("mv_dy", 0.0),
125        length: read_f32("mv_l", 0.9),
126        color: [
127            read_f32("mv_r", 1.0),
128            read_f32("mv_g", 1.0),
129            read_f32("mv_b", 1.0),
130            read_f32("mv_a", 0.0),
131        ],
132    };
133}