Skip to content

@typegpu/kitclock

Clock

Fixed steps, cadences, easing and springs, counted in the recorder next to the passes they drive.

kit.clock

cpu

clock.fixedStep({ step, … }) → FixedStep

clock.cadence(options) → Cadence

clock.ease(dt, rate)

clock.spring({ stiffness, damping, … }) → Spring

clock.spring({ value, damping, stiffness, … }) → VectorSpring

FixedStepreturns
a clock whose advance says how many steps to run each frame
Cadencereturns
a cadence whose due says whether the work runs this frame
dttakes
the frame's length in seconds
ratetakes
how fast the gap closes, per second
Springreturns
a spring you step toward a target each frame
VectorSpringreturns
a spring you step toward a target each frame
types
FixedStepCadenceVectorSpringVectorSpring
FixedStepOptionsfixedStep · 3 fields
step

number

Sets the step duration in seconds, such as 1 / 120.

maxSteps?

number= 4

Limits the steps run per frame.

steady?

boolean= false

Counts a frame longer than both frames before it as the longer of the two.

CadenceOptionscadence · 2 fields
every?

number= none: set this or hz

Runs work every every frames.

hz?

number= none: set this or every

Runs work hz times per second on average.

SpringOptionsspring · 4 fields
stiffness

number

Sets how hard the spring pulls toward its target, per second squared for each unit away.

damping

number

Sets how much speed the spring loses, per second.

value?

number= 0

Sets the starting position.

velocity?

number= 0

Sets the starting speed in units per second.

VectorSpringOptionsspring · 4 fields
value

V

Sets the vector the spring moves in place, and its starting position.

velocity?

ArrayLike<number>= zeros

Sets the starting speed of each component in units per second.

damping

number

Sets how much speed the spring loses, per second.

stiffness

number

Sets how hard the spring pulls toward its target, per second squared for each unit away.

Stall a frame or change the display rate. Step length stays fixed; catch-up frames run at most maxSteps and drop time beyond the cap, shown at one fifth speed.

Display rates vary, and a resumed tab can have a long first frame. A simulation that uses frame time can behave differently at each rate. A fixed step counts how many steps of a set length to run this frame. A cadence tells you when less frequent work is due. Easing and springs move values at the same pace at any rate.

import {
import clock
clock
} from '@typegpu/kit';
const
const sim: clock.FixedStep
sim
=
import clock
clock
.
function fixedStep(options: clock.FixedStepOptions): clock.FixedStep

Accumulates elapsed time for fixed simulation steps. Accumulates nothing while paused. Throws unless step is finite and above 0 and maxSteps is an integer above 0.

@returns ― a clock whose advance says how many steps to run each frame

fixedStep
({
FixedStepOptions.step: number

Sets the step duration in seconds, such as 1 / 120.

step
: 1 / 120,
FixedStepOptions.maxSteps?: number | undefined

Limits the steps run per frame. Carries unused time forward, up to this many steps. Default 4.

maxSteps
: 4 });
const
const giCadence: clock.Cadence
giCadence
=
import clock
clock
.
function cadence(options: clock.CadenceOptions): clock.Cadence

Schedules work every every frames or hz times per second on average. Reports the time since its last run. With hz, it runs in the frame closest to each scheduled tick. At 25 Hz and 60 fps, gaps alternate between two and three frames. Throws unless exactly one of every and hz is set. every must be an integer above 0, and hz must be finite and above 0.

@returns ― a cadence whose due says whether the work runs this frame

cadence
({
every: number

Runs work every every frames. Default none: set this or hz.

every
: 4 });
import frames
frames
.
function create(root: TgpuRoot, record: Recorder): frames.Frames

Records a frame into one encoder and submits once, with a clock. Borrows the root. The synchronous recorder borrows each frame's encoder.

@submits ― always once a frame after your recorder returns, and again at each frame.submit()

@param ― record your frame, recorded into each frame's encoder

@returns ― a loop you run with each frame's time and destroy

