hologram reference

v0.1.0
58 functions · 12 modules
58 functions shown | hologram.h is an index of the public modules. Internal seams, split out so their arithmetic can be host tested, are deliberately not listed.

linalg

source/linalg.h 14 functions

Vectors, and the optics that is really just vector arithmetic. Pure functions over floats, and float rather than double deliberately, so the CPU oracle computes in the same precision the GPU will and an image diff between them measures the tracing rather than the word size.

Conventions, engine-wide: direction vectors are unit length unless a comment says otherwise, surface normals point out of the surface against the arriving ray, and hv3_refract's eta is n_from / n_to with the incoming direction pointing into the surface the way a traced ray does.

TYPES
typedef struct {
    float x, y, z;
} HoloV3;
HoloV3 hv3(float x, float y, float z);

A vector from three components.

HoloV3 hv3_add(HoloV3 a, HoloV3 b);

Component-wise sum.

HoloV3 hv3_sub(HoloV3 a, HoloV3 b);

Component-wise difference.

HoloV3 hv3_mul(HoloV3 a, HoloV3 b);

Component-wise product, which is how colours multiply.

HoloV3 hv3_scale(HoloV3 a, float s);

Every component times s.

float hv3_dot(HoloV3 a, HoloV3 b);

Dot product.

HoloV3 hv3_cross(HoloV3 a, HoloV3 b);

Cross product.

float hv3_len(HoloV3 a);

Euclidean length.

HoloV3 hv3_norm(HoloV3 a);

The unit vector along a, or a itself if its length is about zero.

HoloV3 hv3_lerp(HoloV3 a, HoloV3 b, float t);

Linear interpolation from a to b.

HoloV3 hv3_reflect(HoloV3 d, HoloV3 n);

Mirror reflection. d arrives at a surface with normal n and the result leaves it: r = d - 2(d.n)n, with |r| = |d|.

int hv3_refract(HoloV3 d, HoloV3 n, float eta, HoloV3 *out);

Snell's law. d arrives unit and pointing into the surface, n is the normal on d's side (n.d < 0), eta is n_from / n_to. Writes the unit transmitted direction and returns 1, or returns 0 on total internal reflection, when Snell has no answer and the caller must reflect instead.

int holo_grating_order(HoloV3 d, HoloV3 n, HoloV3 groove, float m_lambda_over_d, HoloV3 *out);

The grating equation in its conical, off-plane vector form. m_lambda_over_d carries the order, the wavelength and the period in one number. The component of the direction along the grooves is conserved (the conical invariant), the component along the dispersion direction picks up m·lambda/d, and the normal component rebalances to keep the result unit. m = 0 is exactly specular reflection. Returns 0 when the order is evanescent and nothing propagates.

void holo_fresnel(float cos_i, float n1, float n2, float *rs, float *rp);

The Fresnel equations, the real ones rather than Schlick's fit. cos_i is the positive cosine of the incidence angle, n1 the index the light arrives in and n2 the one it meets. Writes the s- and p-polarized power reflectances; past the critical angle both are 1. Unpolarized light reflects (rs + rp) / 2, and keeping the two separate is what lets a Stokes vector ride through the same interface.

geometry

source/geometry.h 7 functions

Rays against analytic surfaces. hologram has no triangles: every surface in a scene is one of these closed-form shapes, which is why the tracer can afford to be real time and why every intersection can be tested against algebra instead of against a mesh.

TYPES AND CONSTANTS
/* No hit below this t: a ray leaving a surface must not immediately
   find the surface it left through float error. */
#define HOLO_T_MIN 1e-3f

typedef struct {
    HoloV3 origin;
    HoloV3 dir;      /* unit */
} HoloRay;

typedef struct {
    float  t;        /* distance along the ray, > HOLO_T_MIN */
    HoloV3 point;
    HoloV3 normal;   /* unit, out of the surface on the arriving side */
} HoloHit;

Each intersection routine returns 1 and fills *hit on the nearest intersection past HOLO_T_MIN, or returns 0 and leaves *hit alone.

int holo_ray_sphere(HoloRay r, HoloV3 center, float radius, HoloHit *hit);

A ray against a sphere.

int holo_ray_plane(HoloRay r, HoloV3 point, HoloV3 normal, HoloHit *hit);

A ray against an infinite plane.

