1use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
2use std::sync::Arc;
3
4const NANOS_PER_SEC: u64 = 1_000_000_000;
5const WINDOW_SIZE: usize = 128;
6
7/// Represents a rolling FPS (Frames Per Second) counter.
8///
9/// This struct provides a lock-free mechanism to measure and calculate FPS
10/// continuously, updating with every frame. It uses atomic operations to
11/// ensure thread-safety without the need for locks.
12pub struct FpsCounter {
13 frame_times: [AtomicU64; WINDOW_SIZE],
14 head: AtomicUsize,
15 tail: AtomicUsize,
16}
17
18impl FpsCounter {
19 /// Creates a new `Fps` counter.
20 ///
21 /// Returns an `Arc<Fps>` for safe sharing across threads.
22 pub fn new() -> Arc<Self> {
23 Arc::new(Self {
24 frame_times: std::array::from_fn(|_| AtomicU64::new(0)),
25 head: AtomicUsize::new(0),
26 tail: AtomicUsize::new(0),
27 })
28 }
29
30 /// Increments the FPS counter with a new frame timestamp.
31 ///
32 /// This method updates the internal state to maintain a rolling window
33 /// of frame data for the last second. It uses atomic operations to
34 /// ensure thread-safety.
35 ///
36 /// # Arguments
37 ///
38 /// * `timestamp_ns` - The timestamp of the new frame in nanoseconds.
39 pub fn increment(&self, timestamp_ns: u64) {
40 let mut head = self.head.load(Ordering::Relaxed);
41 let mut tail = self.tail.load(Ordering::Relaxed);
42
43 // Add new timestamp
44 self.frame_times[head].store(timestamp_ns, Ordering::Relaxed);
45 // Increment head and wrap around to 0 if it reaches WINDOW_SIZE
46 head = (head + 1) % WINDOW_SIZE;
47 self.head.store(head, Ordering::Relaxed);
48
49 // Remove old timestamps (older than 1 second)
50 while tail != head {
51 let oldest = self.frame_times[tail].load(Ordering::Relaxed);
52 if timestamp_ns.wrapping_sub(oldest) <= NANOS_PER_SEC {
53 break;
54 }
55 // Increment tail and wrap around to 0 if it reaches WINDOW_SIZE
56 tail = (tail + 1) % WINDOW_SIZE;
57 self.tail.store(tail, Ordering::Relaxed);
58 }
59 }
60
61 /// Calculates and returns the current FPS.
62 ///
63 /// This method computes the FPS based on the frames recorded in the last second.
64 /// It uses atomic loads to ensure thread-safety.
65 ///
66 /// # Returns
67 ///
68 /// The calculated FPS as a `f32`, or 0.0 if no frames have been recorded.
69 pub fn fps(&self) -> f32 {
70 let head = self.head.load(Ordering::Relaxed);
71 let tail = self.tail.load(Ordering::Relaxed);
72
73 if head == tail {
74 return 0.0;
75 }
76
77 let newest =
78 self.frame_times[head.wrapping_sub(1) & (WINDOW_SIZE - 1)].load(Ordering::Relaxed);
79 let oldest = self.frame_times[tail].load(Ordering::Relaxed);
80
81 let time_diff = newest.wrapping_sub(oldest) as f32;
82 if time_diff == 0.0 {
83 return 0.0;
84 }
85
86 let frame_count = if head > tail {
87 head - tail
88 } else {
89 WINDOW_SIZE - tail + head
90 };
91
92 (frame_count as f32 - 1.0) * NANOS_PER_SEC as f32 / time_diff
93 }
94}