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}