Skip to main content

euv_engine/scheduler/
impl.rs

1use super::*;
2
3/// Implements default configuration and state initialization for scheduler types.
4impl Default for SchedulerConfig {
5    /// Constructs a default [`SchedulerConfig`] value.
6    ///
7    /// # Returns
8    ///
9    /// - `SchedulerConfig` - A default-constructed instance with the documented initial state.
10    fn default() -> SchedulerConfig {
11        SchedulerConfig::new(DEFAULT_FIXED_TIMESTEP, DEFAULT_MAX_FRAME_TIME)
12    }
13}
14
15/// Implements `Default` for `SchedulerState` as a freshly created stopped state.
16impl Default for SchedulerState {
17    /// Constructs a default [`SchedulerState`] value.
18    ///
19    /// # Returns
20    ///
21    /// - `SchedulerState` - A default-constructed instance with the documented initial state.
22    fn default() -> SchedulerState {
23        SchedulerState::new(UNINITIALIZED_TIME)
24    }
25}
26
27/// Implements time retrieval and tick execution for `SchedulerState`.
28impl SchedulerState {
29    /// Returns the current high-resolution timestamp in seconds from `performance.now()`.
30    ///
31    /// Falls back to `0.0` when the global window or the `performance.now`
32    /// API is unavailable (for example outside a browser window context).
33    ///
34    /// The `performance` object and its `now` `Function` are page-lifetime
35    /// globals, so they are cached in a thread-local on first use — the
36    /// per-frame cost is one `call0` crossing, not two `Reflect::get` +
37    /// two `JsValue::from_str` allocations.
38    ///
39    /// # Returns
40    ///
41    /// - `f64` - The current time in seconds, or `0.0` when unavailable.
42    pub fn current_time() -> f64 {
43        thread_local! {
44            static PERFORMANCE_NOW: RefCell<Option<(JsValue, Function)>> =
45                const { RefCell::new(None) };
46        }
47        PERFORMANCE_NOW.with(|cell: &RefCell<Option<(JsValue, Function)>>| {
48            let mut borrow: std::cell::RefMut<'_, Option<(JsValue, Function)>> = cell.borrow_mut();
49            if borrow.is_none() {
50                let Some(window_value) = window() else {
51                    return 0.0;
52                };
53                let Ok(performance) = Reflect::get(
54                    window_value.as_ref(),
55                    &JsValue::from_str(PERFORMANCE_OBJECT),
56                ) else {
57                    return 0.0;
58                };
59                let Ok(now_method) =
60                    Reflect::get(&performance, &JsValue::from_str(PERFORMANCE_NOW_METHOD))
61                else {
62                    return 0.0;
63                };
64                *borrow = Some((performance, now_method.unchecked_into()));
65            }
66            let Some((performance, now_function)) = borrow.as_ref() else {
67                return 0.0;
68            };
69            now_function
70                .call0(performance)
71                .ok()
72                .and_then(|v: JsValue| v.as_f64())
73                .map(|millis: f64| millis / 1000.0)
74                .unwrap_or(0.0)
75        })
76    }
77
78    /// Performs one tick of the fixed-timestep scheduler.
79    ///
80    /// Calculates the elapsed frame time, clamps it to `max_frame_time`, accumulates it,
81    /// then runs as many fixed updates as needed. Finally, computes the interpolation
82    /// factor and calls the render callback.
83    ///
84    /// # Arguments
85    ///
86    /// - `&SchedulerConfig` - The scheduler configuration.
87    /// - `&TickHandlerRc` - The handler receiving update and render callbacks.
88    pub fn tick(&mut self, config: &SchedulerConfig, handler: &TickHandlerRc) {
89        let current_time: f64 = Self::current_time();
90        let frame_time: f64 = if self.get_last_time() == UNINITIALIZED_TIME {
91            config.get_fixed_timestep()
92        } else {
93            current_time - self.get_last_time()
94        };
95        self.set_last_time(current_time);
96        let clamped_frame_time: f64 = frame_time.min(config.get_max_frame_time());
97        *self.get_mut_accumulator() += clamped_frame_time;
98        while self.get_accumulator() >= config.get_fixed_timestep() {
99            handler.get_mut().on_update(config.get_fixed_timestep());
100            *self.get_mut_accumulator() -= config.get_fixed_timestep();
101            *self.get_mut_update_count() += 1;
102        }
103        let interpolation: f64 = self.get_accumulator() / config.get_fixed_timestep();
104        handler.get_mut().on_render(interpolation);
105        *self.get_mut_frame_count() += 1;
106    }
107}
108
109/// Implements lifecycle management for `SchedulerHandle`.
110impl SchedulerHandle {
111    /// Stops the scheduler and cancels any pending animation frame request.
112    pub fn stop(&self) {
113        let state: &mut SchedulerState = self.get_state().get_mut();
114        state.set_running(false);
115        if let Some(id) = state.get_mut_raf_id().take() {
116            let Some(window_value) = window() else {
117                // Drop the closure so the box can be collected.
118                let _ = self.get_closure_cell().try_take();
119                return;
120            };
121            let _: Result<(), JsValue> = window_value.cancel_animation_frame(id);
122        }
123        // Drop the closure so the box can be collected.
124        let _ = self.get_closure_cell().try_take();
125    }
126
127    /// Returns whether the scheduler is currently running.
128    ///
129    /// # Returns
130    ///
131    /// - `bool` - True if the scheduler is running.
132    pub fn is_running(&self) -> bool {
133        // SAFETY: caller contract - no mutable access to the same
134        // SchedulerState can be alive alongside this call.
135        self.get_state().get().get_running()
136    }
137
138    /// Returns the total number of fixed update steps executed.
139    ///
140    /// # Returns
141    ///
142    /// - `u64` - The update count.
143    pub fn update_count(&self) -> u64 {
144        self.get_state().get().get_update_count()
145    }
146
147    /// Returns the total number of render frames executed.
148    ///
149    /// # Returns
150    ///
151    /// - `u64` - The frame count.
152    pub fn frame_count(&self) -> u64 {
153        self.get_state().get().get_frame_count()
154    }
155
156    /// Starts the scheduler with the given configuration and handler.
157    ///
158    /// Creates a `requestAnimationFrame`-driven loop that calls `tick`
159    /// on each animation frame. The returned `SchedulerHandle` can be used to stop the scheduler.
160    ///
161    /// When no global window exists (non-browser context), the scheduler is
162    /// not started and an already-stopped handle is returned instead.
163    ///
164    /// # Arguments
165    ///
166    /// - `SchedulerConfig` - The scheduler configuration.
167    /// - `TickHandlerRc` - The handler receiving update and render callbacks.
168    ///
169    /// # Returns
170    ///
171    /// - `SchedulerHandle` - A handle to control the running scheduler.
172    pub fn start(config: SchedulerConfig, handler: TickHandlerRc) -> SchedulerHandle {
173        let state: Rc<EngineCell<SchedulerState>> =
174            Rc::new(EngineCell::new(SchedulerState::new(UNINITIALIZED_TIME)));
175        let closure_cell: RafClosureCell = Rc::new(MaybeEngineCell::new());
176        // Install the initial closure once spawn starts.
177        let state_ref_init: &mut SchedulerState = state.get_mut();
178        state_ref_init.set_running(true);
179        let state_clone: Rc<EngineCell<SchedulerState>> = state.clone();
180        let closure_cell_clone: RafClosureCell = closure_cell.clone();
181        let handler_clone: TickHandlerRc = handler.clone();
182        let raf_closure: Closure<dyn FnMut()> = Closure::wrap(Box::new(move || {
183            {
184                let state_ref: &mut SchedulerState = state_clone.get_mut();
185                if !state_ref.get_running() {
186                    return;
187                }
188                state_ref.tick(&config, &handler_clone);
189            }
190            let state_ro: &SchedulerState = state_clone.get();
191            if state_ro.get_running() {
192                let Some(window_value) = window() else {
193                    return;
194                };
195                let cell: RafClosureCell = closure_cell_clone.clone();
196                let Some(raf_closure) = cell.try_get() else {
197                    return;
198                };
199                let id: i32 = window_value
200                    .request_animation_frame(raf_closure.as_ref().unchecked_ref())
201                    .unwrap_or_default();
202                let state_ref_id: &mut SchedulerState = state_clone.get_mut();
203                state_ref_id.set_raf_id(Some(id));
204            }
205        }));
206        let Some(window_value) = window() else {
207            // No window context: install the closure, mark the scheduler
208            // stopped, and return an inert handle.
209            let state_ref_stop: &mut SchedulerState = state.get_mut();
210            state_ref_stop.set_running(false);
211            let _ = closure_cell.try_set(raf_closure);
212            return SchedulerHandle::new(state, closure_cell);
213        };
214        let id: i32 = window_value
215            .request_animation_frame(raf_closure.as_ref().unchecked_ref())
216            .unwrap_or_default();
217        let state_ref_id: &mut SchedulerState = state.get_mut();
218        state_ref_id.set_raf_id(Some(id));
219        let _ = closure_cell.try_set(raf_closure);
220        SchedulerHandle::new(state, closure_cell)
221    }
222}