Skip to content

@typegpu/kitmachines

State machines

Define states and transitions once, then run them per row on the GPU or per machine on the CPU.

kit.machines

module

machines.create(root, def, { capacity, step, alive?, … }) → Machines

machines.define(name, spec) → Definition

machines.run(def, options?) → Running

rootborrows
the root
deftakes
the definition, from machines.define
alive?takes
whether a slot holds a live row, such as a table's from rows.live(), spread into the options with its capacity
Machinesreturns
rows you step at each fixed step and destroy
nametakes
a label for types and WGSL
spectakes
the events, then the states; machines start in the first
Definitionreturns
a definition that run runs on the CPU and create runs per row on the GPU
Runningreturns
machines that step when you call step
types
StateGuardHookTransitionStateSpecMembersDefinitionAnyDefinitionStateOfEventOfRunningMachines
Optionscreate · 8 fields
capacity

number

Sets the number of row slots, from 0 to capacity - 1.

step

number

Sets the length of a step in seconds, such as a fixed-step clock's sim.step.

alive?

(i: number) => boolean= every slot is live

Tests whether slot i holds a live row in a shader, such as a table's live() function.

span?

() => number= capacity

Returns the highest live slot plus one, read when step records, such as a table's from live().

roll?

(i: number, tick: number) => number= randf.seed2(d.vec2f(d.f32(i), d.f32(tick))), then randf.sample()

Draws a number in [0, 1) for row i at each step, for transitions with a chance or rate.

kernelStarts?

boolean= false

Allows compute shaders to start rows with the definition's start(i, state).

lists?

readonly State<N>[]= none

Lists rows in these states after each step into listed, for work run by each.

random?

() => number= Math.random

Draws a number in [0, 1) to set the initial roll tick of a row started from the CPU.

RunOptionsrun · 2 fields
count?

number= 1

Sets the number of CPU machines, each with its own index.

random?

() => number= Math.random

Draws a number in [0, 1) for each step in a state with chances or rates.

records
  • listed.compactcompute
  • listed.finishcompute
  • listed.drawrender
  • listed.dispatchcompute
  • pipe().stepcompute
  • stepcompute
  • each().dispatchcompute

step(pass) at each fixed step; it uploads the starts and sends made since the last with queue.writeBuffer, which flush() does sooner for a frame that reads them and records no step. Then dispatch(pass) each each's work, after the step whose lists it reads.

owns

3 to 5 buffers ·a uniform ·a pipeline ·liveLists with `lists`

The rows, the CPU's starts and events, the events kernels send when the definition has any, the starts kernels make with kernelStarts, the ticks that seed rolls, the step's length, the step and, with lists, the rows in those states.

allocates

never in a step

lifecycle

initAsync()destroy()[Symbol.dispose]

injects
  • definition

    Its definition's rows, events and starts, which shaders were written against.

binds
  • 1 storage

    state(i), the rows.

  • 1 storage · 1 uniform

    since(i), the rows and the step's length.

  • 1 storage

    send, the events kernels send.

  • 1 storage

    start, the starts kernels make.

  • 2 storage

    each's work, its list and its count.

Drag urgency through 4.2 to 5.2 m/s to see horses keep their gait, or startle all states except bolting. Dots show forty horses by pace and state, arrows mark transitions, and urgency below 0.3 lets them stop to graze.

A horse may walk, gallop, graze or bolt. Tracking this by hand requires a state field, a start time and transition checks each step. Weighted choices need cumulative probabilities, and gait changes need separate speed thresholds to prevent flicker. A state machine defines the states and their transitions. Run it per GPU row with machines.create or on the CPU with machines.run.

import {
import machines
machines
} from '@typegpu/kit';
const
const faster: (speed: number) => (i: number) => boolean
faster
= (
speed: number
speed
: number) => (
i: number
i
: number) => {
'use gpu';
return
function paceOf(i: number): number
paceOf
(
i: number
i
) >
speed: number
speed
;
};
const
const slower: (speed: number) => (i: number) => boolean
slower
= (
speed: number
speed
: number) => (
i: number
i
: number) => {
'use gpu';
return
function paceOf(i: number): number
paceOf
(
i: number
i
) <
speed: number
speed
;
};
const
const startled: {
readonly to: "bolting";
readonly on: "startle";
}
startled
= {
to: "bolting"
to
: 'bolting',
on: "startle"
on
: 'startle' } as
type const = {
readonly to: "bolting";
readonly on: "startle";
}
const
;
export const
const Gait: machines.Definition<"Gait", "bolting" | "walking" | "galloping" | "grazing", "startle">
Gait
=
import machines
machines
.
function define<"Gait", "bolting" | "walking" | "galloping" | "grazing", "startle">(name: "Gait", spec: {
readonly events?: readonly "startle"[] | undefined;
readonly states: Validated<"bolting" | "walking" | "galloping" | "grazing", "startle"> & Unnumbered;
}): machines.Definition<"Gait", "bolting" | "walking" | "galloping" | "grazing", "startle">

