Skip to content

Commit

Permalink
Move FrameClock to new file apps/time.rs.
Browse files Browse the repository at this point in the history
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`.
  • Loading branch information
kpreid committed Apr 11, 2021
1 parent 5085007 commit 8264019
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 117 deletions.
11 changes: 7 additions & 4 deletions all-is-cubes/src/apps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -164,6 +170,3 @@ impl AllIsCubesAppState {
}
}
}

mod input;
pub use input::*;
117 changes: 117 additions & 0 deletions all-is-cubes/src/apps/time.rs
Original file line number Diff line number Diff line change
@@ -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 <https://opensource.org/licenses/MIT>.

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<Instant>,
/// 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()
}
}
113 changes: 0 additions & 113 deletions all-is-cubes/src/universe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -562,119 +562,6 @@ impl CustomFormat<StatusText> 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<Instant>,
/// 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 {}
Expand Down

0 comments on commit 8264019

Please sign in to comment.