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.
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.
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.
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(
constroot:TgpuRoot
root,(
frame:frames.Frame
frame)=>{
const
constpass: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.
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--)
constphysics:{
step(pass:TgpuComputePass):void;
}
physics.
functionstep(pass:TgpuComputePass):void
step(
constpass:TgpuComputePass
pass);
if(
constgiCadence: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))
constgi:{
step(pass:TgpuComputePass,dt:number):void;
}
gi.
functionstep(pass:TgpuComputePass,dt:number):void
step(
constpass:TgpuComputePass
pass,
constgiCadence:clock.Cadence
giCadence.
Cadence.dt: number
Reports the seconds since the last run. Valid when due returns true.
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.
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:
constSTEP:number
STEP});
const
constcount:TgpuMutable<d.U32>
count=
constroot: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.
constu32: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.
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(
constroot:TgpuRoot
root,(
frame:frames.Frame
frame)=>{
const
constpass: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.
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.
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+=(
constgoal:number
goal-
let follow:number
follow)*
import clock
clock.
functionease(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(
constframe:Frame
frame.
Frame.dt: number
Reports seconds since the previous submitted frame. The first frame has a dt of zero.
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:
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.
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(
constlanded:boolean
landed)
constsquash:clock.Spring
squash.
Spring.velocity: number
Holds its speed in units per second. Add to it to kick the spring.
velocity-=2;// a knock
constsquash:clock.Spring
squash.
Spring.step(target: number, dt: number): number
Moves the spring dt seconds toward target and returns the new value.
step(0,
constframe:Frame
frame.
Frame.dt: number
Reports seconds since the previous submitted frame. The first frame has a dt of zero.
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.