Declares states, ordered transitions and events for a state machine. Each state can run hooks when entered or left. Use run on the CPU with plain functions, or create on the GPU with shader functions that take row slots. Needs no root and can be defined at module scope beside shaders.

name brands states as State<'Mood'> and labels WGSL functions and errors. It is not a lookup key. Throws for undeclared transition targets or events, invalid delays, chances or rates, chances that sum above 1, or a transition with both a chance and a rate. Also throws for missing states, duplicate events, more than 31 events, numeric state names or reserved names such as state and __proto__.

@param ― name a label for types and WGSL

@param ― spec the events, then the states; machines start in the first

@returns ― a definition that run runs on the CPU and create runs per row on the GPU

define
('Gait', {
events?: readonly "startle"[] | undefined
events
: ['startle'],
states: Validated<"bolting" | "walking" | "galloping" | "grazing", "startle"> & Unnumbered
states
: {
walking: machines.StateSpec<"bolting" | "walking" | "galloping" | "grazing", "startle">
walking
: {
StateSpec<"bolting" | "walking" | "galloping" | "grazing", "startle">.next?: readonly machines.Transition<"bolting" | "walking" | "galloping" | "grazing", "startle">[] | undefined

Lists transitions in the order each step checks them. The first whose conditions hold is taken.

next
: [
const startled: {
readonly to: "bolting";
readonly on: "startle";
}
startled
,
{
Transition<"bolting" | "walking" | "galloping" | "grazing", "startle">.to: "bolting" | "walking" | "galloping" | "grazing"

Names the destination state.

to
: 'galloping',
Transition<K extends string, E extends string>.when?: machines.Guard | undefined

Allows the transition when the guard returns true. Checked last, after the other conditions hold.

when
:
const faster: (speed: number) => (i: number) => boolean
faster
(5.2) },
{
Transition<"bolting" | "walking" | "galloping" | "grazing", "startle">.to: "bolting" | "walking" | "galloping" | "grazing"

Names the destination state.

to
: 'grazing',
Transition<K extends string, E extends string>.when?: machines.Guard | undefined

Allows the transition when the guard returns true. Checked last, after the other conditions hold.

when
:
function calm(i: number): boolean
calm
,
Transition<K extends string, E extends string>.rate?: number | undefined

Sets an average transition rate per second. For a step of dt seconds, reserves a share 1 - e^(-rate * dt) of the roll after earlier shares. Truncates shares at 1.

rate
: 0.03 },
],
},
galloping: machines.StateSpec<"bolting" | "walking" | "galloping" | "grazing", "startle">
galloping
: {
StateSpec<"bolting" | "walking" | "galloping" | "grazing", "startle">.next?: readonly machines.Transition<"bolting" | "walking" | "galloping" | "grazing", "startle">[] | undefined

Lists transitions in the order each step checks them. The first whose conditions hold is taken.

next
: [
const startled: {
readonly to: "bolting";
readonly on: "startle";
}
startled
, {
Transition<"bolting" | "walking" | "galloping" | "grazing", "startle">.to: "bolting" | "walking" | "galloping" | "grazing"

Names the destination state.

to
: 'walking',
Transition<K extends string, E extends string>.when?: machines.Guard | undefined

Allows the transition when the guard returns true. Checked last, after the other conditions hold.

when
:
const slower: (speed: number) => (i: number) => boolean
slower
(4.2) }] },
grazing: machines.StateSpec<"bolting" | "walking" | "galloping" | "grazing", "startle">
grazing
: {
StateSpec<"bolting" | "walking" | "galloping" | "grazing", "startle">.next?: readonly machines.Transition<"bolting" | "walking" | "galloping" | "grazing", "startle">[] | undefined

Lists transitions in the order each step checks them. The first whose conditions hold is taken.

next
: [
const startled: {
readonly to: "bolting";
readonly on: "startle";
}
startled
, {
Transition<"bolting" | "walking" | "galloping" | "grazing", "startle">.to: "bolting" | "walking" | "galloping" | "grazing"

Names the destination state.

to
: 'walking',
Transition<"bolting" | "walking" | "galloping" | "grazing", "startle">.after?: number | ((i: number) => number) | undefined

Sets the delay in seconds after entering the state. A function computes the delay for machine i.

after
: 4,
Transition<K extends string, E extends string>.rate?: number | undefined

Sets an average transition rate per second. For a step of dt seconds, reserves a share 1 - e^(-rate * dt) of the roll after earlier shares. Truncates shares at 1.

rate
: 1 / 3 }] },
bolting: machines.StateSpec<"bolting" | "walking" | "galloping" | "grazing", "startle">
bolting
: {
StateSpec<"bolting" | "walking" | "galloping" | "grazing", "startle">.next?: readonly machines.Transition<"bolting" | "walking" | "galloping" | "grazing", "startle">[] | undefined

Lists transitions in the order each step checks them. The first whose conditions hold is taken.

next
: [{
Transition<"bolting" | "walking" | "galloping" | "grazing", "startle">.to: "bolting" | "walking" | "galloping" | "grazing"

Names the destination state.

to
: 'galloping',
Transition<"bolting" | "walking" | "galloping" | "grazing", "startle">.after?: number | ((i: number) => number) | undefined

Sets the delay in seconds after entering the state. A function computes the delay for machine i.

after
: 2 }] },
},
});
const
const gaits: machines.Machines<"Gait", "bolting" | "walking" | "galloping" | "grazing", "startle">
gaits
=
import machines
machines
.
function create<"Gait", "bolting" | "walking" | "galloping" | "grazing", "startle">(root: TgpuRoot, def: machines.Definition<"Gait", "bolting" | "walking" | "galloping" | "grazing", "startle">, options: machines.Options<"Gait">): machines.Machines<"Gait", "bolting" | "walking" | "galloping" | "grazing", "startle">

