use std::fmt::Debug;
use crate::extrapolation::{Extrapolatable, ExtrapolationBase};
use crate::interpolation::{Interpolatable, SnapshotBuffer};
#[derive(Debug, Clone, Copy)]
pub struct RenderOpts {
pub interpolate: bool,
pub extrapolate: bool,
}
impl Default for RenderOpts {
fn default() -> Self {
Self {
interpolate: true,
extrapolate: true,
}
}
}
#[derive(Debug, Clone)]
pub struct RemoteView<State: Clone + Debug, Velocity: Clone + Debug> {
buffer: SnapshotBuffer<u64, State>,
latest: Option<(u64, State, Velocity)>,
max_extrapolation_ms: u64,
over_extrapolations: std::cell::Cell<u64>,
}
impl<State, Velocity> RemoteView<State, Velocity>
where
State: Interpolatable<u64> + Extrapolatable<Velocity, f32> + Clone + Debug,
Velocity: Clone + Debug,
{
pub fn new(buffer_size: usize, max_extrapolation_ms: u64) -> Self {
Self {
buffer: SnapshotBuffer::new(buffer_size),
latest: None,
max_extrapolation_ms,
over_extrapolations: std::cell::Cell::new(0),
}
}
pub fn push(&mut self, time_ms: u64, state: State, velocity: Velocity) {
self.buffer.add_snapshot(time_ms, state.clone());
if self.latest.as_ref().is_none_or(|(t, _, _)| time_ms >= *t) {
self.latest = Some((time_ms, state, velocity));
}
}
pub fn render(&self, target: Option<u64>, opts: RenderOpts) -> Option<State> {
let (_, latest_state, latest_vel) = self.latest.as_ref()?;
if !opts.interpolate {
return Some(latest_state.clone());
}
let Some(t) = target else {
return Some(latest_state.clone());
};
match self.buffer.latest_timestamp() {
Some(newest) if opts.extrapolate && t > newest => {
let base = ExtrapolationBase::new(latest_state.clone(), latest_vel.clone(), newest, newest);
let state = base
.get_extrapolated_state(t, self.max_extrapolation_ms, |ms| ms as f32 / 1000.0)
.unwrap_or_else(|| latest_state.clone());
self.over_extrapolations.set(self.over_extrapolations.get() + base.over_extrapolations());
Some(state)
}
_ => Some(self.buffer.get_interpolated_state(t).unwrap_or_else(|| latest_state.clone())),
}
}
pub fn over_extrapolations(&self) -> u64 {
self.over_extrapolations.get()
}
pub fn latest(&self) -> Option<&State> {
self.latest.as_ref().map(|(_, s, _)| s)
}
pub fn oldest_timestamp(&self) -> Option<u64> {
self.buffer.oldest_timestamp()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Clone, Debug, PartialEq)]
struct S {
x: f32,
}
impl Interpolatable<u64> for S {
fn interpolate(&self, other: &Self, t: f32, _a: u64, _b: u64) -> Self {
S {
x: self.x + (other.x - self.x) * t,
}
}
}
impl Extrapolatable<f32, f32> for S {
fn extrapolate_with_velocity(&self, velocity: &f32, dt_secs: f32) -> Self {
S {
x: self.x + velocity * dt_secs,
}
}
}
fn view() -> RemoteView<S, f32> {
RemoteView::new(8, 500)
}
#[test]
fn nothing_renders_before_the_first_push() {
let v = view();
assert!(v.render(Some(100), RenderOpts::default()).is_none());
}
#[test]
fn it_interpolates_between_snapshots() {
let mut v = view();
v.push(100, S { x: 10.0 }, 0.0);
v.push(200, S { x: 20.0 }, 0.0);
let at = v.render(Some(150), RenderOpts { interpolate: true, extrapolate: false }).unwrap();
assert!((at.x - 15.0).abs() < 1e-3, "halfway is 15, got {}", at.x);
}
#[test]
fn interpolation_off_renders_the_raw_newest() {
let mut v = view();
v.push(100, S { x: 10.0 }, 0.0);
v.push(200, S { x: 20.0 }, 0.0);
let raw = v.render(Some(150), RenderOpts { interpolate: false, extrapolate: false }).unwrap();
assert_eq!(raw.x, 20.0, "newest snapshot, not interpolated");
}
#[test]
fn it_dead_reckons_past_the_newest_when_asked() {
let mut v = view();
v.push(100, S { x: 0.0 }, 10.0); v.push(200, S { x: 1.0 }, 10.0);
let far = v.render(Some(700), RenderOpts { interpolate: true, extrapolate: true }).unwrap();
assert!((far.x - 6.0).abs() < 0.2, "extrapolated to ~6, got {}", far.x);
let held = v.render(Some(700), RenderOpts { interpolate: true, extrapolate: false }).unwrap();
assert!((held.x - 1.0).abs() < 1e-3, "held at the newest, got {}", held.x);
}
#[test]
fn a_late_out_of_order_snapshot_does_not_become_the_newest() {
let mut v = view();
v.push(200, S { x: 20.0 }, 1.0);
v.push(100, S { x: 10.0 }, 9.0);
assert_eq!(v.latest().unwrap().x, 20.0);
let at = v.render(Some(150), RenderOpts { interpolate: true, extrapolate: false }).unwrap();
assert!((at.x - 15.0).abs() < 1e-3, "interpolates across the reordered pair, got {}", at.x);
}
#[test]
fn duplicate_timestamps_do_not_panic() {
let mut v = view();
v.push(100, S { x: 10.0 }, 0.0);
v.push(100, S { x: 11.0 }, 0.0);
v.push(200, S { x: 20.0 }, 0.0);
let at = v.render(Some(150), RenderOpts::default());
assert!(at.is_some_and(|s| s.x.is_finite()));
}
#[test]
fn a_single_snapshot_renders_that_snapshot() {
let mut v = view();
v.push(100, S { x: 7.0 }, 0.0);
let at = v.render(Some(150), RenderOpts::default()).unwrap();
assert_eq!(at.x, 7.0);
}
#[test]
fn renders_held_at_the_cap_are_counted_on_the_view() {
let mut v = view(); v.push(100, S { x: 0.0 }, 10.0);
let opts = RenderOpts { interpolate: true, extrapolate: true };
assert_eq!(v.over_extrapolations(), 0);
let _ = v.render(Some(400), opts);
assert_eq!(v.over_extrapolations(), 0, "inside the cap");
let _ = v.render(Some(5000), opts);
let _ = v.render(Some(9000), opts);
assert_eq!(v.over_extrapolations(), 2);
}
#[test]
fn extrapolation_holds_at_the_cap_rather_than_flying_off_or_snapping_back() {
let mut v = view(); v.push(100, S { x: 0.0 }, 10.0);
v.push(200, S { x: 1.0 }, 10.0);
let opts = RenderOpts { interpolate: true, extrapolate: true };
let at_cap = v.render(Some(700), opts).unwrap();
let far = v.render(Some(2200), opts).unwrap();
assert!((far.x - at_cap.x).abs() < 1e-3, "past the cap it must hold steady: {} then {}", at_cap.x, far.x);
assert!(far.x > 1.0, "and hold at the cap, not back at the newest sample: {}", far.x);
let inside = v.render(Some(699), opts).unwrap();
let outside = v.render(Some(701), opts).unwrap();
assert!((outside.x - inside.x).abs() < 0.05, "crossing the cap jumped: {} to {}", inside.x, outside.x);
}
}