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    fn test_fps_limiter_actual_fps() {
204        let mut limiter = FpsLimiter::new(60);
205
206        // Mark rendered and wait a reasonable time for measurement
207        limiter.mark_rendered();
208        thread::sleep(Duration::from_millis(200)); // 0.2 second for more stable measurement
209
210        // Actual FPS should be around 5 (1/0.2), with generous bounds for CI environments
211        let actual_fps = limiter.actual_fps();
212        assert!(
213            actual_fps > 2.0 && actual_fps < 10.0,
214            "Expected FPS between 2-10, got {}",
215            actual_fps
216        );
217    }
218
219    #[test]
220    fn test_fps_limiter_high_fps() {
221        let mut limiter = FpsLimiter::new(240);
222        assert_eq!(limiter.max_fps(), 240);
223
224        // Frame duration should be ~4.16ms for 240 FPS
225        let expected_duration = Duration::from_secs(1) / 240;
226        assert_eq!(limiter.frame_duration(), expected_duration);
227
228        limiter.mark_rendered();
229        assert!(!limiter.should_render());
230
231        // Wait for frame duration
232        thread::sleep(expected_duration + Duration::from_millis(1));
233        assert!(limiter.should_render());
234    }
235
236    #[test]
237    fn test_fps_limiter_low_fps() {
238        let mut limiter = FpsLimiter::new(1);
239        assert_eq!(limiter.max_fps(), 1);
240
241        // Frame duration should be 1 second for 1 FPS
242        assert_eq!(limiter.frame_duration(), Duration::from_secs(1));
243
244        limiter.mark_rendered();
245        assert!(!limiter.should_render());
246
247        // Should not render after 500ms
248        thread::sleep(Duration::from_millis(500));
249        assert!(!limiter.should_render());
250
251        // Should render after 1 second
252        thread::sleep(Duration::from_millis(600));
253        assert!(limiter.should_render());
254    }
255}