From 82640190b5c5a52dd10f971f97bdd6de2e6c7b76 Mon Sep 17 00:00:00 2001 From: Kevin Reid Date: Sat, 10 Apr 2021 21:24:45 -0700 Subject: [PATCH] Move `FrameClock` to new file `apps/time.rs`. The main motivation for this is that it doesn't really belong in the `universe` module, which can now be solely about implementing `Universe`. Additionally, I've been thinking that passing around a `Duration` isn't the best way to express the information that `step` methods need (if nothing else, 1/60 s isn't an integer number of microseconds), and if I do something about that then it'll make sense to have a file for the new type next to `FrameClock`. --- all-is-cubes/src/apps.rs | 11 ++-- all-is-cubes/src/apps/time.rs | 117 ++++++++++++++++++++++++++++++++++ all-is-cubes/src/universe.rs | 113 -------------------------------- 3 files changed, 124 insertions(+), 117 deletions(-) create mode 100644 all-is-cubes/src/apps/time.rs diff --git a/all-is-cubes/src/apps.rs b/all-is-cubes/src/apps.rs index 134920397..3f1f8e574 100644 --- a/all-is-cubes/src/apps.rs +++ b/all-is-cubes/src/apps.rs @@ -10,9 +10,15 @@ use crate::listen::{DirtyFlag, ListenableCell, ListenableSource, ListenerHelper use crate::space::Space; use crate::tools::ToolError; use crate::transactions::Transaction; -use crate::universe::{FrameClock, URef, Universe, UniverseStepInfo}; +use crate::universe::{URef, Universe, UniverseStepInfo}; use crate::vui::Vui; +mod input; +pub use input::*; + +mod time; +pub use time::*; + /// Everything that a game application needs regardless of platform. /// /// Once we have multiplayer / client-server support, this will become the client-side @@ -164,6 +170,3 @@ impl AllIsCubesAppState { } } } - -mod input; -pub use input::*; diff --git a/all-is-cubes/src/apps/time.rs b/all-is-cubes/src/apps/time.rs new file mode 100644 index 000000000..be5d00182 --- /dev/null +++ b/all-is-cubes/src/apps/time.rs @@ -0,0 +1,117 @@ +// Copyright 2020-2021 Kevin Reid under the terms of the MIT License as detailed +// in the accompanying file README.md or . + +use instant::{Duration, Instant}; // wasm-compatible replacement for std::time::Instant + +/// Algorithm for deciding how to execute simulation and rendering frames. +/// Platform-independent; does not consult any clocks, only makes decisions +/// given the provided information. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct FrameClock { + last_absolute_time: Option, + /// Whether there was a step and we should therefore draw a frame. + /// TODO: This might go away in favor of actual dirty-notifications. + render_dirty: bool, + accumulated_step_time: Duration, +} + +impl FrameClock { + const STEP_LENGTH_MICROS: u64 = 1_000_000 / 60; + const STEP_LENGTH: Duration = Duration::from_micros(Self::STEP_LENGTH_MICROS); + /// Number of steps per frame to permit. + /// This sets how low the frame rate can go below STEP_LENGTH before game time + /// slows down. + pub(crate) const CATCH_UP_STEPS: u8 = 2; + const ACCUMULATOR_CAP: Duration = + Duration::from_micros(Self::STEP_LENGTH_MICROS * Self::CATCH_UP_STEPS as u64); + + /// Constructs a new [`FrameClock`]. + /// + /// This operation is independent of the system clock. + pub fn new() -> Self { + Self { + last_absolute_time: None, + render_dirty: true, + accumulated_step_time: Duration::default(), + } + } + + /// Advance the clock using a source of absolute time. + /// + /// This cannot be meaningfully used in combination with + /// [`FrameClock::request_frame()`]. + pub fn advance_to(&mut self, instant: Instant) { + if let Some(last_absolute_time) = self.last_absolute_time { + let delta = instant - last_absolute_time; + self.accumulated_step_time += delta; + self.cap_step_time(); + } + self.last_absolute_time = Some(instant); + } + + /// Reacts to a callback from the environment requesting drawing a frame ASAP if + /// we're going to (i.e. `requestAnimationFrame` on the web). Drives the simulation + /// clock based on this input (it will not advance if no requests are made). + /// + /// Returns whether a frame should actually be rendered now. The caller should also + /// consult [`FrameClock::should_step()`] afterward to schedule game state steps. + /// + /// This cannot be meaningfully used in combination with [`FrameClock::advance_to()`]. + #[must_use] + pub fn request_frame(&mut self, time_since_last_frame: Duration) -> bool { + let result = self.should_draw(); + self.did_draw(); + + self.accumulated_step_time += time_since_last_frame; + self.cap_step_time(); + + result + } + + /// Indicates whether a new frame should be drawn, given the amount of time that this + /// [`FrameClock`] has been informed has passed. + /// + /// When a frame *is* drawn, [`FrameClock::did_draw`]] must be called; otherwise, this + /// will always return true. + pub fn should_draw(&self) -> bool { + self.render_dirty + } + + /// Informs the [`FrameClock`] that a frame was just drawn. + pub fn did_draw(&mut self) { + self.render_dirty = false; + } + + /// Indicates whether [`Universe::step`] should be performed, given the amount of time + /// that this [`FrameClock`] has been informed has passed. + /// + /// When a step *is* performd, [`FrameClock::did_step`] must be called; otherwise, this + /// will always return true. + pub fn should_step(&self) -> bool { + self.accumulated_step_time >= Self::STEP_LENGTH + } + + /// Informs the [`FrameClock`] that a step was just performed. + pub fn did_step(&mut self) { + self.accumulated_step_time -= Self::STEP_LENGTH; + self.render_dirty = true; + } + + /// The timestep value that should be passed to [`Universe::step`] when stepping in + /// response to [`FrameClock::should_step`] returning true. + pub fn step_length(&self) -> Duration { + Self::STEP_LENGTH + } + + fn cap_step_time(&mut self) { + if self.accumulated_step_time > Self::ACCUMULATOR_CAP { + self.accumulated_step_time = Self::ACCUMULATOR_CAP; + } + } +} + +impl Default for FrameClock { + fn default() -> Self { + Self::new() + } +} diff --git a/all-is-cubes/src/universe.rs b/all-is-cubes/src/universe.rs index 946299c65..c28d2e3cf 100644 --- a/all-is-cubes/src/universe.rs +++ b/all-is-cubes/src/universe.rs @@ -562,119 +562,6 @@ impl CustomFormat for UniverseStepInfo { } } -/// Algorithm for deciding how to execute simulation and rendering frames. -/// Platform-independent; does not consult any clocks, only makes decisions -/// given the provided information. -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct FrameClock { - last_absolute_time: Option, - /// Whether there was a step and we should therefore draw a frame. - /// TODO: This might go away in favor of actual dirty-notifications. - render_dirty: bool, - accumulated_step_time: Duration, -} - -impl FrameClock { - const STEP_LENGTH_MICROS: u64 = 1_000_000 / 60; - const STEP_LENGTH: Duration = Duration::from_micros(Self::STEP_LENGTH_MICROS); - /// Number of steps per frame to permit. - /// This sets how low the frame rate can go below STEP_LENGTH before game time - /// slows down. - pub(crate) const CATCH_UP_STEPS: u8 = 2; - const ACCUMULATOR_CAP: Duration = - Duration::from_micros(Self::STEP_LENGTH_MICROS * Self::CATCH_UP_STEPS as u64); - - /// Constructs a new [`FrameClock`]. - /// - /// This operation is independent of the system clock. - pub fn new() -> Self { - Self { - last_absolute_time: None, - render_dirty: true, - accumulated_step_time: Duration::default(), - } - } - - /// Advance the clock using a source of absolute time. - /// - /// This cannot be meaningfully used in combination with - /// [`FrameClock::request_frame()`]. - pub fn advance_to(&mut self, instant: Instant) { - if let Some(last_absolute_time) = self.last_absolute_time { - let delta = instant - last_absolute_time; - self.accumulated_step_time += delta; - self.cap_step_time(); - } - self.last_absolute_time = Some(instant); - } - - /// Reacts to a callback from the environment requesting drawing a frame ASAP if - /// we're going to (i.e. `requestAnimationFrame` on the web). Drives the simulation - /// clock based on this input (it will not advance if no requests are made). - /// - /// Returns whether a frame should actually be rendered now. The caller should also - /// consult [`FrameClock::should_step()`] afterward to schedule game state steps. - /// - /// This cannot be meaningfully used in combination with [`FrameClock::advance_to()`]. - #[must_use] - pub fn request_frame(&mut self, time_since_last_frame: Duration) -> bool { - let result = self.should_draw(); - self.did_draw(); - - self.accumulated_step_time += time_since_last_frame; - self.cap_step_time(); - - result - } - - /// Indicates whether a new frame should be drawn, given the amount of time that this - /// [`FrameClock`] has been informed has passed. - /// - /// When a frame *is* drawn, [`FrameClock::did_draw`]] must be called; otherwise, this - /// will always return true. - pub fn should_draw(&self) -> bool { - self.render_dirty - } - - /// Informs the [`FrameClock`] that a frame was just drawn. - pub fn did_draw(&mut self) { - self.render_dirty = false; - } - - /// Indicates whether [`Universe::step`] should be performed, given the amount of time - /// that this [`FrameClock`] has been informed has passed. - /// - /// When a step *is* performd, [`FrameClock::did_step`] must be called; otherwise, this - /// will always return true. - pub fn should_step(&self) -> bool { - self.accumulated_step_time >= Self::STEP_LENGTH - } - - /// Informs the [`FrameClock`] that a step was just performed. - pub fn did_step(&mut self) { - self.accumulated_step_time -= Self::STEP_LENGTH; - self.render_dirty = true; - } - - /// The timestep value that should be passed to [`Universe::step`] when stepping in - /// response to [`FrameClock::should_step`] returning true. - pub fn step_length(&self) -> Duration { - Self::STEP_LENGTH - } - - fn cap_step_time(&mut self) { - if self.accumulated_step_time > Self::ACCUMULATOR_CAP { - self.accumulated_step_time = Self::ACCUMULATOR_CAP; - } - } -} - -impl Default for FrameClock { - fn default() -> Self { - Self::new() - } -} - mod sealed_gimmick { /// As a supertrait, this prevents a trait from being implemented outside the crate. pub trait Sealed {}