int holo_ray_rect(HoloRay r, HoloV3 corner, HoloV3 edge_u, HoloV3 edge_v, HoloHit *hit);

A finite parallelogram: a corner plus two edge vectors whose lengths are the panel's size. This is what a mirror is made of.

void holo_rect_basis(HoloV3 edge_u, HoloV3 edge_v, HoloV3 *normal, HoloV3 *solve_u, HoloV3 *solve_v);

Everything about a rectangle that does not depend on the ray: the unit normal, and the two vectors that turn a point on the plane into affine u and v with one dot product each. holo_ray_rect recomputes these on every call, which means every ray against every panel: a cross, a normalize and five dot products of work that only depends on the panel. A renderer with more than a couple of mirrors wants them hoisted, so gpu_scene.c computes them once and ships them in the uniform block. solve_u and solve_v are the Gram solve folded flat: where the long form computes ru, rv and det and divides, u is just dot(rel, solve_u).

int holo_ray_rect_pre(HoloRay r, HoloV3 corner, HoloV3 normal, HoloV3 solve_u, HoloV3 solve_v, HoloHit *hit);

holo_ray_rect against a panel whose basis is already in hand.

int holo_ray_dish(HoloRay r, HoloV3 apex, HoloV3 axis, float curv_r, float conic_k, float rim, HoloHit *hit);

A cap of a conic of revolution, in the language optical design quotes them: apex point, unit axis pointing out of the bowl, vertex radius of curvature R, conic constant K (0 a sphere, -1 a paraboloid, -e² an ellipsoid, below -1 a hyperboloid), clipped at rim radius. A paraboloid focuses parallel light at R/2 above the apex because this intersection and its normal say so, and the tests hold both to that.

void holo_basis(HoloV3 axis, HoloV3 *u, HoloV3 *v);

An orthonormal basis around a unit axis, deterministic so the CPU and the GPU build the same one.

camera

source/camera.h 2 functions

The camera is a ray generator: give it a pixel, get the ray that pixel sees along. Nothing here draws. The CPU oracle and the GPU shader both generate rays this way, which is what makes their images comparable.

TYPES
typedef struct {
    HoloV3 pos;
    HoloV3 forward, right, up;   /* orthonormal basis, right-handed */
    float  tan_half_fov;         /* vertical */
    float  aspect;               /* width / height */
} HoloCamera;
HoloCamera holo_camera_make(HoloV3 pos, HoloV3 target, HoloV3 up_hint, float fov_deg, float aspect);

A pinhole camera at pos looking at target. fov_deg is the vertical field of view, and up_hint only breaks the roll ambiguity, so hv3(0,1,0) is the usual answer and it need not be orthogonal to the view.

HoloRay holo_camera_ray(const HoloCamera *cam, float u, float v);

The ray through (u, v) on the image, both in [0,1): u runs left to right and v top to bottom, matching the display's uv. Sample pixel centres as ((x + 0.5) / w, (y + 0.5) / h).

spectrum

source/spectrum.h 4 functions

Wavelengths, and the two ends of a spectral render: how a material responds at one wavelength, and how a pile of single-wavelength intensities becomes an sRGB pixel. The tracer samples twelve fixed wavelengths across the visible band, fixed rather than random because the CPU and the GPU must trace exactly the same rays for the oracle diff to mean anything.

Each sample traces the whole scene with n(lambda) in the glass. The results are weighted by the CIE 1931 colour matching functions, in the Wyman-Sloan-Shirley analytic fits, and mapped to sRGB, normalized so a flat spectrum lands on exact white: hue structure from the human eye, white point from the engine.

CONSTANTS
#define HOLO_WAVELENGTHS 12
float holo_lambda(int i);

Sample i's wavelength in micrometers, evenly spaced from 0.42 to 0.68.

HoloV3 holo_spectral_weight(int i);

The CIE-derived sRGB weight of sample i, so that sum(I_i · weight_i) is the pixel. The weights sum to exactly (1,1,1) across i.

float holo_albedo_at(HoloV3 rgb, float lambda_um);

A material's, or the sky's, reflectance at one wavelength, read from its RGB colour through three smooth bands. Neutral colours are exact. That round trip is not colorimetry, so a red albedo will not survive to the exact same red, but the physics being showcased does not pass through it at all.

float holo_ior_at(float ior_d, float cauchy_b, float lambda_um);