Creates a GPU state machine for every slot in a table or another set of rows. Rows start in the first state without running enter. Call start(slot) when adding a row. Shaders read rows with the definition's state(i) and since(i), and send events with send. Create those pipelines with inject(). Throws for invalid capacity or step duration, invalid or duplicate listed states, more than 256 lists, or lists exceeding maxStorageBufferBindingSize.

@param ― def the definition, from machines.define

@param ― options.alive whether a slot holds a live row, such as a table's from rows.live(), spread into the options with its capacity

@returns ― rows you step at each fixed step and destroy

create
(
const root: TgpuRoot
root
,
const Gait: machines.Definition<"Gait", "bolting" | "walking" | "galloping" | "grazing", "startle">
Gait
, {
Options<N extends string = string>.capacity: number

Sets the number of row slots, from 0 to capacity - 1. Must be an integer in [1, 2^24].

capacity
: 8192,
Options<N extends string = string>.step: number

Sets the length of a step in seconds, such as a fixed-step clock's sim.step. Transitions compile it into their waits and rate shares.

step
:
const sim: clock.FixedStep
sim
.
FixedStep.step: number

Sets the step duration in seconds. A new value applies from the next advance: unstepped time carries over, capped at maxSteps new steps, and time keeps the seconds already stepped. Particles and machines take its value when created and keep it. Throws unless the value is finite and above 0.

step
}).
Machines<"Gait", "bolting" | "walking" | "galloping" | "grazing", "startle">.pipe(fill: Inject): machines.Machines<"Gait", "bolting" | "walking" | "galloping" | "grazing", "startle">

Adds fill to the step pipeline, such as a table's inject() for guards and hooks. Returns the same rows with that fill.

pipe
(
const herd: {
inject(): Inject;
}
herd
.
function inject(): Inject
inject
());
  • A transition requires all its conditions to hold. on checks an event, after checks time in the state, and chance or rate checks a random roll. when, a shader function of the row, runs last. The first matching transition runs, at most once per step.
  • Machines start in the first state. A state is a number, a literal in WGSL: Gait.galloping is 1.
  • galloping leaves below 4.2 m/s and walking above 5.2, so a horse between them keeps its gait instead of flickering.
  • Chance transitions take consecutive shares of one roll per step. Values of 0.05, 0.15 and 0.3 take 5, 15 and 30 % of the rolls. Later transitions get the remaining rolls. If none match, the state rolls again next step.
  • A rate matches once per 1 / rate seconds on average, independent of step length. A calm walking horse starts grazing every 33 s on average. A grazing horse waits at least 4 s, then resumes walking after another 3 s on average.
  • A transition declared outside the definition, like startled, needs as const so its target stays a name.
  • The definition needs no root, so it lives at module scope beside the shaders that read it.
  • TypeScript rejects a target or an event the definition doesn’t declare, and a state named like a number or one of the definition’s members, such as state or start. define throws for those too, and for a negative delay, chances past 1, or a chance and a rate on one transition.

In pipelines piped with gaits.inject(), shaders read a row’s state with Gait.state(i) and its seconds in it with Gait.since(i), and compute shaders send an event with Gait.send.startle(i).

