ferravex_net/
history.rs

1//! Bounded authority history and clock estimates; rendering and transport are independent.
2use std::collections::VecDeque;
3#[derive(Clone, Debug)]
4pub struct History<T> {
5    samples: VecDeque<(u64, T)>,
6    capacity: usize,
7}
8impl<T> History<T> {
9    pub fn new(capacity: usize) -> Self {
10        assert!((1..=4096).contains(&capacity));
11        Self {
12            samples: VecDeque::with_capacity(capacity),
13            capacity,
14        }
15    }
16    pub fn push(&mut self, tick: u64, state: T) -> bool {
17        if self.samples.back().is_some_and(|(last, _)| tick <= *last) {
18            return false;
19        }
20        if self.samples.len() == self.capacity {
21            self.samples.pop_front();
22        }
23        self.samples.push_back((tick, state));
24        true
25    }
26    /// No extrapolation outside retained history: old/untrusted shot times are rejected.
27    pub fn bracket(&self, tick: f64) -> Option<(&T, &T, f32)> {
28        if !tick.is_finite()
29            || tick < self.samples.front()?.0 as f64
30            || tick > self.samples.back()?.0 as f64
31        {
32            return None;
33        }
34        for i in 0..self.samples.len() {
35            let (a, left) = &self.samples[i];
36            if tick == *a as f64 {
37                return Some((left, left, 0.0));
38            }
39            if let Some((b, right)) = self.samples.get(i + 1)
40                && tick < *b as f64
41            {
42                return Some((left, right, ((tick - *a as f64) / (*b - *a) as f64) as f32));
43            }
44        }
45        None
46    }
47    pub fn len(&self) -> usize {
48        self.samples.len()
49    }
50    pub fn is_empty(&self) -> bool {
51        self.samples.is_empty()
52    }
53}
54#[derive(Default, Clone, Copy, Debug)]
55pub struct ClockSync {
56    pub offset_seconds: f64,
57    pub rtt_seconds: f64,
58    samples: u32,
59}
60impl ClockSync {
61    /// NTP-style exchange: local send/receive and server receive/send timestamps.
62    pub fn observe(
63        &mut self,
64        local_send: f64,
65        server_receive: f64,
66        server_send: f64,
67        local_receive: f64,
68    ) -> bool {
69        if ![local_send, server_receive, server_send, local_receive]
70            .iter()
71            .all(|v| v.is_finite())
72            || local_receive < local_send
73            || server_send < server_receive
74        {
75            return false;
76        }
77        let rtt = (local_receive - local_send) - (server_send - server_receive);
78        if !(0.0..=5.0).contains(&rtt) {
79            return false;
80        }
81        let offset = ((server_receive - local_send) + (server_send - local_receive)) * 0.5;
82        let weight = if self.samples == 0 { 1.0 } else { 0.125 };
83        self.offset_seconds += (offset - self.offset_seconds) * weight;
84        self.rtt_seconds += (rtt - self.rtt_seconds) * weight;
85        self.samples = self.samples.saturating_add(1);
86        true
87    }
88    pub fn server_time(&self, local: f64) -> f64 {
89        local + self.offset_seconds
90    }
91}
92#[cfg(test)]
93mod tests {
94    use super::*;
95    #[test]
96    fn old_or_future_shots_are_rejected_and_history_interpolates() {
97        let mut h = History::new(3);
98        for i in 0..5 {
99            assert!(h.push(i, i as f32));
100        }
101        assert_eq!(h.len(), 3);
102        assert!(h.bracket(1.0).is_none());
103        assert!(h.bracket(5.0).is_none());
104        assert!(!h.push(3, 0.0));
105        assert_eq!(h.bracket(2.5), Some((&2.0, &3.0, 0.5)));
106        assert_eq!(h.bracket(4.0), Some((&4.0, &4.0, 0.0)));
107    }
108    #[test]
109    fn clock_offset_accounts_for_server_processing() {
110        let mut c = ClockSync::default();
111        assert!(c.observe(1.0, 11.1, 11.2, 1.3));
112        assert!((c.offset_seconds - 10.0).abs() < 1e-9);
113        assert!((c.rtt_seconds - 0.2).abs() < 1e-9);
114        assert!(!c.observe(f64::NAN, 0., 0., 0.));
115    }
116}