fps.rs

 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}