Skip to documentation
SLOP

tiny.game.clock

Reference 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.

Types and contracts

Public types and contracts.

Values and defaults

Public values and defaults.

No direct callersNo direct callstiny.gameclock
Static calls · unresolved targets: unknown · external targets: unknown.

Source

Called byCallstest; no linkfun.game.src.clocktest: 60 Hz steps carry a three-phase...test; no linkfun.game.src.sessiontest: recorded 60 Hz input replays wi...private; no linkfun.game.src.clock.Clockadvanceclock.ClockcommandStep
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsNo direct callsprivate; no linkfun.game.src.benchmeasureLooptest; no linkfun.game.src.clocktest: 60 Hz steps carry a three-phase...test; no linkfun.game.src.clocktest: rates finer than a nanosecond a...test; no linkfun.game.src.sessiontest: recorded 60 Hz input replays wi...clock.Clockinit
Static calls · unresolved targets: 0 · external targets: 1.
Called byCallsprivate; no linkfun.game.src.benchmeasureLooptest; no linkfun.game.src.clocktest: 60 Hz steps carry a three-phase...private; no linkfun.game.src.clock.Clockadvanceclock.Clocknext
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsNo direct callsprivate; no linkfun.game.src.benchmeasureLooptest; no linkfun.game.src.clocktest: 60 Hz steps carry a three-phase...clock.ClockofferElapsed
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsNo direct callsprivate; no linkfun.game.src.clock.Clockadvanceclock.Configcadence
Static calls · unresolved targets: 0 · external targets: 0.

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

Definitions12
Public names12
Members14
Version26.7.0
Revisiondaab053ee433