tiny.game.clock
Defined in tiny.game.
A clock that turns the time a host measures between draws into fixed simulation steps and numbers each step it hands out.
API (11)
Actions
Public operations.
Clock.commandStep: Advances the clock by exactly one step and returns it, reading no elapsed time.Clock.init: Returns a clock at step 0 forconfig, onceConfig.validateaccepts it.Clock.next: Returns the next planned step and counts it off the current plan.Clock.offerElapsed: Takes the nanoseconds since the caller's last chance to draw and returns a plan of at mostConfig.max_catch_up_stepssteps.Config.cadence: Returns the number of steps in each slower frame: the step rate divided by the slower-frame rate.Config.validate: Checks that the rates can drive a clock.
Types and contracts
Public types and contracts.
Clock: A fixed-step clock that turns elapsed time into planned steps, numbers each step it hands out, and carries the leftover time from one offer to the next.Config: The rates a clock runs at: steps a second, slower frames a second, and the most steps one offer plans.Plan: The result of oneClock.offerElapsedcall: the number of steps to run now and the count of whole steps dropped past the cap.Step: One simulation step: its index, the slower frame it falls in, its position inside that frame, and its length as a fraction of a second.
Values and defaults
Public values and defaults.
nanoseconds_per_second: The number of nanoseconds in one second, one billion.
Source
Source: fun/game/src/clock.zig
zig
//! A clock that turns the time a host measures between draws into fixed simulation steps and//! numbers each step it hands out. A host wants each step to cover the same span of simulated time//! however often it draws the screen. A game whose update logic was written for a second, slower//! rate, for example 20 updates a second inside 60 steps a second, needs each step to know which//! slower update it falls in and its position inside it. A tool that drives the game headless//! advances it one step per command, and the game stays at the step it has reached while the tool//! waits.//!//! The time between two draws varies, so it rarely comes to a whole number of steps, and the time//! left over has to count toward a later step. A long pause between draws owes the simulation many//! steps at once: 100 milliseconds at 60 steps a second owes six.//!//! The caller offers the clock the nanoseconds since its last draw (`Clock.offerElapsed`), and the//! clock turns them into whole steps and keeps the leftover, as an exact integer, for the next//! offer. The clock reads no time of its own, and it leaves to the caller the choice of when to//! draw and whether to blend between steps. Each offer returns the number of steps to run with a//! count of the whole steps dropped past a cap (a *plan*), and the cap//! (`Config.max_catch_up_steps`) is four steps unless the caller sets another. For the//! 100-millisecond pause, the plan holds four steps and counts two dropped. The caller runs every//! planned step, one call to `Clock.next` each, before it offers time again, and the clock asserts//! that order. When a caller breaks that order, `Clock.offerElapsed` and `Clock.next` panic in//! Debug and ReleaseSafe builds, and their behavior is undefined in ReleaseFast and ReleaseSmall//! builds. For a host driven by commands, `Clock.commandStep` advances exactly one step and reads//! no elapsed time. Each step (`Step`) carries its index, counted from zero, and its length as a//! fraction of a second, one over the step rate. Each step also carries which frame of the slower//! rate it falls in (a *slower frame*) and its position inside that frame, counted from zero//! (`phase`). At 60 steps and 20 slower frames a second, each slower frame holds three steps, and//! the positions run 0, 1, 2. `Clock.init` checks the rates with `Config.validate` before it builds//! a clock.const std = @import("std");/// The number of nanoseconds in one second, one billion. `offerElapsed` divides by it to turn/// elapsed nanoseconds into whole steps, and `Config.validate` refuses a step rate above it.pub const nanoseconds_per_second: u64 = 1_000_000_000;/// The rates a clock runs at: steps a second, slower frames a second, and the most steps one offer/// plans. A host fills one in and passes it to `Clock.init`. `Clock.init` checks it with `validate`/// before it builds a clock.pub const Config = struct { /// Simulation steps in one second of simulated time, from 1 to one billion. Each step lasts one /// over this rate of a second. steps_per_second: u32, /// Slower frames in one second. The step rate has to be a whole multiple of it. At 60 steps and /// 20 slower frames a second, each slower frame holds three steps. source_frames_per_second: u32, /// The most steps one `Clock.offerElapsed` call plans, 4 unless the caller sets another value, /// and at least 1. Whole steps past this cap are dropped and counted in `Plan.dropped_steps`. max_catch_up_steps: u32 = 4, /// Checks that the rates can drive a clock. `Clock.init` calls it before it builds a clock. The /// call returns `error.InvalidRate` for a step rate of zero or above one billion a second, a /// slower-frame rate of zero, or a cap of zero. The limit of one step per nanosecond keeps the /// count of whole steps from any offer within 64 bits. The call returns /// `error.NonIntegralCadence` when the remainder of the step rate divided by the slower-frame /// rate is above zero. pub fn validate(self: Config) !void { if (self.steps_per_second == 0 or self.steps_per_second > nanoseconds_per_second or self.source_frames_per_second == 0 or self.max_catch_up_steps == 0) return error.InvalidRate; if (self.steps_per_second % self.source_frames_per_second != 0) return error.NonIntegralCadence; } /// Returns the number of steps in each slower frame: the step rate divided by the slower-frame /// rate. The clock derives each step's slower frame and position from it. The call divides by /// the slower-frame rate, and `validate` refuses a rate of zero. With a rate of zero, the call /// panics in Debug and ReleaseSafe builds, and its behavior is undefined in ReleaseFast and /// ReleaseSmall builds. pub fn cadence(self: Config) u32 { return self.steps_per_second / self.source_frames_per_second; }};/// One simulation step: its index, the slower frame it falls in, its position inside that frame,/// and its length as a fraction of a second. A host passes each step to the game's update, and a/// recording stores it beside that step's input. `Clock.next` and `Clock.commandStep` return one.pub const Step = struct { /// The step's number, counted from zero by the clock that made it. A recording takes steps in /// order of this number, starting at 0. index: u64, /// The slower frame the step falls in: its index divided by the steps in each slower frame. source_frame: u64, /// The step's position inside its slower frame, counted from zero: its index modulo the steps /// in each slower frame. At 60 steps and 20 slower frames a second, the positions run 0, 1, 2. phase: u32, /// The numerator of the step's length in seconds, always 1. dt_numerator: u32, /// The denominator of the step's length in seconds, equal to `Config.steps_per_second`. dt_denominator: u32,};/// The result of one `Clock.offerElapsed` call: the number of steps to run now and the count of/// whole steps dropped past the cap. A host runs `steps` calls to `Clock.next` before it offers/// time again, and reports `dropped_steps` when it is above zero.pub const Plan = struct { /// Steps to run before the next offer, one call to `Clock.next` each. The count is at most /// `Config.max_catch_up_steps`. steps: u32, /// Whole steps past the cap that the clock skipped. No later offer returns them, and a dropped /// step takes no index. The next step the clock hands out takes the next index in sequence, so /// step indexes stay contiguous. The part of a step left over after the whole steps still /// carries to the next offer. dropped_steps: u64,};/// A fixed-step clock that turns elapsed time into planned steps, numbers each step it hands out,/// and carries the leftover time from one offer to the next. A host that draws on its own schedule/// offers it the time since each draw, and a host driven by commands takes one step per command/// from it. The clock keeps the rates it was built with, the next step's index, the leftover time,/// and the count of planned steps still to run. The clock reads no time of its own: elapsed time/// enters only as the argument of `offerElapsed`. The clock asserts that a caller runs every/// planned step before it offers time again or takes a command step. When a caller breaks this/// order, `offerElapsed`, `commandStep` and `next` panic in Debug and ReleaseSafe builds, and their/// behavior is undefined in ReleaseFast and ReleaseSmall builds.pub const Clock = struct { config: Config, /// The index the next step gets, 0 for a new clock. A host that resumes a run sets it before /// the first step to the step it has reached, and reads it back as the current step number. index: u64 = 0, remainder: u64 = 0, pending: u32 = 0, /// Returns a clock at step 0 for `config`, once `Config.validate` accepts it. A host builds its /// clock once, before its loop. The call returns `error.InvalidRate` or /// `error.NonIntegralCadence` from `validate`. pub fn init(config: Config) !Clock { try config.validate(); return .{ .config = config }; } /// Takes the nanoseconds since the caller's last chance to draw and returns a plan of at most /// `Config.max_catch_up_steps` steps. A host calls it once each time it could draw, then runs /// the planned steps. The clock multiplies the nanoseconds by the step rate and adds the /// leftover from the last offer, and it keeps the new leftover for the next offer, all in /// integers. Whole steps past the cap are dropped and counted in the plan. The clock leaves to /// the caller the choice of when to draw and whether to blend the drawn state between steps. /// The call asserts that every step of the previous plan has run. When planned steps are left, /// the call panics in Debug and ReleaseSafe builds, and its behavior is undefined in /// ReleaseFast and ReleaseSmall builds. At 60 steps a second, an offer of 100 milliseconds /// plans four steps and drops two. pub fn offerElapsed(self: *Clock, elapsed_ns: u64) Plan { std.debug.assert(self.pending == 0); const scaled: u128 = @as(u128, elapsed_ns) * self.config.steps_per_second + self.remainder; const whole = scaled / nanoseconds_per_second; self.remainder = @intCast(scaled % nanoseconds_per_second); const cap = self.config.max_catch_up_steps; self.pending = @intCast(@min(whole, @as(u128, cap))); return .{ .steps = self.pending, .dropped_steps = @intCast(whole - @as(u128, self.pending)) }; } /// Advances the clock by exactly one step and returns it, reading no elapsed time. A headless /// host calls it once for each step a step command asks for, and other commands leave the clock /// at its current step. The call asserts that no step of a plan from `offerElapsed` is waiting. /// When planned steps are waiting, the call panics in Debug and ReleaseSafe builds, and its /// behavior is undefined in ReleaseFast and ReleaseSmall builds. pub fn commandStep(self: *Clock) Step { std.debug.assert(self.pending == 0); return self.advance(); } /// Returns the next planned step and counts it off the current plan. A host calls it once for /// each step the last `offerElapsed` planned. The call asserts that a planned step is left. /// Past the end of the plan, the call panics in Debug and ReleaseSafe builds, and its behavior /// is undefined in ReleaseFast and ReleaseSmall builds. pub fn next(self: *Clock) Step { std.debug.assert(self.pending > 0); self.pending -= 1; return self.advance(); } fn advance(self: *Clock) Step { const index = self.index; self.index += 1; const cadence = self.config.cadence(); return .{ .index = index, .source_frame = index / cadence, .phase = @intCast(index % cadence), .dt_numerator = 1, .dt_denominator = self.config.steps_per_second, }; }};test "60 Hz steps carry a three-phase 20 Hz frame and bound catch-up" { var clock = try Clock.init(.{ .steps_per_second = 60, .source_frames_per_second = 20 }); for (0..6) |i| { const step = clock.commandStep(); try std.testing.expectEqual(@as(u64, @intCast(i)), step.index); try std.testing.expectEqual(@as(u64, @intCast(i / 3)), step.source_frame); try std.testing.expectEqual(@as(u32, @intCast(i % 3)), step.phase); } const plan = clock.offerElapsed(100_000_000); try std.testing.expectEqual(@as(u32, 4), plan.steps); try std.testing.expectEqual(@as(u64, 2), plan.dropped_steps); for (0..plan.steps) |_| _ = clock.next(); try std.testing.expectEqual(@as(u64, 10), clock.index);}test "rates finer than a nanosecond are rejected before drop accounting" { try std.testing.expectError(error.InvalidRate, Clock.init(.{ .steps_per_second = 1_000_000_001, .source_frames_per_second = 1, }));}Source: fun/game/src/root.zig:114
zig
pub const clock = @import("clock.zig");Audit
| Definitions | 12 |
|---|---|
| Public names | 12 |
| Members | 14 |
| Version | 26.7.0 |
| Revision | daab053ee433 |