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}