Cauchy dispersion: the index at lambda for a glass quoted as ior at the sodium D line (0.5893 um) with coefficient B in um², so n(D) == ior for every B and B = 0 is achromatic glass. BK7 is roughly ior 1.5168 with B 0.0042; dense flints run several times that.

polar

source/polar.h 7 functions

Polarization: Stokes vectors, Mueller matrices, and the trick that makes them affordable. Every source in hologram is unpolarized, so a camera path never needs the full 4x4 Mueller product, only its first row. Each spectral ray carries that row and a reference frame, and every optical element updates the row in place as srow' = srow · M. When the path reaches a source of intensity S, the camera sees S · srow.i.

Crossed polarizers extinguish because the product of their matrices has a zero corner, not because anything special-cases them. The tests pin the observables: Malus's law, the three-polarizer paradox, Brewster's polarizing angle, the quarter- and half-wave plates, and the TIR phase a Fresnel rhomb is cut to exploit.

TYPES
typedef struct {
    float i, q, u, v;
} HoloSRow;
HoloSRow holo_srow_start(void);

The camera's own row: measure intensity, no analyzer.

HoloSRow holo_srow_rotate(HoloSRow s, float c2, float s2);

srow · R(theta), with theta given as its double angle (cos2t, sin2t). Rotations use double angles computed from dot products, so there is no atan in the hot path.

HoloSRow holo_srow_mueller(HoloSRow s, float a, float b, float c, float d);

srow · M for the interface template [a b 0 0; b a 0 0; 0 0 c d; 0 0 -d c]. Fresnel reflection and refraction, retarders and mirrors are all this shape in their own basis.

HoloSRow holo_srow_polarizer(HoloSRow s);

srow · M for an ideal linear polarizer along the frame.

HoloSRow holo_srow_scale(HoloSRow s, float k);

Every component times k.

void holo_frame_rot(HoloV3 frame, HoloV3 target, HoloV3 dir, float *c2, float *s2);

The double angle rotating frame onto target about dir. All three are unit, and the frames are perpendicular to dir.

int holo_fresnel_amp(float cos_i, float n1, float n2, float *rs, float *rp, float *ts, float *tp, float *f, float *delta);

Fresnel amplitude coefficients, which holo_fresnel squares. Below the critical angle: real rs and tp amplitudes, *f the power-projection factor (n2·cos_t)/(n1·cos_i) so that f·ts·ts = 1 - rs·rs, and *delta = 0. Past it: returns 1, rs and rp are 1 in magnitude, and *delta is the TIR phase difference delta_p - delta_s, the number a Fresnel rhomb is cut to.

cpu_trace

source/cpu_trace.h 5 functions

The CPU reference tracer, hologram's oracle. Every optical behaviour lands here first, in plain testable C, before it lands in a shader. GPU frames are then diffed against images this file renders. It is allowed to be slow and obliged to be right.

A material is three shares that sum to at most 1: mirror (metallic reflection, tinted by albedo, because silver is a colour too), transmit (glass), and the matte remainder (Lambert). Glass brings its own reflection, since the Fresnel equations split the transmit share between refraction and an untinted dielectric reflection angle by angle. A sphere's glass is a volume, so rays bend in and out and can be trapped by TIR; a rect's is a thin pane, one Fresnel interface, a window rather than a prism.

CONSTANTS
#define HOLO_MAX_SPHERES 8
#define HOLO_MAX_RECTS   8
#define HOLO_MAX_DISHES  4

/* Reflections deeper than this add nothing. */
#define HOLO_MAX_BOUNCE 16

/* Glass splits light, so the walk keeps a small stack of pending
   rays. The caps bound the work per pixel and make branch-dropping
   deterministic: the CPU and the GPU drop the exact same branches,
   which the oracle diff depends on. */
#define HOLO_MAX_RAYS 32
#define HOLO_STACK    16
#define HOLO_MIN_TP   0.002f

/* The fraction of the sun a shadowed point still shows. A stand-in
   until bounced light exists to fill shadows honestly. */
#define HOLO_AMBIENT 0.1f

/* A rect can be an ideal optical filter instead of glass. */
#define HOLO_FILTER_NONE 0
#define HOLO_POLARIZER   1
#define HOLO_WAVEPLATE   2

/* m = -1, 0, +1, +2: both first orders for the symmetric spectra,
   the second for order overlap, and the zeroth is specular. */
