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}