sable_core/
time.rs

1//! Time tracking utilities.
2//!
3//! Provides types for tracking elapsed time, frame deltas, and timing operations.
4
5use std::time;
6
7/// Re-export of `std::time::Duration` for convenience.
8pub type Duration = time::Duration;
9
10/// Re-export of `std::time::Instant` for convenience.
11pub type Instant = time::Instant;
12
13/// Time state for tracking frame timing.
14///
15/// Tracks the current time, delta time since last frame, and accumulated time.
16#[derive(Debug, Clone)]
17pub struct Time {
18    /// Time since the start of the application.
19    elapsed: Duration,
20    /// Time since the last frame.
21    delta: Duration,
22    /// Fixed timestep for physics updates.
23    fixed_timestep: Duration,
24    /// Accumulated time for fixed timestep updates.
25    accumulator: Duration,
26    /// The instant when the timer was started.
27    start_instant: Instant,
28    /// The instant of the last update.
29    last_instant: Instant,
30    /// Frame count since start.
31    frame_count: u64,
32    /// Time scale multiplier (1.0 = normal speed).
33    time_scale: f64,
34}
35
36impl Default for Time {
37    fn default() -> Self {
38        Self::new()
39    }
40}
41
42impl Time {
43    /// Creates a new time tracker.
44    #[must_use]
45    pub fn new() -> Self {
46        let now = Instant::now();
47        Self {
48            elapsed: Duration::ZERO,
49            delta: Duration::ZERO,
50            fixed_timestep: Duration::from_secs_f64(1.0 / 60.0), // 60 Hz default
51            accumulator: Duration::ZERO,
52            start_instant: now,
53            last_instant: now,
54            frame_count: 0,
55            time_scale: 1.0,
56        }
57    }
58
59    /// Creates a time tracker with a custom fixed timestep.
60    #[must_use]
61    pub fn with_fixed_timestep(fixed_hz: f64) -> Self {
62        let mut time = Self::new();
63        time.fixed_timestep = Duration::from_secs_f64(1.0 / fixed_hz);
64        time
65    }
66
67    /// Updates the time state. Call once per frame.
68    pub fn update(&mut self) {
69        let now = Instant::now();
70        let raw_delta = now.duration_since(self.last_instant);
71
72        // Apply time scale
73        let scaled_delta = Duration::from_secs_f64(raw_delta.as_secs_f64() * self.time_scale);
74
75        self.delta = scaled_delta;
76        self.elapsed += scaled_delta;
77        self.accumulator += scaled_delta;
78        self.last_instant = now;
79        self.frame_count += 1;
80    }
81
82    /// Updates with a specific delta time (useful for testing or replays).
83    pub fn update_with_delta(&mut self, delta: Duration) {
84        let scaled_delta = Duration::from_secs_f64(delta.as_secs_f64() * self.time_scale);
85
86        self.delta = scaled_delta;
87        self.elapsed += scaled_delta;
88        self.accumulator += scaled_delta;
89        self.frame_count += 1;
90    }
91
92    /// Returns the time elapsed since the start.
93    #[inline]
94    #[must_use]
95    pub fn elapsed(&self) -> Duration {
96        self.elapsed
97    }
98
99    /// Returns the time elapsed in seconds.
100    #[inline]
101    #[must_use]
102    pub fn elapsed_secs(&self) -> f32 {
103        self.elapsed.as_secs_f32()
104    }
105
106    /// Returns the time elapsed in seconds (f64).
107    #[inline]
108    #[must_use]
109    pub fn elapsed_secs_f64(&self) -> f64 {
110        self.elapsed.as_secs_f64()
111    }
112
113    /// Returns the delta time since the last frame.
114    #[inline]
115    #[must_use]
116    pub fn delta(&self) -> Duration {
117        self.delta
118    }
119
120    /// Returns the delta time in seconds.
121    #[inline]
122    #[must_use]
123    pub fn delta_secs(&self) -> f32 {
124        self.delta.as_secs_f32()
125    }
126
127    /// Returns the delta time in seconds (f64).
128    #[inline]
129    #[must_use]
130    pub fn delta_secs_f64(&self) -> f64 {
131        self.delta.as_secs_f64()
132    }
133
134    /// Returns the fixed timestep duration.
135    #[inline]
136    #[must_use]
137    pub fn fixed_timestep(&self) -> Duration {
138        self.fixed_timestep
139    }
140
141    /// Returns the fixed timestep in seconds.
142    #[inline]
143    #[must_use]
144    pub fn fixed_timestep_secs(&self) -> f32 {
145        self.fixed_timestep.as_secs_f32()
146    }
147
148    /// Sets the fixed timestep.
149    #[inline]
150    pub fn set_fixed_timestep(&mut self, timestep: Duration) {
151        self.fixed_timestep = timestep;
152    }
153
154    /// Sets the fixed timestep from a frequency in Hz.
155    #[inline]
156    pub fn set_fixed_timestep_hz(&mut self, hz: f64) {
157        self.fixed_timestep = Duration::from_secs_f64(1.0 / hz);
158    }
159
160    /// Checks if a fixed timestep update should run.
161    ///
162    /// Call this in a loop to consume accumulated time:
163    ///
164    /// ```rust,ignore
165    /// while time.should_do_fixed_update() {
166    ///     physics_step(time.fixed_timestep_secs());
167    /// }
168    /// ```
169    #[inline]
170    pub fn should_do_fixed_update(&mut self) -> bool {
171        if self.accumulator >= self.fixed_timestep {
172            self.accumulator -= self.fixed_timestep;
173            true
174        } else {
175            false
176        }
177    }
178
179    /// Returns the interpolation factor for rendering between fixed updates.
180    ///
181    /// Use this to interpolate visual positions between physics states.
182    #[inline]
183    #[must_use]
184    pub fn interpolation_factor(&self) -> f32 {
185        (self.accumulator.as_secs_f64() / self.fixed_timestep.as_secs_f64()) as f32
186    }
187
188    /// Returns the current frame count.
189    #[inline]
190    #[must_use]
191    pub fn frame_count(&self) -> u64 {
192        self.frame_count
193    }
194
195    /// Returns the average frames per second.
196    #[inline]
197    #[must_use]
198    pub fn fps(&self) -> f64 {
199        if self.elapsed.is_zero() {
200            0.0
201        } else {
202            self.frame_count as f64 / self.elapsed.as_secs_f64()
203        }
204    }
205
206    /// Returns the time scale multiplier.
207    #[inline]
208    #[must_use]
209    pub fn time_scale(&self) -> f64 {
210        self.time_scale
211    }
212
213    /// Sets the time scale multiplier.
214    ///
215    /// - 1.0 = normal speed
216    /// - 0.5 = half speed (slow motion)
217    /// - 2.0 = double speed
218    /// - 0.0 = paused
219    #[inline]
220    pub fn set_time_scale(&mut self, scale: f64) {
221        self.time_scale = scale.max(0.0);
222    }
223
224    /// Pauses time (sets scale to 0).
225    #[inline]
226    pub fn pause(&mut self) {
227        self.time_scale = 0.0;
228    }
229
230    /// Resumes time at normal speed.
231    #[inline]
232    pub fn resume(&mut self) {
233        self.time_scale = 1.0;
234    }
235
236    /// Returns true if time is paused.
237    #[inline]
238    #[must_use]
239    pub fn is_paused(&self) -> bool {
240        self.time_scale == 0.0
241    }
242
243    /// Returns the instant when the timer was started.
244    #[inline]
245    #[must_use]
246    pub fn start_instant(&self) -> Instant {
247        self.start_instant
248    }
249
250    /// Resets all time tracking to initial state.
251    pub fn reset(&mut self) {
252        let now = Instant::now();
253        self.elapsed = Duration::ZERO;
254        self.delta = Duration::ZERO;
255        self.accumulator = Duration::ZERO;
256        self.start_instant = now;
257        self.last_instant = now;
258        self.frame_count = 0;
259        // Keep time_scale and fixed_timestep
260    }
261}
262
263/// A simple stopwatch for measuring elapsed time.
264#[derive(Debug, Clone)]
265pub struct Stopwatch {
266    start: Option<Instant>,
267    elapsed: Duration,
268    running: bool,
269}
270
271impl Default for Stopwatch {
272    fn default() -> Self {
273        Self::new()
274    }
275}
276
277impl Stopwatch {
278    /// Creates a new stopped stopwatch.
279    #[must_use]
280    pub fn new() -> Self {
281        Self {
282            start: None,
283            elapsed: Duration::ZERO,
284            running: false,
285        }
286    }
287
288    /// Creates and starts a new stopwatch.
289    #[must_use]
290    pub fn started() -> Self {
291        let mut sw = Self::new();
292        sw.start();
293        sw
294    }
295
296    /// Starts or resumes the stopwatch.
297    pub fn start(&mut self) {
298        if !self.running {
299            self.start = Some(Instant::now());
300            self.running = true;
301        }
302    }
303
304    /// Stops the stopwatch.
305    pub fn stop(&mut self) {
306        if self.running {
307            if let Some(start) = self.start.take() {
308                self.elapsed += start.elapsed();
309            }
310            self.running = false;
311        }
312    }
313
314    /// Resets the stopwatch to zero.
315    pub fn reset(&mut self) {
316        self.start = None;
317        self.elapsed = Duration::ZERO;
318        self.running = false;
319    }
320
321    /// Resets and starts the stopwatch.
322    pub fn restart(&mut self) {
323        self.reset();
324        self.start();
325    }
326
327    /// Returns the elapsed time.
328    #[must_use]
329    pub fn elapsed(&self) -> Duration {
330        let mut total = self.elapsed;
331        if let Some(start) = self.start {
332            total += start.elapsed();
333        }
334        total
335    }
336
337    /// Returns the elapsed time in seconds.
338    #[must_use]
339    pub fn elapsed_secs(&self) -> f32 {
340        self.elapsed().as_secs_f32()
341    }
342
343    /// Returns true if the stopwatch is running.
344    #[must_use]
345    pub fn is_running(&self) -> bool {
346        self.running
347    }
348}
349
350// ============================================================================
351// Async Time Utilities (requires `async` feature)
352// ============================================================================
353
354/// Async timer utilities using tokio.
355#[cfg(feature = "async")]
356pub mod async_time {
357    use super::Duration;
358
359    /// Sleeps for the specified duration asynchronously.
360    ///
361    /// This is a thin wrapper around `tokio::time::sleep` for convenience.
362    ///
363    /// # Example
364    ///
365    /// ```rust,ignore
366    /// use sable_core::time::async_time::sleep;
367    /// use std::time::Duration;
368    ///
369    /// async fn example() {
370    ///     sleep(Duration::from_millis(100)).await;
371    /// }
372    /// ```
373    pub async fn sleep(duration: Duration) {
374        tokio::time::sleep(duration).await;
375    }
376
377    /// Sleeps for the specified number of seconds asynchronously.
378    pub async fn sleep_secs(secs: f64) {
379        tokio::time::sleep(Duration::from_secs_f64(secs)).await;
380    }
381
382    /// Sleeps for the specified number of milliseconds asynchronously.
383    pub async fn sleep_millis(millis: u64) {
384        tokio::time::sleep(Duration::from_millis(millis)).await;
385    }
386
387    /// An async interval timer that ticks at a fixed rate.
388    ///
389    /// # Example
390    ///
391    /// ```rust,ignore
392    /// use sable_core::time::async_time::Interval;
393    /// use std::time::Duration;
394    ///
395    /// async fn game_loop() {
396    ///     let mut interval = Interval::new(Duration::from_secs_f64(1.0 / 60.0));
397    ///
398    ///     loop {
399    ///         interval.tick().await;
400    ///         // Update game state at 60 Hz
401    ///     }
402    /// }
403    /// ```
404    pub struct Interval {
405        inner: tokio::time::Interval,
406    }
407
408    impl Interval {
409        /// Creates a new interval timer.
410        #[must_use]
411        pub fn new(period: Duration) -> Self {
412            Self {
413                inner: tokio::time::interval(period),
414            }
415        }
416
417        /// Creates an interval timer from a frequency in Hz.
418        #[must_use]
419        pub fn from_hz(hz: f64) -> Self {
420            Self::new(Duration::from_secs_f64(1.0 / hz))
421        }
422
423        /// Waits for the next tick.
424        pub async fn tick(&mut self) -> tokio::time::Instant {
425            self.inner.tick().await
426        }
427
428        /// Returns the period of this interval.
429        #[must_use]
430        pub fn period(&self) -> Duration {
431            self.inner.period()
432        }
433    }
434
435    /// A timeout wrapper for async operations.
436    ///
437    /// # Errors
438    ///
439    /// Returns [`TimeoutError`] if the future does not complete within the specified duration.
440    ///
441    /// # Example
442    ///
443    /// ```rust,ignore
444    /// use sable_core::time::async_time::timeout;
445    /// use std::time::Duration;
446    ///
447    /// async fn example() {
448    ///     match timeout(Duration::from_secs(5), some_async_operation()).await {
449    ///         Ok(result) => println!("Got result: {:?}", result),
450    ///         Err(_) => println!("Operation timed out"),
451    ///     }
452    /// }
453    /// ```
454    pub async fn timeout<F, T>(duration: Duration, future: F) -> Result<T, TimeoutError>
455    where
456        F: std::future::Future<Output = T>,
457    {
458        tokio::time::timeout(duration, future)
459            .await
460            .map_err(|_| TimeoutError)
461    }
462
463    /// Error returned when a timeout expires.
464    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
465    pub struct TimeoutError;
466
467    impl std::fmt::Display for TimeoutError {
468        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
469            write!(f, "operation timed out")
470        }
471    }
472
473    impl std::error::Error for TimeoutError {}
474
475    /// A debouncer that ensures an operation is not called more frequently than a given interval.
476    ///
477    /// Useful for rate-limiting operations like auto-save or network requests.
478    pub struct Debouncer {
479        last_triggered: Option<tokio::time::Instant>,
480        interval: Duration,
481    }
482
483    impl Debouncer {
484        /// Creates a new debouncer with the given minimum interval.
485        #[must_use]
486        pub fn new(interval: Duration) -> Self {
487            Self {
488                last_triggered: None,
489                interval,
490            }
491        }
492
493        /// Checks if enough time has passed since the last trigger.
494        ///
495        /// If true, updates the last triggered time.
496        pub fn should_trigger(&mut self) -> bool {
497            let now = tokio::time::Instant::now();
498            match self.last_triggered {
499                Some(last) if now.duration_since(last) < self.interval => false,
500                _ => {
501                    self.last_triggered = Some(now);
502                    true
503                }
504            }
505        }
506
507        /// Returns the time until the next trigger is allowed.
508        #[must_use]
509        pub fn time_until_ready(&self) -> Duration {
510            match self.last_triggered {
511                Some(last) => {
512                    let elapsed = tokio::time::Instant::now().duration_since(last);
513                    self.interval.saturating_sub(elapsed)
514                }
515                None => Duration::ZERO,
516            }
517        }
518
519        /// Waits until the debouncer is ready to trigger again.
520        pub async fn wait_until_ready(&mut self) {
521            let wait_time = self.time_until_ready();
522            if !wait_time.is_zero() {
523                tokio::time::sleep(wait_time).await;
524            }
525            self.last_triggered = Some(tokio::time::Instant::now());
526        }
527    }
528}
529
530#[cfg(test)]
531mod tests {
532    use super::*;
533    use std::thread::sleep;
534
535    #[test]
536    fn test_time_new() {
537        let time = Time::new();
538        assert_eq!(time.frame_count(), 0);
539        assert_eq!(time.elapsed(), Duration::ZERO);
540        assert_eq!(time.delta(), Duration::ZERO);
541        assert!((time.time_scale() - 1.0).abs() < f64::EPSILON);
542    }
543
544    #[test]
545    fn test_time_update_with_delta() {
546        let mut time = Time::new();
547        let delta = Duration::from_millis(16);
548
549        time.update_with_delta(delta);
550
551        assert_eq!(time.frame_count(), 1);
552        assert_eq!(time.delta(), delta);
553        assert_eq!(time.elapsed(), delta);
554    }
555
556    #[test]
557    fn test_time_multiple_updates() {
558        let mut time = Time::new();
559        let delta = Duration::from_millis(16);
560
561        time.update_with_delta(delta);
562        time.update_with_delta(delta);
563        time.update_with_delta(delta);
564
565        assert_eq!(time.frame_count(), 3);
566        assert_eq!(time.elapsed(), delta * 3);
567    }
568
569    #[test]
570    fn test_time_scale() {
571        let mut time = Time::new();
572        time.set_time_scale(0.5);
573
574        let delta = Duration::from_millis(100);
575        time.update_with_delta(delta);
576
577        // With 0.5 time scale, effective delta should be 50ms
578        assert_eq!(time.delta(), Duration::from_millis(50));
579    }
580
581    #[test]
582    fn test_time_pause_resume() {
583        let mut time = Time::new();
584
585        time.pause();
586        assert!(time.is_paused());
587
588        let delta = Duration::from_millis(100);
589        time.update_with_delta(delta);
590
591        // Paused, so no time should pass
592        assert_eq!(time.elapsed(), Duration::ZERO);
593
594        time.resume();
595        assert!(!time.is_paused());
596
597        time.update_with_delta(delta);
598        assert_eq!(time.elapsed(), delta);
599    }
600
601    #[test]
602    fn test_fixed_timestep() {
603        let mut time = Time::with_fixed_timestep(60.0);
604
605        // Simulate a 32ms frame (almost 2 fixed updates at 60Hz)
606        time.update_with_delta(Duration::from_millis(32));
607
608        let mut count = 0;
609        while time.should_do_fixed_update() {
610            count += 1;
611        }
612
613        // At 60Hz (16.67ms), 32ms should give us 1 update with some remainder
614        assert_eq!(count, 1);
615
616        // Do another frame
617        time.update_with_delta(Duration::from_millis(32));
618
619        let mut count = 0;
620        while time.should_do_fixed_update() {
621            count += 1;
622        }
623
624        // Should catch up with accumulated time
625        assert!(count >= 1);
626    }
627
628    #[test]
629    fn test_interpolation_factor() {
630        let mut time = Time::with_fixed_timestep(60.0);
631
632        // Exactly one fixed timestep
633        time.update_with_delta(time.fixed_timestep());
634        time.should_do_fixed_update();
635
636        // Should be close to 0 after consuming the fixed update
637        assert!(time.interpolation_factor() < 0.1);
638    }
639
640    #[test]
641    fn test_time_reset() {
642        let mut time = Time::new();
643        time.update_with_delta(Duration::from_millis(100));
644        time.update_with_delta(Duration::from_millis(100));
645
646        time.reset();
647
648        assert_eq!(time.frame_count(), 0);
649        assert_eq!(time.elapsed(), Duration::ZERO);
650    }
651
652    #[test]
653    fn test_stopwatch_new() {
654        let sw = Stopwatch::new();
655        assert!(!sw.is_running());
656        assert_eq!(sw.elapsed(), Duration::ZERO);
657    }
658
659    #[test]
660    fn test_stopwatch_started() {
661        let sw = Stopwatch::started();
662        assert!(sw.is_running());
663    }
664
665    #[test]
666    fn test_stopwatch_start_stop() {
667        let mut sw = Stopwatch::new();
668
669        sw.start();
670        assert!(sw.is_running());
671
672        sleep(Duration::from_millis(10));
673
674        sw.stop();
675        assert!(!sw.is_running());
676        assert!(sw.elapsed() >= Duration::from_millis(10));
677    }
678
679    #[test]
680    fn test_stopwatch_accumulates() {
681        let mut sw = Stopwatch::new();
682
683        sw.start();
684        sleep(Duration::from_millis(10));
685        sw.stop();
686
687        let first = sw.elapsed();
688
689        sw.start();
690        sleep(Duration::from_millis(10));
691        sw.stop();
692
693        // Should have accumulated both periods
694        assert!(sw.elapsed() >= first + Duration::from_millis(10));
695    }
696
697    #[test]
698    fn test_stopwatch_reset() {
699        let mut sw = Stopwatch::started();
700        sleep(Duration::from_millis(10));
701
702        sw.reset();
703
704        assert!(!sw.is_running());
705        assert_eq!(sw.elapsed(), Duration::ZERO);
706    }
707
708    #[test]
709    fn test_stopwatch_restart() {
710        let mut sw = Stopwatch::started();
711        sleep(Duration::from_millis(10));
712
713        sw.restart();
714
715        assert!(sw.is_running());
716        // Elapsed should be very small after restart
717        assert!(sw.elapsed() < Duration::from_millis(5));
718    }
719
720    #[test]
721    fn test_fps() {
722        let mut time = Time::new();
723
724        // Simulate 60 frames at 16.67ms each
725        for _ in 0..60 {
726            time.update_with_delta(Duration::from_micros(16667));
727        }
728
729        // FPS should be approximately 60
730        let fps = time.fps();
731        assert!(fps > 59.0 && fps < 61.0, "FPS was {fps}");
732    }
733}