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}