Skip to main content

embedded_3dgfx/
timer.rs

1//! Zero-allocation timer, stopwatch, and fixed-timestep accumulator.
2//!
3//! Inspired by Bevy's `bevy_time` (`Timer`, `Stopwatch`, and fixed step scheduling),
4//! adapted for `no_std` microcontrollers. Works seamlessly with both `f32` seconds
5//! and [`core::time::Duration`].
6
7use core::time::Duration;
8
9/// Behavior of a [`Timer`] when its duration is reached.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
11pub enum TimerMode {
12    /// Stop advancing and stay finished until explicitly reset.
13    #[default]
14    Once,
15    /// Wrap around automatically and restart upon reaching duration.
16    Repeating,
17}
18
19/// Tracks elapsed time with pause, reset, and accumulation controls.
20#[derive(Debug, Clone, Copy, PartialEq, Default)]
21pub struct Stopwatch {
22    elapsed: f32,
23    paused: bool,
24}
25
26impl Stopwatch {
27    /// Create a new unpaused stopwatch with `0.0` elapsed time.
28    pub const fn new() -> Self {
29        Self {
30            elapsed: 0.0,
31            paused: false,
32        }
33    }
34
35    /// Advance elapsed time by `dt` seconds (if not paused).
36    #[inline]
37    pub fn tick(&mut self, dt: f32) -> &mut Self {
38        if !self.paused {
39            self.elapsed += dt;
40        }
41        self
42    }
43
44    /// Advance elapsed time by a [`core::time::Duration`].
45    #[inline]
46    pub fn tick_duration(&mut self, duration: Duration) -> &mut Self {
47        self.tick(duration.as_secs_f32())
48    }
49
50    /// Elapsed time in seconds.
51    #[inline]
52    pub fn elapsed_secs(&self) -> f32 {
53        self.elapsed
54    }
55
56    /// Elapsed time as a [`core::time::Duration`].
57    #[inline]
58    pub fn elapsed(&self) -> Duration {
59        Duration::from_secs_f32(self.elapsed.max(0.0))
60    }
61
62    /// Set elapsed time in seconds.
63    #[inline]
64    pub fn set_elapsed(&mut self, time: f32) {
65        self.elapsed = time;
66    }
67
68    /// Reset elapsed time to `0.0` (does not affect pause state).
69    #[inline]
70    pub fn reset(&mut self) {
71        self.elapsed = 0.0;
72    }
73
74    /// Pause tracking. Calls to [`tick`](Stopwatch::tick) will have no effect.
75    #[inline]
76    pub fn pause(&mut self) {
77        self.paused = true;
78    }
79
80    /// Unpause tracking.
81    #[inline]
82    pub fn unpause(&mut self) {
83        self.paused = false;
84    }
85
86    /// Returns `true` if paused.
87    #[inline]
88    pub fn is_paused(&self) -> bool {
89        self.paused
90    }
91}
92
93/// A timer that tracks duration, progress fraction, completion, and repetition.
94#[derive(Debug, Clone, Copy, PartialEq)]
95pub struct Timer {
96    stopwatch: Stopwatch,
97    duration: f32,
98    mode: TimerMode,
99    just_finished: bool,
100}
101
102impl Default for Timer {
103    fn default() -> Self {
104        Self::from_seconds(1.0, TimerMode::Once)
105    }
106}
107
108impl Timer {
109    /// Create a new timer with a duration in seconds and a [`TimerMode`].
110    pub const fn from_seconds(duration: f32, mode: TimerMode) -> Self {
111        Self {
112            stopwatch: Stopwatch::new(),
113            duration: if duration > 0.0 { duration } else { 0.0 },
114            mode,
115            just_finished: false,
116        }
117    }
118
119    /// Create a new timer from a [`core::time::Duration`].
120    pub fn new(duration: Duration, mode: TimerMode) -> Self {
121        Self::from_seconds(duration.as_secs_f32(), mode)
122    }
123
124    /// Advance the timer by `dt` seconds and evaluate completion.
125    pub fn tick(&mut self, dt: f32) -> &mut Self {
126        if self.stopwatch.is_paused() {
127            self.just_finished = false;
128            return self;
129        }
130
131        if self.mode == TimerMode::Once && self.is_finished() {
132            self.just_finished = false;
133            return self;
134        }
135
136        self.stopwatch.tick(dt);
137
138        if self.stopwatch.elapsed_secs() >= self.duration {
139            self.just_finished = true;
140            match self.mode {
141                TimerMode::Once => {
142                    self.stopwatch.set_elapsed(self.duration);
143                }
144                TimerMode::Repeating => {
145                    if self.duration > 0.0 {
146                        let rem = self.stopwatch.elapsed_secs() % self.duration;
147                        self.stopwatch.set_elapsed(rem);
148                    } else {
149                        self.stopwatch.set_elapsed(0.0);
150                    }
151                }
152            }
153        } else {
154            self.just_finished = false;
155        }
156
157        self
158    }
159
160    /// Advance the timer by a [`core::time::Duration`].
161    #[inline]
162    pub fn tick_duration(&mut self, duration: Duration) -> &mut Self {
163        self.tick(duration.as_secs_f32())
164    }
165
166    /// `true` if the timer is finished (or reached its target during this tick).
167    #[inline]
168    pub fn is_finished(&self) -> bool {
169        self.stopwatch.elapsed_secs() >= self.duration
170    }
171
172    /// `true` only on the tick when the timer crossed or reached its duration.
173    #[inline]
174    pub fn just_finished(&self) -> bool {
175        self.just_finished
176    }
177
178    /// Timer completion percentage between `0.0` and `1.0`.
179    #[inline]
180    pub fn fraction(&self) -> f32 {
181        if self.duration <= 0.0 {
182            1.0
183        } else {
184            (self.stopwatch.elapsed_secs() / self.duration).clamp(0.0, 1.0)
185        }
186    }
187
188    /// Remaining duration in seconds until completion.
189    #[inline]
190    pub fn remaining_secs(&self) -> f32 {
191        (self.duration - self.stopwatch.elapsed_secs()).max(0.0)
192    }
193
194    /// Reset the timer to 0 elapsed time.
195    #[inline]
196    pub fn reset(&mut self) {
197        self.stopwatch.reset();
198        self.just_finished = false;
199    }
200
201    /// Pause the timer.
202    #[inline]
203    pub fn pause(&mut self) {
204        self.stopwatch.pause();
205    }
206
207    /// Unpause the timer.
208    #[inline]
209    pub fn unpause(&mut self) {
210        self.stopwatch.unpause();
211    }
212
213    /// `true` if paused.
214    #[inline]
215    pub fn is_paused(&self) -> bool {
216        self.stopwatch.is_paused()
217    }
218
219    /// Elapsed time in seconds.
220    #[inline]
221    pub fn elapsed_secs(&self) -> f32 {
222        self.stopwatch.elapsed_secs()
223    }
224
225    /// Total target duration in seconds.
226    #[inline]
227    pub fn duration_secs(&self) -> f32 {
228        self.duration
229    }
230}
231
232/// Accumulator for fixed-timestep game / physics loops (e.g. 50 Hz or 60 Hz).
233///
234/// Decouples erratic render framerates from deterministic fixed update steps,
235/// avoiding physics instability on microcontrollers with variable SPI display scanout times.
236#[derive(Debug, Clone, Copy, PartialEq)]
237pub struct FixedTimestep {
238    step: f32,
239    accumulator: f32,
240    max_substeps: u32,
241}
242
243impl FixedTimestep {
244    /// Create a fixed timestep with a given step duration (e.g. `1.0 / 60.0`)
245    /// and a maximum number of substeps per tick (e.g. `4`) to prevent spiral-of-death.
246    pub const fn from_hz(hz: f32, max_substeps: u32) -> Self {
247        let step = if hz > 0.0 { 1.0 / hz } else { 1.0 / 60.0 };
248        Self {
249            step,
250            accumulator: 0.0,
251            max_substeps,
252        }
253    }
254
255    /// Create from step delta in seconds.
256    pub const fn from_step(step: f32, max_substeps: u32) -> Self {
257        Self {
258            step: if step > 0.0 { step } else { 0.01666667 },
259            accumulator: 0.0,
260            max_substeps,
261        }
262    }
263
264    /// Feed frame delta-time `frame_dt` and return an iterator of fixed timesteps to simulate.
265    pub fn update(&mut self, frame_dt: f32) -> FixedStepIter {
266        self.accumulator += frame_dt;
267        let mut steps = 0;
268        while self.accumulator >= self.step && steps < self.max_substeps {
269            self.accumulator -= self.step;
270            steps += 1;
271        }
272
273        // Clamp accumulator if we exceeded max_substeps to prevent spiral of death
274        if self.accumulator >= self.step {
275            self.accumulator = 0.0;
276        }
277
278        FixedStepIter {
279            step: self.step,
280            remaining: steps,
281        }
282    }
283
284    /// Interpolation factor `alpha` in `0.0..1.0` between the previous and current fixed states.
285    /// Useful for smooth render state interpolation.
286    #[inline]
287    pub fn alpha(&self) -> f32 {
288        (self.accumulator / self.step).clamp(0.0, 1.0)
289    }
290
291    /// Fixed step delta in seconds.
292    #[inline]
293    pub fn step_secs(&self) -> f32 {
294        self.step
295    }
296}
297
298/// Iterator yielding fixed timesteps for a frame.
299pub struct FixedStepIter {
300    step: f32,
301    remaining: u32,
302}
303
304impl Iterator for FixedStepIter {
305    type Item = f32;
306
307    #[inline]
308    fn next(&mut self) -> Option<Self::Item> {
309        if self.remaining > 0 {
310            self.remaining -= 1;
311            Some(self.step)
312        } else {
313            None
314        }
315    }
316}
317
318#[cfg(test)]
319mod tests {
320    use super::*;
321
322    #[test]
323    fn test_stopwatch() {
324        let mut sw = Stopwatch::new();
325        assert_eq!(sw.elapsed_secs(), 0.0);
326        sw.tick(0.5);
327        assert_eq!(sw.elapsed_secs(), 0.5);
328        sw.pause();
329        sw.tick(0.5);
330        assert_eq!(sw.elapsed_secs(), 0.5);
331        sw.unpause();
332        sw.tick(0.5);
333        assert_eq!(sw.elapsed_secs(), 1.0);
334        sw.reset();
335        assert_eq!(sw.elapsed_secs(), 0.0);
336    }
337
338    #[test]
339    fn test_timer_once() {
340        let mut timer = Timer::from_seconds(1.0, TimerMode::Once);
341        assert!(!timer.is_finished());
342        assert!(!timer.just_finished());
343        assert_eq!(timer.fraction(), 0.0);
344
345        timer.tick(0.5);
346        assert!(!timer.is_finished());
347        assert_eq!(timer.fraction(), 0.5);
348        assert_eq!(timer.remaining_secs(), 0.5);
349
350        timer.tick(0.5);
351        assert!(timer.is_finished());
352        assert!(timer.just_finished());
353        assert_eq!(timer.fraction(), 1.0);
354
355        timer.tick(0.5);
356        assert!(timer.is_finished());
357        assert!(!timer.just_finished());
358    }
359
360    #[test]
361    fn test_timer_repeating() {
362        let mut timer = Timer::from_seconds(1.0, TimerMode::Repeating);
363        timer.tick(0.7);
364        assert!(!timer.just_finished());
365
366        timer.tick(0.5); // total 1.2 -> wraps to 0.2
367        assert!(timer.just_finished());
368        assert!((timer.elapsed_secs() - 0.2).abs() < 1e-5);
369    }
370
371    #[test]
372    fn test_fixed_timestep() {
373        let mut fixed = FixedTimestep::from_hz(60.0, 4); // 0.0166667s
374        let step = fixed.step_secs();
375
376        // 1 full step + half step in frame
377        let count = fixed.update(step * 1.5).count();
378        assert_eq!(count, 1);
379        assert!(fixed.alpha() > 0.4 && fixed.alpha() < 0.6);
380
381        // Another half step -> triggers second step
382        let count2 = fixed.update(step * 0.5).count();
383        assert_eq!(count2, 1);
384    }
385
386    #[test]
387    fn test_stopwatch_duration_controls_and_pause() {
388        let mut sw = Stopwatch::new();
389        sw.tick_duration(Duration::from_millis(250));
390        assert!((sw.elapsed_secs() - 0.25).abs() < 1e-4);
391        assert_eq!(sw.elapsed(), Duration::from_secs_f32(0.25));
392        sw.set_elapsed(3.0);
393        assert_eq!(sw.elapsed_secs(), 3.0);
394        sw.pause();
395        assert!(sw.is_paused());
396        sw.tick(1.0);
397        assert_eq!(sw.elapsed_secs(), 3.0);
398        sw.unpause();
399        assert!(!sw.is_paused());
400        sw.reset();
401        assert_eq!(sw.elapsed_secs(), 0.0);
402    }
403
404    #[test]
405    fn test_timer_pause_duration_and_reset_paths() {
406        let mut timer = Timer::new(Duration::from_millis(500), TimerMode::Repeating);
407        assert!((timer.duration_secs() - 0.5).abs() < 1e-4);
408        timer.tick(0.1);
409        assert!((timer.elapsed_secs() - 0.1).abs() < 1e-4);
410        timer.pause();
411        assert!(timer.is_paused());
412        timer.tick(0.2);
413        assert!((timer.elapsed_secs() - 0.1).abs() < 1e-4);
414        timer.unpause();
415        timer.tick_duration(Duration::from_millis(500));
416        assert!(timer.just_finished());
417        timer.reset();
418        assert!(!timer.is_finished());
419        assert_eq!(timer.elapsed_secs(), 0.0);
420        let _ = Timer::default();
421    }
422}