Skip to main content

embedded_3dgfx/
perfcounter.rs

1use crate::hardware_profile;
2use core::fmt::Write;
3use core::mem;
4use heapless::String;
5
6// Use embassy_time for embedded targets when the feature is enabled
7#[cfg(feature = "embassy-time")]
8use embassy_time::Instant;
9
10#[cfg(feature = "embassy-time")]
11fn now_us() -> u64 {
12    Instant::now().as_micros() as u64
13}
14
15// Use std::time for desktop/simulator targets when std feature is enabled
16#[cfg(all(feature = "std", not(feature = "embassy-time")))]
17fn now_us() -> u64 {
18    extern crate std;
19    use std::time::{SystemTime, UNIX_EPOCH};
20    SystemTime::now()
21        .duration_since(UNIX_EPOCH)
22        .unwrap()
23        .as_micros() as u64
24}
25
26// Dummy implementation when neither std nor embassy-time is available
27// Users must enable one of these features to use PerformanceCounter
28#[cfg(not(any(feature = "std", feature = "embassy-time")))]
29fn now_us() -> u64 {
30    // Return 0 as a fallback - PerformanceCounter won't work but won't break the build
31    // This allows the library to compile in no_std without timing functionality
32    0
33}
34
35/// Performance counter for measuring rendering performance
36///
37/// **Timing Requirements:**
38/// This module requires the `perfcounter` feature and a timing source:
39/// - For embedded with Embassy: enable both `perfcounter` and `embassy-time` features
40/// - For desktop/simulator: enable `std` feature (includes perfcounter automatically)
41/// - Without a timing source, timing will always return 0
42///
43/// # Example
44/// ```toml
45/// # For embedded with Embassy:
46/// embedded-3dgfx = { version = "0.1", features = ["perfcounter", "embassy-time"] }
47///
48/// # For desktop/simulator:
49/// embedded-3dgfx = { version = "0.1", features = ["std"] }
50///
51/// # Pure no_std without perfcounter:
52/// embedded-3dgfx = { version = "0.1", default-features = false }
53/// ```
54#[derive(Debug)]
55pub struct PerformanceCounter {
56    frame_count: u64,
57    text: String<256>,
58    old_text: String<256>,
59    only_fps: bool,
60    start_time_us: u64,
61    last_measurement_time_us: u64,
62    dwt_frame_start_cycles: u32,
63    dwt_last_measurement_cycles: u32,
64}
65
66impl Default for PerformanceCounter {
67    fn default() -> Self {
68        Self::new()
69    }
70}
71
72impl PerformanceCounter {
73    pub fn new() -> Self {
74        let now = now_us();
75        Self {
76            frame_count: 0,
77            text: String::new(),
78            old_text: String::new(),
79            only_fps: false,
80            start_time_us: now,
81            last_measurement_time_us: now,
82            dwt_frame_start_cycles: 0,
83            dwt_last_measurement_cycles: 0,
84        }
85    }
86
87    pub fn only_fps(&mut self, only_fps: bool) {
88        self.only_fps = only_fps;
89    }
90
91    pub fn get_frametime(&self) -> u64 {
92        now_us().saturating_sub(self.start_time_us)
93    }
94
95    pub fn start_of_frame(&mut self) {
96        self.frame_count += 1;
97        self.text.clear();
98        self.start_time_us = now_us();
99        self.last_measurement_time_us = self.start_time_us;
100        hardware_profile::init_dwt_cycle_counter();
101        if let Some(cycles) = hardware_profile::read_cycle_counter() {
102            self.dwt_frame_start_cycles = cycles;
103            self.dwt_last_measurement_cycles = cycles;
104        }
105    }
106
107    pub fn add_measurement(&mut self, label: &str) {
108        if self.only_fps {
109            return;
110        }
111        let now = now_us();
112        let duration = now.saturating_sub(self.last_measurement_time_us);
113        if let Some(cycles_now) = hardware_profile::read_cycle_counter() {
114            let sample = hardware_profile::sample_cycles(
115                "perf.measurement",
116                self.dwt_last_measurement_cycles,
117                cycles_now,
118            );
119            hardware_profile::emit_trace(sample);
120            let _ = write!(
121                self.text,
122                "{}: {}us ({} cyc)\n",
123                label, duration, sample.cycles
124            );
125            self.dwt_last_measurement_cycles = cycles_now;
126        } else {
127            let _ = write!(self.text, "{}: {}us\n", label, duration);
128        }
129        self.last_measurement_time_us = now;
130    }
131
132    pub fn discard_measurement(&mut self) {
133        mem::swap(&mut self.old_text, &mut self.text);
134    }
135
136    pub fn print(&mut self) {
137        let total_us = self.get_frametime();
138        let fps = if total_us > 0 {
139            1_000_000 / total_us
140        } else {
141            0
142        };
143        if self.only_fps {
144            let _ = write!(self.text, "fps: {}\n", fps);
145            self.old_text = self.text.clone();
146            return;
147        }
148        if let Some(cycles_now) = hardware_profile::read_cycle_counter() {
149            let total_cycles = cycles_now.wrapping_sub(self.dwt_frame_start_cycles);
150            let sample = hardware_profile::sample_cycles(
151                "perf.frame",
152                self.dwt_frame_start_cycles,
153                cycles_now,
154            );
155            hardware_profile::emit_trace(sample);
156            let _ = write!(
157                self.text,
158                "total: {}us ({} cyc)\nfps: {}\n",
159                total_us, total_cycles, fps
160            );
161        } else {
162            let _ = write!(self.text, "total: {}us\nfps: {}\n", total_us, fps);
163        }
164        self.old_text = self.text.clone();
165    }
166
167    pub fn get_text(&self) -> &str {
168        &self.old_text
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    extern crate std;
175    use super::*;
176
177    #[test]
178    fn test_perfcounter_creation() {
179        let perf = PerformanceCounter::new();
180        assert_eq!(perf.get_text(), "");
181        assert_eq!(perf.frame_count, 0);
182    }
183
184    #[test]
185    fn test_perfcounter_default() {
186        let perf = PerformanceCounter::default();
187        assert_eq!(perf.get_text(), "");
188    }
189
190    #[test]
191    fn test_perfcounter_start_of_frame() {
192        let mut perf = PerformanceCounter::new();
193        perf.start_of_frame();
194        assert_eq!(perf.frame_count, 1);
195
196        perf.start_of_frame();
197        assert_eq!(perf.frame_count, 2);
198    }
199
200    #[test]
201    #[cfg(any(feature = "std", feature = "embassy-time"))]
202    fn test_perfcounter_get_frametime() {
203        let mut perf = PerformanceCounter::new();
204        perf.start_of_frame();
205
206        // Sleep a tiny bit to ensure time passes
207        std::thread::sleep(std::time::Duration::from_micros(100));
208
209        let frametime = perf.get_frametime();
210        assert!(frametime >= 100); // At least 100 microseconds
211    }
212
213    #[test]
214    #[cfg(any(feature = "std", feature = "embassy-time"))]
215    fn test_perfcounter_add_measurement() {
216        let mut perf = PerformanceCounter::new();
217        perf.start_of_frame();
218
219        std::thread::sleep(std::time::Duration::from_micros(100));
220        perf.add_measurement("test_label");
221
222        perf.print();
223        let text = perf.get_text();
224
225        // Should contain the label
226        assert!(text.contains("test_label"));
227        // Should contain some duration value
228        assert!(text.contains(":"));
229    }
230
231    #[test]
232    fn test_perfcounter_only_fps_mode() {
233        let mut perf = PerformanceCounter::new();
234        perf.only_fps(true);
235        perf.start_of_frame();
236
237        perf.add_measurement("should_be_ignored");
238
239        perf.print();
240        let text = perf.get_text();
241
242        // Should not contain the label in FPS-only mode
243        assert!(!text.contains("should_be_ignored"));
244        // But should still contain FPS
245        assert!(text.contains("fps"));
246    }
247
248    #[test]
249    #[cfg(any(feature = "std", feature = "embassy-time"))]
250    fn test_perfcounter_discard_measurement() {
251        let mut perf = PerformanceCounter::new();
252        perf.start_of_frame();
253
254        perf.add_measurement("test1");
255        // discard_measurement swaps old_text and text
256        perf.discard_measurement();
257
258        // After discard, the measurement should still be in old_text
259        let text_after = perf.get_text();
260        assert!(text_after.contains("test1"));
261    }
262
263    #[test]
264    fn test_perfcounter_print_includes_fps() {
265        let mut perf = PerformanceCounter::new();
266        perf.start_of_frame();
267
268        std::thread::sleep(std::time::Duration::from_micros(1000));
269
270        perf.print();
271        let text = perf.get_text();
272
273        // Should contain FPS
274        assert!(text.contains("fps"));
275        // Should contain total time
276        assert!(text.contains("total"));
277    }
278
279    #[test]
280    #[cfg(any(feature = "std", feature = "embassy-time"))]
281    fn test_perfcounter_fps_calculation() {
282        let mut perf = PerformanceCounter::new();
283        perf.start_of_frame();
284
285        std::thread::sleep(std::time::Duration::from_millis(10));
286
287        perf.print();
288        let text = perf.get_text();
289
290        // FPS should be less than 100 (since we slept for 10ms)
291        // Just verify the text is generated correctly
292        assert!(text.len() > 0);
293    }
294
295    #[test]
296    #[cfg(any(feature = "std", feature = "embassy-time"))]
297    fn test_perfcounter_multiple_measurements() {
298        let mut perf = PerformanceCounter::new();
299        perf.start_of_frame();
300
301        std::thread::sleep(std::time::Duration::from_micros(100));
302        perf.add_measurement("step1");
303
304        std::thread::sleep(std::time::Duration::from_micros(100));
305        perf.add_measurement("step2");
306
307        perf.print();
308        let text = perf.get_text();
309
310        // Should contain both labels
311        assert!(text.contains("step1"));
312        assert!(text.contains("step2"));
313        assert!(text.contains("total"));
314        assert!(text.contains("fps"));
315    }
316}