Keep objects as rows in buffers, move them on the GPU on a fixed step, and read results back without waiting.
A game with four thousand enemies can’t upload each one’s position every frame. The kit keeps every object as a row in a GPU buffer. The CPU writes the few fields it owns when they change, kernels move the rest, and one pipeline draws them all. What the CPU needs to know comes back a frame or two later, without stalling the frame.
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
constEnemy:table.TableDef<"Enemy",{
radius:d.F32;
},{
position:d.Vec3f;
velocity:d.Vec3f;
health:d.F32;
}>
Enemy=
import table
table.
functiondefine<"Enemy",{
radius:d.F32;
},{
position:d.Vec3f;
velocity:d.Vec3f;
health:d.F32;
}>(name:"Enemy",parts:{
host?:{
radius:d.F32;
}|undefined;
device?:{
position:d.Vec3f;
velocity:d.Vec3f;
health:d.F32;
}|undefined;
}):table.TableDef<"Enemy",{
radius:d.F32;
},{
position:d.Vec3f;
velocity:d.Vec3f;
health:d.F32;
}>
Declares CPU-written host fields and GPU-written device fields using the records accepted by
d.struct. Define it at module scope beside shaders. Throws if no fields are declared, a field
appears in both parts, the CPU writer cannot write a field type such as f16, or a field uses a
reserved member name such as alive.
name brands row ids as RowId<'Enemy'> and labels structs, buffers, WGSL functions and
errors. It is not a lookup key.
@param ― name a label for ids and WGSL
@param ― parts the host fields the CPU writes and the device fields the GPU writes
@returns ― a definition whose getters shaders read and that table.create allocates
define('Enemy',{
host?:{
radius:d.F32;
}|undefined
host:{
radius:d.F32
radius:
import d
d.
constf32:d.F32
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.
@exampleconst value = f32(); // 0
@exampleconst value = f32(1.23); // 1.23
@exampleconst value = f32(true); // 1
f32},// written by the CPU
device?:{
position:d.Vec3f;
velocity: d.Vec3f;
health: d.F32;
}|undefined
device:{
position:d.Vec3f
position:
import d
d.
constvec3f:d.Vec3f
vec3f,
velocity:d.Vec3f
velocity:
import d
d.
constvec3f:d.Vec3f
vec3f,
health:d.F32
health:
import d
d.
constf32:d.F32
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.
@exampleconst value = f32(); // 0
@exampleconst value = f32(1.23); // 1.23
@exampleconst value = f32(true); // 1
f32},// by kernels
});
const
constenemies:table.Table<table.TableDef<"Enemy",{
radius:d.F32;
},{
position:d.Vec3f;
velocity:d.Vec3f;
health:d.F32;
}>>
enemies=
import table
table.
functioncreate<table.TableDef<"Enemy",{
radius:d.F32;
},{
position:d.Vec3f;
velocity:d.Vec3f;
health:d.F32;
}>>(root:TgpuRoot,def:table.TableDef<"Enemy",{
radius:d.F32;
},{
position:d.Vec3f;
velocity:d.Vec3f;
health:d.F32;
}>,options:table.Options<table.TableDef<"Enemy",{
radius:d.F32;
},{
position:d.Vec3f;
velocity:d.Vec3f;
health:d.F32;
}>>):table.Table<table.TableDef<"Enemy",{
radius:d.F32;
},{
position:d.Vec3f;
velocity:d.Vec3f;
health:d.F32;
}>>(+1overload)
Allocates GPU buffers and a CPU copy of host rows. Throws unless capacity is an integer in [1,
2^20].
@param ― def the definition its shaders were written against, from table.define
@returns ― a table you destroy
create(
constroot:TgpuRoot
root,
constEnemy:table.TableDef<"Enemy",{
radius:d.F32;
},{
position:d.Vec3f;
velocity:d.Vec3f;
health:d.F32;
}>
Enemy,{
Options<T extends AnyTableDef>.capacity: number
Sets the table capacity for its lifetime. Must be an integer in [1, 2^20], with slots from 0
to capacity - 1. A row id uses enough bits for the slot and the rest for its generation. The
generation has 29 bits at 8 rows and 12 bits at 2^20 rows.
Counts named events on the GPU and reads them back to the CPU. Kernels add hits, kills, pickups
or other events. The CPU receives the increases a frame or two later. Requires at least one
name, with no duplicates. Throws if names are invalid, hz is not finite and above 0, or
staging is not an integer above 0.
@injects ― per-instance add, for kernels piped with it
@binds ― 1 storage: add, the counters
@owns ― a buffer, a readback: the counters; the readback copies them at hz
@records ― capture after the passes that add; clear before them, whenever the counting starts
again; in frame mode, capture clears after copying.
Returns the increase in each counter since the last take, or the captured counts in frame
mode. Returns each read once, then undefined until the next read arrives. Reuses one object
and updates it in place.
take();// what kernels counted, a frame or two ago
Uploads changes since the last flush with queue.writeBuffer. Writes reach the queue before
the next submit. Call once per frame before passes that read the table. A HistoryTable takes
the frame's encoder and throws without it.
flush();// the CPU's changes, uploaded once
const
constcompute:TgpuComputePass
compute=
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 table splits each row into the fields the CPU writes and the fields kernels write. The CPU’s fields have a mirror on the CPU, so a write costs no GPU call, and flush uploads only the rows that changed. Shaders read either part through the definition, as Enemy.position(i), whichever table they are piped with.
A field kernels move belongs in the device part. In the host part, the next flush of its row would overwrite the kernel’s motion with the CPU’s stale copy:
Hit rows 1 and 6 while a kernel moves every row. The flush uploads host rows between the two tints; storing x in the same struct overwrites GPU motion with stale CPU values.
The kit’s own pools are tables or work the same way:
Every object instances draw is a row of its kind’s table, which your kernels can read and move.
A particle system keeps its particles in a pool, started by CPU bursts or by emit from any kernel.
Machines run a state machine for each row on the GPU, with the state in the rows.
Rows come and go, so a buffer has gaps. Live lists compact the live rows of each group into a list on the GPU and write the count into an indirect draw, so a draw covers only the live rows, with no CPU readback. Particles draw through them, and so can your own tables, grouped by a field such as species.
State that evolves, such as positions and velocities, stays on the GPU and advances in fixed steps. clock.fixedStep turns each frame’s time into a whole number of steps, and a kernel that captures the step length needs no time uploaded. The result is the same at 30 or 144 frames a second. Particles, machines and the flow field in Write a module step this way.
A module’s settings live in a uniform it returns, such as glow.settings. Upload them when a setting changes, not every frame. patch uploads only the fields you pass, as in glow.settings.patch({ threshold: 0.8 }); write uploads every field. Values that change every frame, such as a camera’s, go in a uniform of their own, so a change of exposure doesn’t upload the view again.
The CPU reads GPU results without waiting for them. A read is copied into a staging buffer during a frame and mapped later, so the answer arrives a frame or two after the work:
Events count what kernels report, such as kills or contacts. Counters only increase, so a late read loses nothing.
Readback returns a buffer’s contents, such as the fish tank’s bites.