use std::fmt::Debug;
use std::hash::Hash;
use std::ops::Sub;
use plaza_client_utils::interpolation::{Interpolatable, ToF32};
use crate::history::HistoricalStateBuffer;
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct RenderError {
sum: f64,
worst: f32,
samples: u32,
}
impl RenderError {
pub fn new() -> Self {
Self::default()
}
pub fn observe(&mut self, error: f32) {
if !error.is_finite() {
return;
}
self.sum += error as f64;
self.worst = self.worst.max(error);
self.samples += 1;
}
pub fn merge(&mut self, other: &RenderError) {
self.sum += other.sum;
self.worst = self.worst.max(other.worst);
self.samples += other.samples;
}
pub fn mean(&self) -> f32 {
if self.samples == 0 { 0.0 } else { (self.sum / self.samples as f64) as f32 }
}
pub fn worst(&self) -> f32 {
self.worst
}
pub fn samples(&self) -> u32 {
self.samples
}
pub fn is_empty(&self) -> bool {
self.samples == 0
}
}
pub fn render_error_at<Id, State, Time, D>(
history: &HistoricalStateBuffer<Id, State, Time>,
at: Time,
drawn: impl IntoIterator<Item = (Id, State)>,
distance: D,
) -> RenderError
where
Id: Eq + Hash + Clone + Debug,
State: Clone + Debug + Interpolatable<Time>,
Time: Copy + Debug + Default + PartialOrd + Ord + Sub<Output = Time> + ToF32,
D: Fn(&State, &State) -> f32,
{
let mut out = RenderError::new();
for (id, drawn_state) in drawn {
let Some(truth) = history.get_state_at_or_before(&id, at) else { continue };
out.observe(distance(&drawn_state, &truth));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Clone, Copy, Debug, PartialEq)]
struct P(f32);
impl Interpolatable<u64> for P {
fn interpolate(&self, other: &Self, t: f32, _a: u64, _b: u64) -> Self {
P(self.0 + (other.0 - self.0) * t)
}
}
fn distance(a: &P, b: &P) -> f32 {
(a.0 - b.0).abs()
}
fn moving() -> HistoricalStateBuffer<u8, P, u64> {
let mut history = HistoricalStateBuffer::new(64);
for tick in 0..40u64 {
let t = tick * 16;
history.record_state(1, t, P(t as f32));
}
history
}
#[test]
fn a_client_drawing_the_past_correctly_has_no_error_at_its_own_instant() {
let history = moving();
let now = 39 * 16;
let at = now - 100;
let error = render_error_at(&history, at, [(1u8, P(at as f32))], distance);
assert_eq!(error.samples(), 1);
assert!(error.mean() < 0.01, "{}", error.mean());
}
#[test]
fn the_same_client_measured_against_the_present_is_charged_for_its_delay() {
let history = moving();
let now = 39 * 16;
let at = now - 100;
let against_now = render_error_at(&history, now, [(1u8, P(at as f32))], distance);
assert!(
(against_now.mean() - 100.0).abs() < 1.0,
"the naive comparison should cost exactly the delay, got {}",
against_now.mean()
);
}
#[test]
fn a_real_error_still_shows_up_at_the_render_instant() {
let history = moving();
let at = 20 * 16;
let error = render_error_at(&history, at, [(1u8, P(at as f32 + 12.0))], distance);
assert!((error.mean() - 12.0).abs() < 0.01, "{}", error.mean());
}
#[test]
fn an_entity_the_server_never_recorded_is_skipped_rather_than_scored() {
let history = moving();
let error = render_error_at(&history, 100, [(9u8, P(0.0))], distance);
assert!(error.is_empty(), "an unknown entity was folded into the average");
assert_eq!(error.mean(), 0.0);
}
#[test]
fn merging_weights_by_samples_rather_than_by_client() {
let mut few = RenderError::new();
few.observe(10.0);
let mut many = RenderError::new();
for _ in 0..99 {
many.observe(0.0);
}
let mut both = few;
both.merge(&many);
assert_eq!(both.samples(), 100);
assert!((both.mean() - 0.1).abs() < 1e-5, "{}", both.mean());
assert_eq!(both.worst(), 10.0, "and the worst case survives the averaging");
}
#[test]
fn a_non_finite_distance_is_ignored_rather_than_poisoning_the_mean() {
let mut error = RenderError::new();
error.observe(5.0);
error.observe(f32::NAN);
error.observe(f32::INFINITY);
assert_eq!(error.samples(), 1);
assert_eq!(error.mean(), 5.0);
}
}