const
const move: TgpuComputeFn<{
gid: d.BuiltinGlobalInvocationId;
}>
move
=
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: <{
gid: d.BuiltinGlobalInvocationId;
}>(options: {
in: {
gid: d.BuiltinGlobalInvocationId;
};
workgroupSize: number[];
}) => TgpuComputeFnShell<{
gid: d.BuiltinGlobalInvocationId;
}> (+1 overload)
computeFn
({
workgroupSize: number[]
workgroupSize
: [64],
in: {
gid: d.BuiltinGlobalInvocationId;
}
in
: {
gid: d.BuiltinGlobalInvocationId
gid
:
import d
d
.
const builtin: {
readonly vertexIndex: d.BuiltinVertexIndex;
readonly instanceIndex: d.BuiltinInstanceIndex;
readonly clipDistances: d.BuiltinClipDistances;
readonly position: d.BuiltinPosition;
readonly frontFacing: d.BuiltinFrontFacing;
readonly fragDepth: d.BuiltinFragDepth;
readonly primitiveIndex: d.BuiltinPrimitiveIndex;
readonly sampleIndex: d.BuiltinSampleIndex;
... 11 more ...;
readonly numSubgroups: d.BuiltinNumSubgroups;
}
export builtin
builtin
.
globalInvocationId: d.BuiltinGlobalInvocationId
globalInvocationId
} })(({
gid: d.v3u
gid
,
}) => {
'use gpu';
const
const i: number
i
=
gid: d.v3u
gid
.
v3u.x: number
x
;
if (
const i: number
i
>=
const COUNT: 8192
COUNT
) return;
let
let wanted: number
wanted
=
const cruise: {
$: number;
}
cruise
.
$: number
$
;
if (
const Gait: machines.Definition<"Gait", "walking" | "bolting" | "galloping" | "grazing", "startle">
Gait
.
Members<"Gait", "walking" | "bolting" | "galloping" | "grazing", "startle">.state: TgpuGenericFn
(i: number) => machines.State<"Gait">

Reads the state of row i in a shader. Create pipelines with the inject() from machines.create.

state
(
const i: number
i
) ===
const Gait: machines.Definition<"Gait", "walking" | "bolting" | "galloping" | "grazing", "startle">
Gait
.
grazing: machines.State<"Gait">
grazing
)
let wanted: number
wanted
= 0;
if (
const Gait: machines.Definition<"Gait", "walking" | "bolting" | "galloping" | "grazing", "startle">
Gait
.
Members<"Gait", "walking" | "bolting" | "galloping" | "grazing", "startle">.state: TgpuGenericFn
(i: number) => machines.State<"Gait">

Reads the state of row i in a shader. Create pipelines with the inject() from machines.create.

state
(
const i: number
i
) ===
const Gait: machines.Definition<"Gait", "walking" | "bolting" | "galloping" | "grazing", "startle">
Gait
.
bolting: machines.State<"Gait">
bolting
)
let wanted: number
wanted
= 15.5;
// ... ease the horse's pace towards wanted, and move it
if (
import std
std
.
distance<d.v2f>(a: d.v2f, b: d.v2f): number (+1 overload)
export distance
distance
(
function placeOf(i: number): d.v2f
placeOf
(
const i: number
i
),
const thunder: {
$: d.v2f;
}
thunder
.
$: d.v2f
$
) < 30)
const Gait: machines.Definition<"Gait", "walking" | "bolting" | "galloping" | "grazing", "startle">
Gait
.
Members<"Gait", "walking" | "bolting" | "galloping" | "grazing", "startle">.send: {
readonly startle: TgpuGenericFn<(i: number) => void>;
}

Sends events to row i for its next step. Available only in compute shaders.

send
.
startle: TgpuGenericFn
(i: number) => void
startle
(
const i: number
i
);
});
const
const movement: TgpuComputePipeline
movement
=
const root: TgpuRoot
root
.
WithBinding.pipe(transform: (cfg: Configurable) => Configurable): WithBinding
pipe
(
const herd: {
inject(): Inject;
}
herd
.
function inject(): Inject
inject
())
.
WithBinding.pipe(transform: (cfg: Configurable) => Configurable): WithBinding
pipe
(
const gaits: machines.Machines<"Gait", "walking" | "bolting" | "galloping" | "grazing", "startle">
gaits
.
Machines<"Gait", "walking" | "bolting" | "galloping" | "grazing", "startle">.inject(): Inject

Fills the definition's state, since, send and, with kernelStarts, start for pipelines created from this configuration.

inject
())
.
WithBinding.createComputePipeline<{
gid: d.BuiltinGlobalInvocationId;
}>(descriptor: TgpuComputePipeline.Descriptor<{
gid: d.BuiltinGlobalInvocationId;
}>): TgpuComputePipeline
createComputePipeline
({
compute: TgpuComputeFn<{
gid: d.BuiltinGlobalInvocationId;
}>
compute
:
const move: TgpuComputeFn<{
gid: d.BuiltinGlobalInvocationId;
}>
move
});
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 s: number
s
=
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 s: number
s
> 0;
let s: number
s
--) {
const pass: TgpuComputePass
pass
.
TgpuComputePass.setPipeline(pipeline: TgpuComputePipeline): void

Sets the current

TgpuComputePipeline

for subsequent dispatches

setPipeline
(
const movement: TgpuComputePipeline
movement
);
const pass: TgpuComputePass
pass
.
TgpuComputePass.dispatchWorkgroups(x: number, y?: number, z?: number): void
dispatchWorkgroups
(
const COUNT: 8192
COUNT
/ 64);
const gaits: machines.Machines<"Gait", "walking" | "bolting" | "galloping" | "grazing", "startle">
gaits
.
Machines<"Gait", "walking" | "bolting" | "galloping" | "grazing", "startle">.step(pass: TgpuComputePass): void

Uploads pending CPU starts and events, records one step for every live row, then lists the rows in each state of lists.

step
(
const pass: TgpuComputePass
pass
);
}
const pass: TgpuComputePass
pass
.
TgpuComputePass.end(): void

