use std::collections::VecDeque;
use std::fmt::Debug;
use std::ops::Sub;
pub trait ToF32 {
fn to_f32(self) -> Option<f32>;
}
impl ToF32 for u64 {
fn to_f32(self) -> Option<f32> {
Some(self as f32)
}
}
impl ToF32 for i64 {
fn to_f32(self) -> Option<f32> {
Some(self as f32)
}
}
impl ToF32 for f32 {
fn to_f32(self) -> Option<f32> {
Some(self)
}
}
impl ToF32 for f64 {
fn to_f32(self) -> Option<f32> {
Some(self as f32)
}
}
impl ToF32 for std::time::Duration {
fn to_f32(self) -> Option<f32> {
Some(self.as_secs_f32())
}
}
pub trait Interpolatable<Timestamp>
where
Self: Sized + Clone,
Timestamp: Copy + Debug + PartialOrd,
{
fn interpolate(&self, other: &Self, t: f32, time_a: Timestamp, time_b: Timestamp) -> Self;
}
#[derive(Debug, Clone)]
pub struct ServerSnapshot<Timestamp, StateType> {
pub server_timestamp: Timestamp,
pub state: StateType,
}
#[derive(Debug, Clone)]
pub struct SnapshotBuffer<Timestamp, StateType>
where
Timestamp: Copy + Debug + Ord,
StateType: Clone + Debug,
{
snapshots: VecDeque<ServerSnapshot<Timestamp, StateType>>,
max_buffer_size: usize,
}
impl<Timestamp, StateType> SnapshotBuffer<Timestamp, StateType>
where
Timestamp: Copy + Debug + Ord + Sub<Output = Timestamp> + ToF32,
StateType: Clone + Debug + Interpolatable<Timestamp>,
{
pub fn new(max_buffer_size: usize) -> Self {
if max_buffer_size < 2 {
panic!("SnapshotBuffer max_buffer_size must be at least 2 for interpolation.");
}
Self {
snapshots: VecDeque::with_capacity(max_buffer_size),
max_buffer_size,
}
}
pub fn add_snapshot(&mut self, server_timestamp: Timestamp, state: StateType) {
let new_snapshot = ServerSnapshot {
server_timestamp,
state,
};
if self.snapshots.is_empty() || server_timestamp >= self.snapshots.back().unwrap().server_timestamp {
self.snapshots.push_back(new_snapshot);
} else {
let insertion_point = self
.snapshots
.partition_point(|s| s.server_timestamp < new_snapshot.server_timestamp);
self.snapshots.insert(insertion_point, new_snapshot);
tracing::debug!(
ts = ?server_timestamp,
insert_idx = insertion_point,
"Inserted snapshot into SnapshotBuffer (maintaining chronological order)."
);
}
while self.snapshots.len() > self.max_buffer_size {
self.snapshots.pop_front();
}
tracing::trace!(ts = ?server_timestamp, current_buffer_len = self.snapshots.len(), "Added snapshot. Buffer size {}.", self.snapshots.len());
}
pub fn get_interpolated_state(&self, target_render_time_on_server_timeline: Timestamp) -> Option<StateType> {
let num_snapshots = self.snapshots.len();
if num_snapshots == 0 {
tracing::trace!("SnapshotBuffer is empty, cannot interpolate.");
return None; }
if num_snapshots == 1 {
tracing::trace!("SnapshotBuffer has only one snapshot; returning it directly.");
return Some(self.snapshots[0].state.clone());
}
let oldest_snapshot = &self.snapshots[0];
let newest_snapshot = &self.snapshots[num_snapshots - 1];
if target_render_time_on_server_timeline <= oldest_snapshot.server_timestamp {
tracing::trace!(target = ?target_render_time_on_server_timeline, oldest_ts = ?oldest_snapshot.server_timestamp, "Target time is at or before the oldest snapshot. Clamping to oldest state.");
return Some(oldest_snapshot.state.clone());
}
if target_render_time_on_server_timeline >= newest_snapshot.server_timestamp {
tracing::trace!(target = ?target_render_time_on_server_timeline, newest_ts = ?newest_snapshot.server_timestamp, "Target time is at or after the newest snapshot. Clamping to newest state.");
return Some(newest_snapshot.state.clone());
}
let idx_after = self
.snapshots
.partition_point(|s| s.server_timestamp < target_render_time_on_server_timeline);
if idx_after == 0 || idx_after >= num_snapshots {
tracing::error!(target = ?target_render_time_on_server_timeline, idx_after, num_snapshots, "SnapshotBuffer: Unexpected index from partition_point within interpolation logic. Clamping to newest.");
return Some(newest_snapshot.state.clone()); }
let before_snapshot = &self.snapshots[idx_after - 1];
let after_snapshot = &self.snapshots[idx_after];
let time_b_minus_a = after_snapshot.server_timestamp - before_snapshot.server_timestamp;
let target_minus_a = target_render_time_on_server_timeline - before_snapshot.server_timestamp;
match (time_b_minus_a.to_f32(), target_minus_a.to_f32()) {
(Some(time_b_minus_a_f32), Some(target_minus_a_f32)) => {
if time_b_minus_a_f32.abs() < 1e-9 {
return Some(before_snapshot.state.clone());
}
let t = target_minus_a_f32 / time_b_minus_a_f32;
let t_clamped = t.clamp(0.0, 1.0);
if (t - t_clamped).abs() > 1e-5 && !(-1e-5..=1.0 + 1e-5).contains(&t) {
tracing::warn!(target_time = ?target_render_time_on_server_timeline, ts_a = ?before_snapshot.server_timestamp, ts_b = ?after_snapshot.server_timestamp, t_calc = t, t_clamped, "Interpolation factor t was outside [0,1] and clamped significantly.");
}
Some(before_snapshot.state.interpolate(
&after_snapshot.state,
t_clamped,
before_snapshot.server_timestamp,
after_snapshot.server_timestamp,
))
}
_ => {
tracing::error!(
"Failed to convert time differences to f32 for interpolation. Falling back to 'before' snapshot."
);
Some(before_snapshot.state.clone())
}
}
}
pub fn len(&self) -> usize {
self.snapshots.len()
}
pub fn is_empty(&self) -> bool {
self.snapshots.is_empty()
}
pub fn clear(&mut self) {
self.snapshots.clear();
tracing::debug!("SnapshotBuffer cleared.");
}
pub fn latest_timestamp(&self) -> Option<Timestamp> {
self.snapshots.back().map(|s| s.server_timestamp)
}
pub fn oldest_timestamp(&self) -> Option<Timestamp> {
self.snapshots.front().map(|s| s.server_timestamp)
}
}
#[derive(Debug, Clone)]
pub struct InterpolationClock<T> {
now: Option<T>,
delay: T,
rate: f32,
}
impl<T> InterpolationClock<T>
where
T: Copy + PartialOrd + std::ops::Add<Output = T> + std::ops::Sub<Output = T> + Default,
{
pub fn new(delay: T) -> Self {
Self { now: None, delay, rate: 1.0 }
}
pub fn observe(&mut self, server_time: T) {
if self.now.is_none() {
self.now = Some(server_time);
}
}
pub fn advance(&mut self, dt: T) {
if let Some(now) = self.now {
self.now = Some(now + dt);
}
}
pub fn target(&self) -> Option<T> {
self.now.map(|now| if now >= self.delay { now - self.delay } else { T::default() })
}
pub fn started(&self) -> bool {
self.now.is_some()
}
pub fn delay(&self) -> T {
self.delay
}
pub fn set_delay(&mut self, delay: T) {
self.delay = delay;
}
}
impl InterpolationClock<u64> {
pub fn resync(&mut self, newest_server_time_ms: u64, strength: f32) {
let strength = strength.clamp(0.0, 1.0);
self.now = Some(match self.now {
Some(now) => {
let corrected = now as f32 + (newest_server_time_ms as f32 - now as f32) * strength;
corrected.max(0.0) as u64
}
None => newest_server_time_ms,
});
}
pub fn observe_rate(&mut self, newest_server_time_ms: u64, max_rate_adjust: f32) {
let max_rate_adjust = max_rate_adjust.clamp(0.0, 1.0);
match self.now {
None => {
self.now = Some(newest_server_time_ms);
self.rate = 1.0;
}
Some(now) => {
let scale = (self.delay as f32).max(1.0);
let error = newest_server_time_ms as f32 - now as f32;
let normalized = (error / scale).clamp(-1.0, 1.0);
self.rate = 1.0 + max_rate_adjust * normalized;
}
}
}
pub fn advance_scaled(&mut self, dt_ms: u64) {
if let Some(now) = self.now {
let scaled = (dt_ms as f32 * self.rate).round() as u64;
self.now = Some(now + scaled);
}
}
pub fn playback_rate(&self) -> f32 {
self.rate
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::ClientTimeMs;
#[derive(Debug, Clone, PartialEq)]
struct TestRenderState {
position: f32,
}
impl Interpolatable<ClientTimeMs> for TestRenderState {
fn interpolate(&self, other: &Self, t: f32, _time_a: ClientTimeMs, _time_b: ClientTimeMs) -> Self {
TestRenderState {
position: self.position + (other.position - self.position) * t,
}
}
}
const MAX_BUF_SIZE_INTERP: usize = 5;
#[test]
fn new_buffer_is_empty_and_correct_size_interp() {
let buffer = SnapshotBuffer::<ClientTimeMs, TestRenderState>::new(MAX_BUF_SIZE_INTERP);
assert!(buffer.is_empty());
assert_eq!(buffer.len(), 0);
assert_eq!(buffer.max_buffer_size, MAX_BUF_SIZE_INTERP);
}
#[test]
#[should_panic]
fn new_buffer_panics_if_size_less_than_2_interp() {
let _buffer = SnapshotBuffer::<ClientTimeMs, TestRenderState>::new(1);
}
#[test]
fn add_snapshots_maintains_order_and_capacity_interp() {
let mut buffer = SnapshotBuffer::<ClientTimeMs, TestRenderState>::new(3);
buffer.add_snapshot(100, TestRenderState { position: 10.0 });
buffer.add_snapshot(200, TestRenderState { position: 20.0 });
assert_eq!(buffer.len(), 2);
buffer.add_snapshot(300, TestRenderState { position: 30.0 });
assert_eq!(buffer.len(), 3);
assert_eq!(buffer.snapshots[0].server_timestamp, 100);
assert_eq!(buffer.snapshots[2].server_timestamp, 300);
buffer.add_snapshot(400, TestRenderState { position: 40.0 });
assert_eq!(buffer.len(), 3);
assert_eq!(buffer.snapshots[0].server_timestamp, 200);
assert_eq!(buffer.snapshots[2].server_timestamp, 400);
}
#[test]
fn add_out_of_order_snapshot_interp() {
let mut buffer = SnapshotBuffer::<ClientTimeMs, TestRenderState>::new(3);
buffer.add_snapshot(100, TestRenderState { position: 10.0 });
buffer.add_snapshot(300, TestRenderState { position: 30.0 });
buffer.add_snapshot(200, TestRenderState { position: 20.0 });
assert_eq!(buffer.len(), 3);
assert_eq!(buffer.snapshots[0].server_timestamp, 100);
assert_eq!(buffer.snapshots[1].server_timestamp, 200);
assert_eq!(buffer.snapshots[2].server_timestamp, 300);
}
#[test]
fn add_duplicate_timestamp_snapshot_interp() {
let mut buffer = SnapshotBuffer::<ClientTimeMs, TestRenderState>::new(3);
buffer.add_snapshot(100, TestRenderState { position: 10.0 });
buffer.add_snapshot(200, TestRenderState { position: 20.0 }); buffer.add_snapshot(200, TestRenderState { position: 20.5 });
assert_eq!(buffer.len(), 3);
assert_eq!(buffer.snapshots[0].server_timestamp, 100);
assert_eq!(buffer.snapshots[1].server_timestamp, 200);
assert_eq!(
buffer.snapshots[1].state.position, 20.0,
"First-arriving duplicate keeps its position"
);
assert_eq!(buffer.snapshots[2].server_timestamp, 200);
assert_eq!(
buffer.snapshots[2].state.position, 20.5,
"Later-arriving duplicate is appended after it"
);
}
#[test]
fn get_interpolated_state_basic_interp() {
let mut buffer = SnapshotBuffer::<ClientTimeMs, TestRenderState>::new(MAX_BUF_SIZE_INTERP);
buffer.add_snapshot(100, TestRenderState { position: 10.0 });
buffer.add_snapshot(200, TestRenderState { position: 30.0 });
let interpolated = buffer.get_interpolated_state(150).unwrap();
assert!((interpolated.position - 20.0).abs() < f32::EPSILON);
let at_start_exact = buffer.get_interpolated_state(100).unwrap();
assert!((at_start_exact.position - 10.0).abs() < f32::EPSILON);
let at_end_exact = buffer.get_interpolated_state(200).unwrap();
assert!((at_end_exact.position - 30.0).abs() < f32::EPSILON);
}
#[test]
fn get_interpolated_state_clamps_to_edges_interp() {
let mut buffer = SnapshotBuffer::<ClientTimeMs, TestRenderState>::new(MAX_BUF_SIZE_INTERP);
buffer.add_snapshot(100, TestRenderState { position: 10.0 });
buffer.add_snapshot(200, TestRenderState { position: 30.0 });
let before_oldest = buffer.get_interpolated_state(50).unwrap();
assert!((before_oldest.position - 10.0).abs() < f32::EPSILON);
let after_newest = buffer.get_interpolated_state(250).unwrap();
assert!((after_newest.position - 30.0).abs() < f32::EPSILON);
}
#[test]
fn get_interpolated_state_insufficient_data_interp() {
let mut buffer = SnapshotBuffer::<ClientTimeMs, TestRenderState>::new(MAX_BUF_SIZE_INTERP);
assert!(buffer.get_interpolated_state(150).is_none());
buffer.add_snapshot(100, TestRenderState { position: 10.0 }); let state_single = buffer.get_interpolated_state(150).unwrap();
assert_eq!(state_single.position, 10.0); let state_single_exact = buffer.get_interpolated_state(100).unwrap();
assert_eq!(state_single_exact.position, 10.0);
}
#[test]
fn timestamps_and_clear_interp() {
let mut buffer = SnapshotBuffer::<ClientTimeMs, TestRenderState>::new(3);
assert_eq!(buffer.oldest_timestamp(), None);
assert_eq!(buffer.latest_timestamp(), None);
buffer.add_snapshot(100, TestRenderState { position: 1.0 });
buffer.add_snapshot(200, TestRenderState { position: 2.0 });
assert_eq!(buffer.oldest_timestamp(), Some(100));
assert_eq!(buffer.latest_timestamp(), Some(200));
buffer.clear();
assert!(buffer.is_empty());
assert_eq!(buffer.oldest_timestamp(), None);
}
#[test]
fn interpolation_between_three_points_interp() {
let mut buffer = SnapshotBuffer::<ClientTimeMs, TestRenderState>::new(3);
buffer.add_snapshot(100, TestRenderState { position: 0.0 });
buffer.add_snapshot(200, TestRenderState { position: 100.0 });
buffer.add_snapshot(300, TestRenderState { position: 150.0 });
let interp1 = buffer.get_interpolated_state(150).unwrap();
assert!((interp1.position - 50.0).abs() < f32::EPSILON);
let interp2 = buffer.get_interpolated_state(250).unwrap();
assert!((interp2.position - 125.0).abs() < f32::EPSILON);
let interp_exact_middle = buffer.get_interpolated_state(200).unwrap();
assert!((interp_exact_middle.position - 100.0).abs() < f32::EPSILON);
}
#[test]
fn interpolation_with_duplicate_timestamps_in_buffer() {
let mut buffer = SnapshotBuffer::<ClientTimeMs, TestRenderState>::new(5);
buffer.add_snapshot(100, TestRenderState { position: 10.0 });
buffer.add_snapshot(150, TestRenderState { position: 15.0 }); buffer.add_snapshot(150, TestRenderState { position: 15.5 }); buffer.add_snapshot(200, TestRenderState { position: 20.0 });
let interpolated = buffer.get_interpolated_state(150).unwrap();
assert_eq!(interpolated.position, 15.0, "Should pick the first state if target matches timestamp when multiple exist at that ts from partition_point logic");
let interpolated2 = buffer.get_interpolated_state(175).unwrap();
assert!((interpolated2.position - 17.75).abs() < f32::EPSILON);
}
}
#[cfg(test)]
mod clock_tests {
use super::InterpolationClock;
#[test]
fn it_yields_no_target_until_the_first_observation() {
let clock = InterpolationClock::new(100u64);
assert!(!clock.started());
assert_eq!(clock.target(), None);
}
#[test]
fn the_target_trails_the_estimate_by_the_delay() {
let mut clock = InterpolationClock::new(100u64);
clock.observe(1000);
assert_eq!(clock.target(), Some(900), "1000 observed, rendered 100 in the past");
clock.advance(50);
assert_eq!(clock.target(), Some(950), "advanced by a frame");
}
#[test]
fn only_the_first_observation_sets_the_clock() {
let mut clock = InterpolationClock::new(100u64);
clock.observe(1000);
clock.advance(50);
clock.observe(5000); assert_eq!(clock.target(), Some(950));
}
#[test]
fn the_target_never_goes_before_zero() {
let mut clock = InterpolationClock::new(100u64);
clock.observe(30); assert_eq!(clock.target(), Some(0), "clamped, the buffer clamps to its oldest anyway");
}
#[test]
fn it_works_on_a_duration_timeline() {
use std::time::Duration;
let mut clock = InterpolationClock::new(Duration::from_millis(100));
clock.observe(Duration::from_secs(2));
clock.advance(Duration::from_millis(50));
assert_eq!(clock.target(), Some(Duration::from_millis(1950)));
}
#[test]
fn set_delay_changes_the_target() {
let mut clock = InterpolationClock::new(100u64);
clock.observe(1000);
assert_eq!(clock.target(), Some(900));
clock.set_delay(250);
assert_eq!(clock.delay(), 250);
assert_eq!(clock.target(), Some(750), "the bigger buffer renders further in the past");
}
#[test]
fn resync_starts_the_clock_when_unstarted() {
let mut clock = InterpolationClock::new(100u64);
clock.resync(500, 0.5);
assert_eq!(clock.target(), Some(400));
}
#[test]
fn resync_pulls_a_drifted_estimate_back_toward_the_newest() {
let mut clock = InterpolationClock::new(100u64);
clock.observe(1000);
clock.advance(500);
clock.resync(1100, 0.5);
assert_eq!(clock.target(), Some(1200), "1300 estimate minus 100 delay");
}
#[test]
fn repeated_resync_converges_on_the_stream() {
let mut clock = InterpolationClock::new(100u64);
clock.observe(2000); for _ in 0..50 {
clock.resync(1000, 0.3); }
assert_eq!(clock.target(), Some(900));
}
#[test]
fn observe_rate_starts_the_clock_at_real_time() {
let mut clock = InterpolationClock::new(100u64);
clock.observe_rate(500, 0.1);
assert_eq!(clock.target(), Some(400));
assert_eq!(clock.playback_rate(), 1.0, "no drift yet, so real time");
}
#[test]
fn observe_rate_speeds_up_when_behind_and_slows_when_ahead() {
let mut clock = InterpolationClock::new(100u64);
clock.observe_rate(1000, 0.1);
clock.observe_rate(1100, 0.1);
assert!(clock.playback_rate() > 1.0, "behind the stream: speed up, got {}", clock.playback_rate());
clock.observe_rate(900, 0.1);
assert!(clock.playback_rate() < 1.0, "ahead of the stream: slow down, got {}", clock.playback_rate());
}
#[test]
fn the_rate_adjustment_is_bounded() {
let mut clock = InterpolationClock::new(100u64);
clock.observe_rate(1000, 0.1);
clock.observe_rate(1_000_000, 0.1);
assert!((clock.playback_rate() - 1.1).abs() < 1e-6, "capped at +10%, got {}", clock.playback_rate());
}
#[test]
fn advance_scaled_dilates_by_the_rate() {
let mut clock = InterpolationClock::new(100u64);
clock.observe_rate(1000, 0.1);
clock.observe_rate(1100, 0.1); assert!((clock.playback_rate() - 1.1).abs() < 1e-6);
clock.advance_scaled(100);
assert_eq!(clock.target(), Some(1010));
}
#[test]
fn rate_synced_playback_tracks_a_steady_stream() {
let delay = 100u64;
let mut clock = InterpolationClock::new(delay);
let mut newest = 10_000u64;
clock.observe_rate(newest, 0.1);
for _ in 0..200 {
newest += 50;
clock.observe_rate(newest, 0.1);
clock.advance_scaled(50);
}
let target = clock.target().unwrap();
let ideal = newest - delay;
let gap = (target as i64 - ideal as i64).abs();
assert!(gap < 60, "target tracked the stream within a frame, gap {gap} (target {target}, ideal {ideal})");
}
}