hojicha_runtime/program/
fps_limiter.rs

1//! FPS limiting logic for controlling render frequency
2
3use std::time::{Duration, Instant};
4
5/// Controls the frame rate of rendering
6#[derive(Debug, Clone)]
7pub struct FpsLimiter {
8    max_fps: u16,
9    last_render: Instant,
10    frame_duration: Duration,
11}
12
13impl FpsLimiter {
14    /// Create a new FPS limiter
15    pub fn new(max_fps: u16) -> Self {
16        let frame_duration = if max_fps > 0 {
17            Duration::from_secs(1) / max_fps as u32
18        } else {
19            Duration::ZERO
20        };
21
22        Self {
23            max_fps,
24            last_render: Instant::now(),
25            frame_duration,
26        }
27    }
28
29    /// Check if enough time has passed to render the next frame
30    pub fn should_render(&self) -> bool {
31        if self.max_fps == 0 {
32            // No FPS limit
33            return true;
34        }
35
36        self.last_render.elapsed() >= self.frame_duration
37    }
38
39    /// Mark that a frame has been rendered
40    pub fn mark_rendered(&mut self) {
41        self.last_render = Instant::now();
42    }
43
44    /// Get the time remaining until the next frame should be rendered
45    pub fn time_until_next_frame(&self) -> Duration {
46        if self.max_fps == 0 {
47            return Duration::ZERO;
48        }
49
50        let elapsed = self.last_render.elapsed();
51        if elapsed >= self.frame_duration {
52            Duration::ZERO
53        } else {
54            self.frame_duration - elapsed
55        }
56    }
57
58    /// Update the maximum FPS
59    pub fn set_max_fps(&mut self, max_fps: u16) {
60        self.max_fps = max_fps;
61        self.frame_duration = if max_fps > 0 {
62            Duration::from_secs(1) / max_fps as u32
63        } else {
64            Duration::ZERO
65        };
66    }
67
68    /// Get the current maximum FPS
69    pub fn max_fps(&self) -> u16 {
70        self.max_fps
71    }
72
73    /// Get the frame duration
74    pub fn frame_duration(&self) -> Duration {
75        self.frame_duration
76    }
77
78    /// Calculate the actual FPS based on the last render time
79    pub fn actual_fps(&self) -> f64 {
80        let elapsed = self.last_render.elapsed();
81        if elapsed.as_secs_f64() > 0.0 {
82            1.0 / elapsed.as_secs_f64()
83        } else {
84            0.0
85        }
86    }
87
88    /// Reset the limiter
89    pub fn reset(&mut self) {
90        self.last_render = Instant::now();
91    }
92}
93
94impl Default for FpsLimiter {
95    fn default() -> Self {
96        Self::new(60) // Default to 60 FPS
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use std::thread;
104
105    #[test]
106    fn test_fps_limiter_creation() {
107        let limiter = FpsLimiter::new(60);
108        assert_eq!(limiter.max_fps(), 60);
109        assert_eq!(limiter.frame_duration(), Duration::from_secs(1) / 60);
110    }
111
112    #[test]
113    fn test_fps_limiter_no_limit() {
114        let limiter = FpsLimiter::new(0);
115        assert_eq!(limiter.max_fps(), 0);
116        assert_eq!(limiter.frame_duration(), Duration::ZERO);
117        assert!(limiter.should_render());
118        assert_eq!(limiter.time_until_next_frame(), Duration::ZERO);
119    }
120
121    #[test]
122    fn test_fps_limiter_should_render() {
123        let mut limiter = FpsLimiter::new(30); // Use lower FPS for more reliable testing
124
125        // Mark as rendered to start fresh
126        limiter.mark_rendered();
127
128        // Should not render immediately after marking
129        assert!(!limiter.should_render());
130
131        // Wait for frame duration (1/30 sec = ~33ms)
132        thread::sleep(Duration::from_millis(40)); // Add buffer for test reliability
133
134        // Should render now
135        assert!(limiter.should_render());
136    }
137
138    #[test]
139    fn test_fps_limiter_time_until_next_frame() {
140        let mut limiter = FpsLimiter::new(60);
141        limiter.mark_rendered();
142
143        let time_until = limiter.time_until_next_frame();
144        assert!(time_until <= Duration::from_secs(1) / 60);
145
146        // Wait past frame duration
147        thread::sleep(Duration::from_millis(20));
148
149        let time_until = limiter.time_until_next_frame();
150        assert_eq!(time_until, Duration::ZERO);
151    }
152
153    #[test]
154    fn test_fps_limiter_set_max_fps() {
155        let mut limiter = FpsLimiter::new(60);
156        assert_eq!(limiter.max_fps(), 60);
157
158        limiter.set_max_fps(30);
159        assert_eq!(limiter.max_fps(), 30);
160        assert_eq!(limiter.frame_duration(), Duration::from_secs(1) / 30);
161
162        limiter.set_max_fps(0);
163        assert_eq!(limiter.max_fps(), 0);
164        assert_eq!(limiter.frame_duration(), Duration::ZERO);
165    }
166
167    #[test]
168    fn test_fps_limiter_reset() {
169        let mut limiter = FpsLimiter::new(60);
170
171        // Mark as rendered and wait
172        limiter.mark_rendered();
173        thread::sleep(Duration::from_millis(20));
174
175        // Reset should update last_render to now
176        limiter.reset();
177
178        // Should not render immediately after reset
179        assert!(!limiter.should_render());
180    }
181
182    #[test]
183    fn test_fps_limiter_default() {
184        let limiter = FpsLimiter::default();
185        assert_eq!(limiter.max_fps(), 60);
186    }
187
188    #[test]
189    fn test_fps_limiter_various_fps_values() {
190        let fps_values = vec![1, 24, 30, 60, 120, 144, 240];
191
192        for fps in fps_values {
193            let limiter = FpsLimiter::new(fps);
194            assert_eq!(limiter.max_fps(), fps);
195            assert_eq!(
196                limiter.frame_duration(),
197                Duration::from_secs(1) / fps as u32
198            );
199        }
200    }
201
202    #[test]
203    #[ignore = "Flaky test due to timing dependencies"]
204    fn test_fps_limiter_actual_fps() {
205        let mut limiter = FpsLimiter::new(60);
206
207        // Mark rendered and wait a specific time
208        limiter.mark_rendered();
209        thread::sleep(Duration::from_millis(100)); // 0.1 second
210
211        // Actual FPS should be around 10 (1/0.1)
212        let actual_fps = limiter.actual_fps();
213        assert!(actual_fps > 8.0 && actual_fps < 12.0);
214    }
215
216    #[test]
217    fn test_fps_limiter_high_fps() {
218        let mut limiter = FpsLimiter::new(240);
219        assert_eq!(limiter.max_fps(), 240);
220
221        // Frame duration should be ~4.16ms for 240 FPS
222        let expected_duration = Duration::from_secs(1) / 240;
223        assert_eq!(limiter.frame_duration(), expected_duration);
224
225        limiter.mark_rendered();
226        assert!(!limiter.should_render());
227
228        // Wait for frame duration
229        thread::sleep(expected_duration + Duration::from_millis(1));
230        assert!(limiter.should_render());
231    }
232
233    #[test]
234    fn test_fps_limiter_low_fps() {
235        let mut limiter = FpsLimiter::new(1);
236        assert_eq!(limiter.max_fps(), 1);
237
238        // Frame duration should be 1 second for 1 FPS
239        assert_eq!(limiter.frame_duration(), Duration::from_secs(1));
240
241        limiter.mark_rendered();
242        assert!(!limiter.should_render());
243
244        // Should not render after 500ms
245        thread::sleep(Duration::from_millis(500));
246        assert!(!limiter.should_render());
247
248        // Should render after 1 second
249        thread::sleep(Duration::from_millis(600));
250        assert!(limiter.should_render());
251    }
252}