Completes the recording of this compute pass

end
();
});

step(pass) updates every live row once. A newly started row runs only its state’s enter. Other rows advance their time by one step and check transitions. A transition runs the old state’s exit, changes the state and runs its enter. Both hooks are shader functions of the row. Put ongoing state work in your own kernels, as move does by branching on Gait.state(i).

The skeletons run 300 machines this way. Each enter rolls the next action’s length or destination, writes its motion to the skeleton’s row and starts its clip with the poses verbs. An attacker’s enter walks a neighbour grid to turn on the nearest skeleton.

  • Gait.state(i) reads the old state in exit and the new one in enter. A transition to the state it leaves runs both and starts since from 0.
  • In a guard, Gait.since(i) reads time before this step. after compares against time including this step.
  • An event lasts until the row’s next step, which handles or drops it. Events sent after a frame’s last step wait for the next frame. A hook’s event to its own row reaches the next step. An event to another row may reach this step or the next.
  • A guard or hook that reads another row’s state reads it before or after that row’s step.
  • after rounds delays up to whole steps. A 4 s delay at 1/60 s takes 240 steps. Use after: (i) => … for a per-row delay in seconds, evaluated every step.
  • A state with chances or rates rolls once a step from randf, seeded by row and step. Pass roll(i, tick) to draw it another way, such as from a blue-noise texture.
  • Gait.roll(i) draws a number in [0, 1) from roll in guards, hooks and after functions. An after function runs every step, so a delay rolled there changes each step. For a fixed random delay, roll in enter, store the result in a field and read it from after.
  • pipe(fill) fills resources read by guards, hooks, alive and roll, such as the herd’s buffers. These functions share the step’s pipeline. alive and roll can use its fills or carry their own.
  • alive skips the slots without a row. A table gives it with its capacity and span, so a step runs no thread above the span: machines.create(root, Gait, { ...horses.live(), step: sim.step }).
  • rows is the buffer of each row’s state and steps in it, for a readback or a test.

move runs for every horse and branches on its state. A group of 32 lanes executes a state’s branch whenever any of its rows uses that state. Scattered bolting horses therefore make nearly every group execute the bolt branch. Pass lists to collect rows by state each step. each runs work only on one listed state’s rows.

const
const gaits: machines.Machines<"Gait", "walking" | "galloping" | "grazing" | "bolting", "startle">
gaits
=
import machines
machines
.
function create<"Gait", "walking" | "galloping" | "grazing" | "bolting", "startle">(root: TgpuRoot, def: machines.Definition<"Gait", "walking" | "galloping" | "grazing" | "bolting", "startle">, options: machines.Options<"Gait">): machines.Machines<"Gait", "walking" | "galloping" | "grazing" | "bolting", "startle">

Creates a GPU state machine for every slot in a table or another set of rows. Rows start in the first state without running enter. Call start(slot) when adding a row. Shaders read rows with the definition's state(i) and since(i), and send events with send. Create those pipelines with inject(). Throws for invalid capacity or step duration, invalid or duplicate listed states, more than 256 lists, or lists exceeding maxStorageBufferBindingSize.

@param ― def the definition, from machines.define

@param ― options.alive whether a slot holds a live row, such as a table's from rows.live(), spread into the options with its capacity

@returns ― rows you step at each fixed step and destroy

create
(
const root: TgpuRoot
root
,
const Gait: machines.Definition<"Gait", "walking" | "galloping" | "grazing" | "bolting", "startle">
Gait
, {
Options<N extends string = string>.capacity: number

Sets the number of row slots, from 0 to capacity - 1. Must be an integer in [1, 2^24].

capacity
:
const COUNT: 8192
COUNT
,
Options<N extends string = string>.step: number

Sets the length of a step in seconds, such as a fixed-step clock's sim.step. Transitions compile it into their waits and rate shares.

step
:
const sim: clock.FixedStep
sim
.
FixedStep.step: number

Sets the step duration in seconds. A new value applies from the next advance: unstepped time carries over, capped at maxSteps new steps, and time keeps the seconds already stepped. Particles and machines take its value when created and keep it. Throws unless the value is finite and above 0.

step
,
Options<"Gait">.lists?: readonly machines.State<"Gait">[] | undefined

Lists rows in these states after each step into listed, for work run by each. Uses one word per row for each listed state. Accepts at most 256 states. Default none.

lists
: [
const Gait: machines.Definition<"Gait", "walking" | "galloping" | "grazing" | "bolting", "startle">
Gait
.
bolting: machines.State<"Gait">
bolting
],
});
const
const bolt: Work
bolt
=
const gaits: machines.Machines<"Gait", "walking" | "galloping" | "grazing" | "bolting", "startle">
gaits
.
Machines<"Gait", "walking" | "galloping" | "grazing" | "bolting", "startle">.each(state: machines.State<"Gait">, work: (i: number) => void): Work

