#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct FixedTimestep {
step_ms: u64,
accumulated_ms: u64,
max_frame_ms: u64,
dropped_ms: u64,
}
impl FixedTimestep {
pub fn from_step_ms(step_ms: u64) -> Self {
assert!(step_ms > 0, "a fixed timestep must be greater than zero");
Self {
step_ms,
accumulated_ms: 0,
max_frame_ms: DEFAULT_MAX_FRAME_MS,
dropped_ms: 0,
}
}
pub fn from_hz(hz: u32) -> Self {
assert!(hz > 0 && hz <= 1000, "a timestep rate must be between 1 and 1000 Hz, got {hz}");
Self::from_step_ms((1000 / hz) as u64)
}
pub fn with_max_frame_ms(mut self, max_frame_ms: u64) -> Self {
self.max_frame_ms = max_frame_ms;
self
}
pub fn advance(&mut self, elapsed_ms: u64) -> Steps {
if elapsed_ms > self.max_frame_ms {
self.dropped_ms += elapsed_ms - self.max_frame_ms;
}
self.accumulated_ms += elapsed_ms.min(self.max_frame_ms);
let count = self.accumulated_ms / self.step_ms;
self.accumulated_ms -= count * self.step_ms;
Steps {
remaining: count as u32,
step_ms: self.step_ms,
}
}
pub fn set_step_ms(&mut self, step_ms: u64) {
assert!(step_ms > 0, "a fixed timestep must be greater than zero");
self.step_ms = step_ms;
}
pub fn step_ms(&self) -> u64 {
self.step_ms
}
pub fn step_secs(&self) -> f32 {
self.step_ms as f32 / 1000.0
}
pub fn pending_ms(&self) -> u64 {
self.accumulated_ms
}
pub fn alpha(&self) -> f32 {
self.accumulated_ms as f32 / self.step_ms as f32
}
pub fn dropped_ms(&self) -> u64 {
self.dropped_ms
}
pub fn reset(&mut self) {
self.accumulated_ms = 0;
}
}
pub const DEFAULT_MAX_FRAME_MS: u64 = 250;
#[derive(Clone, Copy, Debug)]
pub struct Steps {
remaining: u32,
step_ms: u64,
}
impl Steps {
pub fn len(&self) -> usize {
self.remaining as usize
}
pub fn is_empty(&self) -> bool {
self.remaining == 0
}
}
impl Iterator for Steps {
type Item = u64;
fn next(&mut self) -> Option<u64> {
if self.remaining == 0 {
return None;
}
self.remaining -= 1;
Some(self.step_ms)
}
fn size_hint(&self) -> (usize, Option<usize>) {
(self.remaining as usize, Some(self.remaining as usize))
}
}
impl ExactSizeIterator for Steps {}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Periodic {
interval_ms: u64,
accumulated_ms: u64,
}
impl Periodic {
pub fn new(interval_ms: u64) -> Self {
assert!(interval_ms > 0, "a period must be greater than zero");
Self { interval_ms, accumulated_ms: 0 }
}
pub fn from_hz(hz: u32) -> Self {
assert!(hz > 0 && hz <= 1000, "a period rate must be between 1 and 1000 Hz, got {hz}");
Self::new((1000 / hz) as u64)
}
pub fn set_interval_ms(&mut self, interval_ms: u64) {
assert!(interval_ms > 0, "a period must be greater than zero");
self.interval_ms = interval_ms;
}
pub fn interval_ms(&self) -> u64 {
self.interval_ms
}
pub fn due(&mut self, elapsed_ms: u64) -> bool {
self.accumulated_ms += elapsed_ms;
if self.accumulated_ms >= self.interval_ms {
self.accumulated_ms -= self.interval_ms;
return true;
}
false
}
pub fn advance(&mut self, elapsed_ms: u64) -> u32 {
self.accumulated_ms += elapsed_ms;
let count = self.accumulated_ms / self.interval_ms;
self.accumulated_ms -= count * self.interval_ms;
count as u32
}
pub fn remaining_ms(&self) -> u64 {
self.interval_ms.saturating_sub(self.accumulated_ms)
}
pub fn reset(&mut self) {
self.accumulated_ms = 0;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn elapsed_time_is_spent_in_whole_steps() {
let mut t = FixedTimestep::from_step_ms(16);
assert_eq!(t.advance(33).len(), 2, "33ms buys two 16ms steps");
assert_eq!(t.pending_ms(), 1, "and keeps the leftover");
assert_eq!(t.advance(15).len(), 1, "which the next frame spends");
assert_eq!(t.pending_ms(), 0);
}
#[test]
fn a_step_yields_the_duration_to_advance_by() {
let mut t = FixedTimestep::from_hz(60);
assert_eq!(t.step_ms(), 16);
let steps: Vec<u64> = t.advance(50).collect();
assert_eq!(steps, vec![16, 16, 16]);
assert!((t.step_secs() - 0.016).abs() < 1e-6);
}
#[test]
fn the_remainder_carries_so_the_average_rate_is_exact() {
let mut t = FixedTimestep::from_step_ms(16);
let mut steps = 0;
for _ in 0..600 {
steps += t.advance(17).len();
}
assert_eq!(steps, 637, "carried remainders were lost");
}
#[test]
fn a_backgrounded_tab_cannot_dump_a_burst_of_steps_on_resume() {
let mut t = FixedTimestep::from_step_ms(16).with_max_frame_ms(100);
let steps = t.advance(60_000).len();
assert_eq!(steps, 6, "a minute asleep produced {steps} steps in one frame");
assert_eq!(t.dropped_ms(), 59_900, "and the time it refused is visible, not silent");
}
#[test]
fn ordinary_frames_are_never_clamped() {
let mut t = FixedTimestep::from_step_ms(16);
for _ in 0..100 {
t.advance(33);
}
assert_eq!(t.dropped_ms(), 0, "a doubled frame time is not a stall");
}
#[test]
fn alpha_is_the_fraction_of_a_step_left_over() {
let mut t = FixedTimestep::from_step_ms(20);
t.advance(30);
assert!((t.alpha() - 0.5).abs() < 1e-6, "alpha was {}", t.alpha());
t.advance(10);
assert!(t.alpha().abs() < 1e-6);
}
#[test]
fn a_reset_drops_the_remainder_but_keeps_the_ledger() {
let mut t = FixedTimestep::from_step_ms(16).with_max_frame_ms(50);
assert_eq!(t.advance(1000).len(), 3);
t.advance(10);
assert_eq!(t.pending_ms(), 12);
t.reset();
assert_eq!(t.pending_ms(), 0, "a rebuilt world starts from a clean step");
assert_eq!(t.dropped_ms(), 950, "but the session's dropped time is not a per-world figure");
}
#[test]
fn changing_the_step_takes_effect_without_restarting_it() {
let mut t = FixedTimestep::from_step_ms(100);
t.advance(90);
assert_eq!(t.pending_ms(), 90);
t.set_step_ms(50);
assert_eq!(t.advance(0).len(), 1, "already past the new step, so it is owed one");
assert_eq!(t.pending_ms(), 40);
}
#[test]
fn simulated_time_tracks_wall_time_when_nothing_stalls() {
let mut t = FixedTimestep::from_step_ms(16);
let mut simulated = 0u64;
let mut wall = 0u64;
for frame in 0..1000u64 {
let dt = 14 + frame % 7;
wall += dt;
simulated += t.advance(dt).map(|step| step).sum::<u64>();
}
assert!(wall - simulated < 16, "simulated {simulated} against wall {wall}");
assert_eq!(t.dropped_ms(), 0);
}
#[test]
fn zero_elapsed_time_produces_no_steps() {
let mut t = FixedTimestep::from_step_ms(16);
assert_eq!(t.advance(0).len(), 0);
assert_eq!(t.pending_ms(), 0);
}
#[test]
fn a_period_fires_at_most_once_per_advance_and_carries_the_rest() {
let mut p = Periodic::new(100);
assert!(!p.due(60));
assert!(p.due(60), "120ms covers the period");
assert_eq!(p.remaining_ms(), 80, "the extra 20ms carried");
assert!(!p.due(70));
assert!(p.due(10));
}
#[test]
fn a_period_can_report_every_occurrence_when_each_one_matters() {
let mut p = Periodic::new(100);
assert_eq!(p.advance(350), 3);
assert_eq!(p.remaining_ms(), 50);
}
#[test]
fn changing_the_interval_takes_effect_without_restarting_it() {
let mut p = Periodic::new(100);
p.due(90);
p.set_interval_ms(50);
assert!(p.due(0), "already past the new interval, so it is due immediately");
assert_eq!(p.interval_ms(), 50);
}
#[test]
fn a_period_keeps_its_phase_across_a_long_frame() {
let mut p = Periodic::new(100);
assert!(p.due(250));
assert_eq!(p.remaining_ms(), 0, "150ms of the stall is still owed");
assert!(p.due(0));
assert_eq!(p.remaining_ms(), 50);
}
}