use crate::{
timestamp::{FloatTimestamp, Timestamp},
Config,
};
use std::ops::{Deref, DerefMut};
use tracing::{trace, warn};
pub trait Stepper {
fn step(&mut self);
}
pub(crate) trait FixedTimestepper: Stepper {
fn last_completed_timestamp(&self) -> Timestamp;
fn reset_last_completed_timestamp(&mut self, corrected_timestamp: Timestamp);
fn post_update(&mut self, _timestep_overshoot_seconds: f64) {}
}
#[derive(PartialEq, Eq, Debug)]
pub(crate) enum TerminationCondition {
LastUndershoot,
FirstOvershoot,
}
impl TerminationCondition {
pub fn decompose_float_timestamp(
&self,
float_timestamp: FloatTimestamp,
timestep_seconds: f64,
) -> (Timestamp, f64) {
let timestamp = match self {
TerminationCondition::LastUndershoot => float_timestamp.floor(),
TerminationCondition::FirstOvershoot => float_timestamp.ceil(),
};
let overshoot_seconds =
(FloatTimestamp::from(timestamp) - float_timestamp).as_seconds(timestep_seconds);
(timestamp, overshoot_seconds)
}
pub fn should_terminate(
&self,
current_overshoot_seconds: f64,
next_overshoot_seconds: f64,
) -> bool {
match self {
TerminationCondition::LastUndershoot => next_overshoot_seconds > 0.0,
TerminationCondition::FirstOvershoot => current_overshoot_seconds >= 0.0,
}
}
}
#[derive(Debug)]
pub(crate) struct TimeKeeper<T: FixedTimestepper, const TERMINATION_CONDITION: TerminationCondition>
{
stepper: T,
timestep_overshoot_seconds: f64,
config: Config,
}
impl<T: FixedTimestepper, const TERMINATION_CONDITION: TerminationCondition>
TimeKeeper<T, TERMINATION_CONDITION>
{
pub fn new(stepper: T, config: Config) -> Self {
Self {
stepper,
timestep_overshoot_seconds: 0.0,
config,
}
}
pub fn update(&mut self, delta_seconds: f64, server_seconds_since_startup: f64) {
let compensated_delta_seconds =
self.delta_seconds_compensate_for_drift(delta_seconds, server_seconds_since_startup);
self.advance_stepper(compensated_delta_seconds);
self.timeskip_if_needed(server_seconds_since_startup);
self.stepper.post_update(self.timestep_overshoot_seconds);
trace!("Completed: {:?}", self.stepper.last_completed_timestamp());
}
pub fn current_logical_timestamp(&self) -> FloatTimestamp {
FloatTimestamp::from(self.stepper.last_completed_timestamp())
- FloatTimestamp::from_seconds(
self.timestep_overshoot_seconds,
self.config.timestep_seconds,
)
}
pub fn target_logical_timestamp(&self, server_seconds_since_startup: f64) -> FloatTimestamp {
FloatTimestamp::from_seconds(server_seconds_since_startup, self.config.timestep_seconds)
}
pub fn timestamp_drift_seconds(&self, server_seconds_since_startup: f64) -> f64 {
let frame_drift = self.current_logical_timestamp()
- self.target_logical_timestamp(server_seconds_since_startup);
let seconds_drift = frame_drift.as_seconds(self.config.timestep_seconds);
trace!(
"target logical timestamp: {:?}, current logical timestamp: {:?}, drift: {:?} ({} secs)",
self.target_logical_timestamp(server_seconds_since_startup),
self.current_logical_timestamp(),
frame_drift,
seconds_drift,
);
seconds_drift
}
fn delta_seconds_compensate_for_drift(
&self,
delta_seconds: f64,
server_seconds_since_startup: f64,
) -> f64 {
let timestamp_drift_seconds = {
let drift = self.timestamp_drift_seconds(server_seconds_since_startup - delta_seconds);
if drift.abs() < self.config.timestep_seconds * 0.5 {
0.0
} else {
warn!(
"Timestamp has drifted by {} seconds. This should not happen too often.",
drift
);
drift
}
};
let uncapped_compensated_delta_seconds = (delta_seconds - timestamp_drift_seconds).max(0.0);
let compensated_delta_seconds = if uncapped_compensated_delta_seconds
> self.config.update_delta_seconds_max
{
warn!("Attempted to advance more than the allowed delta seconds ({}). This should not happen too often.", uncapped_compensated_delta_seconds);
self.config.update_delta_seconds_max
} else {
uncapped_compensated_delta_seconds
};
trace!(
"Timestamp drift before advance: {:?}, delta_seconds: {:?}, adjusted delta_seconds: {:?}",
timestamp_drift_seconds,
delta_seconds,
compensated_delta_seconds
);
compensated_delta_seconds
}
fn advance_stepper(&mut self, delta_seconds: f64) {
self.timestep_overshoot_seconds -= delta_seconds;
loop {
let next_overshoot_seconds =
self.timestep_overshoot_seconds + self.config.timestep_seconds;
if TERMINATION_CONDITION
.should_terminate(self.timestep_overshoot_seconds, next_overshoot_seconds)
{
break;
}
self.stepper.step();
self.timestep_overshoot_seconds = next_overshoot_seconds;
}
}
fn timeskip_if_needed(&mut self, server_seconds_since_startup: f64) {
let drift_seconds = self.timestamp_drift_seconds(server_seconds_since_startup);
trace!("Timestamp drift after advance: {} sec", drift_seconds,);
if drift_seconds.abs() >= self.config.timestamp_skip_threshold_seconds {
let (corrected_timestamp, corrected_overshoot_seconds) = TERMINATION_CONDITION
.decompose_float_timestamp(
self.target_logical_timestamp(server_seconds_since_startup),
self.config.timestep_seconds,
);
warn!(
"TimeKeeper is too far behind. Skipping timestamp from {:?} to {:?} with overshoot from {} to {}",
self.stepper.last_completed_timestamp(),
corrected_timestamp,
self.timestep_overshoot_seconds,
corrected_overshoot_seconds,
);
self.stepper
.reset_last_completed_timestamp(corrected_timestamp);
self.timestep_overshoot_seconds = corrected_overshoot_seconds;
}
}
}
impl<T: FixedTimestepper, const C: TerminationCondition> Deref for TimeKeeper<T, C> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.stepper
}
}
impl<T: FixedTimestepper, const C: TerminationCondition> DerefMut for TimeKeeper<T, C> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.stepper
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::timestamp;
use float_cmp::approx_eq;
use itertools::iproduct;
use test_log::test;
use tracing::info;
const CONFIG: Config = Config::new();
struct MockStepper {
steps: i16,
last_completed_timestamp: Timestamp,
}
impl MockStepper {
fn new(initial_timestamp: Timestamp) -> Self {
Self {
steps: 0,
last_completed_timestamp: initial_timestamp,
}
}
}
impl Stepper for MockStepper {
fn step(&mut self) {
self.steps += 1;
self.last_completed_timestamp.increment();
}
}
impl FixedTimestepper for MockStepper {
fn last_completed_timestamp(&self) -> Timestamp {
self.last_completed_timestamp
}
fn reset_last_completed_timestamp(&mut self, corrected_timestamp: Timestamp) {
self.last_completed_timestamp = corrected_timestamp;
}
}
fn assert_approx_eq(lhs: f64, rhs: f64, subtest: &str, message: &str) {
assert!(
approx_eq!(f64, lhs, rhs, epsilon = 0.000000000001),
"{}\n{}\nlhs={}\nrhs={}",
subtest,
message,
lhs,
rhs
);
}
#[test]
fn test_termination_condition_last_undershoot_exact() {
let mut timekeeper: TimeKeeper<MockStepper, { TerminationCondition::LastUndershoot }> =
TimeKeeper::new(MockStepper::new(Timestamp::default()), CONFIG);
timekeeper.update(CONFIG.timestep_seconds, CONFIG.timestep_seconds);
assert_eq!(timekeeper.steps, 1);
}
#[test]
fn test_termination_condition_last_undershoot_below() {
let mut timekeeper: TimeKeeper<MockStepper, { TerminationCondition::LastUndershoot }> =
TimeKeeper::new(MockStepper::new(Timestamp::default()), CONFIG);
timekeeper.update(CONFIG.timestep_seconds * 0.5, CONFIG.timestep_seconds * 0.5);
assert_eq!(timekeeper.steps, 0);
}
#[test]
fn test_termination_condition_last_undershoot_above() {
let mut timekeeper: TimeKeeper<MockStepper, { TerminationCondition::LastUndershoot }> =
TimeKeeper::new(MockStepper::new(Timestamp::default()), CONFIG);
timekeeper.update(CONFIG.timestep_seconds * 1.5, CONFIG.timestep_seconds * 1.5);
assert_eq!(timekeeper.steps, 1);
}
#[test]
fn test_termination_condition_first_overshoot_exact() {
let mut timekeeper: TimeKeeper<MockStepper, { TerminationCondition::FirstOvershoot }> =
TimeKeeper::new(MockStepper::new(Timestamp::default()), CONFIG);
timekeeper.update(CONFIG.timestep_seconds, CONFIG.timestep_seconds);
assert_eq!(timekeeper.steps, 1);
}
#[test]
fn test_termination_condition_first_overshoot_below() {
let mut timekeeper: TimeKeeper<MockStepper, { TerminationCondition::FirstOvershoot }> =
TimeKeeper::new(MockStepper::new(Timestamp::default()), CONFIG);
timekeeper.update(CONFIG.timestep_seconds * 0.5, CONFIG.timestep_seconds * 0.5);
assert_eq!(timekeeper.steps, 1);
}
#[test]
fn test_termination_condition_first_overshoot_above() {
let mut timekeeper: TimeKeeper<MockStepper, { TerminationCondition::FirstOvershoot }> =
TimeKeeper::new(MockStepper::new(Timestamp::default()), CONFIG);
timekeeper.update(CONFIG.timestep_seconds * 1.5, CONFIG.timestep_seconds * 1.5);
assert_eq!(timekeeper.steps, 2);
}
#[test]
fn when_update_with_timestamp_drifted_within_the_frame_then_timestamp_drift_is_ignored() {
for (small_drift_seconds, initial_wrapped_count, initial_timestamp, frames_per_update) in iproduct!(
&[
0.0f64,
CONFIG.timestep_seconds * 0.001f64,
-CONFIG.timestep_seconds * 0.001f64,
CONFIG.timestep_seconds * 0.499f64,
-CONFIG.timestep_seconds * 0.499f64,
],
&[0.0, 1.0],
×tamp::tests::interesting_timestamps(),
&[1.0, 1.7, 2.0, 2.5,]
) {
let subtest = format!(
"Subtest [drift: {}, wrapped_count: {}, initial timestep: {:?}, frames per update: {}]",
small_drift_seconds, initial_wrapped_count, initial_timestamp, frames_per_update
);
info!("{}", subtest);
let mut timekeeper: TimeKeeper<MockStepper, { TerminationCondition::FirstOvershoot }> =
TimeKeeper::new(MockStepper::new(*initial_timestamp), CONFIG);
let initial_seconds_since_startup = initial_timestamp
.as_seconds(CONFIG.timestep_seconds)
+ initial_wrapped_count * 16.0f64.exp2() * CONFIG.timestep_seconds;
assert_approx_eq(
timekeeper.timestamp_drift_seconds(initial_seconds_since_startup),
0.0f64,
&subtest,
"Precondition: Zero drift from initial time",
);
assert_approx_eq(
timekeeper
.timestamp_drift_seconds(initial_seconds_since_startup - small_drift_seconds),
*small_drift_seconds,
&subtest,
"Precondition: Correct drift calculation before update",
);
let delta_seconds = CONFIG.timestep_seconds * frames_per_update;
let drifted_seconds_since_startup =
initial_seconds_since_startup + delta_seconds - small_drift_seconds;
timekeeper.update(delta_seconds, drifted_seconds_since_startup);
assert_approx_eq(
timekeeper.timestamp_drift_seconds(drifted_seconds_since_startup),
*small_drift_seconds,
&subtest,
"Condition: Drift ignored in update",
);
assert_eq!(
timekeeper.steps,
frames_per_update.ceil() as i16,
"{}\nCondition: All needed frames are stepped through",
subtest
);
}
}
#[test]
fn when_update_with_timestamp_drifted_beyond_a_frame_then_timestamp_gets_corrected() {
for (moderate_drift_seconds, initial_wrapped_count, initial_timestamp, frames_per_update) in iproduct!(
&[
CONFIG.timestep_seconds * 0.5f64,
-CONFIG.timestep_seconds * 0.5f64,
],
&[0.0, 1.0],
×tamp::tests::interesting_timestamps(),
&[1.0, 1.7, 2.0, 2.5]
) {
let subtest = format!(
"Subtest [drift: {}, wrapped_count: {}, initial timestep: {:?}, frames per update: {}]",
moderate_drift_seconds, initial_wrapped_count, initial_timestamp, frames_per_update
);
info!("{}", subtest);
let mut timekeeper: TimeKeeper<MockStepper, { TerminationCondition::FirstOvershoot }> =
TimeKeeper::new(MockStepper::new(*initial_timestamp), CONFIG);
let initial_seconds_since_startup = initial_timestamp
.as_seconds(CONFIG.timestep_seconds)
+ initial_wrapped_count * 16.0f64.exp2() * CONFIG.timestep_seconds;
assert_approx_eq(
timekeeper.timestamp_drift_seconds(initial_seconds_since_startup),
0.0f64,
&subtest,
"Precondition: Zero drift from initial time",
);
assert_approx_eq(
timekeeper.timestamp_drift_seconds(
initial_seconds_since_startup - moderate_drift_seconds,
),
*moderate_drift_seconds,
&subtest,
"Precondition: Correct drift calculation before update",
);
let delta_seconds = CONFIG.timestep_seconds * frames_per_update;
let drifted_seconds_since_startup =
initial_seconds_since_startup + delta_seconds - moderate_drift_seconds;
timekeeper.update(delta_seconds, drifted_seconds_since_startup);
assert_approx_eq(
timekeeper.timestamp_drift_seconds(drifted_seconds_since_startup),
0.0f64,
&subtest,
"Condition: Drift corrected after update",
);
assert_eq!(
timekeeper.steps,
i16::from(timekeeper.last_completed_timestamp() - *initial_timestamp),
"{}\nCondition: All needed frames are stepped through",
subtest
);
}
}
#[test]
fn when_update_with_timestamp_drifting_beyond_threshold_then_timestamps_are_skipped() {
const MINIMUM_SKIPPABLE_DELTA_SECONDS: f64 =
CONFIG.timestamp_skip_threshold_seconds + CONFIG.update_delta_seconds_max;
for (big_drift_seconds, initial_wrapped_count, initial_timestamp, frames_per_update) in iproduct!(
&[
MINIMUM_SKIPPABLE_DELTA_SECONDS,
-MINIMUM_SKIPPABLE_DELTA_SECONDS,
MINIMUM_SKIPPABLE_DELTA_SECONDS * 2.0,
-MINIMUM_SKIPPABLE_DELTA_SECONDS * 2.0,
],
&[0.0, 1.0],
×tamp::tests::interesting_timestamps(),
&[1.0, 1.7, 2.0, 2.5]
) {
let subtest = format!(
"Subtest [drift: {}, wrapped_count: {}, initial timestep: {:?}, frames per update: {}]",
big_drift_seconds, initial_wrapped_count, initial_timestamp, frames_per_update
);
info!("{}", subtest);
let mut timekeeper: TimeKeeper<MockStepper, { TerminationCondition::FirstOvershoot }> =
TimeKeeper::new(MockStepper::new(*initial_timestamp), CONFIG);
let initial_seconds_since_startup = initial_timestamp
.as_seconds(CONFIG.timestep_seconds)
+ initial_wrapped_count * 16.0f64.exp2() * CONFIG.timestep_seconds;
assert_approx_eq(
timekeeper.timestamp_drift_seconds(initial_seconds_since_startup),
0.0f64,
&subtest,
"Precondition: Zero drift from initial time",
);
assert_approx_eq(
timekeeper
.timestamp_drift_seconds(initial_seconds_since_startup - big_drift_seconds),
*big_drift_seconds,
&subtest,
"Precondition: Correct drift calculation before update",
);
let delta_seconds = CONFIG.timestep_seconds * frames_per_update;
let drifted_seconds_since_startup =
initial_seconds_since_startup + delta_seconds - big_drift_seconds;
timekeeper.update(delta_seconds, drifted_seconds_since_startup);
assert_approx_eq(
timekeeper.timestamp_drift_seconds(drifted_seconds_since_startup),
0.0f64,
&subtest,
"Condition: Drift corrected after update",
);
let expected_step_count = if big_drift_seconds.is_sign_positive() {
0
} else {
(CONFIG.update_delta_seconds_max / CONFIG.timestep_seconds).ceil() as i16 + 1
};
assert_eq!(
timekeeper.steps, expected_step_count,
"{}\nCondition: Frames pass the limit are not stepped through",
subtest
);
}
}
#[test]
fn while_updating_with_changing_delta_seconds_then_timestamp_should_not_be_drifting() {
for (initial_wrapped_count, initial_timestamp) in
iproduct!(&[0.0, 1.0], ×tamp::tests::interesting_timestamps())
{
let subtest = format!(
"Subtest [wrapped_count: {}, initial timestep: {:?}]",
initial_wrapped_count, initial_timestamp
);
info!("{}", subtest);
let mut timekeeper: TimeKeeper<MockStepper, { TerminationCondition::FirstOvershoot }> =
TimeKeeper::new(MockStepper::new(*initial_timestamp), CONFIG);
let mut seconds_since_startup = initial_timestamp.as_seconds(CONFIG.timestep_seconds)
+ initial_wrapped_count * 16.0f64.exp2() * CONFIG.timestep_seconds;
assert_approx_eq(
timekeeper.timestamp_drift_seconds(seconds_since_startup),
0.0f64,
&subtest,
"Precondition: Zero drift from initial time",
);
for frames_per_update in &[1.0, 1.7, 0.5, 2.5, 2.0] {
let delta_seconds = CONFIG.timestep_seconds * frames_per_update;
seconds_since_startup += delta_seconds;
timekeeper.update(delta_seconds, seconds_since_startup);
assert_approx_eq(
timekeeper.timestamp_drift_seconds(seconds_since_startup),
0.0f64,
&subtest,
"Condition: Drift remains at zero after update",
);
}
}
}
}