Skip to main content

ranim_core/
scene_evaluator.rs

1//! A lightweight session driver for scene evaluation (no ECS).
2//!
3//! The [`SceneEvaluator`] owns the lowered animation cells and exposes ONE
4//! interaction: [`sample_at`](SceneEvaluator::sample_at), a stateful function
5//! of time. Each cell evaluates itself at the target (direction management —
6//! forward, backward reset+replay, equal project — is internal to each stateful
7//! node), so the session needs no clock bookkeeping beyond remembering the last
8//! target.
9
10use crate::{
11    SealedRanimScene, TimeMark,
12    animation::{
13        eval::OutputContext,
14        node::{AnimNode, AnimationInfo},
15    },
16    audio::MASTER_SAMPLE_RATE,
17    frame::EvaluatedFrame,
18};
19
20/// Lightweight scene evaluation session.
21pub struct SceneEvaluator {
22    cells: Vec<AnimNode>,
23    total_secs: f64,
24    audio: std::sync::Arc<[f32]>,
25    time_marks: Vec<(f64, TimeMark)>,
26    clock: f64,
27}
28
29impl SceneEvaluator {
30    /// Consume a sealed scene and create a driving session.
31    ///
32    /// `logic_fps` is retained only for call-site compatibility; it no longer
33    /// drives stepping (each iterative segment owns its `sim_step`).
34    #[allow(unused_variables)]
35    pub fn new(scene: SealedRanimScene, logic_fps: f64) -> Self {
36        Self {
37            cells: scene.animations,
38            total_secs: scene.total_secs,
39            audio: scene.audio,
40            time_marks: scene.time_marks,
41            clock: 0.0,
42        }
43    }
44
45    /// Total scene duration.
46    pub fn total_secs(&self) -> f64 {
47        self.total_secs
48    }
49
50    /// The top-level animation cells.
51    ///
52    /// Crate-internal: the audio tests walk these with a point-semantics
53    /// reference mixer that the seal-time bake must reproduce exactly.
54    #[cfg(test)]
55    pub(crate) fn cells(&self) -> &[AnimNode] {
56        &self.cells
57    }
58
59    /// Whether any sound leaves live in the tree.
60    pub fn has_audio(&self) -> bool {
61        self.cells.iter().any(AnimNode::has_audio)
62    }
63
64    /// The baked audio plane: interleaved stereo at the master sample rate
65    /// over `[0, total_secs]` (shared, cheap to clone). Empty when the scene
66    /// has no sound leaves.
67    pub fn audio(&self) -> &std::sync::Arc<[f32]> {
68        &self.audio
69    }
70
71    /// The scene's audio over `[0, out_secs]` as a fresh interleaved stereo
72    /// buffer.
73    ///
74    /// The audio plane was already mixed once at seal
75    /// ([`RanimScene::seal`](crate::RanimScene::seal)); this is a prefix copy
76    /// of that baked buffer, zero-padded past the scene end.
77    pub fn mix_audio(&self, out_secs: f64, sample_rate: u32) -> Vec<f32> {
78        assert_eq!(
79            sample_rate, MASTER_SAMPLE_RATE,
80            "the baked audio lives at the master sample rate"
81        );
82        let out_frames = (out_secs * sample_rate as f64).ceil() as usize;
83        let baked_frames = self.audio.len() / 2;
84        let copy = out_frames.min(baked_frames);
85        let mut out = Vec::with_capacity(out_frames * 2);
86        out.extend_from_slice(&self.audio[..copy * 2]);
87        out.resize(out_frames * 2, 0.0);
88        out
89    }
90
91    /// Scene time marks.
92    pub fn time_marks(&self) -> &[(f64, TimeMark)] {
93        &self.time_marks
94    }
95
96    /// Hierarchical runtime animation information for preview tooling.
97    pub fn animation_infos(&self) -> Vec<AnimationInfo> {
98        self.cells.iter().map(AnimNode::animation_info).collect()
99    }
100
101    /// Last sampled target (the `clock` reading for preview tooling).
102    pub fn clock(&self) -> f64 {
103        self.clock
104    }
105
106    /// Sample the scene at `render_secs` — the ONLY session interaction.
107    ///
108    /// Every top-level cell evaluates itself at the target (forward/backward
109    /// direction management is internal), and complete leaf outputs carry the
110    /// `(animation_id, part)` identities of `SealedRanimScene::eval_at_sec`.
111    /// Replaces `out` with the new sample while reusing its batch storage.
112    pub fn sample_at(&mut self, render_secs: f64, out: &mut EvaluatedFrame) {
113        out.clear();
114        for (source_id, cell) in self.cells.iter().enumerate() {
115            cell.eval_at(render_secs, &mut OutputContext::new(out, source_id));
116        }
117        self.clock = render_secs;
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    use crate::{
125        RanimScene, SealedRanimScene,
126        animation::{
127            build::{PlaybackExt, Unplaced},
128            eval::{Eval, iterative::Iterative},
129        },
130        seq,
131    };
132
133    #[derive(Clone, Default)]
134    struct PositionX(f32);
135
136    /// A stateless segment: x = alpha.
137    struct ProgressX;
138
139    impl Eval for ProgressX {
140        type Output = PositionX;
141
142        fn eval_alpha(&self, alpha: f64) -> PositionX {
143            PositionX(alpha as f32)
144        }
145    }
146
147    fn xs_of(frame: &EvaluatedFrame) -> Vec<f32> {
148        frame
149            .items::<PositionX>()
150            .iter()
151            .map(|item| item.value.0)
152            .collect()
153    }
154
155    fn cv(v: f64, logical_secs: f64) -> impl Unplaced {
156        Iterative::from_fn(
157            PositionX::default(),
158            move |state: &mut PositionX, _, delta| {
159                state.0 += (v * logical_secs * delta) as f32;
160            },
161        )
162        .with_steps(120)
163    }
164
165    fn assert_x(frame: &EvaluatedFrame, expected: f32) {
166        let values = xs_of(frame);
167        assert_eq!(values.len(), 1);
168        assert!(
169            (values[0] - expected).abs() < 1e-5,
170            "{values:?} vs {expected}"
171        );
172    }
173
174    #[test]
175    fn functional_scene_matches_pure_eval() {
176        let mut scene = RanimScene::new();
177        scene.play(ProgressX.with_duration(2.0));
178        let sealed = scene.seal();
179
180        let mut ev = SceneEvaluator::new(sealed, 120.0);
181        for sec in [0.0, 0.25, 0.5, 1.0, 1.5, 2.0] {
182            let mut frame = EvaluatedFrame::new();
183            ev.sample_at(sec, &mut frame);
184            let expected = (sec / 2.0) as f32;
185            let got = xs_of(&frame);
186            assert_eq!(got, vec![expected], "at sec={sec}");
187        }
188    }
189
190    #[test]
191    fn repeated_samples_replace_the_whole_output_frame() {
192        let mut scene = RanimScene::new();
193        scene.play(ProgressX.with_duration(2.0));
194        let mut ev = SceneEvaluator::new(scene.seal(), 120.0);
195        let mut frame = EvaluatedFrame::new();
196        frame.push((99, 0), String::from("previous output"));
197
198        for sec in [0.5, 1.5, 0.25, 1.5] {
199            ev.sample_at(sec, &mut frame);
200
201            assert_eq!(frame.len(), 1);
202            assert!(frame.items::<String>().is_empty());
203            let items = frame.items::<PositionX>();
204            assert_eq!(items[0].id, (0, 0));
205            assert_eq!(items[0].order, 0);
206            assert_x(&frame, (sec / 2.0) as f32);
207        }
208    }
209
210    #[test]
211    fn ordinary_samples_move_complete_outputs_and_holds_replay_original_types() {
212        use crate::animation::{compose::sequence::AnimSequence, eval::pure::Pure};
213        use std::sync::{
214            Arc,
215            atomic::{AtomicUsize, Ordering},
216        };
217
218        struct Raw {
219            values: Vec<u32>,
220            clones: Arc<AtomicUsize>,
221        }
222        impl Clone for Raw {
223            fn clone(&self) -> Self {
224                self.clones.fetch_add(1, Ordering::SeqCst);
225                Self {
226                    values: self.values.clone(),
227                    clones: Arc::clone(&self.clones),
228                }
229            }
230        }
231
232        let clones = Arc::new(AtomicUsize::new(0));
233        let animation = {
234            let clones = Arc::clone(&clones);
235            Pure::new(move |_| Raw {
236                values: vec![42, 43],
237                clones: Arc::clone(&clones),
238            })
239        };
240        let mut sequence = AnimSequence::new();
241        sequence.push(animation.with_duration(1.0)).hold(1.0);
242        assert_eq!(clones.load(Ordering::SeqCst), 0);
243        let mut scene = RanimScene::new();
244        scene.play(sequence);
245        let mut evaluator = scene.seal().into_evaluator(120.0);
246        let mut frame = EvaluatedFrame::new();
247
248        evaluator.sample_at(0.5, &mut frame);
249        assert_eq!(clones.load(Ordering::SeqCst), 0);
250        assert_eq!(frame.items::<Raw>()[0].value.values, [42, 43]);
251        evaluator.sample_at(1.5, &mut frame);
252        assert_eq!(clones.load(Ordering::SeqCst), 1);
253        assert_eq!(frame.items::<Raw>()[0].id, (0, 0));
254        assert_eq!(frame.items::<Raw>()[0].value.values, [42, 43]);
255    }
256
257    #[test]
258    fn repeated_holds_preserve_heterogeneous_container_and_transform_outputs() {
259        use crate::{
260            animation::{compose::sequence::AnimSequence, eval::Static},
261            glam::DVec3,
262            stack,
263            traits::Translation,
264            transformed::Transformed,
265        };
266
267        type Wrapped = Transformed<Vec<(u32, String)>, Translation>;
268        let wrapped = Wrapped::new(vec![(7, String::from("nested"))], Translation(DVec3::X));
269        let mut sequence = AnimSequence::new();
270        sequence
271            .push(stack![
272                Static(vec![10_u32, 20]).with_duration(1.0),
273                Static(wrapped.clone()).with_duration(1.0),
274            ])
275            .hold(1.0)
276            .hold(1.0);
277        let mut scene = RanimScene::new();
278        scene.play(sequence);
279        let mut evaluator = scene.seal().into_evaluator(120.0);
280        let mut frame = EvaluatedFrame::new();
281
282        for sec in [0.5, 1.5, 2.5] {
283            evaluator.sample_at(sec, &mut frame);
284            assert_eq!(frame.len(), 2);
285            let vector = &frame.items::<Vec<u32>>()[0];
286            assert_eq!(vector.value, [10, 20]);
287            assert_eq!((vector.id, vector.order), ((0, 0), 0));
288            let item = &frame.items::<Wrapped>()[0];
289            assert_eq!(item.value, wrapped);
290            assert_eq!((item.id, item.order), ((0, 1), 1));
291            assert!(frame.items::<u32>().is_empty());
292            assert!(frame.items::<(u32, String)>().is_empty());
293        }
294    }
295
296    #[test]
297    fn iterative_leaves_step_along_the_logic_grid() {
298        let mut scene = RanimScene::new();
299        scene.play(cv(1.0, 2.0).with_duration(2.0).at(0.0));
300        let mut ev = SceneEvaluator::new(scene.seal(), 120.0);
301        for sec in [0.0, 0.5, 1.0, 1.5, 2.0] {
302            let mut frame = EvaluatedFrame::new();
303            ev.sample_at(sec, &mut frame);
304            assert_x(&frame, sec as f32);
305        }
306
307        // Nested inside a sequence the leaf still steps on its own timeline.
308        let mut scene = RanimScene::new();
309        scene.play(
310            seq![
311                cv(1.0, 1.0).with_duration(1.0),
312                cv(1.0, 1.0).with_duration(1.0)
313            ]
314            .at(0.0),
315        );
316        let mut ev = SceneEvaluator::new(scene.seal(), 120.0);
317        for (sec, expected) in [(1.5, 0.5), (2.0, 1.0)] {
318            let mut frame = EvaluatedFrame::new();
319            ev.sample_at(sec, &mut frame);
320            assert_x(&frame, expected);
321        }
322    }
323
324    #[test]
325    fn iterative_seek_matches_forward_and_resets_nested_leaves() {
326        fn run(scene: SealedRanimScene, backward: bool) -> Vec<Vec<f32>> {
327            let mut ev = SceneEvaluator::new(scene, 120.0);
328            let mut trace = Vec::new();
329            for sec in [0.3, 0.7, 1.1, 1.9, 2.6] {
330                if backward {
331                    // Jump backwards below the first sample so every leaf
332                    // has to re-simulate from the start.
333                    ev.sample_at(0.2, &mut EvaluatedFrame::new());
334                }
335                let mut frame = EvaluatedFrame::new();
336                ev.sample_at(sec, &mut frame);
337                trace.push(xs_of(&frame));
338            }
339            trace
340        }
341
342        let single = || {
343            let mut scene = RanimScene::new();
344            scene.play(cv(2.0, 3.0).with_duration(3.0).at(0.0));
345            scene.seal()
346        };
347        let forward = run(single(), false);
348        assert_eq!(forward, run(single(), true));
349        assert!((forward[2][0] - 2.2).abs() < 1e-5);
350
351        let nested = || {
352            let mut scene = RanimScene::new();
353            scene.play(
354                seq![
355                    cv(1.0, 1.0).with_duration(1.0),
356                    cv(1.0, 1.0).with_duration(1.0)
357                ]
358                .at(0.0),
359            );
360            scene.seal()
361        };
362        assert_eq!(run(nested(), false), run(nested(), true));
363    }
364}