onedrop_engine/engine/
preset_slot.rs

1//! Per-preset state bundle.
2//!
3//! Groups everything that varies between presets — the preset itself, the
4//! evaluator carrying its variable context, the pre-compiled per-vertex
5//! executor, the per-wave / per-shape compiled equations, and the
6//! [`RenderState`] their per-frame evaluation produces.
7//!
8//! `MilkEngine` holds one `PresetSlot` for the active preset plus an
9//! `Option<PresetSlot>` for the outgoing preset that keeps evaluating on
10//! the renderer's secondary chain during a crossfade.
11
12use crate::warp_eval::WarpExecutor;
13use onedrop_eval::{CompiledBlock, CompiledBytecode, MilkEvaluator, Node, PerPointParallelism};
14use onedrop_parser::MilkPreset;
15use onedrop_renderer::RenderState;
16use std::collections::hash_map::DefaultHasher;
17use std::hash::{Hash, Hasher};
18
19/// Pre-compiled equation Nodes for one `wavecode_N` block. We compile
20/// each phase exactly once per `load_preset`; per-frame and per-point
21/// evaluation just runs the cached `Node`s, skipping the regex-based
22/// preprocess that [`MilkEvaluator::eval`] performs every call. This is
23/// load-bearing for performance: a single 512-sample wave with 5
24/// per-point statements runs the loop 2 560 times per frame.
25///
26/// `init` and `per_frame` are wrapped in [`CompiledBlock`] so they
27/// transparently pick the bytecode VM when the block lowers cleanly
28/// (~80 % of corpus presets) and fall back to evalexpr nodes otherwise.
29/// `per_point` keeps its raw [`Vec<Node>`] + separate
30/// [`Option<CompiledBytecode>`] because the parallel-samples branch in
31/// `compute_one_wave` needs them addressable individually (carry-safe
32/// presets fan samples out across rayon workers, with each worker
33/// running either path).
34#[derive(Debug)]
35pub(crate) struct CompiledWave {
36    pub(crate) init: CompiledBlock,
37    pub(crate) per_frame: CompiledBlock,
38    pub(crate) per_point: Vec<Node>,
39    /// Cached carry-analysis verdict — `Safe` means samples can be
40    /// evaluated in parallel via rayon (no cross-sample state),
41    /// `Sequential` means we must thread the previous sample's output
42    /// into the next call (MD2 default semantics).
43    pub(crate) per_point_parallelism: PerPointParallelism,
44    /// Bytecode-VM compilation of `per_point`. `Some` when every node
45    /// in the block lowered cleanly; `None` when at least one operator
46    /// or function falls outside the VM's supported set (rand,
47    /// gmegabuf, loop, …) — in that case `compute_one_wave` falls back
48    /// to the evalexpr `Node` path so behaviour is preserved.
49    pub(crate) per_point_bytecode: Option<CompiledBytecode>,
50}
51
52impl Default for CompiledWave {
53    fn default() -> Self {
54        Self {
55            init: CompiledBlock::empty(),
56            per_frame: CompiledBlock::empty(),
57            per_point: Vec::new(),
58            per_point_parallelism: PerPointParallelism::Sequential,
59            per_point_bytecode: None,
60        }
61    }
62}
63
64/// Pre-compiled equations for one `shapecode_N` block. Same rationale as
65/// `CompiledWave` — shapes can run their per-frame block up to 1024
66/// times per shape per frame, so caching the compiled form is
67/// non-optional. Both phases use [`CompiledBlock`] so the bytecode VM
68/// kicks in transparently when the block fully lowers.
69#[derive(Debug, Default)]
70pub(crate) struct CompiledShape {
71    pub(crate) init: CompiledBlock,
72    pub(crate) per_frame: CompiledBlock,
73}
74
75/// Everything a single preset needs to render itself: the parsed `.milk`
76/// data, an evaluator context owning its variables, the per-vertex executor
77/// pre-compiled against the preset's `per_pixel_equations`, the per-wave /
78/// per-shape compiled equation lists, and the render state the per-frame
79/// loop writes into.
80///
81/// A `PresetSlot::default()` carries an empty evaluator + no preset, which
82/// matches the pre-V.2 "no preset loaded" behaviour: per-frame and
83/// per-vertex evaluation are no-ops but the renderer still receives a
84/// valid (default) `RenderState`.
85pub(crate) struct PresetSlot {
86    pub(crate) preset: Option<MilkPreset>,
87    pub(crate) evaluator: MilkEvaluator,
88    pub(crate) warp_executor: WarpExecutor,
89    pub(crate) state: RenderState,
90    /// Global `state.time` captured at the moment this preset was
91    /// loaded — used to derive MD2's `progress` (`[0, 1]` position
92    /// within the preset's display window). Global `state.time`
93    /// itself carries across preset changes for continuity, so we
94    /// need a separate anchor to recover per-preset elapsed time.
95    pub(crate) preset_loaded_time: f32,
96    pub(crate) compiled_waves: Vec<CompiledWave>,
97    pub(crate) waves_need_init: Vec<bool>,
98    pub(crate) compiled_shapes: Vec<CompiledShape>,
99    pub(crate) shapes_need_init: Vec<bool>,
100    /// Pre-compiled main-preset `per_frame` equations. Populated at
101    /// `load_preset` so the per-frame loop in
102    /// [`super::tick_slot_evaluator`] skips the regex-based reparse
103    /// that the legacy [`MilkEvaluator::eval_per_frame`] path runs on
104    /// every call. When the bytecode VM lowers cleanly (the common
105    /// case), `per_frame` eval becomes a flat opcode loop too.
106    pub(crate) compiled_per_frame: CompiledBlock,
107    /// Pre-compiled main-preset `per_frame_init` equations. Run once
108    /// at preset load (mirroring the wave/shape `init` semantics) and
109    /// then never again. Stored only so we can run them at the right
110    /// moment without re-parsing the source.
111    pub(crate) compiled_per_frame_init: CompiledBlock,
112}
113
114impl Default for PresetSlot {
115    fn default() -> Self {
116        Self {
117            preset: None,
118            evaluator: MilkEvaluator::new(),
119            warp_executor: WarpExecutor::new(),
120            state: RenderState::default(),
121            preset_loaded_time: 0.0,
122            compiled_waves: Vec::new(),
123            waves_need_init: Vec::new(),
124            compiled_shapes: Vec::new(),
125            shapes_need_init: Vec::new(),
126            compiled_per_frame: CompiledBlock::empty(),
127            compiled_per_frame_init: CompiledBlock::empty(),
128        }
129    }
130}
131
132/// Run a [`CompiledBlock`] against `eval`, calling `on_err(node_index,
133/// err)` for each evalexpr-fallback node that fails. Centralises the
134/// "prefer bytecode (infallible) / log per-equation failures on the
135/// Nodes path" pattern shared by `wave_phase`, `shape_phase` and the
136/// per-frame eval in [`MilkEngine::update`].
137pub(crate) fn run_block_with_logger<F>(
138    eval: &mut MilkEvaluator,
139    block: &CompiledBlock,
140    mut on_err: F,
141) where
142    F: FnMut(usize, onedrop_eval::EvalError),
143{
144    if let Some(bc) = block.bytecode() {
145        bc.run(eval.context_mut());
146    } else {
147        for (i, node) in block.nodes().iter().enumerate() {
148            if let Err(e) = eval.eval_compiled(node) {
149                on_err(i, e);
150            }
151        }
152    }
153}
154
155/// Stable preset id used to seed `sampler_rand0X` resolution. Derived from
156/// the preset's comp shader text (when present) — identical shaders share
157/// the same rand pick; distinct ones diverge. Falls back to a hash of the
158/// per-frame equations when the comp shader is absent.
159pub(crate) fn preset_id_for(preset: &MilkPreset) -> String {
160    let mut hasher = DefaultHasher::new();
161    if let Some(comp) = &preset.comp_shader {
162        comp.hash(&mut hasher);
163    } else {
164        for eq in &preset.per_frame_equations {
165            eq.hash(&mut hasher);
166        }
167    }
168    format!("preset-{:016x}", hasher.finish())
169}