1use std::ops::{Add, Sub};
8use std::time::Duration;
9
10#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
13pub struct StateTime(pub i64);
14
15impl StateTime {
16 pub const ZERO: StateTime = StateTime(0);
17 pub const MAX: StateTime = StateTime(i64::MAX);
18 pub const MIN: StateTime = StateTime(i64::MIN);
19
20 #[inline]
21 pub fn from_micros(micros: i64) -> Self {
22 StateTime(micros)
23 }
24
25 #[inline]
26 pub fn from_millis(millis: i64) -> Self {
27 StateTime(millis * 1000)
28 }
29
30 #[inline]
31 pub fn from_secs_f64(secs: f64) -> Self {
32 StateTime((secs * 1_000_000.0) as i64)
33 }
34
35 #[inline]
36 pub fn as_micros(self) -> i64 {
37 self.0
38 }
39
40 #[inline]
41 pub fn as_millis(self) -> i64 {
42 self.0 / 1000
43 }
44
45 #[inline]
46 pub fn as_secs_f64(self) -> f64 {
47 self.0 as f64 / 1_000_000.0
48 }
49
50 #[inline]
52 pub fn to_wire_offset(self, reference: StateTime) -> i32 {
53 let diff_100us = (self.0 - reference.0) / 100;
54 diff_100us.clamp(i32::MIN as i64, i32::MAX as i64) as i32
55 }
56
57 #[inline]
59 pub fn from_wire_offset(reference: StateTime, offset_100us: i32) -> Self {
60 StateTime(reference.0 + (offset_100us as i64 * 100))
61 }
62
63 #[inline]
64 pub fn saturating_add(self, duration: Duration) -> Self {
65 StateTime(self.0.saturating_add(duration.as_micros() as i64))
66 }
67
68 #[inline]
69 pub fn saturating_sub(self, duration: Duration) -> Self {
70 StateTime(self.0.saturating_sub(duration.as_micros() as i64))
71 }
72}
73
74impl Add<Duration> for StateTime {
75 type Output = StateTime;
76
77 #[inline]
78 fn add(self, rhs: Duration) -> Self::Output {
79 StateTime(self.0 + rhs.as_micros() as i64)
80 }
81}
82
83impl Sub<Duration> for StateTime {
84 type Output = StateTime;
85
86 #[inline]
87 fn sub(self, rhs: Duration) -> Self::Output {
88 StateTime(self.0 - rhs.as_micros() as i64)
89 }
90}
91
92impl Sub<StateTime> for StateTime {
93 type Output = Duration;
94
95 #[inline]
96 fn sub(self, rhs: StateTime) -> Self::Output {
97 let diff = self.0 - rhs.0;
98 if diff >= 0 {
99 Duration::from_micros(diff as u64)
100 } else {
101 Duration::ZERO
102 }
103 }
104}
105
106impl std::fmt::Debug for StateTime {
107 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108 write!(f, "τs({:.3}ms)", self.as_millis() as f64)
109 }
110}
111
112#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
115pub struct PerceptualTime(pub u64);
116
117impl PerceptualTime {
118 pub const ZERO: PerceptualTime = PerceptualTime(0);
119
120 #[inline]
121 pub fn from_micros(micros: u64) -> Self {
122 PerceptualTime(micros)
123 }
124
125 #[inline]
126 pub fn from_millis(millis: u64) -> Self {
127 PerceptualTime(millis * 1000)
128 }
129
130 #[inline]
131 pub fn from_secs_f64(secs: f64) -> Self {
132 PerceptualTime((secs * 1_000_000.0) as u64)
133 }
134
135 #[inline]
136 pub fn as_micros(self) -> u64 {
137 self.0
138 }
139
140 #[inline]
141 pub fn as_millis(self) -> u64 {
142 self.0 / 1000
143 }
144
145 #[inline]
146 pub fn as_secs_f64(self) -> f64 {
147 self.0 as f64 / 1_000_000.0
148 }
149
150 #[inline]
151 pub fn saturating_add(self, duration: Duration) -> Self {
152 PerceptualTime(self.0.saturating_add(duration.as_micros() as u64))
153 }
154}
155
156impl Add<Duration> for PerceptualTime {
157 type Output = PerceptualTime;
158
159 #[inline]
160 fn add(self, rhs: Duration) -> Self::Output {
161 PerceptualTime(self.0 + rhs.as_micros() as u64)
162 }
163}
164
165impl Sub<PerceptualTime> for PerceptualTime {
166 type Output = Duration;
167
168 #[inline]
169 fn sub(self, rhs: PerceptualTime) -> Self::Output {
170 Duration::from_micros(self.0.saturating_sub(rhs.0))
171 }
172}
173
174impl std::fmt::Debug for PerceptualTime {
175 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176 write!(f, "τp({:.3}ms)", self.as_millis() as f64)
177 }
178}
179
180#[derive(Clone, Copy, Debug)]
183pub struct RealityWindow {
184 pub τs: StateTime,
186 pub hc: Duration,
188 pub hp: Duration,
190}
191
192impl RealityWindow {
193 pub fn new(τs: StateTime, hc: Duration, hp: Duration) -> Self {
194 RealityWindow { τs, hc, hp }
195 }
196
197 #[inline]
199 pub fn left(&self) -> StateTime {
200 self.τs.saturating_sub(self.hc)
201 }
202
203 #[inline]
205 pub fn right(&self) -> StateTime {
206 self.τs.saturating_add(self.hp)
207 }
208
209 #[inline]
211 pub fn contains(&self, t: StateTime) -> bool {
212 t >= self.left() && t <= self.right()
213 }
214
215 pub fn classify(&self, t: StateTime) -> TimePosition {
217 if t < self.left() {
218 TimePosition::TooLate
219 } else if t < self.τs {
220 TimePosition::Correctable
221 } else if t <= self.τs + Duration::from_millis(5) {
222 TimePosition::Current
223 } else if t <= self.right() {
224 TimePosition::Predictable
225 } else {
226 TimePosition::TooEarly
227 }
228 }
229}
230
231#[derive(Clone, Copy, Debug, PartialEq, Eq)]
233pub enum TimePosition {
234 TooLate,
236 Correctable,
238 Current,
240 Predictable,
242 TooEarly,
244}
245
246#[derive(Clone, Copy, Debug, Default)]
248pub struct TimeIntent {
249 pub τs_offset: i32,
251 pub deadline: Option<i32>,
253}
254
255impl TimeIntent {
256 pub fn new(τs_offset: i32) -> Self {
257 TimeIntent {
258 τs_offset,
259 deadline: None,
260 }
261 }
262
263 pub fn ts_offset(&self) -> i32 {
264 self.τs_offset
265 }
266
267 pub fn with_deadline(mut self, deadline: i32) -> Self {
268 self.deadline = Some(deadline);
269 self
270 }
271
272 pub fn to_absolute(&self, reference: StateTime) -> StateTime {
274 StateTime::from_wire_offset(reference, self.τs_offset)
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281
282 #[test]
283 fn test_state_time_wire_roundtrip() {
284 let reference = StateTime::from_millis(1000);
285 let time = StateTime::from_millis(1050);
286
287 let offset = time.to_wire_offset(reference);
288 let recovered = StateTime::from_wire_offset(reference, offset);
289
290 assert!((time.0 - recovered.0).abs() < 100);
292 }
293
294 #[test]
295 fn test_reality_window_classification() {
296 let τs = StateTime::from_millis(1000);
297 let hc = Duration::from_millis(100);
298 let hp = Duration::from_millis(50);
299 let rw = RealityWindow::new(τs, hc, hp);
300
301 assert_eq!(
303 rw.classify(StateTime::from_millis(850)),
304 TimePosition::TooLate
305 );
306
307 assert_eq!(
309 rw.classify(StateTime::from_millis(950)),
310 TimePosition::Correctable
311 );
312
313 assert_eq!(
315 rw.classify(StateTime::from_millis(1000)),
316 TimePosition::Current
317 );
318
319 assert_eq!(
321 rw.classify(StateTime::from_millis(1030)),
322 TimePosition::Predictable
323 );
324
325 assert_eq!(
327 rw.classify(StateTime::from_millis(1100)),
328 TimePosition::TooEarly
329 );
330 }
331
332 #[test]
333 fn test_perceptual_time_monotonic() {
334 let t1 = PerceptualTime::from_millis(100);
335 let t2 = t1 + Duration::from_millis(10);
336
337 assert!(t2 > t1);
338 assert_eq!(t2 - t1, Duration::from_millis(10));
339 }
340}