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}