Skip to main content

khora_core/time/
mod.rs

1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! The per-frame [`Time`] resource — the engine's single clock.
16//!
17//! `Time` carries the real wall-clock frame delta, the fixed simulation
18//! step, and the render-interpolation factor. It is the bridge between the
19//! decoupled fixed-timestep simulation and the variable-rate render:
20//!
21//! - The simulation advances in whole [`fixed_delta_seconds`](Time::fixed_delta_seconds)
22//!   steps via an accumulator in the scheduler, so it is frame-rate
23//!   independent (deterministic).
24//! - Rendering happens once per frame; to stay smooth between sim steps the
25//!   render path blends the previous and current transforms by
26//!   [`interpolation_alpha`](Time::interpolation_alpha).
27//!
28//! The scheduler publishes a fresh `Time` each frame; game code and Flows
29//! read it. It lives in [`Runtime::resources`](crate::Runtime) behind an
30//! `Arc<RwLock<Time>>` so the scheduler (which holds `Arc<Runtime>`) can
31//! write it while Flows and game `update` (which borrow `&Runtime`) read it.
32
33use std::sync::{Arc, RwLock};
34
35/// Default fixed simulation step — 60 Hz.
36pub const DEFAULT_FIXED_DELTA_SECONDS: f32 = 1.0 / 60.0;
37
38/// Per-frame timing state shared between the simulation and the render path.
39#[derive(Debug, Clone, Copy, PartialEq)]
40pub struct Time {
41    /// Real wall-clock time elapsed since the previous frame, in seconds,
42    /// clamped to a sane maximum to avoid a "spiral of death" after a long
43    /// stall (debugger break, asset hitch). This is the delta game code
44    /// should use for variable-rate logic.
45    pub delta_seconds: f32,
46    /// The fixed simulation step in seconds — the cadence the deterministic
47    /// fixed-timestep agents (physics) advance at. Defaults to 1/60.
48    pub fixed_delta_seconds: f32,
49    /// Render-interpolation factor in `[0, 1)`: the fraction of a fixed step
50    /// the accumulator carries past the last whole sim step. Render-only
51    /// transform blending uses it; it never affects simulation semantics.
52    pub interpolation_alpha: f32,
53    /// Monotonic frame counter, incremented once per rendered frame.
54    pub frame: u64,
55}
56
57impl Default for Time {
58    fn default() -> Self {
59        Self {
60            delta_seconds: DEFAULT_FIXED_DELTA_SECONDS,
61            fixed_delta_seconds: DEFAULT_FIXED_DELTA_SECONDS,
62            interpolation_alpha: 0.0,
63            frame: 0,
64        }
65    }
66}
67
68impl Time {
69    /// Creates a `Time` with the given fixed step and otherwise default
70    /// values (zero real delta, zero alpha, frame 0).
71    #[must_use]
72    pub fn with_fixed_delta(fixed_delta_seconds: f32) -> Self {
73        Self {
74            fixed_delta_seconds,
75            ..Self::default()
76        }
77    }
78}
79
80/// Shared, interior-mutable handle to the engine's [`Time`] resource.
81///
82/// Registered in [`Runtime::resources`](crate::Runtime) under this type so
83/// the scheduler can publish a fresh `Time` each frame (write lock) while
84/// Flows and game code read it (read lock).
85pub type SharedTime = Arc<RwLock<Time>>;
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn default_uses_60hz_fixed_step() {
93        let t = Time::default();
94        assert_eq!(t.fixed_delta_seconds, 1.0 / 60.0);
95        assert_eq!(t.delta_seconds, 1.0 / 60.0);
96        assert_eq!(t.interpolation_alpha, 0.0);
97        assert_eq!(t.frame, 0);
98    }
99
100    #[test]
101    fn with_fixed_delta_overrides_step_only() {
102        let t = Time::with_fixed_delta(1.0 / 120.0);
103        assert_eq!(t.fixed_delta_seconds, 1.0 / 120.0);
104        assert_eq!(t.interpolation_alpha, 0.0);
105        assert_eq!(t.frame, 0);
106    }
107}