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;
17
18pub(crate) struct Runtime {
19    pub fps: f64,
20    pub frame: Cell<u32>,
21    /// Virtual time of the task being polled. It can lag behind the frame
22    /// time by a fraction of a frame: when a tween ends between frames, the
23    /// next tween starts at the exact end time, so timings do not drift.
24    pub task_time: Cell<f64>,
25    pub spawned: RefCell<Vec<Task>>,
26    pub meta: RefCell<Meta>,
27    pub meta_changed: Cell<bool>,
28    pub rng: RefCell<ChaCha8Rng>,
29}
30
31impl Runtime {
32    pub fn new(fps: f64, meta: Meta, seed: u64) -> Rc<Self> {
33        assert!(fps > 0.0 && fps.is_finite(), "fps must be positive");
34        Rc::new(Self {
35            fps,
36            frame: Cell::new(0),
37            task_time: Cell::new(0.0),
38            spawned: RefCell::new(Vec::new()),
39            meta: RefCell::new(meta),
40            meta_changed: Cell::new(false),
41            rng: RefCell::new(ChaCha8Rng::seed_from_u64(seed)),
42        })
43    }
44
45    pub fn now(&self) -> f64 {
46        f64::from(self.frame.get()) / self.fps
47    }
48}
49
50thread_local! {
51    static CURRENT: RefCell<Option<Rc<Runtime>>> = const { RefCell::new(None) };
52}
53
54/// Restores the previously installed runtime on drop.
55pub(crate) struct Installed(Option<Rc<Runtime>>);
56
57pub(crate) fn install(rt: &Rc<Runtime>) -> Installed {
58    Installed(CURRENT.with(|c| c.borrow_mut().replace(rt.clone())))
59}
60
61impl Drop for Installed {
62    fn drop(&mut self) {
63        let prev = self.0.take();
64        CURRENT.with(|c| *c.borrow_mut() = prev);
65    }
66}
67
68pub(crate) fn with<R>(f: impl FnOnce(&Runtime) -> R) -> R {
69    CURRENT.with(|c| {
70        let c = c.borrow();
71        let rt = c
72            .as_ref()
73            .expect("oxframe: this function can only be called while a scene is running");
74        f(rt)
75    })
76}
77
78/// Index of the frame being computed.
79pub fn frame() -> u32 {
80    with(|rt| rt.frame.get())
81}
82
83/// Frames per second of the running project.
84pub fn fps() -> f64 {
85    with(|rt| rt.fps)
86}
87
88/// Duration of one frame in seconds, `1 / fps`.
89pub fn dt() -> f64 {
90    with(|rt| 1.0 / rt.fps)
91}
92
93/// Time of the current frame, `frame / fps` seconds.
94pub fn now() -> f64 {
95    with(|rt| rt.now())
96}
97
98/// Virtual time of the current task — where the next animation starts.
99pub fn time() -> f64 {
100    with(|rt| rt.task_time.get())
101}
102
103/// Value of tunable `name` from `meta.json`; `default` is recorded there on
104/// first use. Use through the `tune!` macro.
105pub fn tunable(name: &str, default: f64) -> f64 {
106    with(|rt| {
107        let mut meta = rt.meta.borrow_mut();
108        if let Some(v) = meta.tunables.get(name) {
109            return *v;
110        }
111        meta.tunables.insert(name.to_owned(), default);
112        rt.meta_changed.set(true);
113        default
114    })
115}
116
117/// Named easing from `meta.json`; `default` is recorded there on first use.
118/// Use through the `ease!` macro.
119pub fn ease_named(name: &str, default: Option<CubicBezier>) -> CubicBezier {
120    with(|rt| {
121        let mut meta = rt.meta.borrow_mut();
122        if let Some(v) = meta.eases.get(name) {
123            return CubicBezier::from_array(*v);
124        }
125        let default = default.unwrap_or(presets::DEFAULT);
126        meta.eases.insert(name.to_owned(), default.to_array());
127        rt.meta_changed.set(true);
128        default
129    })
130}
131
132/// Marker time from `meta.json`; the current task time is recorded on first
133/// encounter.
134pub(crate) fn marker(name: &str) -> f64 {
135    with(|rt| {
136        let mut meta = rt.meta.borrow_mut();
137        if let Some(v) = meta.markers.get(name) {
138            return *v;
139        }
140        let t = rt.task_time.get();
141        meta.markers.insert(name.to_owned(), t);
142        rt.meta_changed.set(true);
143        t
144    })
145}
146
147/// Copy of the running scene's markers, eases and tunables.
148pub fn meta_snapshot() -> Meta {
149    with(|rt| rt.meta.borrow().clone())
150}
151
152/// Runs `f` with the scene's random generator. It is seeded from the
153/// project seed and the scene name, so results are reproducible; never use
154/// `rand::rng()` or other entropy sources in scenes.
155pub fn with_rng<R>(f: impl FnOnce(&mut ChaCha8Rng) -> R) -> R {
156    with(|rt| f(&mut rt.rng.borrow_mut()))
157}
158
159/// Uniform random number in `[0, 1)` from the scene's seeded generator.
160pub fn random() -> f64 {
161    use rand::RngExt;
162    with_rng(|r| r.random::<f64>())
163}
164
165/// Uniform random number in `[min, max)` from the scene's seeded generator.
166pub fn random_range(min: f64, max: f64) -> f64 {
167    min + (max - min) * random()
168}