create
(
const root: TgpuRoot
root
, (
frame: frames.Frame
frame
) => {
const
const pass: TgpuComputePass
pass
=
frame: frames.Frame
frame
.
Frame.encoder: TgpuCommandEncoder

Records commands for submission after the recorder returns. submit() replaces this encoder. Read it at each call instead of keeping it, and let the frame submit it.

encoder
.
TgpuCommandEncoder.beginComputePass(descriptor?: TgpuComputePassDescriptor): TgpuComputePass

Begins recording a compute pass

beginComputePass
();
for (let
let steps: number
steps
=
const sim: clock.FixedStep
sim
.
FixedStep.advance(dt: number): number

Adds dt seconds and returns the number of steps to run. Each step lasts step seconds, so a GPU kernel can use a fixed duration. All steps in a frame see the same queued CPU writes. Keep a step counter on the GPU for values that must change per step.

advance
(
frame: frames.Frame
frame
.
Frame.dt: number

Reports seconds since the previous submitted frame. The first frame has a dt of zero.

dt
);
let steps: number
steps
> 0;
let steps: number
steps
--)
const physics: {
step(pass: TgpuComputePass): void;
}
physics
.
function step(pass: TgpuComputePass): void
step
(
const pass: TgpuComputePass
pass
);
if (
const giCadence: clock.Cadence
giCadence
.
Cadence.due(dt: number): boolean

Adds dt seconds and returns whether work is due this frame. Returns true on the first call.

due
(
frame: frames.Frame
frame
.
Frame.dt: number

Reports seconds since the previous submitted frame. The first frame has a dt of zero.

dt
))
const gi: {
step(pass: TgpuComputePass, dt: number): void;
}
gi
.
function step(pass: TgpuComputePass, dt: number): void
step
(
const pass: TgpuComputePass
pass
,
const giCadence: clock.Cadence
giCadence
.
Cadence.dt: number

Reports the seconds since the last run. Valid when due returns true.

dt
);
const pass: TgpuComputePass
pass
.
TgpuComputePass.end(): void

Completes the recording of this compute pass

end
();
});
  • advance(dt) returns the number of steps to run, capped at maxSteps, which defaults to 4. It carries over at most another maxSteps of time and drops the rest to bound catch-up work. You need no extra clamp on dt. This cap and frames.animate’s maxDt bound a long frame.
  • Every step lasts step, so a GPU simulation dispatches its kernel that many times in one pass with the length baked in. A value that changes per step comes from a count of the steps.
  • time is the simulated seconds, the sum of every step run. Write it to a uniform for animation that follows the simulation.
  • Set step to change the step length, such as a fixed number of steps over a beat whose length is a setting. The next advance runs steps of the new length on the time already carried, at most maxSteps of them. Particles and machines read the step once, when created.
  • With steady: true, a frame longer than both frames before it counts only as long as the longer of them, so a simulation too heavy to catch up runs no extra steps after a frame that misses the display.
  • paused stops time from accumulating. reset() forgets time not yet stepped, after a reload or a long pause.
  • debt is accumulated time not yet simulated. alpha expresses it in steps, capped at 1, for drawing between the last two steps at the display rate.
  • ahead is alpha * step, in seconds. Write it to a uniform once per frame to extrapolate along an object’s velocity. Particles take the fixed step and write their own value.

A queue write such as buffer.write enters the queue when you call it. Encoded commands enter the queue at submit. All writes made before that submit run before its first command, including writes made after recording a pass that reads them.

Count steps on the GPU. A dispatch that increments a counter after each step gives every step its own number. Use it to derive time, seed randomness or select a buffer from a pair.

const
const STEP: number
STEP
= 1 / 120;
const
const sim: clock.FixedStep
sim
=
import clock
clock
.
function fixedStep(options: clock.FixedStepOptions): clock.FixedStep

Accumulates elapsed time for fixed simulation steps. Accumulates nothing while paused. Throws unless step is finite and above 0 and maxSteps is an integer above 0.

@returns ― a clock whose advance says how many steps to run each frame

