Skip to main content

oxframe_core/
runtime.rs

1//! Per-thread context of the scene being executed.
2//!
3//! The [`Executor`](crate::Executor) installs its runtime for the duration
4//! of each step, so free functions like [`now`] and [`wait_until`]
5//! (crate::wait_until) work inside scene code without passing handles.
6
7use std::cell::{Cell, RefCell};
8use std::rc::Rc;
9
10use oxframe_protocol::Meta;
11use rand::SeedableRng;
12use rand_chacha::ChaCha8Rng;
13
14use crate::CubicBezier;
15use crate::ease::presets;
16use crate::flow::Task;
17use crate::transition::Recorded;
18
19pub(crate) struct Runtime {
20    pub fps: f64,
21    pub frame: Cell<u32>,
22    /// Virtual time of the task being polled. It can lag behind the frame
23    /// time by a fraction of a frame: when a tween ends between frames, the
24    /// next tween starts at the exact end time, so timings do not drift.
25    pub task_time: Cell<f64>,
26    pub spawned: RefCell<Vec<Task>>,
27    pub meta: RefCell<Meta>,
28    pub meta_changed: Cell<bool>,
29    pub rng: RefCell<ChaCha8Rng>,
30    /// Set by the engine: this scene has one before it, so a transition blends.
31    pub has_previous: Cell<bool>,
32    /// The transition declared by the scene, if it starts one.
33    pub transition: RefCell<Option<Recorded>>,
34    /// Sound effects scheduled by `sound()`, in scene time.
35    pub sounds: RefCell<Vec<crate::SoundClip>>,
36    /// Spoken lines scheduled by `voice()`, in scene time.
37    pub voices: RefCell<Vec<crate::VoiceLine>>,
38    /// Stops and notes for `oxframe present`, in scene time.
39    pub slides: RefCell<Vec<crate::SlideMark>>,
40}
41
42impl Runtime {
43    pub fn new(fps: f64, meta: Meta, seed: u64) -> Rc<Self> {
44        assert!(fps > 0.0 && fps.is_finite(), "fps must be positive");
45        Rc::new(Self {
46            fps,
47            frame: Cell::new(0),
48            task_time: Cell::new(0.0),
49            spawned: RefCell::new(Vec::new()),
50            meta: RefCell::new(meta),
51            meta_changed: Cell::new(false),
52            rng: RefCell::new(ChaCha8Rng::seed_from_u64(seed)),
53            has_previous: Cell::new(false),
54            transition: RefCell::new(None),
55            sounds: RefCell::new(Vec::new()),
56            voices: RefCell::new(Vec::new()),
57            slides: RefCell::new(Vec::new()),
58        })
59    }
60
61    pub fn now(&self) -> f64 {
62        f64::from(self.frame.get()) / self.fps
63    }
64}
65
66thread_local! {
67    static CURRENT: RefCell<Option<Rc<Runtime>>> = const { RefCell::new(None) };
68}
69
70/// Restores the previously installed runtime on drop.
71pub(crate) struct Installed(Option<Rc<Runtime>>);
72
73pub(crate) fn install(rt: &Rc<Runtime>) -> Installed {
74    Installed(CURRENT.with(|c| c.borrow_mut().replace(rt.clone())))
75}
76
77impl Drop for Installed {
78    fn drop(&mut self) {
79        let prev = self.0.take();
80        CURRENT.with(|c| *c.borrow_mut() = prev);
81    }
82}
83
84pub(crate) fn with<R>(f: impl FnOnce(&Runtime) -> R) -> R {
85    CURRENT.with(|c| {
86        let c = c.borrow();
87        let rt = c
88            .as_ref()
89            .expect("oxframe: this function can only be called while a scene is running");
90        f(rt)
91    })
92}
93
94/// Index of the frame being computed.
95pub fn frame() -> u32 {
96    with(|rt| rt.frame.get())
97}
98
99/// Frames per second of the running project.
100pub fn fps() -> f64 {
101    with(|rt| rt.fps)
102}
103
104/// Duration of one frame in seconds, `1 / fps`.
105pub fn dt() -> f64 {
106    with(|rt| 1.0 / rt.fps)
107}
108
109/// Time of the current frame, `frame / fps` seconds.
110pub fn now() -> f64 {
111    with(|rt| rt.now())
112}
113
114/// Whether the running scene has a scene before it.
115pub fn has_previous() -> bool {
116    with(|rt| rt.has_previous.get())
117}
118
119/// Records `spec`, letting `meta.json` override its duration the way markers
120/// do. Returns the duration to wait. Panics if the scene already started one.
121pub fn begin_transition(mut spec: Recorded) -> f64 {
122    with(|rt| {
123        if rt.transition.borrow().is_some() {
124            panic!("oxframe: a scene can declare only one transition; call it once, at the start");
125        }
126        let stored = rt.meta.borrow().transition.as_ref().map(|t| t.seconds);
127        if let Some(seconds) = stored {
128            spec.seconds = if seconds.is_finite() {
129                seconds.max(0.0)
130            } else {
131                0.0
132            };
133        } else if spec.seconds.is_finite() && spec.seconds > 0.0 {
134            let seconds = spec.seconds;
135            rt.meta.borrow_mut().transition = Some(oxframe_protocol::TransitionMeta { seconds });
136            rt.meta_changed.set(true);
137        } else {
138            spec.seconds = 0.0;
139        }
140        let seconds = spec.seconds;
141        if seconds > 0.0 {
142            *rt.transition.borrow_mut() = Some(spec);
143        }
144        seconds
145    })
146}
147
148/// Virtual time of the current task — where the next animation starts.
149pub fn time() -> f64 {
150    with(|rt| rt.task_time.get())
151}
152
153/// Value of tunable `name` from `meta.json`; `default` is recorded there on
154/// first use. Use through the `tune!` macro.
155pub fn tunable(name: &str, default: f64) -> f64 {
156    with(|rt| {
157        let mut meta = rt.meta.borrow_mut();
158        if let Some(v) = meta.tunables.get(name) {
159            return *v;
160        }
161        meta.tunables.insert(name.to_owned(), default);
162        rt.meta_changed.set(true);
163        default
164    })
165}
166
167/// Named easing from `meta.json`; `default` is recorded there on first use.
168/// Use through the `ease!` macro.
169pub fn ease_named(name: &str, default: Option<CubicBezier>) -> CubicBezier {
170    with(|rt| {
171        let mut meta = rt.meta.borrow_mut();
172        if let Some(v) = meta.eases.get(name) {
173            return CubicBezier::from_array(*v);
174        }
175        let default = default.unwrap_or(presets::DEFAULT);
176        meta.eases.insert(name.to_owned(), default.to_array());
177        rt.meta_changed.set(true);
178        default
179    })
180}
181
182/// Marker time from `meta.json`; the current task time is recorded on first
183/// encounter.
184pub(crate) fn marker(name: &str) -> f64 {
185    with(|rt| {
186        let mut meta = rt.meta.borrow_mut();
187        if let Some(v) = meta.markers.get(name) {
188            return *v;
189        }
190        let t = rt.task_time.get();
191        meta.markers.insert(name.to_owned(), t);
192        rt.meta_changed.set(true);
193        t
194    })
195}
196
197/// Copy of the running scene's markers, eases and tunables.
198pub fn meta_snapshot() -> Meta {
199    with(|rt| rt.meta.borrow().clone())
200}
201
202/// Runs `f` with the scene's random generator. It is seeded from the
203/// project seed and the scene name, so results are reproducible; never use
204/// `rand::rng()` or other entropy sources in scenes.
205pub fn with_rng<R>(f: impl FnOnce(&mut ChaCha8Rng) -> R) -> R {
206    with(|rt| f(&mut rt.rng.borrow_mut()))
207}
208
209/// Uniform random number in `[0, 1)` from the scene's seeded generator.
210pub fn random() -> f64 {
211    use rand::RngExt;
212    with_rng(|r| r.random::<f64>())
213}
214
215/// Uniform random number in `[min, max)` from the scene's seeded generator.
216pub fn random_range(min: f64, max: f64) -> f64 {
217    min + (max - min) * random()
218}
219
220/// A generator seeded by the caller. Drawing with this does not advance the
221/// scene's [`random`] generator, and the same seed always yields the same
222/// sequence, including after a seek.
223#[derive(Clone)]
224pub struct SeededRng(ChaCha8Rng);
225
226impl SeededRng {
227    /// Generator for `seed`. The same seed is the same sequence.
228    pub fn new(seed: u64) -> Self {
229        Self(ChaCha8Rng::seed_from_u64(seed))
230    }
231
232    /// Uniform number in `[0, 1)`.
233    pub fn next_f64(&mut self) -> f64 {
234        use rand::RngExt;
235        self.0.random::<f64>()
236    }
237
238    /// Uniform number in `span` (`end` is excluded).
239    pub fn range(&mut self, span: std::ops::Range<f64>) -> f64 {
240        let w = span.end - span.start;
241        span.start + w * self.next_f64()
242    }
243}