use crate::time::{Nanos, StepBudget, Timestamp, Timestep};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct FrameLoop {
timestep: Timestep,
budget: StepBudget,
last: Timestamp,
bank: Nanos,
tick: u64,
}
impl FrameLoop {
#[must_use]
pub const fn new(timestep: Timestep, budget: StepBudget, start: Timestamp) -> Self {
Self {
timestep,
budget,
last: start,
bank: Nanos::ZERO,
tick: 0,
}
}
pub fn begin_frame(&mut self, now: Timestamp) -> FramePlan {
let dt = self.timestep.nanos().get();
let bank = self
.bank
.get()
.saturating_add(now.saturating_since(self.last).get());
let due = bank / dt;
let steps = u32::try_from(due)
.unwrap_or(u32::MAX)
.min(self.budget.get().get());
let run = u64::from(steps);
let remainder = Nanos::from_nanos(bank % dt);
let plan = FramePlan {
first_tick: self.tick,
steps,
dropped: due - run,
remainder,
dt: self.timestep,
};
self.bank = remainder;
self.tick = self.tick.saturating_add(run);
self.last = now;
plan
}
pub fn resync(&mut self, now: Timestamp) {
self.last = now;
}
#[must_use]
pub const fn tick(&self) -> u64 {
self.tick
}
#[must_use]
pub const fn simulated(&self) -> Nanos {
Nanos::from_nanos(self.tick.saturating_mul(self.timestep.nanos().get()))
}
#[must_use]
pub const fn remainder(&self) -> Nanos {
self.bank
}
#[must_use]
pub const fn timestep(&self) -> Timestep {
self.timestep
}
#[must_use]
pub const fn budget(&self) -> StepBudget {
self.budget
}
}
#[must_use = "a frame plan's steps must be executed and its alpha rendered"]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct FramePlan {
first_tick: u64,
steps: u32,
dropped: u64,
remainder: Nanos,
dt: Timestep,
}
impl FramePlan {
#[must_use]
pub const fn steps(&self) -> Steps {
Steps {
next: self.first_tick,
remaining: self.steps,
dt: self.dt,
}
}
#[must_use]
pub const fn step_count(&self) -> u32 {
self.steps
}
#[must_use]
pub const fn dt(&self) -> Timestep {
self.dt
}
#[must_use]
pub const fn first_tick(&self) -> u64 {
self.first_tick
}
#[must_use]
pub const fn dropped(&self) -> u64 {
self.dropped
}
#[must_use]
pub const fn remainder(&self) -> Nanos {
self.remainder
}
#[must_use]
pub const fn timestep(&self) -> Timestep {
self.dt
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Step {
pub tick: u64,
pub dt: Nanos,
pub sim_time: Nanos,
}
#[derive(Clone, Debug)]
pub struct Steps {
next: u64,
remaining: u32,
dt: Timestep,
}
impl Iterator for Steps {
type Item = Step;
fn next(&mut self) -> Option<Step> {
if self.remaining == 0 {
return None;
}
self.remaining -= 1;
let tick = self.next;
self.next = tick.saturating_add(1);
let dt = self.dt.nanos().get();
Some(Step {
tick,
dt: Nanos::from_nanos(dt),
sim_time: Nanos::from_nanos(tick.saturating_mul(dt)),
})
}
fn size_hint(&self) -> (usize, Option<usize>) {
let remaining = usize::try_from(self.remaining).unwrap_or(usize::MAX);
(remaining, Some(remaining))
}
}
impl ExactSizeIterator for Steps {}
impl core::iter::FusedIterator for Steps {}
#[cfg(test)]
mod tests {
use super::{FrameLoop, StepBudget, Timestamp, Timestep};
use crate::time::Nanos;
use core::num::{NonZeroU32, NonZeroU64};
const DT: u64 = 16_666_667;
fn at(nanos: u64) -> Timestamp {
Timestamp::from_nanos(nanos)
}
fn timestep(nanos: u64) -> Timestep {
Timestep::from_nanos(NonZeroU64::new(nanos).expect("non-zero"))
}
fn budget(steps: u32) -> StepBudget {
StepBudget::new(NonZeroU32::new(steps).expect("non-zero"))
}
fn loop_at_60hz() -> FrameLoop {
FrameLoop::new(Timestep::HZ_60, StepBudget::DEFAULT, at(0))
}
#[test]
fn a_fresh_loop_reports_its_configuration_and_an_empty_schedule() {
let frame = loop_at_60hz();
assert_eq!(frame.timestep(), Timestep::HZ_60);
assert_eq!(frame.budget(), StepBudget::DEFAULT);
assert_eq!(frame.tick(), 0);
assert_eq!(frame.remainder(), Nanos::ZERO);
assert_eq!(frame.simulated(), Nanos::ZERO);
}
#[test]
fn no_elapsed_time_yields_no_steps() {
let mut frame = loop_at_60hz();
let plan = frame.begin_frame(at(0));
assert_eq!(plan.step_count(), 0);
assert_eq!(plan.dropped(), 0);
assert_eq!(plan.first_tick(), 0);
assert_eq!(plan.remainder(), Nanos::ZERO);
assert_eq!(plan.timestep(), Timestep::HZ_60);
assert_eq!(plan.steps().count(), 0);
}
#[test]
fn exactly_one_timestep_yields_one_step_and_an_empty_bank() {
let mut frame = loop_at_60hz();
let plan = frame.begin_frame(at(DT));
assert_eq!(plan.step_count(), 1);
assert_eq!(plan.remainder(), Nanos::ZERO);
assert_eq!(frame.tick(), 1);
assert_eq!(frame.simulated(), Nanos::from_nanos(DT));
}
#[test]
fn the_remainder_carries_across_frames() {
let mut frame = loop_at_60hz();
let first = frame.begin_frame(at(DT + 5));
assert_eq!(first.step_count(), 1);
assert_eq!(first.remainder(), Nanos::from_nanos(5));
let second = frame.begin_frame(at(DT + 5 + DT - 5));
assert_eq!(second.step_count(), 1);
assert_eq!(second.remainder(), Nanos::ZERO);
assert_eq!(frame.tick(), 2);
}
#[test]
fn sub_timestep_frames_bank_until_a_step_is_due() {
let mut frame = loop_at_60hz();
let half = DT / 2;
assert_eq!(frame.begin_frame(at(half)).step_count(), 0);
assert_eq!(frame.remainder(), Nanos::from_nanos(half));
assert_eq!(frame.begin_frame(at(2 * half)).step_count(), 0);
assert_eq!(frame.remainder(), Nanos::from_nanos(DT - 1));
assert_eq!(frame.begin_frame(at(2 * half + 1)).step_count(), 1);
assert_eq!(frame.remainder(), Nanos::ZERO);
}
#[test]
fn several_whole_timesteps_run_in_one_frame_while_the_budget_allows() {
let mut frame = loop_at_60hz();
let plan = frame.begin_frame(at(3 * DT + 7));
assert_eq!(plan.step_count(), 3);
assert_eq!(plan.dropped(), 0);
assert_eq!(plan.remainder(), Nanos::from_nanos(7));
}
#[test]
fn a_stall_is_clamped_and_the_refused_steps_are_reported() {
let mut frame = loop_at_60hz();
let plan = frame.begin_frame(at(200_000_000));
assert_eq!(plan.step_count(), 5);
assert_eq!(plan.dropped(), 200_000_000 / DT - 5);
assert_eq!(plan.remainder(), Nanos::from_nanos(200_000_000 % DT));
let recovered = frame.begin_frame(at(200_000_000 + DT));
assert_eq!(recovered.step_count(), 1);
assert_eq!(recovered.dropped(), 0);
}
#[test]
fn a_saturated_bank_drops_billions_of_steps_without_wrapping() {
let mut frame = loop_at_60hz();
let plan = frame.begin_frame(at(u64::MAX));
assert_eq!(plan.step_count(), 5);
assert_eq!(plan.dropped(), u64::MAX / DT - 5);
assert_eq!(plan.remainder(), Nanos::from_nanos(u64::MAX % DT));
assert_eq!(frame.tick(), 5);
}
#[test]
fn the_refused_count_exceeds_the_thirty_two_bit_range() {
let mut frame = FrameLoop::new(timestep(1), budget(1), at(0));
let plan = frame.begin_frame(at(u64::MAX));
assert_eq!(plan.step_count(), 1);
assert_eq!(plan.dropped(), u64::MAX - 1);
assert!(plan.dropped() > u64::from(u32::MAX));
}
#[test]
fn a_backwards_clock_advances_nothing_and_leaves_the_bank_alone() {
let mut frame = loop_at_60hz();
let _ = frame.begin_frame(at(DT + 11));
let backwards = frame.begin_frame(at(1));
assert_eq!(backwards.step_count(), 0);
assert_eq!(backwards.dropped(), 0);
assert_eq!(backwards.remainder(), Nanos::from_nanos(11));
assert_eq!(frame.tick(), 1);
assert_eq!(frame.begin_frame(at(1 + DT)).step_count(), 1);
}
#[test]
fn resync_discards_the_gap_but_keeps_the_tick_and_the_remainder() {
let mut frame = loop_at_60hz();
let _ = frame.begin_frame(at(DT + 11));
assert_eq!(frame.tick(), 1);
frame.resync(at(10_000_000_000));
assert_eq!(frame.tick(), 1);
assert_eq!(frame.remainder(), Nanos::from_nanos(11));
let plan = frame.begin_frame(at(10_000_000_000 + DT - 11));
assert_eq!(plan.step_count(), 1, "the pause was not banked");
assert_eq!(plan.dropped(), 0);
assert_eq!(plan.remainder(), Nanos::ZERO);
}
#[test]
fn the_simulated_clock_is_tick_times_timestep_and_saturates() {
let half = u64::MAX / 2;
let mut frame = FrameLoop::new(timestep(half), budget(2), at(0));
let _ = frame.begin_frame(at(u64::MAX));
assert_eq!(frame.tick(), 2);
assert_eq!(frame.simulated(), Nanos::from_nanos(2 * half));
let _ = frame.begin_frame(at(0));
let plan = frame.begin_frame(at(u64::MAX));
assert_eq!(plan.step_count(), 2);
assert_eq!(frame.tick(), 4);
assert_eq!(frame.simulated(), Nanos::from_nanos(u64::MAX));
let last = plan.steps().last().expect("two steps");
assert_eq!(last.tick, 3);
assert_eq!(last.sim_time, Nanos::from_nanos(u64::MAX));
}
#[test]
fn a_due_count_beyond_the_thirty_two_bit_range_is_still_budget_bounded() {
let mut frame = FrameLoop::new(timestep(1), budget(u32::MAX), at(0));
let plan = frame.begin_frame(at(u64::MAX));
assert_eq!(plan.step_count(), u32::MAX);
assert_eq!(plan.dropped(), u64::MAX - u64::from(u32::MAX));
assert_eq!(frame.tick(), u64::from(u32::MAX));
}
#[test]
fn the_steps_of_a_plan_are_consecutive_ticks_with_exact_simulation_times() {
let mut frame = loop_at_60hz();
let _ = frame.begin_frame(at(DT));
let plan = frame.begin_frame(at(4 * DT));
assert_eq!(plan.first_tick(), 1);
let steps: Vec<_> = plan.steps().collect();
assert_eq!(steps.len(), 3);
for (offset, step) in steps.iter().enumerate() {
let tick = 1 + offset as u64;
assert_eq!(step.tick, tick);
assert_eq!(step.dt, Nanos::from_nanos(DT));
assert_eq!(step.sim_time, Nanos::from_nanos(tick * DT));
}
assert_eq!(frame.simulated(), Nanos::from_nanos(4 * DT));
}
#[test]
fn the_step_iterator_reports_its_exact_length_and_then_stays_empty() {
let mut frame = loop_at_60hz();
let plan = frame.begin_frame(at(2 * DT));
let mut steps = plan.steps();
assert_eq!(steps.len(), 2);
assert_eq!(steps.size_hint(), (2, Some(2)));
assert!(steps.next().is_some());
assert_eq!(steps.len(), 1);
assert!(steps.next().is_some());
assert_eq!(steps.len(), 0);
assert!(steps.next().is_none());
assert!(steps.next().is_none(), "fused");
assert_eq!(plan.steps().count(), 2);
}
#[test]
fn the_remainder_is_the_exact_position_between_two_steps() {
let mut frame = loop_at_60hz();
let plan = frame.begin_frame(at(DT));
assert_eq!(plan.remainder(), Nanos::from_nanos(0));
assert_eq!(plan.timestep().nanos().get(), DT);
let plan = frame.begin_frame(at(DT + DT / 2));
assert_eq!(plan.remainder(), Nanos::from_nanos(DT / 2));
for dt in [
16_666_667_u64,
4_166_667,
8_000_000,
33_333_333,
1_000_000_000,
] {
let mut frame = FrameLoop::new(timestep(dt), budget(1), at(0));
let plan = frame.begin_frame(at(dt - 1));
assert_eq!(plan.remainder(), Nanos::from_nanos(dt - 1));
assert!(
plan.remainder().get() < plan.timestep().nanos().get(),
"the remainder must stay a proper fraction of the step"
);
}
}
#[test]
fn the_absorbed_accumulator_reproduces_hello_engines_committed_output() {
let pattern = [15_000_000_u64, 16_666_667, 18_000_000, 33_333_334];
let mut frame = loop_at_60hz();
let mut now = 0u64;
let mut dropped = 0u64;
for delta in pattern.iter().copied().cycle().take(60) {
now += delta;
dropped += frame.begin_frame(at(now)).dropped();
}
assert_eq!(now, 1_245_000_015, "time submitted");
assert_eq!(frame.tick(), 74, "ticks executed");
assert_eq!(
frame.remainder(),
Nanos::from_nanos(11_666_657),
"time pending"
);
assert_eq!(dropped, 0, "the pattern never reaches the budget");
assert_eq!(frame.simulated().get() + frame.remainder().get(), now);
}
}