use std::time::Duration;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct FixedTimestep {
step_nanos: u64,
accumulated_nanos: u64,
max_frame_nanos: u64,
max_steps: Option<u32>,
dropped_nanos: u64,
}
fn whole_nanos(duration: Duration) -> u64 {
u64::try_from(duration.as_nanos()).expect("a timestep duration fits in u64 nanoseconds")
}
impl FixedTimestep {
pub fn from_step(step: Duration) -> Self {
assert!(!step.is_zero(), "a fixed timestep must be greater than zero");
Self {
step_nanos: whole_nanos(step),
accumulated_nanos: 0,
max_frame_nanos: whole_nanos(DEFAULT_MAX_FRAME),
max_steps: None,
dropped_nanos: 0,
}
}
pub fn from_step_ms(step_ms: u64) -> Self {
Self::from_step(Duration::from_millis(step_ms))
}
pub fn from_hz(hz: u32) -> Self {
assert!(hz > 0, "a timestep rate must be greater than zero Hz");
Self::from_step(Duration::from_secs_f64(1.0 / f64::from(hz)))
}
pub fn with_max_frame_ms(mut self, max_frame_ms: u64) -> Self {
self.max_frame_nanos = whole_nanos(Duration::from_millis(max_frame_ms));
self
}
pub fn with_max_steps(mut self, max_steps: u32) -> Self {
self.max_steps = Some(max_steps);
self
}
pub fn advance(&mut self, elapsed_ms: u64) -> Steps {
let elapsed_nanos = elapsed_ms.saturating_mul(1_000_000);
if elapsed_nanos > self.max_frame_nanos {
self.dropped_nanos += elapsed_nanos - self.max_frame_nanos;
}
self.accumulated_nanos += elapsed_nanos.min(self.max_frame_nanos);
let mut count = self.accumulated_nanos / self.step_nanos;
self.accumulated_nanos -= count * self.step_nanos;
if let Some(cap) = self.max_steps {
if count > cap as u64 {
self.dropped_nanos += (count - cap as u64) * self.step_nanos + self.accumulated_nanos;
self.accumulated_nanos = 0;
count = cap as u64;
}
}
Steps {
remaining: count as u32,
step: Duration::from_nanos(self.step_nanos),
}
}
pub fn set_step(&mut self, step: Duration) {
assert!(!step.is_zero(), "a fixed timestep must be greater than zero");
self.step_nanos = whole_nanos(step);
}
pub fn set_step_ms(&mut self, step_ms: u64) {
self.set_step(Duration::from_millis(step_ms));
}
pub fn step(&self) -> Duration {
Duration::from_nanos(self.step_nanos)
}
pub fn step_secs(&self) -> f32 {
self.step().as_secs_f32()
}
pub fn pending_ms(&self) -> u64 {
self.accumulated_nanos / 1_000_000
}
pub fn alpha(&self) -> f32 {
self.accumulated_nanos as f32 / self.step_nanos as f32
}
pub fn dropped_ms(&self) -> u64 {
self.dropped_nanos / 1_000_000
}
pub fn reset(&mut self) {
self.accumulated_nanos = 0;
}
}
pub const DEFAULT_MAX_FRAME: Duration = Duration::from_millis(250);
#[derive(Clone, Copy, Debug)]
pub struct Steps {
remaining: u32,
step: Duration,
}
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 = Duration;
fn next(&mut self) -> Option<Duration> {
if self.remaining == 0 {
return None;
}
self.remaining -= 1;
Some(self.step)
}
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_nanos: u64,
accumulated_nanos: u64,
}
impl Periodic {
pub fn new(interval_ms: u64) -> Self {
Self::from_interval(Duration::from_millis(interval_ms))
}
pub fn from_interval(interval: Duration) -> Self {
assert!(!interval.is_zero(), "a period must be greater than zero");
Self {
interval_nanos: whole_nanos(interval),
accumulated_nanos: 0,
}
}
pub fn from_hz(hz: u32) -> Self {
assert!(hz > 0, "a period rate must be greater than zero Hz");
Self::from_interval(Duration::from_secs_f64(1.0 / f64::from(hz)))
}
pub fn set_interval_ms(&mut self, interval_ms: u64) {
self.set_interval(Duration::from_millis(interval_ms));
}
pub fn set_interval(&mut self, interval: Duration) {
assert!(!interval.is_zero(), "a period must be greater than zero");
self.interval_nanos = whole_nanos(interval);
}
pub fn interval(&self) -> Duration {
Duration::from_nanos(self.interval_nanos)
}
pub fn due(&mut self, elapsed_ms: u64) -> bool {
self.accumulated_nanos += elapsed_ms.saturating_mul(1_000_000);
if self.accumulated_nanos >= self.interval_nanos {
self.accumulated_nanos -= self.interval_nanos;
return true;
}
false
}
pub fn advance(&mut self, elapsed_ms: u64) -> u32 {
self.accumulated_nanos += elapsed_ms.saturating_mul(1_000_000);
let count = self.accumulated_nanos / self.interval_nanos;
self.accumulated_nanos -= count * self.interval_nanos;
count as u32
}
pub fn remaining_ms(&self) -> u64 {
self.interval_nanos.saturating_sub(self.accumulated_nanos) / 1_000_000
}
pub fn reset(&mut self) {
self.accumulated_nanos = 0;
}
}
#[cfg(test)]
mod rate_tests {
use super::*;
#[test]
fn a_rate_that_does_not_divide_a_thousand_is_exact_anyway() {
assert_eq!(FixedTimestep::from_hz(60).step(), Duration::from_secs_f64(1.0 / 60.0));
assert_eq!(FixedTimestep::from_hz(60).step().as_nanos(), 16_666_667);
assert_eq!(FixedTimestep::from_hz(50).step(), Duration::from_millis(20));
let mut clock = FixedTimestep::from_hz(60);
let steps: usize = (0..100).map(|_| clock.advance(10).len()).sum();
assert_eq!(steps, 59, "a second of elapsed time is {steps} steps of 16.666667ms");
let steps: usize = (0..9_900).map(|_| clock.advance(10).len()).sum();
assert_eq!(steps + 59, 5_999, "a hundred seconds stays within one step of 60Hz");
}
#[test]
fn a_period_rate_is_as_exact_as_a_step_rate() {
assert_eq!(Periodic::from_hz(60).interval(), Duration::from_secs_f64(1.0 / 60.0));
let mut p = Periodic::from_hz(60);
let fired: u32 = (0..100).map(|_| p.advance(10)).sum();
assert_eq!(fired, 59, "a second is {fired} periods of 16.666667ms, not 62");
}
#[test]
fn a_simulation_delta_taken_from_the_step_cannot_disagree_with_it() {
let step = FixedTimestep::from_hz(60);
assert_eq!(step.step_secs(), step.step().as_secs_f32());
assert!((step.step_secs() - 1.0 / 60.0).abs() < 1e-7);
}
}
#[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(), Duration::from_secs_f64(1.0 / 60.0));
let steps: Vec<Duration> = t.advance(50).collect();
assert_eq!(steps, vec![t.step(); 2], "50ms buys two 16.667ms steps");
assert!((t.step_secs() - 1.0 / 60.0).abs() < 1e-7);
}
#[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 = Duration::ZERO;
let mut wall = Duration::ZERO;
for frame in 0..1000u64 {
let dt = 14 + frame % 7;
wall += Duration::from_millis(dt);
simulated += t.advance(dt).sum::<Duration>();
}
assert!(wall - simulated < Duration::from_millis(16), "simulated {simulated:?} against wall {wall:?}");
assert_eq!(t.dropped_ms(), 0);
}
#[test]
fn a_slow_tick_over_a_fast_driver_caps_in_ticks_and_abandons_the_rest() {
let mut t = FixedTimestep::from_step_ms(600).with_max_steps(3).with_max_frame_ms(u64::MAX / 2_000_000);
assert_eq!(t.advance(1_250).len(), 2, "an ordinary wake is not capped");
assert_eq!(t.pending_ms(), 50, "and carries its remainder");
let steps = t.advance(120_000).len();
assert_eq!(steps, 3, "a stall runs the cap");
assert_eq!(t.pending_ms(), 0, "what was still owed is abandoned");
assert!(t.dropped_ms() > 100_000, "and counted rather than silent: {}", t.dropped_ms());
t.set_step_ms(50);
assert_eq!(t.advance(200).len(), 3, "three 50ms ticks under the same cap");
}
#[test]
fn landing_exactly_on_the_cap_keeps_the_remainder() {
let mut t = FixedTimestep::from_step_ms(100).with_max_steps(3).with_max_frame_ms(1000);
assert_eq!(t.advance(320).len(), 3);
assert_eq!(t.pending_ms(), 20, "exactly at the cap is not over it");
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(), Duration::from_millis(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);
}
}