Skip to main content

elura_netcode/
interpolation.rs

1use std::collections::VecDeque;
2use std::time::Duration;
3
4use serde::{Deserialize, Serialize};
5
6use crate::{NetcodeError, NetcodeResult};
7
8/// Remote-state buffer and adaptive interpolation-delay parameters.
9#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
10#[serde(default, deny_unknown_fields)]
11#[non_exhaustive]
12pub struct InterpolationConfig {
13    /// Authoritative server simulation frequency.
14    pub tick_rate: u32,
15    /// Maximum remote states retained in Tick order.
16    pub capacity: usize,
17    /// Normal render delay before measured jitter or late arrivals are added.
18    pub base_delay_ticks: f64,
19    /// Minimum adaptive render delay.
20    pub min_delay_ticks: f64,
21    /// Maximum adaptive render delay.
22    pub max_delay_ticks: f64,
23    /// Exponential smoothing weight for arrival jitter and late-sample pressure.
24    pub smoothing: f64,
25    /// Number of additional delay ticks added per measured jitter Tick.
26    pub jitter_multiplier: f64,
27    /// Maximum additional delay caused by repeatedly late samples.
28    pub late_sample_penalty_ticks: f64,
29    /// Maximum interpolation-delay movement after one newest-state observation.
30    pub max_adjustment_per_sample_ticks: f64,
31}
32
33impl Default for InterpolationConfig {
34    fn default() -> Self {
35        Self {
36            tick_rate: 30,
37            capacity: 128,
38            base_delay_ticks: 2.0,
39            min_delay_ticks: 1.0,
40            max_delay_ticks: 8.0,
41            smoothing: 0.1,
42            jitter_multiplier: 2.0,
43            late_sample_penalty_ticks: 2.0,
44            max_adjustment_per_sample_ticks: 0.25,
45        }
46    }
47}
48
49impl InterpolationConfig {
50    /// Validates Tick, capacity, smoothing, and delay bounds.
51    pub fn validate(&self) -> NetcodeResult<()> {
52        let values = [
53            self.base_delay_ticks,
54            self.min_delay_ticks,
55            self.max_delay_ticks,
56            self.smoothing,
57            self.jitter_multiplier,
58            self.late_sample_penalty_ticks,
59            self.max_adjustment_per_sample_ticks,
60        ];
61        if !(1..=240).contains(&self.tick_rate) || self.capacity < 2 {
62            return Err(NetcodeError::InvalidConfig(
63                "interpolation Tick rate or capacity is invalid",
64            ));
65        }
66        if values
67            .iter()
68            .any(|value| !value.is_finite() || *value < 0.0)
69            || self.smoothing <= 0.0
70            || self.smoothing > 1.0
71            || self.min_delay_ticks > self.base_delay_ticks
72            || self.base_delay_ticks > self.max_delay_ticks
73            || self.max_adjustment_per_sample_ticks == 0.0
74        {
75            return Err(NetcodeError::InvalidConfig(
76                "interpolation delay parameters are invalid",
77            ));
78        }
79        Ok(())
80    }
81}
82
83/// Result of inserting one remote state.
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub enum InterpolationInsert {
86    /// A state newer than every buffered Tick was inserted.
87    Newest,
88    /// A previously missing older Tick was inserted out of order.
89    Late,
90    /// A duplicate Tick replaced its buffered state.
91    Replaced,
92}
93
94/// Current interpolation timing statistics.
95#[derive(Debug, Clone, Copy, PartialEq)]
96pub struct InterpolationStats {
97    /// Smoothed absolute arrival variation expressed in simulation ticks.
98    pub jitter_ticks: f64,
99    /// Smoothed fraction of observations that arrived behind the newest buffered Tick.
100    pub late_sample_pressure: f64,
101    /// Current adaptive render delay.
102    pub delay_ticks: f64,
103    /// Number of out-of-order states inserted.
104    pub late_samples: u64,
105    /// Number of duplicate Tick states replaced.
106    pub replaced_samples: u64,
107}
108
109/// Two buffered states and interpolation factor selected for rendering.
110#[derive(Debug, Clone, Copy)]
111pub struct InterpolationSample<'a, S> {
112    /// Fractional server Tick selected for rendering.
113    pub render_tick: f64,
114    /// Tick of the earlier state.
115    pub previous_tick: u64,
116    /// Earlier state supplied to application interpolation.
117    pub previous: &'a S,
118    /// Tick of the later state.
119    pub next_tick: u64,
120    /// Later state supplied to application interpolation.
121    pub next: &'a S,
122    /// Clamped blend factor between `previous` and `next`.
123    pub alpha: f64,
124    /// `true` when rendering is ahead of the newest state and the newest state is held.
125    pub holding_newest: bool,
126}
127
128/// Tick-ordered remote state buffer with adaptive jitter delay.
129#[derive(Debug, Clone)]
130pub struct InterpolationBuffer<S> {
131    config: InterpolationConfig,
132    states: VecDeque<(u64, S)>,
133    last_arrival: Option<Duration>,
134    last_newest: Option<(u64, Duration)>,
135    jitter_ticks: f64,
136    late_pressure: f64,
137    delay_ticks: f64,
138    late_samples: u64,
139    replaced_samples: u64,
140}
141
142impl<S> InterpolationBuffer<S> {
143    /// Creates an empty remote-state buffer.
144    pub fn new(config: InterpolationConfig) -> NetcodeResult<Self> {
145        config.validate()?;
146        Ok(Self {
147            config,
148            states: VecDeque::with_capacity(config.capacity),
149            last_arrival: None,
150            last_newest: None,
151            jitter_ticks: 0.0,
152            late_pressure: 0.0,
153            delay_ticks: config.base_delay_ticks,
154            late_samples: 0,
155            replaced_samples: 0,
156        })
157    }
158
159    /// Inserts one remote state observed at a local monotonic arrival time.
160    pub fn insert(
161        &mut self,
162        tick: u64,
163        state: S,
164        arrived_at: Duration,
165    ) -> NetcodeResult<InterpolationInsert> {
166        if tick == 0 {
167            return Err(NetcodeError::InvalidInput(
168                "interpolation state Tick must be positive",
169            ));
170        }
171        if self
172            .last_arrival
173            .is_some_and(|last_arrival| arrived_at < last_arrival)
174        {
175            return Err(NetcodeError::InvalidSample(
176                "interpolation arrival time moved backwards",
177            ));
178        }
179        let newest = self.states.back().map(|(tick, _)| *tick);
180        let state_index = self
181            .states
182            .binary_search_by_key(&tick, |(buffered_tick, _)| *buffered_tick);
183        let disposition = match state_index {
184            Ok(_) => {
185                self.replaced_samples = self.replaced_samples.saturating_add(1);
186                InterpolationInsert::Replaced
187            }
188            Err(_) if newest.is_some_and(|newest| tick < newest) => {
189                self.late_samples = self.late_samples.saturating_add(1);
190                InterpolationInsert::Late
191            }
192            Err(_) => InterpolationInsert::Newest,
193        };
194
195        let late_observation = matches!(disposition, InterpolationInsert::Late);
196        self.late_pressure +=
197            ((u8::from(late_observation) as f64) - self.late_pressure) * self.config.smoothing;
198        if matches!(disposition, InterpolationInsert::Newest) {
199            if let Some((previous_tick, previous_arrival)) = self.last_newest {
200                let tick_gap = tick.saturating_sub(previous_tick);
201                if tick_gap > 0 {
202                    let actual = arrived_at.saturating_sub(previous_arrival).as_secs_f64();
203                    let expected = tick_gap as f64 / f64::from(self.config.tick_rate);
204                    let variation_ticks =
205                        (actual - expected).abs() * f64::from(self.config.tick_rate);
206                    self.jitter_ticks +=
207                        (variation_ticks - self.jitter_ticks) * self.config.smoothing;
208                }
209            }
210            self.last_newest = Some((tick, arrived_at));
211        }
212        self.last_arrival = Some(arrived_at);
213
214        let target_delay = (self.config.base_delay_ticks
215            + self.jitter_ticks * self.config.jitter_multiplier
216            + self.late_pressure * self.config.late_sample_penalty_ticks)
217            .clamp(self.config.min_delay_ticks, self.config.max_delay_ticks);
218        let adjustment = (target_delay - self.delay_ticks).clamp(
219            -self.config.max_adjustment_per_sample_ticks,
220            self.config.max_adjustment_per_sample_ticks,
221        );
222        self.delay_ticks += adjustment;
223
224        match state_index {
225            Ok(index) => self.states[index].1 = state,
226            Err(index) => self.states.insert(index, (tick, state)),
227        }
228        if self.states.len() > self.config.capacity {
229            self.states.pop_front();
230        }
231        Ok(disposition)
232    }
233
234    /// Selects buffered states around the adaptive delayed render Tick.
235    pub fn sample(&self, estimated_server_tick: f64) -> NetcodeResult<InterpolationSample<'_, S>> {
236        if !estimated_server_tick.is_finite() || estimated_server_tick < 0.0 {
237            return Err(NetcodeError::InvalidSample(
238                "estimated server Tick must be finite and non-negative",
239            ));
240        }
241        let Some((oldest_tick, oldest)) = self.states.front() else {
242            return Err(NetcodeError::InterpolationBufferEmpty);
243        };
244        let (newest_tick, newest) = self.states.back().unwrap();
245        let render_tick = (estimated_server_tick - self.delay_ticks).max(0.0);
246        if render_tick <= *oldest_tick as f64 {
247            return Ok(InterpolationSample {
248                render_tick,
249                previous_tick: *oldest_tick,
250                previous: oldest,
251                next_tick: *oldest_tick,
252                next: oldest,
253                alpha: 0.0,
254                holding_newest: false,
255            });
256        }
257        if render_tick >= *newest_tick as f64 {
258            return Ok(InterpolationSample {
259                render_tick,
260                previous_tick: *newest_tick,
261                previous: newest,
262                next_tick: *newest_tick,
263                next: newest,
264                alpha: 0.0,
265                holding_newest: render_tick > *newest_tick as f64,
266            });
267        }
268
269        let previous_bound = render_tick.floor() as u64;
270        let next_bound = render_tick.ceil() as u64;
271        let previous_index = self
272            .states
273            .binary_search_by_key(&previous_bound, |(tick, _)| *tick)
274            .unwrap_or_else(|index| index - 1);
275        let next_index = self
276            .states
277            .binary_search_by_key(&next_bound, |(tick, _)| *tick)
278            .unwrap_or_else(|index| index);
279        let (previous_tick, previous) = &self.states[previous_index];
280        let (next_tick, next) = &self.states[next_index];
281        let span = next_tick.saturating_sub(*previous_tick);
282        let alpha = if span == 0 {
283            0.0
284        } else {
285            ((render_tick - *previous_tick as f64) / span as f64).clamp(0.0, 1.0)
286        };
287        Ok(InterpolationSample {
288            render_tick,
289            previous_tick: *previous_tick,
290            previous,
291            next_tick: *next_tick,
292            next,
293            alpha,
294            holding_newest: false,
295        })
296    }
297
298    /// Returns current jitter and adaptive-delay measurements.
299    pub fn stats(&self) -> InterpolationStats {
300        InterpolationStats {
301            jitter_ticks: self.jitter_ticks,
302            late_sample_pressure: self.late_pressure,
303            delay_ticks: self.delay_ticks,
304            late_samples: self.late_samples,
305            replaced_samples: self.replaced_samples,
306        }
307    }
308
309    /// Returns the number of buffered remote states.
310    pub fn len(&self) -> usize {
311        self.states.len()
312    }
313
314    /// Returns whether no remote state has arrived.
315    pub fn is_empty(&self) -> bool {
316        self.states.is_empty()
317    }
318
319    /// Clears buffered states and timing measurements.
320    pub fn reset(&mut self) {
321        self.states.clear();
322        self.last_arrival = None;
323        self.last_newest = None;
324        self.jitter_ticks = 0.0;
325        self.late_pressure = 0.0;
326        self.delay_ticks = self.config.base_delay_ticks;
327        self.late_samples = 0;
328        self.replaced_samples = 0;
329    }
330}