fps_timer/
lib.rs

1use std::{
2    hint, thread,
3    time::{Duration, Instant},
4};
5
6/// Timer instance
7pub struct Timer {
8    /// instant of the previous call to frame()
9    previous: Instant,
10    /// instant of the previous call to log()
11    previous_log: Instant,
12    /// target time for the next frame
13    target: Instant,
14    /// target time for the next log
15    log_target: Instant,
16    /// time between two frames
17    delta_time: Duration,
18    /// time interval between two logs
19    log_interval: Duration,
20    /// count of frames the last time log() was called
21    prev_framecount: u64,
22    /// current frame count
23    framecount: u64,
24    /// maximum amount of frames to lag behind
25    max_delay_frames: u32,
26    /// improved_accuracy
27    high_precision: bool,
28}
29
30/// since thread::sleep usually is not accurate down to the millisecond, we
31/// only suspend the thread for max(delay - 1ms, 0)
32/// and spin in a loop for the rest of the time
33///
34/// returns the last measured timestamp
35fn sleep_until_high_precision(target: Instant) -> Instant {
36    // calculate approximate duration until target time
37    let now = Instant::now();
38
39    // early out to avoid additional measurement
40    if now >= target {
41        return now;
42    }
43
44    // calculate the required wait duration
45    let approx_duration = target.duration_since(now);
46
47    // sleep for a maximum of 1ms less than the approximate required delay
48    // (0.250ms on unix)
49    #[cfg(unix)]
50    const MAX_BUSY_WAIT: Duration = Duration::from_micros(250);
51    #[cfg(not(unix))]
52    const MAX_BUSY_WAIT: Duration = Duration::from_millis(1);
53    if approx_duration > MAX_BUSY_WAIT {
54        thread::sleep(approx_duration - MAX_BUSY_WAIT);
55    }
56
57    busy_wait_until(target)
58}
59
60fn sleep_until(target: Instant) -> Instant {
61    // calculate approximate duration until target time
62    let now = Instant::now();
63
64    // early out to avoid additional measurement
65    if now >= target {
66        return now;
67    }
68
69    let suspend_duration = target - now;
70    thread::sleep(suspend_duration);
71    busy_wait_until(target)
72}
73
74fn busy_wait_until(target: Instant) -> Instant {
75    // spin until target time is reached and return it
76    loop {
77        let time = Instant::now();
78        if time >= target {
79            break time;
80        }
81        hint::spin_loop();
82    }
83}
84
85/// A struct holding information about the previous logging interval
86#[derive(Debug)]
87pub struct Log {
88    /// average delta time between frames since the last call to [`Timer::log`]
89    delta_avg: Duration,
90}
91
92impl Log {
93    /// frame time averaged over the interval since the last call to [`Timer::log`]
94    pub fn delta_time_avg(&self) -> Duration {
95        self.delta_avg
96    }
97
98    /// frame time averaged over the interval since the last call to [`Timer::log`]
99    /// in milliseconds
100    pub fn delta_time_avg_ms(&self) -> f64 {
101        self.delta_avg.as_secs_f64() * 1000.
102    }
103
104    /// fps averaged over the interval since the last call to [`Timer::log`]
105    pub fn fps_average(&self) -> f64 {
106        1. / self.delta_avg.as_secs_f64()
107    }
108}
109
110impl Default for Timer {
111    fn default() -> Self {
112        let now = Instant::now();
113        let delta_time = Duration::from_secs_f64(1.0 / 60.);
114        let log_interval = Duration::from_millis(100);
115        Self {
116            framecount: 0,
117            log_interval,
118            previous: now,
119            target: now + delta_time,
120            previous_log: now,
121            prev_framecount: 0,
122            log_target: now + log_interval,
123            delta_time,
124            max_delay_frames: 2,
125            high_precision: true,
126        }
127    }
128}
129
130impl Timer {
131    /// Sets the logging interval of this timer to `log_interval`.
132    ///
133    /// # Arguments
134    /// * `log_interval` - logging interval as used by [`Self::log`]
135    ///
136    /// # Returns
137    /// [`Self`] the (modified) timer
138    ///
139    /// # Example
140    /// ```rust
141    /// use std::time::Duration;
142    /// use fps_timer::Timer;
143    /// let mut timer = Timer::default()
144    ///     .log_interval(Duration::from_millis(100))
145    ///     .fps(240.);
146    /// ```
147    pub fn log_interval(mut self, log_interval: Duration) -> Self {
148        self.log_interval = log_interval;
149        self.log_target = self.previous + log_interval;
150        self
151    }
152
153    /// Sets the target frametime to the specified amount.
154    ///
155    /// # Arguments
156    /// * `delta` - target frametime
157    ///
158    /// # Returns
159    /// [`Self`] the (modified) timer
160    ///
161    /// # Example
162    /// ```rust
163    /// use std::time::Duration;
164    /// use fps_timer::Timer;
165    /// let mut timer = Timer::default()
166    ///     .frame_time(Duration::from_secs_f64(1. / 60.));
167    /// ```
168    pub fn frame_time(mut self, delta: Duration) -> Self {
169        self.delta_time = delta;
170        self.target = self.previous + delta;
171        self
172    }
173
174    /// Sets the framerate target to the specified amount.
175    ///
176    /// # Arguments
177    /// * `fps` - target framerate
178    ///
179    /// # Returns
180    /// [`Self`] the (modified) timer
181    ///
182    /// # Example
183    /// ```rust
184    /// use fps_timer::Timer;
185    /// let mut timer = Timer::default()
186    ///     .fps(60.);
187    /// ```
188    pub fn fps(self, fps: f64) -> Self {
189        let duration = match fps {
190            0. => Duration::ZERO,
191            fps => Duration::from_secs_f64(1. / fps),
192        };
193        self.frame_time(duration)
194    }
195
196    /// Enable or disable improved accuracy for this timer.
197    ///
198    /// Enabling high precision makes the timer more precise
199    /// at the cost of higher power consumption because
200    /// part of the duration is awaited in a busy spinloop.
201    ///
202    /// Defaults to `true`
203    ///
204    /// # Arguments
205    /// * `enable` - whether or not to enable higher precision
206    ///
207    /// # Returns
208    /// [`Self`] the (modified) timer
209    ///
210    /// # Example
211    /// ```rust
212    /// use fps_timer::Timer;
213    /// let mut timer = Timer::default()
214    ///     .fps(60.);
215    /// ```
216    pub fn high_precision(mut self, enabled: bool) -> Self {
217        self.high_precision = enabled;
218        self
219    }
220
221    /// Waits until the specified frametime target is reached
222    /// and returns the [`Duration`] since the last call
223    /// to [`Self::frame()`] of this [`Timer`] (= frametime).
224    ///
225    /// # Example
226    /// ```no_run
227    /// use std::time::Duration;
228    /// use fps_timer::Timer;
229    ///
230    /// fn update(dt: Duration) {
231    ///     // game logic
232    /// }
233    ///
234    /// fn main()  {
235    ///     let mut timer = Timer::default();
236    ///     loop {
237    ///         let delta_time = timer.frame();
238    ///         update(delta_time);
239    ///     }
240    /// }
241    /// ```
242    pub fn frame(&mut self) -> Duration {
243        // increment framecount
244        self.framecount += 1;
245
246        // get current time
247        let mut current = Instant::now();
248
249        if self.delta_time > Duration::ZERO {
250            // calculate if frame was too late
251            let behind = if current > self.target {
252                current - self.target
253            } else {
254                Duration::ZERO
255            };
256
257            // If the frame is more than `slack` behind,
258            // we update the target to the current time,
259            // scheduling the next frame for `current + delta_time`.
260            //
261            // Otherwise, the next frame is scheduled for
262            // `prev_target + delta_time` to allow the timer to catch up.
263            if behind > self.slack() {
264                self.target = current;
265            }
266
267            // wait until target instant if needed
268            if current < self.target {
269                current = if self.high_precision {
270                    sleep_until_high_precision(self.target)
271                } else {
272                    sleep_until(self.target)
273                };
274            }
275
276            // update target time
277            self.target += self.delta_time;
278        }
279
280        // calculate frame_time and update previous time
281        let frame_time = current.duration_since(self.previous);
282        self.previous = current;
283        frame_time
284    }
285
286    /// returns [`Some<Log>`], holding information
287    /// about the previous logging interval, every time
288    /// the interval specified by [`Timer::log_interval`] has passed
289    /// and [`None`] otherwise
290    pub fn log(&mut self) -> Option<Log> {
291        // check if it's time to log fps
292        let current = self.previous;
293        if current < self.log_target {
294            return None;
295        }
296
297        // frames since last log (guaranteed to be at least 1)
298        let frames = (self.framecount - self.prev_framecount) as u32;
299        if frames == 0 {
300            return None;
301        }
302
303        // avg frametime = duration / (frames in this duration)
304        let delta_avg = current.duration_since(self.previous_log) / frames;
305
306        // set time of current and next log (current time + log interval)
307        self.log_target = current + self.log_interval;
308        self.previous_log = current;
309        self.prev_framecount = self.framecount;
310
311        Some(Log { delta_avg })
312    }
313
314    /// The slack of the timer, i.e. the amount of time in which a game
315    /// is allowed to lag behind while allowing it to catch up.
316    /// If the game lags behind more than this slack, the target frame
317    /// time is relaxed to not fall behind completely.
318    fn slack(&self) -> Duration {
319        self.max_delay_frames * self.delta_time
320    }
321}