fixedStep
({
FixedStepOptions.step: number

Sets the step duration in seconds, such as 1 / 120.

step
:
const STEP: number
STEP
});
const
const count: TgpuMutable<d.U32>
count
=
const root: TgpuRoot
root
.
TgpuRoot.createMutable<d.U32>(typeSchema: d.U32, initial?: number | ((buffer: TgpuBuffer<NoInfer<d.U32>>) => void) | undefined): TgpuMutable<d.U32> (+1 overload)

Allocates memory on the GPU, allows passing data between host and shader. Can be mutated in-place on the GPU. For a general-purpose buffer, use

TgpuRoot.createBuffer

.

@param ― typeSchema The type of data that this buffer will hold.

@param ― initial Either initial value of the buffer, or an initializer to execute on the mapped buffer. (optional)

createMutable
(
import d
d
.
const u32: d.U32
export u32

A schema that represents an unsigned 32-bit integer value. (equivalent to u32 in WGSL)

Can also be called to cast a value to an u32 in accordance with WGSL casting rules.

@example const value = u32(); // 0

@example const value = u32(7); // 7

@example const value = u32(3.14); // 3

@example const value = u32(-1); // 4294967295

@example const value = u32(-3.1); // 0

u32
);
const
const counting: TgpuComputePipeline
counting
=
const root: TgpuRoot
root
.
WithBinding.createComputePipeline<{}>(descriptor: TgpuComputePipeline.Descriptor<{}>): TgpuComputePipeline
createComputePipeline
({
compute: TgpuComputeFn<{}>
compute
:
const tgpu: {
const: typeof import("node_modules/typegpu/core/constant/tgpuConstant").constant;
fn: typeof import("node_modules/typegpu/core/function/tgpuFn").fn;
comptime: typeof import("node_modules/typegpu/core/function/comptime").comptime;
resolve: typeof import("node_modules/typegpu/core/resolve/tgpuResolve").resolve;
resolveWithContext: typeof import("node_modules/typegpu/core/resolve/tgpuResolve").resolveWithContext;
init: typeof import("node_modules/typegpu/core/root/init").init;
initFromDevice: typeof import("node_modules/typegpu/core/root/init").initFromDevice;
slot: typeof import("node_modules/typegpu/core/slot/slot").slot;
lazy: typeof import("node_modules/typegpu/core/slot/lazy").lazy;
... 10 more ...;
'~unstable': typeof import("node_modules/typegpu/tgpuUnstable");
}

@module ― typegpu

tgpu
.
computeFn: (options: {
workgroupSize: number[];
}) => TgpuComputeFnShell<{}> (+1 overload)
computeFn
({
workgroupSize: number[]
workgroupSize
: [1] })(() => {
'use gpu';
const count: TgpuMutable<d.U32>
count
.
TgpuMutable<U32>.$: number
$
+= 1;
}),
});
/** When the step that calls it starts, in seconds. */
const
const stepTime: () => number

When the step that calls it starts, in seconds.

stepTime
= () => {
'use gpu';
return
import d
d
.
function f32(v?: number | boolean): number
export f32

A schema that represents a 32-bit float value. (equivalent to f32 in WGSL)

Can also be called to cast a value to an f32.

@example const value = f32(); // 0

@example const value = f32(1.23); // 1.23

@example const value = f32(true); // 1

f32
(
const count: TgpuMutable<d.U32>
count
.
TgpuMutable<U32>.$: number
$
) *
const STEP: number
STEP
;
};
import frames
frames
.
function create(root: TgpuRoot, record: Recorder): frames.Frames

Records a frame into one encoder and submits once, with a clock. Borrows the root. The synchronous recorder borrows each frame's encoder.

@submits ― always once a frame after your recorder returns, and again at each frame.submit()

@param ― record your frame, recorded into each frame's encoder

@returns ― a loop you run with each frame's time and destroy

