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}