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        if !cfg!(target_arch = "wasm32") {
44            return 0.0;
45        }
46        thread_local! {
47            static PERFORMANCE_NOW: RefCell<Option<(JsValue, Function)>> =
48                const { RefCell::new(None) };
49        }
50        PERFORMANCE_NOW.with(|cell: &RefCell<Option<(JsValue, Function)>>| {
51            let mut borrow: std::cell::RefMut<'_, Option<(JsValue, Function)>> = cell.borrow_mut();
52            if borrow.is_none() {
53                let Some(window_value) = window() else {
54                    return 0.0;
55                };
56                let Ok(performance) = Reflect::get(
57                    window_value.as_ref(),
58                    &JsValue::from_str(PERFORMANCE_OBJECT),
59                ) else {
60                    return 0.0;
61                };
62                let Ok(now_method) =
63                    Reflect::get(&performance, &JsValue::from_str(PERFORMANCE_NOW_METHOD))
64                else {
65                    return 0.0;
66                };
67                *borrow = Some((performance, now_method.unchecked_into()));
68            }
69            let Some((performance, now_function)) = borrow.as_ref() else {
70                return 0.0;
71            };
72            now_function
73                .call0(performance)
74                .ok()
75                .and_then(|v: JsValue| v.as_f64())
76                .map(|millis: f64| millis / 1000.0)
77                .unwrap_or(0.0)
78        })
79    }
80
81    /// Performs one tick of the fixed-timestep scheduler.
82    ///
83    /// Calculates the elapsed frame time, clamps it to `max_frame_time`, accumulates it,
84    /// then runs as many fixed updates as needed. Finally, computes the interpolation
85    /// factor and calls the render callback. When an input cell is supplied, its
86    /// per-frame edge state is cleared after the render callback so the next frame
87    /// observes only the edges that happened during that frame.
88    ///
89    /// Registered tasks are advanced immediately after each handler
90    /// `on_update` call, not once per frame: a frame may run several fixed
91    /// steps, and every one of them must advance the tasks by the same
92    /// `fixed_timestep` that gameplay logic receives. Driving tasks after
93    /// the handler callback keeps ordering explicit — gameplay logic
94    /// registered in `on_update` observes tasks that have already advanced
95    /// for this step.
96    ///
97    /// # Arguments
98    ///
99    /// - `&SchedulerConfig` - The scheduler configuration.
100    /// - `&TickHandlerRc` - The handler receiving update and render callbacks.
101    /// - `Option<&TaskRegistryRc>` - The task registry to advance each fixed
102    ///   step, or `None` when no tasks are registered.
103    /// - `Option<&InputStateCell>` - The shared input state to close out, or `None`
104    ///   when no input listeners are registered.
105    pub fn tick(
106        &mut self,
107        config: &SchedulerConfig,
108        handler: &TickHandlerRc,
109        tasks: Option<&TaskRegistryRc>,
110        input_cell: Option<&InputStateCell>,
111    ) {
112        let current_time: f64 = Self::current_time();
113        let frame_time: f64 = if self.get_last_time() == UNINITIALIZED_TIME {
114            config.get_fixed_timestep()
115        } else {
116            current_time - self.get_last_time()
117        };
118        self.set_last_time(current_time);
119        let clamped_frame_time: f64 = frame_time.min(config.get_max_frame_time());
120        *self.get_mut_accumulator() += clamped_frame_time;
121        while self.get_accumulator() >= config.get_fixed_timestep() {
122            handler.get_mut().on_update(config.get_fixed_timestep());
123            if let Some(registry) = tasks {
124                registry.get_mut().update_all(config.get_fixed_timestep());
125            }
126            *self.get_mut_accumulator() -= config.get_fixed_timestep();
127            *self.get_mut_update_count() += 1;
128        }
129        let interpolation: f64 = self.get_accumulator() / config.get_fixed_timestep();
130        handler.get_mut().on_render(interpolation);
131        *self.get_mut_frame_count() += 1;
132        if let Some(cell) = input_cell {
133            cell.get_mut().end_frame();
134        }
135    }
136}
137
138/// Implements registration and per-step advancement for [`TaskRegistry`].
139impl TaskRegistry {
140    /// Registers an updater and returns a handle that can remove it again.
141    ///
142    /// The returned [`TaskHandle`] stores the task's insertion index, so
143    /// unregistering does not scan the task list for a matching pointer.
144    /// Because removal preserves the relative order of the surviving
145    /// tasks, a handle only stays valid while no *earlier* task has been
146    /// removed; [`TaskRegistry::unregister`] re-resolves the index against
147    /// the current list rather than trusting a stale one.
148    ///
149    /// # Arguments
150    ///
151    /// - `T` - The updater to drive each fixed step. Must implement
152    ///   [`Updatable`] and be `'static` so it can be boxed into the
153    ///   heterogeneous task list.
154    ///
155    /// # Returns
156    ///
157    /// - `TaskHandle` - A handle used to unregister the task.
158    pub fn register<T>(&mut self, task: T) -> TaskHandle
159    where
160        T: Updatable + 'static,
161    {
162        let id: u64 = self.get_mut_tasks().len() as u64;
163        self.get_mut_tasks().push(Box::new(task));
164        TaskHandle::new(id)
165    }
166
167    /// Removes a previously registered task, returning whether it was found.
168    ///
169    /// A task is identified by its [`TaskHandle`]. The removal keeps the
170    /// insertion order of the remaining tasks intact, which is what the
171    /// per-step update contract promises.
172    ///
173    /// # Arguments
174    ///
175    /// - `&TaskHandle` - The handle returned by [`TaskRegistry::register`].
176    ///
177    /// # Returns
178    ///
179    /// - `bool` - `true` if a task was removed, `false` if the handle did
180    ///   not match any registered task.
181    pub fn unregister(&mut self, handle: &TaskHandle) -> bool {
182        let index: usize = handle.get_id() as usize;
183        if index >= self.get_tasks().len() {
184            return false;
185        }
186        self.get_mut_tasks().remove(index);
187        true
188    }
189
190    /// Advances every registered task by `delta_time` seconds.
191    ///
192    /// Tasks are updated in registration order. The whole list is walked
193    /// even if a task unregisters another one later in the list, because
194    /// the borrow of the task vector is held for the duration of the walk;
195    /// deferring mutation to the next step keeps the iteration well-defined.
196    ///
197    /// # Arguments
198    ///
199    /// - `f64` - The fixed delta time in seconds.
200    pub fn update_all(&mut self, delta_time: f64) {
201        for task in self.get_mut_tasks().iter_mut() {
202            task.update(delta_time);
203        }
204    }
205
206    /// Returns the number of registered tasks.
207    ///
208    /// # Returns
209    ///
210    /// - `usize` - The task count.
211    pub fn len(&self) -> usize {
212        self.get_tasks().len()
213    }
214
215    /// Returns whether the registry holds no tasks.
216    ///
217    /// # Returns
218    ///
219    /// - `bool` - `true` if no tasks are registered.
220    pub fn is_empty(&self) -> bool {
221        self.get_tasks().is_empty()
222    }
223
224    /// Removes every registered task.
225    pub fn clear(&mut self) {
226        self.get_mut_tasks().clear();
227    }
228}
229
230/// Implements lifecycle management for `SchedulerHandle`.
231impl SchedulerHandle {
232    /// Stops the scheduler and cancels any pending animation frame request.
233    pub fn stop(&self) {
234        let state: &mut SchedulerState = self.get_state().get_mut();
235        state.set_running(false);
236        if let Some(id) = state.get_mut_raf_id().take() {
237            let Some(window_value) = window() else {
238                // Drop the closure so the box can be collected.
239                let _ = self.get_closure_cell().try_take();
240                return;
241            };
242            let _: Result<(), JsValue> = window_value.cancel_animation_frame(id);
243        }
244        // Drop the closure so the box can be collected.
245        let _ = self.get_closure_cell().try_take();
246    }
247
248    /// Returns whether the scheduler is currently running.
249    ///
250    /// # Returns
251    ///
252    /// - `bool` - True if the scheduler is running.
253    pub fn is_running(&self) -> bool {
254        // SAFETY: caller contract - no mutable access to the same
255        // SchedulerState can be alive alongside this call.
256        self.get_state().get().get_running()
257    }
258
259    /// Returns the total number of fixed update steps executed.
260    ///
261    /// # Returns
262    ///
263    /// - `u64` - The update count.
264    pub fn update_count(&self) -> u64 {
265        self.get_state().get().get_update_count()
266    }
267
268    /// Returns the total number of render frames executed.
269    ///
270    /// # Returns
271    ///
272    /// - `u64` - The frame count.
273    pub fn frame_count(&self) -> u64 {
274        self.get_state().get().get_frame_count()
275    }
276
277    /// Registers a task to be advanced on every fixed step.
278    ///
279    /// The task is driven by [`SchedulerState::tick`] immediately after the
280    /// handler's `on_update` callback, using the same
281    /// [`SchedulerConfig::get_fixed_timestep`] delta.
282    ///
283    /// # Arguments
284    ///
285    /// - `&TaskRegistryRc` - The registry to add the task to.
286    /// - `T` - The updater to register. Must implement [`Updatable`].
287    ///
288    /// # Returns
289    ///
290    /// - `TaskHandle` - A handle used to unregister the task.
291    pub fn register_task<T>(registry: &TaskRegistryRc, task: T) -> TaskHandle
292    where
293        T: Updatable + 'static,
294    {
295        registry.get_mut().register(task)
296    }
297
298    /// Starts the scheduler with the given configuration and handler.
299    ///
300    /// Creates a `requestAnimationFrame`-driven loop that calls `tick`
301    /// on each animation frame. The returned `SchedulerHandle` can be used to stop the scheduler.
302    ///
303    /// When no global window exists (non-browser context), the scheduler is
304    /// not started and an already-stopped handle is returned instead.
305    ///
306    /// # Arguments
307    ///
308    /// - `SchedulerConfig` - The scheduler configuration.
309    /// - `TickHandlerRc` - The handler receiving update and render callbacks.
310    /// - `Option<&TaskRegistryRc>` - The task registry advanced on every
311    ///   fixed step, or `None` when no tasks are registered.
312    /// - `Option<&InputStateCell>` - The shared input state whose per-frame edge
313    ///   state is cleared at the end of every frame, or `None` when input
314    ///   listeners are not registered.
315    ///
316    /// # Returns
317    ///
318    /// - `SchedulerHandle` - A handle to control the running scheduler.
319    pub fn start(
320        config: SchedulerConfig,
321        handler: TickHandlerRc,
322        tasks: Option<&TaskRegistryRc>,
323        input_cell: Option<&InputStateCell>,
324    ) -> SchedulerHandle {
325        let state: Rc<EngineCell<SchedulerState>> =
326            Rc::new(EngineCell::new(SchedulerState::new(UNINITIALIZED_TIME)));
327        let closure_cell: RafClosureCell = Rc::new(MaybeEngineCell::new());
328        // Install the initial closure once spawn starts.
329        let state_ref_init: &mut SchedulerState = state.get_mut();
330        state_ref_init.set_running(true);
331        let state_clone: Rc<EngineCell<SchedulerState>> = state.clone();
332        let closure_cell_clone: RafClosureCell = closure_cell.clone();
333        let handler_clone: TickHandlerRc = handler.clone();
334        let tasks_clone: Option<TaskRegistryRc> = tasks.map(Rc::clone);
335        let input_cell_clone: Option<InputStateCell> = input_cell.map(Rc::clone);
336        let raf_closure: Closure<dyn FnMut()> = Closure::wrap(Box::new(move || {
337            {
338                let state_ref: &mut SchedulerState = state_clone.get_mut();
339                if !state_ref.get_running() {
340                    return;
341                }
342                state_ref.tick(
343                    &config,
344                    &handler_clone,
345                    tasks_clone.as_ref(),
346                    input_cell_clone.as_ref(),
347                );
348            }
349            let state_ro: &SchedulerState = state_clone.get();
350            if state_ro.get_running() {
351                let Some(window_value) = window() else {
352                    return;
353                };
354                let cell: RafClosureCell = closure_cell_clone.clone();
355                let Some(raf_closure) = cell.try_get() else {
356                    return;
357                };
358                let id: i32 = window_value
359                    .request_animation_frame(raf_closure.as_ref().unchecked_ref())
360                    .unwrap_or_default();
361                let state_ref_id: &mut SchedulerState = state_clone.get_mut();
362                state_ref_id.set_raf_id(Some(id));
363            }
364        }));
365        let Some(window_value) = window() else {
366            // No window context: install the closure, mark the scheduler
367            // stopped, and return an inert handle.
368            let state_ref_stop: &mut SchedulerState = state.get_mut();
369            state_ref_stop.set_running(false);
370            let _ = closure_cell.try_set(raf_closure);
371            return SchedulerHandle::new(state, closure_cell);
372        };
373        let id: i32 = window_value
374            .request_animation_frame(raf_closure.as_ref().unchecked_ref())
375            .unwrap_or_default();
376        let state_ref_id: &mut SchedulerState = state.get_mut();
377        state_ref_id.set_raf_id(Some(id));
378        let _ = closure_cell.try_set(raf_closure);
379        SchedulerHandle::new(state, closure_cell)
380    }
381}