Creates work that runs the shader function work(i) for each row listed in state by the last step. Rows run in no defined order. state must be in lists. The pipeline includes the rows' inject() and their piped fills.

each
(
const Gait: machines.Definition<"Gait", "walking" | "galloping" | "grazing" | "bolting", "startle">
Gait
.
bolting: machines.State<"Gait">
bolting
, (
i: number
i
) => {
'use gpu';
function flee(i: number): void
flee
(
i: number
i
); // ... away from the thunder
})
.
Work.pipe(fill: Inject): Work

Adds fill to the work pipeline, such as a table's inject(), and returns the work.

pipe
(
const herd: {
inject(): Inject;
}
herd
.
function inject(): Inject
inject
());
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 s: number
s
=
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 s: number
s
> 0;
let s: number
s
--) {
const gaits: machines.Machines<"Gait", "walking" | "galloping" | "grazing" | "bolting", "startle">
gaits
.
Machines<"Gait", "walking" | "galloping" | "grazing" | "bolting", "startle">.step(pass: TgpuComputePass): void

Uploads pending CPU starts and events, records one step for every live row, then lists the rows in each state of lists.

step
(
const pass: TgpuComputePass
pass
);
const bolt: Work
bolt
.
Work.dispatch(pass: TgpuComputePass): void

Records the work over the rows the last compact or finish listed.

dispatch
(
const pass: TgpuComputePass
pass
);
}
const pass: TgpuComputePass
pass
.
TgpuComputePass.end(): void

Completes the recording of this compute pass

end
();
});
  • Each step fills lists with the live rows in the states they end the step in, in no order. dispatch uses the lists from the most recent step.
  • The work pipeline inherits the machine’s fills, including those for Gait.state(i) and Gait.since(i). Pipe in fills for any other resources it reads.
  • Each list uses one word per row. At most 256 states can have lists.
  • gaits.listed holds the lists as live lists, group k for lists[k]. Read a row with gaits.listed.slot(k, i) in a kernel of your own and run it with listed.dispatch.

Rows sit in the first state without running its enter. start(slot, state?) puts a row in a state as you add it, and its enter runs at the next step. The stampede starts its whole herd galloping this way.

  • A start skips the old state’s exit and drops the row’s waiting events, from the CPU or from kernels.
  • send.startle(slot) sends an event from the CPU.
  • The next step uploads starts and events with queue.writeBuffer. Consecutive started slots use one write. Call flush() before the frame’s steps when a frame may draw without stepping.
  • With kernelStarts: true, compute shaders can start rows with Gait.start(i, Gait.bolting). The row applies the start at its next step. A kernel start takes precedence over a CPU start in the same step.

A definition with plain functions for guards and hooks runs on the CPU with machines.run. The horses in the figure above run this way, and a step allocates nothing.

const
const lasted: (i: number) => number
lasted
= (
i: number
i
: number) =>
const actors: {
lasts: number;
}[]
actors
[
i: number
i
]!.
lasts: number
lasts
; // seconds, set as each state starts
const
const stood: readonly [{
readonly to: "walking";
readonly after: (i: number) => number;
readonly chance: 0.7;
}, {
readonly to: "idling";
readonly after: (i: number) => number;
}]
stood
= [
{
to: "walking"
to
: 'walking',
after: (i: number) => number
after
:
const lasted: (i: number) => number
lasted
,
chance: 0.7
chance
: 0.7 },
{
to: "idling"
to
: 'idling',
after: (i: number) => number
after
:
const lasted: (i: number) => number
lasted
},
] as
type const = readonly [{
readonly to: "walking";
readonly after: (i: number) => number;
readonly chance: 0.7;
}, {
readonly to: "idling";
readonly after: (i: number) => number;
}]
const
;
const
const Doing: machines.Definition<"Doing", "walking" | "idling" | "waking" | "taunting" | "attacking", never>
Doing
=
import machines
machines
.
function define<"Doing", "walking" | "idling" | "waking" | "taunting" | "attacking", never>(name: "Doing", spec: {
readonly events?: readonly never[] | undefined;
readonly states: Validated<"walking" | "idling" | "waking" | "taunting" | "attacking", never> & Unnumbered;
}): machines.Definition<"Doing", "walking" | "idling" | "waking" | "taunting" | "attacking", never>

Declares states, ordered transitions and events for a state machine. Each state can run hooks when entered or left. Use run on the CPU with plain functions, or create on the GPU with shader functions that take row slots. Needs no root and can be defined at module scope beside shaders.

name brands states as State<'Mood'> and labels WGSL functions and errors. It is not a lookup key. Throws for undeclared transition targets or events, invalid delays, chances or rates, chances that sum above 1, or a transition with both a chance and a rate. Also throws for missing states, duplicate events, more than 31 events, numeric state names or reserved names such as state and __proto__.

