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