use core::time::Duration;
use crate::platform::Instant;
const MAX_TICKS_PER_FRAME: u32 = 8;
pub(crate) const MIN_TICK_INTERVAL: Duration = Duration::from_micros(1);
pub(crate) struct TickInterval {
current: Duration,
requested: Duration,
}
impl TickInterval {
pub(crate) fn new(interval: Duration) -> Self {
let interval = interval.max(MIN_TICK_INTERVAL);
Self {
current: interval,
requested: interval,
}
}
pub(crate) fn current(&self) -> Duration {
self.current
}
pub(crate) fn set(&mut self, interval: Duration) {
self.requested = interval.max(MIN_TICK_INTERVAL);
}
pub(crate) fn advance(&mut self) {
self.current = self.requested;
}
}
#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct FrameTime {
pub(crate) dt: Duration,
pub(crate) elapsed: Duration,
pub(crate) alpha: f32,
}
pub(crate) struct Clock {
started: Instant,
frame_started: Instant,
unsimulated: Duration,
frame_dt: Duration,
elapsed: Duration,
}
impl Clock {
pub(crate) fn new(now: Instant) -> Self {
Self {
started: now,
frame_started: now,
unsimulated: Duration::ZERO,
frame_dt: Duration::ZERO,
elapsed: Duration::ZERO,
}
}
pub(crate) fn frame_at(&mut self, now: Instant, tick_interval: Duration) -> u32 {
self.frame_dt = now.saturating_duration_since(self.frame_started);
self.frame_started = now;
self.elapsed = now.saturating_duration_since(self.started);
self.unsimulated += self.frame_dt;
let interval = tick_interval.max(MIN_TICK_INTERVAL).as_nanos();
let owed = self.unsimulated.as_nanos() / interval;
let leftover = self.unsimulated.as_nanos() % interval;
self.unsimulated = Duration::from_nanos(leftover as u64);
owed.min(u128::from(MAX_TICKS_PER_FRAME)) as u32
}
pub(crate) fn frame_time(&self, tick_interval: Duration) -> FrameTime {
FrameTime {
dt: self.frame_dt,
elapsed: self.elapsed,
alpha: self
.unsimulated
.div_duration_f32(tick_interval.max(MIN_TICK_INTERVAL)),
}
}
pub(crate) fn elapsed(&self) -> Duration {
self.elapsed
}
}
#[cfg(test)]
mod tests {
use super::*;
const STEP: Duration = Duration::from_millis(30);
fn epoch() -> Instant {
Instant::now()
}
fn clock() -> (Clock, Instant) {
let start = epoch();
(Clock::new(start), start)
}
fn frame_at(clock: &mut Clock, now: Instant) -> u32 {
clock.frame_at(now, STEP)
}
#[test]
fn frames_shorter_than_a_step_tick_zero_or_one_time() {
let (mut clock, start) = clock();
let frame = STEP * 3 / 5;
let ticks: Vec<u32> = (1..=10)
.map(|n| frame_at(&mut clock, start + frame * n))
.collect();
assert!(ticks.iter().all(|&ticks| ticks <= 1), "{ticks:?}");
assert_eq!(ticks.iter().sum::<u32>(), 6, "{ticks:?}");
}
#[test]
fn long_frames_tick_several_times() {
let (mut clock, start) = clock();
assert_eq!(frame_at(&mut clock, start + STEP * 9 / 2), 4);
assert_eq!(
frame_at(&mut clock, start + STEP * 5),
1,
"the half step completed"
);
assert_eq!(frame_at(&mut clock, start + STEP * 5), 0, "no time passed");
}
#[test]
fn a_stall_is_clamped_and_its_backlog_abandoned() {
let (mut clock, start) = clock();
let stall = Duration::from_secs(10);
assert_eq!(frame_at(&mut clock, start + stall), MAX_TICKS_PER_FRAME);
assert_eq!(
frame_at(&mut clock, start + stall + STEP),
1,
"the backlog did not carry"
);
}
#[test]
fn alpha_stays_within_one_step() {
let (mut clock, start) = clock();
for frame in 1..=200 {
frame_at(&mut clock, start + STEP * 37 / 100 * frame);
let alpha = clock.frame_time(STEP).alpha;
assert!((0.0..1.0).contains(&alpha), "{alpha}");
}
}
#[test]
fn timings_track_the_readings() {
let start = epoch() + Duration::from_secs(100);
let mut clock = Clock::new(start);
frame_at(&mut clock, start + Duration::from_millis(500));
let time = clock.frame_time(STEP);
assert_eq!(time.dt, Duration::from_millis(500));
assert_eq!(time.elapsed, Duration::from_millis(500));
}
#[test]
fn steps_accumulate_without_drift() {
let (mut clock, start) = clock();
let mut since_start = Duration::ZERO;
let frame = STEP / 3;
let ticks: u32 = (0..30_000)
.map(|_| {
since_start += frame;
frame_at(&mut clock, start + since_start)
})
.sum();
assert_eq!(ticks, 10_000);
}
#[test]
fn a_step_requested_is_taken_from_the_next_batch_on() {
let mut interval = TickInterval::new(STEP);
interval.advance();
interval.set(STEP * 2);
assert_eq!(interval.current(), STEP, "the running batch keeps its step");
interval.advance();
assert_eq!(interval.current(), STEP * 2, "the next takes the new one");
interval.set(Duration::ZERO);
interval.advance();
assert_eq!(
interval.current(),
MIN_TICK_INTERVAL,
"and never a zero step"
);
}
#[test]
fn a_zero_step_never_reaches_the_accumulator() {
let (mut clock, start) = clock();
assert_eq!(
clock.frame_at(start + STEP, Duration::ZERO),
MAX_TICKS_PER_FRAME
);
assert!(clock.frame_time(Duration::ZERO).alpha.is_finite());
}
#[test]
fn a_clock_that_never_advances_never_ticks() {
let start = epoch() + Duration::from_secs(7);
let mut clock = Clock::new(start);
assert_eq!(frame_at(&mut clock, start), 0);
assert_eq!(
frame_at(&mut clock, start - Duration::from_secs(1)),
0,
"time going backwards is ignored"
);
assert_eq!(clock.frame_time(STEP).alpha, 0.0);
}
}