@param ― name a label for types and WGSL

@param ― spec the events, then the states; machines start in the first

@returns ― a definition that run runs on the CPU and create runs per row on the GPU

define
('Doing', {
states: Validated<"walking" | "idling" | "waking" | "taunting" | "attacking", never> & Unnumbered
states
: {
waking: machines.StateSpec<"walking" | "idling" | "waking" | "taunting" | "attacking", never>
waking
: {
StateSpec<"walking" | "idling" | "waking" | "taunting" | "attacking", never>.next?: readonly machines.Transition<"walking" | "idling" | "waking" | "taunting" | "attacking", never>[] | undefined

Lists transitions in the order each step checks them. The first whose conditions hold is taken.

next
:
const stood: readonly [{
readonly to: "walking";
readonly after: (i: number) => number;
readonly chance: 0.7;
}, {
readonly to: "idling";
readonly after: (i: number) => number;
}]
stood
},
idling: machines.StateSpec<"walking" | "idling" | "waking" | "taunting" | "attacking", never>
idling
: {
StateSpec<K extends string, E extends string>.enter?: machines.Hook | undefined

Runs when a step enters the state, after the prior state's exit.

enter
:
function idle(i: number): void
idle
,
StateSpec<"walking" | "idling" | "waking" | "taunting" | "attacking", never>.next?: readonly machines.Transition<"walking" | "idling" | "waking" | "taunting" | "attacking", never>[] | undefined

Lists transitions in the order each step checks them. The first whose conditions hold is taken.

next
:
const stood: readonly [{
readonly to: "walking";
readonly after: (i: number) => number;
readonly chance: 0.7;
}, {
readonly to: "idling";
readonly after: (i: number) => number;
}]
stood
},
walking: machines.StateSpec<"walking" | "idling" | "waking" | "taunting" | "attacking", never>
walking
: {
StateSpec<K extends string, E extends string>.enter?: machines.Hook | undefined

Runs when a step enters the state, after the prior state's exit.

enter
:
function wander(i: number): void
wander
,
StateSpec<"walking" | "idling" | "waking" | "taunting" | "attacking", never>.next?: readonly machines.Transition<"walking" | "idling" | "waking" | "taunting" | "attacking", never>[] | undefined

Lists transitions in the order each step checks them. The first whose conditions hold is taken.

next
: [
{
Transition<"walking" | "idling" | "waking" | "taunting" | "attacking", never>.to: "walking" | "idling" | "waking" | "taunting" | "attacking"

Names the destination state.

to
: 'taunting',
Transition<"walking" | "idling" | "waking" | "taunting" | "attacking", never>.after?: number | ((i: number) => number) | undefined

Sets the delay in seconds after entering the state. A function computes the delay for machine i.

after
:
const lasted: (i: number) => number
lasted
,
Transition<K extends string, E extends string>.chance?: number | undefined

Assigns the transition a share of one roll in [0, 1) per step. The state's chances and rates take consecutive shares of that roll in order. Chances of 0.05, 0.15 and 0.3 receive 5, 15 and 30 % respectively. The chances must sum to at most 1.

chance
: 0.15 },
{
Transition<"walking" | "idling" | "waking" | "taunting" | "attacking", never>.to: "walking" | "idling" | "waking" | "taunting" | "attacking"

Names the destination state.

to
: 'attacking',
Transition<"walking" | "idling" | "waking" | "taunting" | "attacking", never>.after?: number | ((i: number) => number) | undefined

Sets the delay in seconds after entering the state. A function computes the delay for machine i.

after
:
const lasted: (i: number) => number
lasted
,
Transition<K extends string, E extends string>.chance?: number | undefined

Assigns the transition a share of one roll in [0, 1) per step. The state's chances and rates take consecutive shares of that roll in order. Chances of 0.05, 0.15 and 0.3 receive 5, 15 and 30 % respectively. The chances must sum to at most 1.

chance
: 0.3 },
{
Transition<"walking" | "idling" | "waking" | "taunting" | "attacking", never>.to: "walking" | "idling" | "waking" | "taunting" | "attacking"

Names the destination state.

to
: 'idling',
Transition<"walking" | "idling" | "waking" | "taunting" | "attacking", never>.after?: number | ((i: number) => number) | undefined

Sets the delay in seconds after entering the state. A function computes the delay for machine i.

after
:
const lasted: (i: number) => number
lasted
},
],
},
taunting: machines.StateSpec<"walking" | "idling" | "waking" | "taunting" | "attacking", never>
taunting
: {
StateSpec<K extends string, E extends string>.enter?: machines.Hook | undefined

Runs when a step enters the state, after the prior state's exit.

enter
: (
i: number
i
) =>
function play(i: number, clip: "taunt" | "attack"): void
play
(
i: number
i
, 'taunt'),
StateSpec<"walking" | "idling" | "waking" | "taunting" | "attacking", never>.next?: readonly machines.Transition<"walking" | "idling" | "waking" | "taunting" | "attacking", never>[] | undefined

Lists transitions in the order each step checks them. The first whose conditions hold is taken.

next
:
const stood: readonly [{
readonly to: "walking";
readonly after: (i: number) => number;
readonly chance: 0.7;
}, {
readonly to: "idling";
readonly after: (i: number) => number;
}]
stood
},
attacking: machines.StateSpec<"walking" | "idling" | "waking" | "taunting" | "attacking", never>
attacking
: {
StateSpec<K extends string, E extends string>.enter?: machines.Hook | undefined

Runs when a step enters the state, after the prior state's exit.

enter
: (
i: number
i
) =>
function play(i: number, clip: "taunt" | "attack"): void
play
(
i: number
i
, 'attack'),
StateSpec<"walking" | "idling" | "waking" | "taunting" | "attacking", never>.next?: readonly machines.Transition<"walking" | "idling" | "waking" | "taunting" | "attacking", never>[] | undefined

Lists transitions in the order each step checks them. The first whose conditions hold is taken.

next
:
const stood: readonly [{
readonly to: "walking";
readonly after: (i: number) => number;
readonly chance: 0.7;
}, {
readonly to: "idling";
readonly after: (i: number) => number;
}]
stood
},
},
});
const
const doing: machines.Running<"Doing", "walking" | "idling" | "waking" | "taunting" | "attacking", never>
doing
=
import machines
machines
.
function run<"Doing", "walking" | "idling" | "waking" | "taunting" | "attacking", never>(def: machines.Definition<"Doing", "walking" | "idling" | "waking" | "taunting" | "attacking", never>, options?: machines.RunOptions): machines.Running<"Doing", "walking" | "idling" | "waking" | "taunting" | "attacking", never>