create
(
const root: TgpuRoot
root
, (
frame: frames.Frame
frame
) => {
const
const pass: TgpuComputePass
pass
=
frame: frames.Frame
frame
.
Frame.encoder: TgpuCommandEncoder

Records commands for submission after the recorder returns. submit() replaces this encoder. Read it at each call instead of keeping it, and let the frame submit it.

encoder
.
TgpuCommandEncoder.beginComputePass(descriptor?: TgpuComputePassDescriptor): TgpuComputePass

Begins recording a compute pass

beginComputePass
();
for (let
let steps: number
steps
=
const sim: clock.FixedStep
sim
.
FixedStep.advance(dt: number): number

Adds dt seconds and returns the number of steps to run. Each step lasts step seconds, so a GPU kernel can use a fixed duration. All steps in a frame see the same queued CPU writes. Keep a step counter on the GPU for values that must change per step.

advance
(
frame: frames.Frame
frame
.
Frame.dt: number

Reports seconds since the previous submitted frame. The first frame has a dt of zero.

dt
);
let steps: number
steps
> 0;
let steps: number
steps
--) {
const field: {
step(pass: TgpuComputePass): void;
}
field
.
function step(pass: TgpuComputePass): void
step
(
const pass: TgpuComputePass
pass
);
const pass: TgpuComputePass
pass
.
TgpuComputePass.setPipeline(pipeline: TgpuComputePipeline): void

Sets the current

TgpuComputePipeline

for subsequent dispatches

setPipeline
(
const counting: TgpuComputePipeline
counting
);
const pass: TgpuComputePass
pass
.
TgpuComputePass.dispatchWorkgroups(x: number, y?: number, z?: number): void
dispatchWorkgroups
(1);
}
const pass: TgpuComputePass
pass
.
TgpuComputePass.end(): void

Completes the recording of this compute pass

end
();
});
  • A kernel calls stepTime, or an accessor takes it as its fill, root.with(time, stepTime), so a module that reads its time through an accessor gets the step’s own.
  • Seed with the count, randf.seed2(d.vec2f(i, count.$)), and take the current half of a pair from count.$ & 1.
  • A kernel can’t read the count in the dispatch that advances it because its threads run in no order. If the step’s last kernel doesn’t read it, its thread 0 can advance it and save the dispatch.
  • Upload CPU-computed per-step values, such as body positions, in one array per frame. Include first, the step count at the frame’s start, tracked on the CPU. Each step reads its value at count.$ - first.

Set up per-pass algorithm values inside the module. The jump flood uses a different step size in each of ten passes. It uses an immediate when root.enabledWgslLanguageFeatures includes immediate_address_space, or a bind group per pass otherwise. If neither approach works, submit per step.

Change the display rate or stall a frame. every follows frame count; hz follows time and restarts its schedule after a stall, shown at half speed with one tick per frame.
  • { every: n } runs on every nth frame, so its rate follows the display. every: 4 runs 15 times per second at 60 Hz and 36 times at 144 Hz.
  • { hz: rate } runs in the frame closest to each tick of a schedule 1 / rate apart, so at 60 fps hz: 25 alternates gaps of two and three frames. After a stall it runs once and starts the schedule again.
  • due(dt) counts the frame and says whether the work runs, always on the first call. The cadence’s dt is then the time since its last run.
  • reset() makes the next call run. Use it when resuming work after a condition changes, such as global illumination after scene geometry settles.
  • Values that would run once or never, such as hz: 0 or maxSteps: 0, throw a RangeError.

Use an if in the recorder for work controlled by a flag or trigger.

A value that moves a fixed fraction of the way to its goal each frame moves faster at 144 Hz than at 60 Hz. clock.ease(dt, rate) returns the fraction for a frame dt seconds long, 1 - Math.exp(-dt * rate), so the gap closes at the same pace at any frame rate:

import {
import clock
clock
} from '@typegpu/kit';
// Half the gap closes every 87 ms, ln 2 / 8 seconds
let follow: number
follow
+= (
const goal: number
goal
-
let follow: number
follow
) *
import clock
clock
.
function ease(dt: number, rate: number): number