#define HOLO_GRATING_ORDERS 4
extern const int holo_grating_m[HOLO_GRATING_ORDERS];
TYPES
typedef struct {
    HoloV3 center;
    float  radius;
    HoloV3 albedo;
    float  mirror;
    float  transmit;
    float  ior;        /* at the sodium D line */
    float  disperse;   /* Cauchy B in um^2; 0 = achromatic */
} HoloSphere;

typedef struct {
    HoloV3 corner;
    HoloV3 edge_u, edge_v;   /* lengths are the panel's size */
    HoloV3 albedo;
    float  mirror, transmit, ior, disperse;
    int    filter;           /* NONE / POLARIZER / WAVEPLATE */
    float  filter_angle;     /* radians from edge_u toward edge_v */
    float  retard;           /* waveplate retardance at the D line */
    float  grating_period;   /* um; 1.2 is 833 lines/mm */
    float  grating_angle;
    float  order_w[HOLO_GRATING_ORDERS];
} HoloRect;

typedef struct {
    HoloV3 apex;
    HoloV3 axis;       /* unit, out of the bowl */
    float  curv_r;     /* vertex radius of curvature */
    float  conic_k;    /* 0 sphere, -1 paraboloid, <-1 hyperboloid */
    float  rim;
    HoloV3 albedo;
    float  mirror;
} HoloDish;

typedef struct {
    HoloSphere spheres[HOLO_MAX_SPHERES];  int sphere_count;
    HoloRect   rects[HOLO_MAX_RECTS];      int rect_count;
    HoloDish   dishes[HOLO_MAX_DISHES];    int dish_count;

    int    has_floor;
    float  floor_y;
    HoloV3 floor_a, floor_b;    /* 1m checker */
    float  floor_mirror;

    HoloV3 sun_dir;             /* unit, scene toward the sun */
    HoloV3 horizon, zenith;     /* the sky */

    /* The sun as a visible disk. This is what makes focusing
       visible: a mirror that sends your eye-ray into the sun
       shows you the sun, and at a paraboloid's focus every
       point of the dish does. */
    float  sun_disk_cos;
    float  sun_disk_intensity;
} HoloScene;
HoloV3 holo_trace_ray(const HoloScene *scene, HoloRay ray);

The colour a single ray sees, mirror bounces included. RGB light, so glass refracts at its D-line index and dispersion is invisible.

float holo_trace_lambda(const HoloScene *scene, HoloRay ray, float lambda_um);

The intensity a single ray sees at one wavelength: albedos read through holo_albedo_at, glass refracting at n(lambda). The walk, its caps and its culls are the same as holo_trace_ray's, and one wavelength at a time is the only difference.

HoloV3 holo_trace_ray_spectral(const HoloScene *scene, HoloRay ray);

The colour a single ray sees spectrally: HOLO_WAVELENGTHS traces of holo_trace_lambda, folded through the CIE weights. Where no dispersive glass is struck this agrees with holo_trace_ray on neutral scenes; where it is, wavelengths part ways and fringes are real.

void holo_trace_image(const HoloScene *scene, const HoloCamera *cam, int w, int h, float *rgb);

Render the whole frame into rgb (w·h·3 floats, rows top-down, linear and roughly 0 to 1). Tone mapping is the caller's problem, as it will be the swapchain's on the GPU.

void holo_trace_image_spectral(const HoloScene *scene, const HoloCamera *cam, int w, int h, float *rgb);

The same frame rendered through holo_trace_ray_spectral.

gpu_scene

source/gpu_scene.h 1 function

The scene as the shader sees it: one uniform block, float4 by float4. HoloGpuScene and the uniform block in each of the three shader dialects are the same layout written four times over, and they must change together. The oracle diff is what catches them drifting apart, and test_gpu_layout holds the GLSL tracer's hand-written slot map to offsetof so reordering the struct fails a test rather than quietly making the GPU read the camera out of the sun. Fields pack HLSL-style, so every float3 is followed by a float that rides in its fourth lane.

Two constraints show through this struct. A grating rect repurposes the glass and albedo lanes it never uses, because the shader cannot afford two more dynamically indexed arrays: fxc's indexable register file tops out and silently aliases the overflow into other arrays. And up to two gratings live in scalar, non-array fields the shader reads statically, for the same reason. More than two gratings render as matte black on the GPU, though the CPU has no such limit.