Runs a definition on the CPU with every machine in the first state. Runs that state's enter at the first step. Throws unless count is an integer above 0.

@allocates ― never in a step

@param ― def the definition, from machines.define

@returns ― machines that step when you call step

run
(
const Doing: machines.Definition<"Doing", "walking" | "idling" | "waking" | "taunting" | "attacking", never>
Doing
, {
RunOptions.count?: number | undefined

Sets the number of CPU machines, each with its own index. Default 1.

count
: 300 });
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 doing: machines.Running<"Doing", "walking" | "idling" | "waking" | "taunting" | "attacking", never>
doing
.
Running<"Doing", "walking" | "idling" | "waking" | "taunting" | "attacking", never>.step(dt: number): void

Advances every machine by dt seconds.

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

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

dt
));
  • state(i) and since(i) read machine i, send.<event>(i) sends it an event, and start(i, state?) puts it in a state at once. Its enter runs at the next step, or at the one after when a hook calls start.
  • An event sent from a hook reaches the step after.
  • A delay matches at the first step that reaches it. The next state counts time from that step, so each timed transition can add up to one step of delay.
  • subscribe(listener) calls back after a step that changed a state and after a start, for a game’s flow in React: useSyncExternalStore(flow.subscribe, () => flow.state()).
  • random draws the rolls, Math.random by default. Pass a seeded one for tests that repeat.
machines.create machines.run
Machines a row a slot, capacity of them; alive skips some count, which all step
Guards, hooks, after functions 'use gpu' functions of the slot plain functions of the index
Time in a state whole steps of the step given to create the seconds each step(dt) adds
The first state rows sit in it without its enter its enter runs at the first step
Rolls randf or your roll(i, tick), from a tick random() starts; Gait.roll(i) in guards and hooks random()
State and time Gait.state(i), Gait.since(i) in shaders state(i), since(i)
Events send on the CPU, Gait.send in compute shaders send
Starts start(slot) on the CPU, read once the next step or flush() uploads it; Gait.start(i, state) in compute shaders, read from the next step start(i), read at once, telling subscribe
  • A definition is flat, with no nested states, concurrent regions or history states.
  • One transition a step, so a chain of three takes three steps.
  • At most 31 events a definition; a row holds them as bits until its next step.
  • On the GPU, after and since count steps of the step the rows were created with, compiled into the transitions. Steps recorded at another length read wrong.
  • On the GPU, a slot keeps its row’s last state when the row dies. Start the slot again for the next row in it, from the CPU or a kernel.
  • On the GPU, rows started from the CPU begin their rolls at a tick random draws, Math.random by default, so two runs roll alike only with a seeded one.
  • A pipeline reads one machines.create instance per definition. If two are piped in, the last fills Gait’s functions.
  • The step pipeline uses 2 to 7 storage buffers and at most one uniform. These hold the rows and the CPU’s starts and events, and as needed the kernels’ events and starts, the roll ticks, and the lists and their counts. Of the 8 storage bindings WebGPU guarantees per stage, this leaves 1 to 6 for guards and hooks.