Returns the fraction of the way to a goal to move in a frame dt seconds long, 1 - Math.exp(-dt * rate), so a value eases at the same pace at any frame rate. Move by it each frame, as in at += (goal - at) * clock.ease(frame.dt, 8). At rate 8, half the gap closes in 87 ms.

@param ― dt the frame's length in seconds

@param ― rate how fast the gap closes, per second

@returns ― a fraction from 0 to 1

ease
(
const frame: Frame
frame
.
Frame.dt: number

Reports seconds since the previous submitted frame. The first frame has a dt of zero.

dt
, 8);

Cameras ease towards their goal the same way.

A spring follows its target with speed of its own, so it overshoots and settles where easing only slows down. Knock it by adding to its velocity. clock.spring steps the exact motion, so it settles the same at any frame rate and stays stable through a long frame:

import {
import clock
clock
} from '@typegpu/kit';
const
const squash: clock.Spring
squash
=
import clock
clock
.
function spring(options: clock.SpringOptions): clock.Spring (+1 overload)

Creates a damped spring that moves a value toward a target. It steps the exact motion, so it settles the same at any frame rate and stays stable for long frames. Pass a vector as value to move each of its components in place, without allocating. Throws unless stiffness and damping are finite and not negative.

@returns ― a spring you step toward a target each frame

spring
({
SpringOptions.stiffness: number

Sets how hard the spring pulls toward its target, per second squared for each unit away. Higher values settle faster.

stiffness
: 160,
SpringOptions.damping: number

Sets how much speed the spring loses, per second. 2 * Math.sqrt(stiffness) settles fastest without overshooting; lower values bounce.

damping
: 10 });
const
const tilt: clock.VectorSpring<number[]>
tilt
=
import clock
clock
.
function spring<number[]>(options: clock.VectorSpringOptions<number[]>): clock.VectorSpring<number[]> (+1 overload)

Creates a damped spring that moves a value toward a target. It steps the exact motion, so it settles the same at any frame rate and stays stable for long frames. Pass a vector as value to move each of its components in place, without allocating. Throws unless stiffness and damping are finite and not negative.

@returns ― a spring you step toward a target each frame

spring
({
stiffness: number

Sets how hard the spring pulls toward its target, per second squared for each unit away. Higher values settle faster.

stiffness
: 55,
damping: number

Sets how much speed the spring loses, per second. 2 * Math.sqrt(stiffness) settles fastest without overshooting; lower values bounce.

damping
: 4.5,
VectorSpringOptions<number[]>.value: number[]

Sets the vector the spring moves in place, and its starting position.

value
: [0, 0, 0] });
if (
const landed: boolean
landed
)
const squash: clock.Spring
squash
.
Spring.velocity: number

Holds its speed in units per second. Add to it to kick the spring.

velocity
-= 2; // a knock
const squash: clock.Spring
squash
.
Spring.step(target: number, dt: number): number

Moves the spring dt seconds toward target and returns the new value.

step
(0,
const frame: Frame
frame
.
Frame.dt: number

Reports seconds since the previous submitted frame. The first frame has a dt of zero.

dt
);
const tilt: clock.VectorSpring<number[]>
tilt
.
VectorSpring<number[]>.step(target: ArrayLike<number>, dt: number): number[]

Moves each component dt seconds toward target's and returns value.

step
(
const lean: [number, number, number]
lean
,
const frame: Frame
frame
.
Frame.dt: number

Reports seconds since the previous submitted frame. The first frame has a dt of zero.

dt
); // moves tilt.value in place
  • damping of 2 * Math.sqrt(stiffness) settles fastest without overshooting. Lower values bounce.
  • A vector spring moves the array or vector you pass as value in place and allocates nothing per step.
  • Set value to jump. The spring keeps its velocity.
  • A hz run can come up to half a frame early, so hz sets an average rate. Work that needs a minimum gap checks dt.
  • Cadences don’t stagger their phases. Work at the same rate or at rates that divide each other runs in the same frames. For example, 10, 20 and 30 Hz work all runs every 100 ms.