void holo_gpu_scene_fill(HoloGpuScene *gpu, const HoloScene *scene, const HoloCamera *cam, int spectral);

Write scene and camera into the block. The camera's aspect is not carried: the shader derives it from the framebuffer size in its uniforms, so the image stays right when the window is resized. spectral chooses the shader's path, with 0 tracing RGB and 1 tracing per wavelength.

display

source/display.h 6 functions

The window, the GPU device, and the surface the tracer renders through. This is the only file that talks to sokol. hologram draws every frame the same way: one fullscreen quad, one fragment shader, uniforms describing the scene and the camera. The design resolution follows the games at 640x480, scaled to whatever the window really is, and the shader letterboxes from the real pixel size.

There is one tracer written in three dialects, one file each: HLSL for D3D11, GLSL for GL and GLES3/WebGL2, MSL for Metal. The file is read from disk at startup rather than compiled in, so the tracer can be edited and an example relaunched without recompiling, and a game ships the file for its backend beside the binary.

TYPES
/* Keep 16-byte aligned the way constant buffers want; grow it
   only from the end. */
typedef struct {
    float width;    /* framebuffer size in pixels */
    float height;
    float time;     /* seconds since the window opened */
    float _pad;
} HoloDisplayUniforms;

typedef struct {
    const char *title;
    int         width;       /* 0 -> 640x480 */
    int         height;
    const char *fs_source;   /* fragment shader for the quad */

    /* A game's own uniform block, uploaded every frame. It must
       START with a HoloDisplayUniforms. */
    void *uniforms;
    int   uniforms_size;

    void (*before_frame)(void);   /* run the simulation, write the camera */
    void (*after_frame)(void);    /* read pixels back, count frames, quit */
    void (*event_cb)(const struct sapp_event *ev);
} HoloDisplayDesc;
const char *holo_shader_path(void);

The tracer's source file for the backend this build targets.

int holo_load_shader(char *buf, int buf_size);

Read that file into buf, NUL-terminated. Returns 1, or 0 after printing why: no such file, whose usual cause is being run from somewhere other than the repository root, or a file too large for buf. The second is worth catching, because a silently truncated shader fails much later as an unexplained compile error.

struct sapp_desc holo_display_app(const HoloDisplayDesc *desc);

Fill in sokol's sapp_desc from ours. sokol owns main(), so a game's entry point is sokol_main() returning holo_display_app(&desc), and the callbacks then run inside the frame loop.

void holo_display_frame(void);

The frame body: upload uniforms, draw the quad, present. Called by the internal frame callback, and exposed for games that add their own.

float holo_display_time(void);

Seconds since init, as the uniforms will report it.

int holo_display_read_frame(unsigned char *rgba, int w, int h);

Copy the frame most recently drawn into rgba (w·h·4 bytes, rows top-down), which must match the real framebuffer size. Returns 1, or 0 where the backend cannot read back. D3D11, GL/GLES3 and Metal each have an implementation, but only the D3D11 one has actually run; the other two are written and unproven. This exists for the oracle, whose image diff needs the GPU's actual pixels.

input

source/input.h 3 functions

Keyboard and mouse, folded to what a first-person game asks each frame: which keys are held, and how far the mouse moved since it was last asked. The event decode is sokol's; this module is the bookkeeping in between, plus mouse capture, so a click looks and Escape lets go.

TYPES
typedef struct {
    unsigned char held[512];    /* indexed by sapp_keycode */
    float mouse_dx, mouse_dy;   /* holo_input_look consumes */
} HoloInput;
void holo_input_event(HoloInput *in, const struct sapp_event *ev);

Feed every event from the display's event callback through this. It handles capture itself: left click locks the mouse for looking, Escape unlocks.

int holo_input_held(const HoloInput *in, int keycode);

Is this sapp_keycode held right now?

void holo_input_look(HoloInput *in, float *dx, float *dy);

The mouse movement since the last call, then zero, so call it once per frame. Returns nothing while the mouse is not captured.

collision

source/collision.h 1 function

Walking: the only physics a mirror maze needs. A walker is a vertical capsule reduced to what matters, a horizontal radius and a height, moving through a floor plane and a set of axis-aligned boxes. Movement resolves one axis at a time, which is what makes walking into a wall at an angle slide along it instead of sticking: the blocked axis clamps and the free axis keeps its velocity.

