all_is_cubes_ui/apps/
time.rs

1use all_is_cubes::math::{PositiveSign, ZeroOne, zo64};
2use all_is_cubes::time::{Duration, Instant, TickSchedule};
3#[cfg(doc)]
4use all_is_cubes::universe::Universe;
5
6/// Algorithm for deciding how to execute simulation and rendering frames.
7/// Platform-independent; does not consult any clocks, only makes decisions
8/// given the provided information.
9#[derive(Clone, Debug, Eq, PartialEq)]
10pub struct FrameClock {
11    schedule: TickSchedule,
12
13    last_absolute_time: Option<Instant>,
14
15    /// Whether there was a step and we should therefore draw a frame.
16    /// TODO: This might go away in favor of actual dirty-notifications.
17    render_dirty: bool,
18
19    accumulated_step_time: Duration,
20
21    draw_fps_counter: FpsCounter,
22}
23
24impl FrameClock {
25    /// Number of steps per frame to permit.
26    /// This sets how low the frame rate can go below the step length before game time
27    /// slows down.
28    pub(crate) const CATCH_UP_STEPS: u32 = 2;
29
30    /// Constructs a new [`FrameClock`].
31    ///
32    /// This operation is independent of the system clock.
33    pub fn new(schedule: TickSchedule) -> Self {
34        Self {
35            schedule,
36            last_absolute_time: None,
37            render_dirty: true,
38            accumulated_step_time: Duration::ZERO,
39            draw_fps_counter: FpsCounter::default(),
40        }
41    }
42
43    #[doc(hidden)] // TODO: Decide whether we want FpsCounter in our public API
44    pub fn draw_fps_counter(&self) -> &FpsCounter {
45        &self.draw_fps_counter
46    }
47
48    fn step_length(&self) -> Duration {
49        self.schedule.delta_t()
50    }
51}
52
53impl FrameClock {
54    /// Advance the clock using a source of absolute time.
55    ///
56    /// This cannot be meaningfully used in combination with
57    /// [`FrameClock::request_frame()`] or [`FrameClock::advance_by()`].
58    pub fn advance_to(&mut self, instant: Instant) {
59        if let Some(last_absolute_time) = self.last_absolute_time {
60            let delta = instant.saturating_duration_since(last_absolute_time);
61            self.accumulated_step_time += delta;
62            self.cap_step_time();
63        }
64        self.last_absolute_time = Some(instant);
65    }
66
67    /// Advance the clock using a source of relative time.
68    pub fn advance_by(&mut self, duration: Duration) {
69        self.accumulated_step_time += duration;
70        self.cap_step_time();
71    }
72
73    /// Reacts to a callback from the environment requesting drawing a frame ASAP if
74    /// we're going to (i.e. `requestAnimationFrame` on the web). Drives the simulation
75    /// clock based on this input (it will not advance if no requests are made).
76    ///
77    /// Returns whether a frame should actually be rendered now. The caller should also
78    /// consult [`FrameClock::should_step()`] afterward to schedule game state steps.
79    ///
80    /// This cannot be meaningfully used in combination with [`FrameClock::advance_to()`].
81    #[must_use]
82    pub fn request_frame(&mut self, time_since_last_frame: Duration) -> bool {
83        let result = self.should_draw();
84        self.did_draw();
85
86        self.advance_by(time_since_last_frame);
87
88        result
89    }
90
91    /// Returns the next time at which [`FrameClock::should_step()`], and then
92    /// [`FrameClock::should_draw()`], should be consulted.
93    ///
94    /// [`FrameClock::advance_to()`] must have previously been called to give an absolute
95    /// time reference.
96    pub fn next_step_or_draw_time(&self) -> Option<Instant> {
97        Some(self.last_absolute_time? + self.step_length())
98    }
99
100    /// Indicates whether a new frame should be drawn, given the amount of time that this
101    /// [`FrameClock`] has been informed has passed.
102    ///
103    /// When a frame *is* drawn, [`FrameClock::did_draw`]] must be called; otherwise, this
104    /// will always return true.
105    pub fn should_draw(&self) -> bool {
106        self.render_dirty
107    }
108
109    /// Informs the [`FrameClock`] that a frame was just drawn.
110    pub fn did_draw(&mut self) {
111        self.render_dirty = false;
112        self.draw_fps_counter.record_frame();
113    }
114
115    /// Indicates whether [`Universe::step()`] should be performed,
116    /// given the amount of time that this [`FrameClock`] has been informed has passed.
117    ///
118    /// When a step *is* performd, [`FrameClock::did_step`] must be called; otherwise, this
119    /// will always return true.
120    pub fn should_step(&self) -> bool {
121        self.accumulated_step_time >= self.step_length()
122    }
123
124    /// Informs the [`FrameClock`] that a step was just performed.
125    ///
126    /// The caller must also provide an updated schedule, in case it has changed.
127    pub fn did_step(&mut self, schedule: TickSchedule) {
128        self.accumulated_step_time -= self.step_length();
129        self.render_dirty = true;
130        self.schedule = schedule;
131    }
132
133    fn cap_step_time(&mut self) {
134        let cap = self.step_length() * Self::CATCH_UP_STEPS;
135        if self.accumulated_step_time > cap {
136            self.accumulated_step_time = cap;
137        }
138    }
139}
140
141/// Counts frame time / frames-per-second against real time as defined by [`Instant::now`].
142#[derive(Clone, Debug, Eq, PartialEq)]
143#[doc(hidden)] // TODO: Decide whether we want FpsCounter in our public API
144pub struct FpsCounter {
145    average_frame_time_seconds: Option<PositiveSign<f64>>,
146    last_frame: Option<Instant>,
147}
148
149impl FpsCounter {
150    pub const fn new() -> Self {
151        Self {
152            average_frame_time_seconds: None,
153            last_frame: None,
154        }
155    }
156
157    pub fn period_seconds(&self) -> f64 {
158        match self.average_frame_time_seconds {
159            Some(nnt) => nnt.into_inner(),
160            None => f64::NAN,
161        }
162    }
163
164    pub fn frames_per_second(&self) -> f64 {
165        self.period_seconds().recip()
166    }
167}
168
169impl FpsCounter {
170    pub fn record_frame(&mut self) {
171        let this_frame = Instant::now();
172
173        let this_seconds = self
174            .last_frame
175            .and_then(|l| {
176                if this_frame > l {
177                    Some(this_frame.saturating_duration_since(l))
178                } else {
179                    None
180                }
181            })
182            .and_then(|duration| PositiveSign::try_from(duration.as_secs_f64()).ok());
183        if let Some(this_seconds) = this_seconds {
184            self.average_frame_time_seconds = Some(
185                if let Some(previous) = self.average_frame_time_seconds.filter(|v| v.is_finite()) {
186                    const MIX: ZeroOne<f64> = zo64(1.0 / 8.0);
187                    this_seconds * MIX + previous * MIX.complement()
188                } else {
189                    // recover from any weirdness or initial state
190                    this_seconds
191                },
192            );
193        }
194        self.last_frame = Some(this_frame);
195    }
196}
197
198impl Default for FpsCounter {
199    fn default() -> Self {
200        Self::new()
201    }
202}