1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
use super::*;
/// Implements default configuration and state initialization for scheduler types.
impl Default for SchedulerConfig {
fn default() -> SchedulerConfig {
SchedulerConfig::new(DEFAULT_FIXED_TIMESTEP, DEFAULT_MAX_FRAME_TIME)
}
}
/// Implements `Default` for `SchedulerState` as a freshly created stopped state.
impl Default for SchedulerState {
fn default() -> SchedulerState {
SchedulerState::new(UNINITIALIZED_TIME)
}
}
/// Implements time retrieval and tick execution for `SchedulerState`.
impl SchedulerState {
/// Returns the current high-resolution timestamp in seconds from `performance.now()`.
///
/// Falls back to `0.0` when the global window or the `performance.now`
/// API is unavailable (for example outside a browser window context).
///
/// # Returns
///
/// - `f64` - The current time in seconds, or `0.0` when unavailable.
pub fn current_time() -> f64 {
let Some(window_value) = window() else {
return 0.0;
};
let Ok(performance) = Reflect::get(
window_value.as_ref(),
&JsValue::from_str(PERFORMANCE_OBJECT),
) else {
return 0.0;
};
let Ok(now_value) = Reflect::get(&performance, &JsValue::from_str(PERFORMANCE_NOW_METHOD))
else {
return 0.0;
};
let Some(now_millis) = now_value.as_f64() else {
return 0.0;
};
now_millis / 1000.0
}
/// Performs one tick of the fixed-timestep scheduler.
///
/// Calculates the elapsed frame time, clamps it to `max_frame_time`, accumulates it,
/// then runs as many fixed updates as needed. Finally, computes the interpolation
/// factor and calls the render callback.
///
/// # Arguments
///
/// - `&SchedulerConfig` - The scheduler configuration.
/// - `&TickHandlerRc` - The handler receiving update and render callbacks.
pub fn tick(&mut self, config: &SchedulerConfig, handler: &TickHandlerRc) {
let current_time: f64 = Self::current_time();
let frame_time: f64 = if self.get_last_time() == UNINITIALIZED_TIME {
config.get_fixed_timestep()
} else {
current_time - self.get_last_time()
};
self.set_last_time(current_time);
let clamped_frame_time: f64 = frame_time.min(config.get_max_frame_time());
*self.get_mut_accumulator() += clamped_frame_time;
while self.get_accumulator() >= config.get_fixed_timestep() {
handler.get_mut().on_update(config.get_fixed_timestep());
*self.get_mut_accumulator() -= config.get_fixed_timestep();
*self.get_mut_update_count() += 1;
}
let interpolation: f64 = self.get_accumulator() / config.get_fixed_timestep();
handler.get_mut().on_render(interpolation);
*self.get_mut_frame_count() += 1;
}
}
/// Implements lifecycle management for `SchedulerHandle`.
impl SchedulerHandle {
/// Stops the scheduler and cancels any pending animation frame request.
pub fn stop(&self) {
let state: &mut SchedulerState = self.get_state().get_mut();
state.set_running(false);
if let Some(id) = state.get_mut_raf_id().take() {
let Some(window_value) = window() else {
// Drop the closure so the box can be collected.
let _ = self.get_closure_cell().try_take();
return;
};
let _: Result<(), JsValue> = window_value.cancel_animation_frame(id);
}
// Drop the closure so the box can be collected.
let _ = self.get_closure_cell().try_take();
}
/// Returns whether the scheduler is currently running.
///
/// # Returns
///
/// - `bool` - True if the scheduler is running.
pub fn is_running(&self) -> bool {
// SAFETY: caller contract - no mutable access to the same
// SchedulerState can be alive alongside this call.
self.get_state().get().get_running()
}
/// Returns the total number of fixed update steps executed.
///
/// # Returns
///
/// - `u64` - The update count.
pub fn update_count(&self) -> u64 {
self.get_state().get().get_update_count()
}
/// Returns the total number of render frames executed.
///
/// # Returns
///
/// - `u64` - The frame count.
pub fn frame_count(&self) -> u64 {
self.get_state().get().get_frame_count()
}
/// Starts the scheduler with the given configuration and handler.
///
/// Creates a `requestAnimationFrame`-driven loop that calls `tick`
/// on each animation frame. The returned `SchedulerHandle` can be used to stop the scheduler.
///
/// When no global window exists (non-browser context), the scheduler is
/// not started and an already-stopped handle is returned instead.
///
/// # Arguments
///
/// - `SchedulerConfig` - The scheduler configuration.
/// - `TickHandlerRc` - The handler receiving update and render callbacks.
///
/// # Returns
///
/// - `SchedulerHandle` - A handle to control the running scheduler.
pub fn start(config: SchedulerConfig, handler: TickHandlerRc) -> SchedulerHandle {
let state: Rc<EngineCell<SchedulerState>> =
Rc::new(EngineCell::new(SchedulerState::new(UNINITIALIZED_TIME)));
let closure_cell: RafClosureCell = Rc::new(MaybeEngineCell::new());
// Install the initial closure once spawn starts.
let state_ref_init: &mut SchedulerState = state.get_mut();
state_ref_init.set_running(true);
let state_clone: Rc<EngineCell<SchedulerState>> = state.clone();
let closure_cell_clone: RafClosureCell = closure_cell.clone();
let handler_clone: TickHandlerRc = handler.clone();
let raf_closure: Closure<dyn FnMut()> = Closure::wrap(Box::new(move || {
{
let state_ref: &mut SchedulerState = state_clone.get_mut();
if !state_ref.get_running() {
return;
}
state_ref.tick(&config, &handler_clone);
}
let state_ro: &SchedulerState = state_clone.get();
if state_ro.get_running() {
let Some(window_value) = window() else {
return;
};
let cell: RafClosureCell = closure_cell_clone.clone();
let Some(raf_closure) = cell.try_get() else {
return;
};
let id: i32 = window_value
.request_animation_frame(raf_closure.as_ref().unchecked_ref())
.unwrap_or(0);
let state_ref_id: &mut SchedulerState = state_clone.get_mut();
state_ref_id.set_raf_id(Some(id));
}
}));
let Some(window_value) = window() else {
// No window context: install the closure, mark the scheduler
// stopped, and return an inert handle.
let state_ref_stop: &mut SchedulerState = state.get_mut();
state_ref_stop.set_running(false);
let _ = closure_cell.try_set(raf_closure);
return SchedulerHandle::new(state, closure_cell);
};
let id: i32 = window_value
.request_animation_frame(raf_closure.as_ref().unchecked_ref())
.unwrap_or(0);
let state_ref_id: &mut SchedulerState = state.get_mut();
state_ref_id.set_raf_id(Some(id));
let _ = closure_cell.try_set(raf_closure);
SchedulerHandle::new(state, closure_cell)
}
}