Skip to main content

renew_frame/
lib.rs

1//! Fixed-timestep frame scheduling: the deterministic accumulator, the
2//! step budget that bounds a stall, and the interpolation factor for
3//! rendering between steps.
4//!
5//! The loop is a passive integer state machine. It owns no loop, drives no
6//! application, knows nothing of rendering, GPUs or windows, and never
7//! reads a clock — it *cannot*, having no dependency that offers one. Its
8//! whole job is one total function: [`FrameLoop::begin_frame`] answers
9//! *given the schedule so far and this instant, how many fixed steps are
10//! due, how many did the budget refuse, and how far between steps is the
11//! renderer.* The caller reads the one clock, executes the steps, and
12//! renders.
13//!
14//! ```
15//! use renew_frame::{FrameLoop, FrameStats, StepBudget, Timestamp, Timestep};
16//!
17//! let mut frame = FrameLoop::new(
18//!     Timestep::HZ_60,
19//!     StepBudget::DEFAULT,
20//!     Timestamp::from_nanos(0),
21//! );
22//! let mut stats = FrameStats::new();
23//!
24//! // A headless driver: no clock is read, so the whole run is a pure
25//! // function of the timestamp sequence and is byte-comparable across
26//! // runs, processes and machines.
27//! for k in 1..=600u64 {
28//!     let now = Timestamp::from_nanos(k.saturating_mul(16_666_667));
29//!     let plan = frame.begin_frame(now);
30//!     for step in plan.steps() {
31//!         let _ = (step.tick, step.dt, step.sim_time); // advance the world here
32//!     }
33//!     // Render between steps with `renew_math::Alpha::new(...)`,
34//!     // built from `plan.remainder()` and `plan.timestep()`.
35//!     stats.absorb(&plan);
36//! }
37//!
38//! assert_eq!(stats.frames(), 600);
39//! assert_eq!(stats.ticks(), 600);
40//! assert_eq!(stats.steps_dropped(), 0);
41//! ```
42//!
43//! # Contract
44//!
45//! - **Deterministic.** For a fixed build and platform, [`FrameLoop`]
46//!   is a pure function of `(timestep, budget, start, the sequence of
47//!   timestamps passed to begin_frame)`. It reads no clock, allocates
48//!   nothing, spawns nothing, and holds no iteration-order-dependent
49//!   state. A headless run supplies that sequence synthetically and is
50//!   reproducible; a realtime run supplies a measured one, which is a
51//!   different *input trace*, not nondeterministic *code*.
52//! - **Nothing can fail.** Non-zero types, a saturating bank and a
53//!   saturating delta between them leave no error to report, so
54//!   `begin_frame` returns no `Result` — an uninhabitable error variant
55//!   would be a lie about the API. Nothing here panics and nothing
56//!   unwinds.
57//! - **The plan must be executed.** The one available contract violation —
58//!   a caller that ignores its plan — is unobservable from inside, so it
59//!   is contract text with `#[must_use]` as the mitigation rather than an
60//!   assertion. A skipped plan silently desynchronizes the simulation from
61//!   the tick counter.
62//! - **Clamp and discard, always reported.** Steps beyond the budget are
63//!   discarded, never banked: keeping the surplus *is* the spiral of
64//!   death. Simulation time therefore falls permanently behind the wall
65//!   clock, and [`FramePlan::dropped`] is the exact, non-optional record
66//!   of by how much.
67//! - **`alpha` is never an input to simulation.** It is a render-side hint
68//!   in `[0, 1)`, and it is deliberately excluded from the schedule
69//!   digest.
70//! - **Zero dependencies, and this crate never logs.** A dropped step is
71//!   reported through the returned plan; whether that is a log line is the
72//!   caller's decision.
73//!
74//! # Extension points
75//!
76//! None. There is no trait, no `dyn`, and no runtime polymorphism here —
77//! the manifest says so and CI holds the crate to it. The growth point is
78//! named rather than pre-built: a trait arrives when a second
79//! implementation exists.
80
81// This crate reports; it does not print. Diagnostics belong to the caller,
82// which is what keeps the dependency list empty.
83// The determinism rule in the language standard: a simulation crate does not
84// perform floating-point arithmetic whose result can reach digested state.
85// Denied here rather than left to review — the lint covers operators only, so
86// it is necessary and not sufficient, but what it does cover it covers with
87// teeth.
88//
89// This crate held the tree's only exemption: the interpolation factor was
90// computed here, with an `allow` at the expression. It is gone. The
91// factor moved to `renew-math` — a crate a simulation is mechanically
92// forbidden from reaching — and this crate now performs no floating-point
93// arithmetic at all. There is no `allow` below, and adding one would be a
94// change to the language standard, not a local decision.
95#![deny(clippy::print_stdout, clippy::print_stderr, clippy::float_arithmetic)]
96
97mod digest;
98mod report;
99mod schedule;
100mod time;
101
102pub use digest::StateHash;
103pub use report::{FrameStats, FrameStatsJson, FrameTiming, FrameTimingJson};
104pub use schedule::{FrameLoop, FramePlan, Step, Steps};
105pub use time::{Nanos, StepBudget, Timestamp, Timestep};