Created
November 28, 2022 19:45
-
-
Save gamemachine/4b2b73a2c5725535946ba857aa323d3d to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use std::num::Wrapping; | |
use bincode::{Encode, Decode}; | |
// Time is monotonic time (OS ticks) not clock time. The difference is precision vs accuracy. | |
// clock time can have huge variances, be different by over 100ms even when queried within a few ms of each other. Accurate but not precise. | |
// OS ticks are very precise over short intervals. But can drift very quickly over longer periods. | |
// We consume snapshots in sequence order, possibly skipping missing sequences. | |
// The interpolation state time represents our progression through the server timeline. Our server timeline is based on received snapshots. We advance through this timeline using local delta time. | |
// The key is that both client and server are using monotonic time using the same time units. | |
// We can and will drift over time relative to server time due to the following: | |
// - monotonic time drift | |
// - packet loss | |
// - network latency | |
// - server lag | |
//The snapshot buffer is solving for a different problem. Always having something to interpolate towards even if you have packet loss. We are intentionally adding latency to gain smoothness of motion. | |
// If our point on the timeline gets too far behind, new snapshots will wrap around and run past our buffer position. Eventually resulting in a big leap forward in time. | |
// Note that our timeline is represented by snapshots we have actually received. | |
// This case is simple we speed up our movement through the timeline based on some threshold. | |
// If we hit the end of our timeline we no longer have a position to interpolate towards. This can happen naturally just from monotonic time drift. Server lag and network latency impact this also. | |
// Here we can adapt by using larger snapshot buffers, and/or slowing our movement through the server timeline dynamically. | |
#[derive(Clone, Copy, Default, Debug, Encode, Decode)] | |
#[repr(C)] | |
pub struct Snapshot { | |
pub time: f64, | |
pub sequence: u16 | |
} | |
impl Snapshot { | |
pub fn new(time: f64, sequence: u16) -> Self { | |
Snapshot { | |
time, | |
sequence | |
} | |
} | |
} | |
#[derive(Clone, Copy, Debug)] | |
pub struct SnapshotSequence { | |
pub sequence: Wrapping<u16> | |
} | |
impl SnapshotSequence { | |
pub fn new(initial_value: u16) -> Self { | |
SnapshotSequence { | |
sequence: Wrapping(initial_value) | |
} | |
} | |
pub fn increment(sequence: u16) -> u16 { | |
let mut wrapper = Wrapping(sequence); | |
wrapper += 1; | |
wrapper.0 | |
} | |
pub fn current(&self) -> u16 { | |
self.sequence.0 | |
} | |
pub fn next(&mut self) -> u16 { | |
self.sequence += 1; | |
self.sequence.0 | |
} | |
} | |
pub struct SnapshotBuffer { | |
pub buffer: Vec<Option<Snapshot>>, | |
last_sequence: Option<u16>, | |
playout_delay: u16 | |
} | |
impl SnapshotBuffer { | |
pub const BUFFER_LEN: usize = 256; | |
pub fn new(playout_delay: u16) -> Self { | |
let mut buffer = SnapshotBuffer { | |
buffer: Vec::new(), | |
last_sequence: None, | |
playout_delay | |
}; | |
for _ in 0..Self::BUFFER_LEN { | |
buffer.buffer.push(None); | |
} | |
buffer | |
} | |
pub fn last(&self) -> Option<&Snapshot> { | |
if let Some(last_sequence) = self.last_sequence { | |
self.get(last_sequence) | |
} else { | |
None | |
} | |
} | |
pub fn index(&self, sequence: u16) -> usize { | |
(sequence % Self::BUFFER_LEN as u16).into() | |
} | |
fn get_previous(&self, sequence: u16) -> Option<&Snapshot> { | |
let mut prev_seq = Wrapping(sequence); | |
prev_seq -= 1; | |
let prev_index = self.index(prev_seq.0); | |
self.buffer[prev_index].as_ref() | |
} | |
// add a snapshot. Must have a timestamp later then the snapshot associated with the previous sequence number | |
// Since we wrap our buffers it's possible for very old snapshots to arrive and get inserted over newer ones unless we check timestamps | |
pub fn push(&mut self, snap: Snapshot) -> Option<usize> { | |
if let Some(previous) = self.get_previous(snap.sequence) { | |
if snap.time <= previous.time { | |
return None; | |
} | |
} | |
let index = self.index(snap.sequence); | |
self.buffer[index] = Some(snap); | |
self.last_sequence = Some(snap.sequence); | |
Some(index) | |
} | |
pub fn get(&self, sequence: u16) -> Option<&Snapshot> { | |
let index = self.index(sequence); | |
self.buffer[index].as_ref() | |
} | |
/// get a starting snapshot that is at least playout_delay sequences ago | |
pub fn get_start(&self) -> Option<&Snapshot> { | |
if let Some(last_sequence) = self.last_sequence { | |
let last = Wrapping(last_sequence); | |
let start = last - Wrapping(self.playout_delay); | |
let index = self.index(start.0); | |
self.buffer[index].as_ref() | |
} else { | |
None | |
} | |
} | |
// get the next snapshot that is timestamped after this one. Look forward the entire buffer len. | |
pub fn get_next(&self, sequence: u16) -> Option<&Snapshot> { | |
if let Some(current) = self.get(sequence) { | |
let len = Self::BUFFER_LEN as u16; | |
for i in 0..len { | |
let next = Wrapping(sequence + i + 1); | |
let index = self.index(next.0); | |
if let Some(snap) = &self.buffer[index] { | |
if snap.time > current.time { | |
return Some(snap); | |
} | |
} | |
} | |
} | |
return None | |
} | |
} | |
#[derive(Clone, Copy, Default, Debug)] | |
#[repr(C)] | |
pub struct Interpolation { | |
pub start: Snapshot, | |
pub end: Snapshot, | |
pub t: f64, | |
pub time: f64 | |
} | |
pub struct InterpolationState { | |
start: Option<Snapshot>, | |
end: Option<Snapshot>, | |
time: f64, | |
buffer: SnapshotBuffer | |
} | |
impl InterpolationState { | |
pub fn new(buffer: SnapshotBuffer) -> Self { | |
InterpolationState { | |
start: None, | |
end: None, | |
time: 0.0, | |
buffer | |
} | |
} | |
pub fn has_start(&self) -> bool { | |
self.start.is_some() | |
} | |
pub fn has_end(&self) -> bool { | |
self.end.is_some() | |
} | |
pub fn add_snapshot(&mut self, snap: Snapshot) -> Option<usize> { | |
self.buffer.push(snap) | |
} | |
pub fn interpolate(&mut self, delta_time: f64) -> Option<Interpolation> { | |
self.initialize_if_needed(); | |
if self.start.is_none() && self.end.is_none() { | |
return None; | |
} | |
self.update_end(); | |
if let Some(start) = &self.start { | |
if let Some(end) = &self.end { | |
let a = self.time - start.time; | |
let b = end.time - start.time; | |
let t = f64::clamp(a / b, 0.0, 1.0); | |
let result = Interpolation { | |
start: *start, | |
end: *end, | |
t, | |
time: self.time | |
}; | |
self.time += delta_time; | |
return Some(result); | |
} | |
} | |
None | |
} | |
// initially we have no start/end and time is not set. | |
fn initialize_if_needed(&mut self) { | |
if self.start.is_none() && self.end.is_none() { | |
if let Some(start) = self.buffer.get_start() { | |
if let Some(end) = self.buffer.get_next(start.sequence) { | |
self.time = start.time; | |
self.start = Some(*start); | |
self.end = Some(*end); | |
} | |
} | |
} | |
} | |
// if we have interpolated to/past the end snapshot, we set the end snapshot as our new start, and try to find a new end in the snapshot buffer. | |
fn update_end(&mut self) { | |
if self.start.is_some() { | |
if let Some(end) = self.end { | |
if self.time >= end.time { | |
self.start = Some(end); | |
self.end = None; | |
} | |
} | |
} | |
if self.end.is_none() { | |
if let Some(start) = self.start { | |
if let Some(next) = self.buffer.get_next(start.sequence) { | |
self.end = Some(*next); | |
} | |
} | |
} | |
} | |
} | |
#[cfg(test)] | |
mod tests { | |
use crate::interpolation::{SnapshotSequence}; | |
use super::{SnapshotBuffer, Snapshot, InterpolationState}; | |
#[test] | |
fn interpolate() { | |
let buffer = SnapshotBuffer::new(3); | |
let mut state = InterpolationState::new(buffer); | |
state.add_snapshot(Snapshot::new(1.0, 0)); | |
state.add_snapshot(Snapshot::new(2.0, 1)); | |
state.add_snapshot(Snapshot::new(3.0, 2)); | |
state.add_snapshot(Snapshot::new(4.0, 3)); | |
let result = state.interpolate(1.02).unwrap(); | |
assert_eq!(0, result.start.sequence); | |
assert_eq!(1, result.end.sequence); | |
let result = state.interpolate(1.02).unwrap(); | |
assert_eq!(1, result.start.sequence); | |
assert_eq!(2, result.end.sequence); | |
let result = state.interpolate(1.02).unwrap(); | |
assert_eq!(2, result.start.sequence); | |
assert_eq!(3, result.end.sequence); | |
let result = state.interpolate(1.02); | |
assert!(result.is_none()); | |
state.add_snapshot(Snapshot::new(5.0, 4)); | |
let result = state.interpolate(1.02).unwrap(); | |
assert_eq!(3, result.start.sequence); | |
assert_eq!(4, result.end.sequence); | |
// skip a sequence | |
state.add_snapshot(Snapshot::new(6.0, 6)); | |
let result = state.interpolate(1.02).unwrap(); | |
assert_eq!(4, result.start.sequence); | |
assert_eq!(6, result.end.sequence); | |
let result = state.interpolate(1.02); | |
assert!(result.is_none()); | |
assert!(state.has_start()); | |
assert!(!state.has_end()); | |
} | |
#[test] | |
fn snapshot_sequence() { | |
let mut wrapping = SnapshotSequence::new(u16::MAX); | |
assert_eq!(0, wrapping.next()); | |
assert_eq!(1, wrapping.next()); | |
} | |
#[test] | |
fn push_validation() { | |
let mut buffer = SnapshotBuffer::new(2); | |
let _index = buffer.push(Snapshot::new(0.0, 0)).unwrap(); | |
let _index = buffer.push(Snapshot::new(1.0, 1)).unwrap(); | |
let _index = buffer.push(Snapshot::new(2.0, 2)).unwrap(); | |
let _index = buffer.push(Snapshot::new(3.0, 3)).unwrap(); | |
let _index = buffer.push(Snapshot::new(4.0, 4)).unwrap(); | |
let _index = buffer.push(Snapshot::new(5.0, 5)).unwrap(); | |
// too old | |
assert!(buffer.push(Snapshot::new(4.0, 5)).is_none()); | |
assert!(buffer.push(Snapshot::new(4.0, 6)).is_none()); | |
assert!(buffer.push(Snapshot::new(6.0, 6)).is_some()); | |
assert!(buffer.push(Snapshot::new(7.0, 7)).is_some()); | |
// wraps at max len | |
let _index = buffer.push(Snapshot::new(8.0, SnapshotBuffer::BUFFER_LEN as u16)).unwrap(); | |
assert_eq!(0, _index); | |
} | |
#[test] | |
fn next_skips_missing_snapshot() { | |
let mut buffer = SnapshotBuffer::new(2); | |
buffer.push(Snapshot::new(1.0, 0)); | |
buffer.push(Snapshot::new(3.0, 3)); | |
let end = buffer.get_next(0).unwrap(); | |
assert_eq!(3, end.sequence); | |
} | |
#[test] | |
fn get_next_valid_ranges() { | |
let mut buffer = SnapshotBuffer::new(2); | |
buffer.push(Snapshot::new(1.0, 0)); | |
let seq = (SnapshotBuffer::BUFFER_LEN - 1) as u16; | |
buffer.push(Snapshot::new(3.0, seq)); | |
let next = buffer.get_next(0).unwrap(); | |
assert_eq!(255, next.sequence); | |
let mut buffer = SnapshotBuffer::new(2); | |
buffer.push(Snapshot::new(1.0, 0)); | |
let seq = SnapshotBuffer::BUFFER_LEN as u16; | |
buffer.push(Snapshot::new(3.0, seq)); | |
let next = buffer.get_next(0); | |
assert!(next.is_none()); | |
} | |
#[test] | |
fn get_start_enough_history() { | |
let mut buffer = SnapshotBuffer::new(2); | |
buffer.push(Snapshot::new(1.0, 0)); | |
buffer.push(Snapshot::new(2.0, 1)); | |
// not enough history | |
let start = buffer.get_start(); | |
assert!(start.is_none()); | |
buffer.push(Snapshot::new(3.0, 2)); | |
let start = buffer.get_start().unwrap(); | |
assert_eq!(0, start.sequence); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment