pub const DEFAULT_HZ: f32 = 60.0;
const MAX_FRAME_DT_MULTIPLIER: f32 = 1.0;
#[derive(Debug, Clone, Copy)]
pub struct FixedClock {
pub fixed_dt: f32,
accumulator: f32,
max_frame_dt: f32,
pub alpha: f32,
pub steps_this_frame: u32,
}
impl FixedClock {
#[must_use]
pub fn new() -> Self {
Self::with_hz(DEFAULT_HZ)
}
#[must_use]
pub fn with_hz(hz: f32) -> Self {
assert!(hz > 0.0, "FixedClock: hz must be positive, got {hz}");
let fixed_dt = 1.0 / hz;
Self {
fixed_dt,
accumulator: 0.0,
max_frame_dt: fixed_dt * MAX_FRAME_DT_MULTIPLIER,
alpha: 0.0,
steps_this_frame: 0,
}
}
pub fn begin_frame(&mut self, real_dt: f32) {
self.accumulator += real_dt.min(self.max_frame_dt);
self.steps_this_frame = 0;
}
pub fn step(&mut self) -> bool {
if self.accumulator >= self.fixed_dt {
self.accumulator -= self.fixed_dt;
self.steps_this_frame += 1;
true
} else {
false
}
}
pub fn finish(&mut self) {
self.alpha = self.accumulator / self.fixed_dt;
}
}
impl Default for FixedClock {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use wasm_bindgen_test::wasm_bindgen_test;
#[wasm_bindgen_test(unsupported = test)]
fn custom_hz_clamps_to_its_own_step() {
let mut clock = FixedClock::with_hz(30.0);
clock.begin_frame(0.5);
assert!(clock.step());
assert!(!clock.step());
clock.finish();
assert_eq!(clock.alpha.to_bits(), 0);
assert_eq!(clock.steps_this_frame, 1);
}
fn run(real_dt: f32, frames: usize) -> Vec<u32> {
let mut clock = FixedClock::new();
let mut counts = Vec::with_capacity(frames);
for _ in 0..frames {
clock.begin_frame(real_dt);
while clock.step() {}
clock.finish();
counts.push(clock.steps_this_frame);
assert!(
(0.0..1.0).contains(&clock.alpha),
"alpha must be in [0,1): got {}",
clock.alpha
);
}
counts
}
#[wasm_bindgen_test(unsupported = test)]
fn cadence_never_oscillates_with_clamp() {
for hz in [60.0_f32, 75.0, 120.0, 144.0] {
let counts = run(1.0 / hz, 600);
let steady = &counts[2..];
assert!(
steady.iter().all(|&c| c <= 1),
"{hz} Hz: a frame ran >1 step (sample: {:?})",
&steady[..steady.len().min(20)]
);
}
}
#[wasm_bindgen_test(unsupported = test)]
fn at_fixed_rate_cadence_is_one() {
let counts = run(1.0 / 60.0, 300);
let steady = &counts[2..];
assert!(
steady.iter().all(|&c| c == 1),
"60 Hz steady cadence should be all 1s, got sample: {:?}",
&steady[..steady.len().min(20)]
);
}
#[wasm_bindgen_test(unsupported = test)]
fn faster_render_never_exceeds_one_step() {
for hz in [120.0_f32, 144.0, 240.0] {
let counts = run(1.0 / hz, 300);
assert!(
counts.iter().all(|&c| c <= 1),
"{hz} Hz exceeded 1 step/frame"
);
}
}
#[wasm_bindgen_test(unsupported = test)]
fn slow_render_is_clamped_to_one_step() {
let counts = run(1.0 / 30.0, 300);
let steady = &counts[2..];
assert!(
steady.iter().all(|&c| c == 1),
"30 Hz should clamp to exactly 1 step/frame, got sample: {:?}",
&steady[..steady.len().min(20)]
);
}
#[wasm_bindgen_test(unsupported = test)]
fn custom_hz_sets_fixed_dt() {
let clock = FixedClock::with_hz(30.0);
assert!((clock.fixed_dt - 1.0 / 30.0).abs() < f32::EPSILON);
}
}