subtr_actor/collector/
frame_resolution.rs1const 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}