vimp_engine_core/client/
divergence.rs1use std::collections::VecDeque;
16
17use serde_json::{Value, json};
18
19use crate::config::{DivergenceConfig, PLAYER_STATE_LEN};
20
21#[derive(Clone, Copy, PartialEq, Eq)]
23pub enum Source {
24 Camera,
26 State,
28}
29
30impl Source {
31 fn as_str(self) -> &'static str {
32 match self {
33 Source::Camera => "camera",
34 Source::State => "state",
35 }
36 }
37}
38
39pub struct Observation<'a> {
41 pub source: Source,
42 pub predicted: &'a [f32],
45 pub authoritative: &'a [f32; PLAYER_STATE_LEN],
46 pub server_time: f64,
47 pub local_now: f64,
48 pub offset: f64,
49 pub input_seq: u32,
50 pub replayed: Option<(f64, f64, usize)>,
53}
54
55pub struct DivergenceTracker {
56 cfg: DivergenceConfig,
57 records: VecDeque<Value>,
58 samples: u64,
59 violations: u64,
60 dropped: u64,
61 max_delta: [f32; PLAYER_STATE_LEN],
62}
63
64impl DivergenceTracker {
65 pub fn new(mut cfg: DivergenceConfig) -> Self {
66 cfg.capacity = cfg.capacity.max(1);
69
70 Self {
71 cfg,
72 records: VecDeque::new(),
73 samples: 0,
74 violations: 0,
75 dropped: 0,
76 max_delta: [0.0; PLAYER_STATE_LEN],
77 }
78 }
79
80 pub fn observe(&mut self, obs: Observation) {
84 self.samples += 1;
85
86 let width = obs.predicted.len().min(PLAYER_STATE_LEN);
87 let mut deltas = Vec::with_capacity(width);
88 let mut exceeded = Vec::new();
89
90 for index in 0..width {
91 let delta = obs.predicted[index] - obs.authoritative[index];
92
93 deltas.push(delta);
94
95 if delta.abs() > self.max_delta[index] {
96 self.max_delta[index] = delta.abs();
97 }
98
99 if delta.abs() > self.cfg.threshold(index) {
100 exceeded.push(index);
101 }
102 }
103
104 if exceeded.is_empty() {
105 return;
106 }
107
108 self.violations += 1;
109
110 if self.records.len() >= self.cfg.capacity {
111 self.records.pop_front();
112 self.dropped += 1;
113 }
114
115 let thresholds: Vec<Value> = (0..width)
116 .map(|index| json!(round4(self.cfg.threshold(index))))
117 .collect();
118
119 self.records.push_back(json!({
120 "source": obs.source.as_str(),
121 "serverTime": obs.server_time,
122 "localNow": obs.local_now,
123 "offset": obs.offset,
124 "inputSeq": obs.input_seq,
125 "replayed": obs.replayed.map(|(from, to, count)| json!({
126 "from": from,
127 "to": to,
128 "count": count,
129 })),
130 "predicted": floats(&obs.predicted[..width]),
131 "authoritative": floats(&obs.authoritative[..width]),
132 "delta": floats(&deltas),
133 "exceeded": exceeded,
134 "thresholds": thresholds,
135 }));
136 }
137
138 pub fn take_json(&mut self) -> String {
142 let records: Vec<Value> = self.records.drain(..).collect();
143
144 json!({
145 "samples": self.samples,
146 "violations": self.violations,
147 "dropped": self.dropped,
148 "maxDelta": floats(&self.max_delta),
149 "records": records,
150 })
151 .to_string()
152 }
153}
154
155fn floats(values: &[f32]) -> Vec<Value> {
156 values.iter().map(|v| json!(round4(*v))).collect()
157}
158
159fn round4(value: f32) -> f64 {
161 ((value as f64) * 10_000.0).round() / 10_000.0
162}