Skip to main content

subtr_actor/collector/
frame_resolution.rs

1const FRAME_RESOLUTION_EPSILON_SECONDS: f32 = 1e-4;
2
3#[derive(Debug, Clone, Copy, Default, PartialEq)]
4pub enum StatsFrameResolution {
5    #[default]
6    EveryFrame,
7    TimeStep {
8        seconds: f32,
9    },
10}
11
12#[derive(Debug, Clone, Copy, PartialEq)]
13pub(crate) enum FinalStatsFrameAction {
14    Append { dt: f32 },
15    ReplaceLast { dt: f32 },
16}
17
18#[derive(Debug, Clone, Copy)]
19pub(crate) struct StatsFramePersistenceController {
20    resolution: StatsFrameResolution,
21    next_emit_time: Option<f32>,
22    last_emitted_frame_number: Option<usize>,
23    last_emitted_time: Option<f32>,
24    last_emitted_dt: f32,
25}
26
27impl StatsFramePersistenceController {
28    pub(crate) fn new(resolution: StatsFrameResolution) -> Self {
29        Self {
30            resolution,
31            next_emit_time: None,
32            last_emitted_frame_number: None,
33            last_emitted_time: None,
34            last_emitted_dt: 0.0,
35        }
36    }
37
38    pub(crate) fn on_frame(&mut self, frame_number: usize, current_time: f32) -> Option<f32> {
39        if self.last_emitted_time.is_none() {
40            return Some(self.record_emit(frame_number, current_time));
41        }
42
43        match self.resolution {
44            StatsFrameResolution::EveryFrame => Some(self.record_emit(frame_number, current_time)),
45            StatsFrameResolution::TimeStep { seconds } => {
46                if !seconds.is_finite() || seconds <= 0.0 {
47                    return Some(self.record_emit(frame_number, current_time));
48                }
49
50                let next_emit_time = self.next_emit_time.unwrap_or(current_time + seconds);
51                if current_time + FRAME_RESOLUTION_EPSILON_SECONDS < next_emit_time {
52                    self.next_emit_time = Some(next_emit_time);
53                    return None;
54                }
55
56                let dt = self.record_emit(frame_number, current_time);
57                let mut advanced_next_emit_time = next_emit_time;
58                while advanced_next_emit_time <= current_time + FRAME_RESOLUTION_EPSILON_SECONDS {
59                    advanced_next_emit_time += seconds;
60                }
61                self.next_emit_time = Some(advanced_next_emit_time);
62                Some(dt)
63            }
64        }
65    }
66
67    pub(crate) fn final_frame_action(
68        &self,
69        frame_number: usize,
70        current_time: f32,
71    ) -> Option<FinalStatsFrameAction> {
72        let Some(last_emitted_time) = self.last_emitted_time else {
73            return Some(FinalStatsFrameAction::Append { dt: 0.0 });
74        };
75
76        if self.last_emitted_frame_number == Some(frame_number) {
77            return Some(FinalStatsFrameAction::ReplaceLast {
78                dt: self.last_emitted_dt,
79            });
80        }
81
82        Some(FinalStatsFrameAction::Append {
83            dt: (current_time - last_emitted_time).max(0.0),
84        })
85    }
86
87    fn record_emit(&mut self, frame_number: usize, current_time: f32) -> f32 {
88        let dt = self
89            .last_emitted_time
90            .map(|last_time| (current_time - last_time).max(0.0))
91            .unwrap_or(0.0);
92        self.last_emitted_frame_number = Some(frame_number);
93        self.last_emitted_time = Some(current_time);
94        self.last_emitted_dt = dt;
95        self.next_emit_time = match self.resolution {
96            StatsFrameResolution::EveryFrame => None,
97            StatsFrameResolution::TimeStep { seconds } if seconds.is_finite() && seconds > 0.0 => {
98                Some(current_time + seconds)
99            }
100            StatsFrameResolution::TimeStep { .. } => None,
101        };
102        dt
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::{FinalStatsFrameAction, StatsFramePersistenceController, StatsFrameResolution};
109
110    #[test]
111    fn every_frame_resolution_emits_every_frame() {
112        let mut controller = StatsFramePersistenceController::new(StatsFrameResolution::EveryFrame);
113
114        assert_eq!(controller.on_frame(10, 0.0), Some(0.0));
115        assert_eq!(controller.on_frame(11, 0.1), Some(0.1));
116        assert_eq!(controller.on_frame(12, 0.25), Some(0.15));
117        assert_eq!(
118            controller.final_frame_action(12, 0.25),
119            Some(FinalStatsFrameAction::ReplaceLast { dt: 0.15 })
120        );
121    }
122
123    #[test]
124    fn timestep_resolution_emits_crossings_and_appends_final_frame() {
125        let mut controller =
126            StatsFramePersistenceController::new(StatsFrameResolution::TimeStep { seconds: 0.5 });
127
128        assert_eq!(controller.on_frame(0, 0.0), Some(0.0));
129        assert_eq!(controller.on_frame(1, 0.2), None);
130        assert_eq!(controller.on_frame(2, 0.49), None);
131        assert_eq!(controller.on_frame(3, 0.5), Some(0.5));
132        assert_eq!(controller.on_frame(4, 0.74), None);
133        match controller.final_frame_action(4, 0.74) {
134            Some(FinalStatsFrameAction::Append { dt }) => {
135                assert!(
136                    (dt - 0.24).abs() < 1e-6,
137                    "expected dt close to 0.24, got {dt}"
138                );
139            }
140            action => panic!("expected append action, got {action:?}"),
141        }
142    }
143}