goud_engine/sdk/
debug_overlay.rs1use std::collections::VecDeque;
8
9const DEFAULT_WINDOW_CAPACITY: usize = 120;
11
12#[derive(Debug, Clone, Copy, Default)]
21#[repr(C)]
22pub struct FpsStats {
23 pub current_fps: f32,
25 pub min_fps: f32,
27 pub max_fps: f32,
29 pub avg_fps: f32,
31 pub frame_time_ms: f32,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Default)]
41#[repr(C)]
42pub enum OverlayCorner {
43 #[default]
45 TopLeft = 0,
46 TopRight = 1,
48 BottomLeft = 2,
50 BottomRight = 3,
52}
53
54#[derive(Debug, Clone)]
63pub struct DebugOverlay {
64 enabled: bool,
66 corner: OverlayCorner,
68 update_interval: f32,
70 frame_times: VecDeque<f32>,
72 cached_stats: FpsStats,
74 time_since_update: f32,
76}
77
78impl DebugOverlay {
79 pub fn new(update_interval: f32) -> Self {
83 Self {
84 enabled: false,
85 corner: OverlayCorner::default(),
86 update_interval,
87 frame_times: VecDeque::with_capacity(DEFAULT_WINDOW_CAPACITY),
88 cached_stats: FpsStats::default(),
89 time_since_update: 0.0,
90 }
91 }
92
93 pub fn update(&mut self, delta_time: f32) {
95 if self.frame_times.len() >= DEFAULT_WINDOW_CAPACITY {
97 self.frame_times.pop_front();
98 }
99 self.frame_times.push_back(delta_time);
100
101 self.time_since_update += delta_time;
102 if self.time_since_update >= self.update_interval {
103 self.time_since_update = 0.0;
104 self.recompute_stats(delta_time);
105 }
106 }
107
108 #[inline]
110 pub fn stats(&self) -> FpsStats {
111 self.cached_stats
112 }
113
114 #[inline]
116 pub fn set_enabled(&mut self, enabled: bool) {
117 self.enabled = enabled;
118 }
119
120 #[inline]
122 pub fn is_enabled(&self) -> bool {
123 self.enabled
124 }
125
126 #[inline]
128 pub fn set_corner(&mut self, corner: OverlayCorner) {
129 self.corner = corner;
130 }
131
132 #[inline]
134 pub fn corner(&self) -> OverlayCorner {
135 self.corner
136 }
137
138 #[inline]
140 pub fn set_update_interval(&mut self, interval: f32) {
141 self.update_interval = interval;
142 }
143
144 fn recompute_stats(&mut self, current_delta: f32) {
149 if self.frame_times.is_empty() {
150 self.cached_stats = FpsStats::default();
151 return;
152 }
153
154 let mut sum: f32 = 0.0;
155 let mut min_dt = f32::MAX;
156 let mut max_dt: f32 = 0.0;
157
158 for &dt in &self.frame_times {
159 sum += dt;
160 if dt < min_dt {
161 min_dt = dt;
162 }
163 if dt > max_dt {
164 max_dt = dt;
165 }
166 }
167
168 let count = self.frame_times.len() as f32;
169 let avg_dt = sum / count;
170
171 self.cached_stats = FpsStats {
172 current_fps: if current_delta > 0.0 {
173 1.0 / current_delta
174 } else {
175 0.0
176 },
177 min_fps: if max_dt > 0.0 { 1.0 / max_dt } else { 0.0 },
178 max_fps: if min_dt > 0.0 { 1.0 / min_dt } else { 0.0 },
179 avg_fps: if avg_dt > 0.0 { 1.0 / avg_dt } else { 0.0 },
180 frame_time_ms: current_delta * 1000.0,
181 };
182 }
183}
184
185impl Default for DebugOverlay {
186 fn default() -> Self {
187 Self::new(0.5)
188 }
189}
190
191#[cfg(test)]
196mod tests {
197 use super::*;
198
199 #[test]
200 fn test_new_overlay_has_default_stats() {
201 let overlay = DebugOverlay::new(0.5);
202 let stats = overlay.stats();
203 assert_eq!(stats.current_fps, 0.0);
204 assert_eq!(stats.avg_fps, 0.0);
205 assert_eq!(stats.min_fps, 0.0);
206 assert_eq!(stats.max_fps, 0.0);
207 assert_eq!(stats.frame_time_ms, 0.0);
208 }
209
210 #[test]
211 fn test_overlay_disabled_by_default() {
212 let overlay = DebugOverlay::new(0.5);
213 assert!(!overlay.is_enabled());
214 }
215
216 #[test]
217 fn test_set_enabled() {
218 let mut overlay = DebugOverlay::new(0.5);
219 overlay.set_enabled(true);
220 assert!(overlay.is_enabled());
221 overlay.set_enabled(false);
222 assert!(!overlay.is_enabled());
223 }
224
225 #[test]
226 fn test_set_corner() {
227 let mut overlay = DebugOverlay::new(0.5);
228 assert_eq!(overlay.corner(), OverlayCorner::TopLeft);
229 overlay.set_corner(OverlayCorner::BottomRight);
230 assert_eq!(overlay.corner(), OverlayCorner::BottomRight);
231 }
232
233 #[test]
234 fn test_stats_computed_after_interval() {
235 let mut overlay = DebugOverlay::new(0.5);
236
237 for _ in 0..10 {
239 overlay.update(0.016); }
241 let stats = overlay.stats();
243 assert_eq!(stats.current_fps, 0.0);
244
245 overlay.update(0.34);
248 let stats = overlay.stats();
249 assert!((stats.current_fps - 1.0 / 0.34).abs() < 0.01);
251 assert!(stats.avg_fps > 0.0);
252 }
253
254 #[test]
255 fn test_stats_with_known_frame_times() {
256 let mut overlay = DebugOverlay::new(0.0);
258
259 overlay.update(0.010);
261 overlay.update(0.020);
262 overlay.update(0.030);
263
264 let stats = overlay.stats();
265
266 assert!((stats.current_fps - 33.333).abs() < 0.1);
268
269 assert!((stats.avg_fps - 50.0).abs() < 0.1);
271
272 assert!((stats.min_fps - 33.333).abs() < 0.1);
274
275 assert!((stats.max_fps - 100.0).abs() < 0.1);
277
278 assert!((stats.frame_time_ms - 30.0).abs() < 0.1);
280 }
281
282 #[test]
283 fn test_rolling_window_eviction() {
284 let mut overlay = DebugOverlay::new(0.0);
285
286 for _ in 0..150 {
288 overlay.update(0.016);
289 }
290
291 assert_eq!(overlay.frame_times.len(), DEFAULT_WINDOW_CAPACITY);
293 }
294
295 #[test]
296 fn test_single_frame() {
297 let mut overlay = DebugOverlay::new(0.0);
298 overlay.update(0.016);
299
300 let stats = overlay.stats();
301 assert!((stats.current_fps - 62.5).abs() < 0.1);
302 assert!((stats.avg_fps - 62.5).abs() < 0.1);
303 assert!((stats.frame_time_ms - 16.0).abs() < 0.1);
304 }
305
306 #[test]
307 fn test_zero_delta_time() {
308 let mut overlay = DebugOverlay::new(0.0);
309 overlay.update(0.0);
310
311 let stats = overlay.stats();
312 assert_eq!(stats.current_fps, 0.0);
314 assert_eq!(stats.frame_time_ms, 0.0);
315 }
316
317 #[test]
318 fn test_update_interval_respects_timing() {
319 let mut overlay = DebugOverlay::new(1.0);
320
321 for _ in 0..60 {
323 overlay.update(0.016);
324 }
325 assert_eq!(overlay.stats().current_fps, 0.0); overlay.update(0.016); overlay.update(0.04); assert!(overlay.stats().current_fps > 0.0);
332 }
333
334 #[test]
335 fn test_set_update_interval() {
336 let mut overlay = DebugOverlay::new(1.0);
337 overlay.set_update_interval(0.1);
338
339 for _ in 0..10 {
341 overlay.update(0.016);
342 }
343 assert!(overlay.stats().current_fps > 0.0);
345 }
346
347 #[test]
348 fn test_default_overlay() {
349 let overlay = DebugOverlay::default();
350 assert!(!overlay.is_enabled());
351 assert_eq!(overlay.corner(), OverlayCorner::TopLeft);
352 }
353
354 #[test]
355 fn test_overlay_corner_default() {
356 assert_eq!(OverlayCorner::default(), OverlayCorner::TopLeft);
357 }
358
359 #[test]
360 fn test_fps_stats_default() {
361 let stats = FpsStats::default();
362 assert_eq!(stats.current_fps, 0.0);
363 assert_eq!(stats.min_fps, 0.0);
364 assert_eq!(stats.max_fps, 0.0);
365 assert_eq!(stats.avg_fps, 0.0);
366 assert_eq!(stats.frame_time_ms, 0.0);
367 }
368}