Boxes are expanded by the radius, so the walker is a point against rounded rooms. There are no rigid bodies, no impulses and no broadphase: rooms have a dozen walls, and Crystal Mirror Maze's player needs exactly this and gravity.

TYPES AND CONSTANTS
#define HOLO_MAX_WALLS 16

typedef struct {
    HoloV3 min, max;
} HoloAabb;

typedef struct {
    HoloV3 pos;        /* the feet */
    HoloV3 vel;
    int    grounded;
} HoloWalker;

typedef struct {
    float radius;      /* horizontal capsule radius */
    float height;      /* feet to crown; eyes sit somewhat below */
    float gravity;     /* positive; pulls -y */
    float floor_y;
    HoloAabb walls[HOLO_MAX_WALLS];
    int wall_count;
} HoloWalkWorld;
void holo_walk_step(HoloWalker *w, const HoloWalkWorld *world, float dt);

One simulation step: apply gravity, move each axis, resolve against the walls and the floor, update grounded. vel.x and vel.z are the caller's to set each step, since walking is kinematic, while vel.y belongs to gravity and jumps.

oracle

source/oracle.h 2 functions

Holding the GPU to the CPU reference tracer. holo_oracle_diff reads the frame most recently presented, renders the same frame through cpu_trace.c, and compares. Call it from a display after_frame callback, a few frames in.

The two images are float twins rather than bit twins, since drivers reassociate math, so the bar is a mean error under 1/255 and under 0.75% of pixels off by more than 8/255. The outliers are razor edges: silhouettes, and glass at the critical angle, where a grazing ray resolves differently on each side.

TYPES
typedef struct {
    double mean;          /* mean abs error, in 1/255 levels */
    int    max;           /* worst single channel, in 1/255 levels */
    double outlier_pct;   /* % of pixels with any channel off by > 8 */
    int    width, height;
} HoloOracleStats;
int holo_oracle_diff(const HoloScene *scene, const HoloCamera *cam, int spectral, HoloOracleStats *stats);

Returns 1 when the GPU frame matches the oracle within the bars above, and 0 when it does not or the backend cannot read pixels back. The camera must be built with the real framebuffer aspect, and spectral must say which path the GPU rendered so the CPU renders the same one.

int holo_oracle_dump(const HoloScene *scene, const HoloCamera *cam, int spectral, const void *gpu_scene, int gpu_scene_size, const char *name);

Write out what a tracer outside this process needs in order to be held to the same oracle: the uniform block exactly as the shader receives it, and the CPU's frame encoded the way holo_oracle_diff encodes it before comparing. This exists because a readback needs the backend it runs on, so the GL and Metal tracers cannot always be run where they are written. tools/gldiff renders shaders/trace.glsl in a WebGL2 context and compares against these two files, which is how the GLSL tracer is held to the oracle from a machine with no GL toolchain at all. Writes build/<name>_params.bin (the raw block) and build/<name>_ref.bin (two int32 of width and height, then width·height·3 encoded bytes), both in native byte order. Returns 0 if either file cannot be written.

timestep

source/timestep.h 6 functions

The fixed-step accumulator, ported verbatim from magnolia. Everything here is arithmetic, which is why it is a separate file: the counting is the part that decides whether a game runs at the same speed on a stalled frame as on a clean one, and counting mixed into calls to the clock is untestable.

A frame that owed more steps than TIMESTEP_MAX_STEPS was a stall rather than a slow frame. The remaining backlog is dropped rather than replayed: catching up on two seconds of hitstun by running two seconds of it at once is not catching up, it is teleporting, and it is what makes a game speed up after a load.

CONSTANTS
#define TIMESTEP_MAX_STEPS 16
void timestep_set_hz(int hz);

Set the fixed rate. hz <= 0 turns fixed stepping off, which is the default, so a game that never asks for it is unaffected.

int timestep_hz(void);

The rate currently set.

float timestep_dt(void);

Seconds in one step, or 0 when disabled. This is the number a fixed-step game should be integrating with, not however long the frame really took.

void timestep_advance(float dt);

Add a frame's real elapsed time and work out how many steps it owes. Called once per frame; a test calls it directly.

int timestep_steps(void);

Steps owed by the frame most recently advanced.

void timestep_reset(void);

Forget the accumulated remainder and the step count, leaving the rate alone. Time before a reset should not owe steps after it.