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.compact→compute
listed.finish→compute
listed.draw→render
listed.dispatch→compute
pipe().step→compute
step→compute
each().dispatch→compute
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.
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.
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
Allows the transition when the guard returns true. Checked last, after the other conditions
hold.
when:
functioncalm(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.
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.
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
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:
constsim: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.
Adds fill to the step pipeline, such as a table's inject() for guards and hooks. Returns
the same rows with that fill.
pipe(
constherd:{
inject():Inject;
}
herd.
functioninject():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).
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.
Uploads pending CPU starts and events, records one step for every live row, then lists the
rows in each state of lists.
step(
constpass:TgpuComputePass
pass);
}
constpass: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.
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
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:
constCOUNT: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:
constsim: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.
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.
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.
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.
Uploads pending CPU starts and events, records one step for every live row, then lists the
rows in each state of lists.
step(
constpass:TgpuComputePass
pass);
constbolt:Work
bolt.
Work.dispatch(pass: TgpuComputePass): void
Records the work over the rows the last compact or finish listed.
dispatch(
constpass:TgpuComputePass
pass);
}
constpass: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.
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
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:
constlasted:(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.
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:
constlasted:(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.
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.
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.
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.