Keeps last frame's rows, which code compiled with history.previously reads, such as a drawing's motion.
records
flush→encoder
flush() once a frame, before the passes that read the table: it uploads the frame's changes with queue.writeBuffer. With history, flush(encoder) also records copies that keep last frame's rows.
owns
3 or 4 buffers
The host and device rows, the live bits and the span; a part without fields has no buffer. With history, a part's buffer holds last frame's rows after its own, and 2 more per part hold the uploads and a copy on the way.
lifecycle
✓destroy()✓[Symbol.dispose]
injects
definition
Its definition's rows, live bits and span, which shaders were written against.
binds
1 storage
A field, per part, host or device, such as Boid.position(i); row(i) binds both parts.
1 storage · 1 uniform
alive(i), the live bits and the span; span() and slotOf(id) bind only the uniform.
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.
A game may keep enemies, pickups, lights or fish in a GPU buffer. If the CPU and kernels write the same fields, a stale CPU copy can overwrite a kernel’s update, such as the start of a death animation.
A table separates CPU-owned host fields from GPU-owned device fields in different buffers. Uploading host rows preserves the device fields that kernels wrote.
import{
import frames
frames,
import table
table}from'@typegpu/kit';
const
constEnemy:table.TableDef<"Enemy",{
species:d.U32;
tint:d.Vec4f;
radius:d.F32;
},{
position:d.Vec3f;
velocity:d.Vec3f;
health:d.F32;
}>
Enemy=
import table
table.
functiondefine<"Enemy",{
species:d.U32;
tint:d.Vec4f;
radius:d.F32;
},{
position:d.Vec3f;
velocity:d.Vec3f;
health:d.F32;
}>(name:"Enemy",parts:{
host?:{
species:d.U32;
tint:d.Vec4f;
radius:d.F32;
}|undefined;
device?:{
position:d.Vec3f;
velocity:d.Vec3f;
health:d.F32;
}|undefined;
}):table.TableDef<"Enemy",{
species:d.U32;
tint:d.Vec4f;
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?:{
species:d.U32;
tint: d.Vec4f;
radius: d.F32;
}|undefined
host:{
species:d.U32
species:
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.
@exampleconst value = u32(); // 0
@exampleconst value = u32(7); // 7
@exampleconst value = u32(3.14); // 3
@exampleconst value = u32(-1); // 4294967295
@exampleconst value = u32(-3.1); // 0
u32,
tint:d.Vec4f
tint:
import d
d.
constvec4f:d.Vec4f
vec4f,
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",{
species:d.U32;
tint:d.Vec4f;
radius:d.F32;
},{
position:d.Vec3f;
velocity:d.Vec3f;
health:d.F32;
}>>
enemies=
import table
table.
functioncreate<table.TableDef<"Enemy",{
species:d.U32;
tint:d.Vec4f;
radius:d.F32;
},{
position:d.Vec3f;
velocity:d.Vec3f;
health:d.F32;
}>>(root:TgpuRoot,def:table.TableDef<"Enemy",{
species:d.U32;
tint:d.Vec4f;
radius:d.F32;
},{
position:d.Vec3f;
velocity:d.Vec3f;
health:d.F32;
}>,options:table.Options<table.TableDef<"Enemy",{
species:d.U32;
tint:d.Vec4f;
radius:d.F32;
},{
position:d.Vec3f;
velocity:d.Vec3f;
health:d.F32;
}>>):table.Table<table.TableDef<"Enemy",{
species:d.U32;
tint:d.Vec4f;
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",{
species:d.U32;
tint:d.Vec4f;
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.
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();// uploads what changed, before any pass reads the table
table.define takes no root, so it lives at module scope beside its shaders. Its name brands the ids and names the WGSL (EnemyHost, Enemy_position); nothing looks it up.
Either part may be omitted. A host-only table holds instance data, such as Blockfall’s blocks. A field in both parts is a type error, and define throws for it and for f16 and bool fields.
The capacity is fixed, at most 2^20, and add throws when the table is full. Fields start at defaults, or zero, and add() with no values allocates nothing. count is the live rows; clear() removes them all.
The types take the definition: table.Table<typeof Enemy>, table.Options, table.Fields.
enemies.buffers holds the host, device and live buffers and the index uniform (the span and the id mask), for a readback or a bind group of your own.
table.rows(Enemy, 'device', capacity) gives a part’s array schema, for a buffer or history of your own. It throws for a capacity a table can’t have, or a part with no fields.
add returns a row id, a u32 containing the slot and generation with the type RowId<'Enemy'>. A plain number or another definition’s id is a type error.
remove(id) bumps the slot’s generation; a field handle given the stale id throws, and has(id) checks first. idAt(slot) turns a slot read back from the GPU into an id.
Ids belong to the definition. Another table of Enemy accepts the same id if its slot has the same generation. Keep each table’s ids separate.
Generations start at 1, so a zeroed u32 is never a live id.
The capacity determines how many bits the slot needs. The generation uses the remaining bits. Once a slot’s generation wraps, an old id is valid again. A bullet spawned and freed every frame takes the same slot every frame, so at 120 fps it wraps after:
A vector field’s write and get also take any array of numbers, such as a Float32Array from a math library. write reads its first components.
Matrices, arrays and nested structs have only write. A matrix may be a Float32Array in its padded layout.
Device fields have no get. A device write sets an absolute value before the frame’s GPU work, for a teleport or a revive. Apply relative changes, such as damage, with a command.
Click cells, add or remove rows, then flush. Outlines mark changed fields, and the highlight marks uploaded cells.
Nothing reaches the GPU until flush(). Call it once per frame, before any pass that reads the table. Without history it needs no encoder, because queue.writeBuffer runs before the next submit. It uploads:
written host rows in runs, bridging gaps of fewer than 16 unwritten rows from the CPU copy;
each written device row, from its first written field to its last;
new rows’ device values, whole;
adjacent device rows whose uploads meet, such as new rows and rows written from their first field to their last, as one run;
A drawing’s motion output runs its vertex code a second time, compiled with history.previously. To give rows that move a motion of their own, create the table with history: true. That code then reads last frame’s rows:
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",{
species:d.U32;
tint:d.Vec4f;
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.
capacity:4096,
history:true
Keeps last frame's rows, which code compiled with history.previously reads, such as a
drawing's motion. create then returns a HistoryTable, whose flush takes the frame's
encoder: it copies the rows before the frame's changes, so a row added this frame starts where
it is. Default false.
Uploads changes since the last flush and records into encoder the copies that keep last
frame's rows. Call once per frame before passes that read the table.
flush(
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);// copies last frame's rows aside, then lands the uploads
// ... kernels that move them, passes that draw them
});
The table is a HistoryTable, whose flush takes the frame’s encoder. Held as a plain Table, it throws without one. A row added this frame starts last frame where it is, so it doesn’t streak in from the origin.
Kernels and render pipelines read and write the current rows, as without history. Only code compiled with previously reads last frame’s.
Last frame’s rows follow the current ones in the same buffer, from slot capacity, so reading them binds no more buffers. Read rows through the getters, such as Enemy.position(i): Enemy.device.$[i] always reads the current rows.
Each flush copies the device rows across the span. It copies host rows only in the frame after an upload changed them.
A write that jumps a row, such as a teleport, reads as motion for a frame.
Slots never move. An id stays valid until its row is removed, and other buffers can refer to rows by slot. The span is the highest live slot plus one. add takes the lowest free slot to keep the span short. Kernels skip dead rows but still read memory across the span. A span with half its rows dead at random costs about twice as much as dense rows.
Play matching additions and removals in the table and a last-freed-first list. Kernels visit the full span, so a larger live fraction means less wasted work.
A dead instance costs as much vertex work as a live one, so don’t draw over the span. Live lists list each group’s live slots on the GPU every frame. They draw them indirectly at the cost of dense rows.
Live lists group rows by a field, such as species: d.u32. Groups share one capacity in any mix; rows with different fields belong in different tables.
To light and draw meshes, use a set of instances. A set is a table whose add takes a mesh, and its renderer draws the live rows of each mesh.
Shaders use the definition’s field getters, row(i), alive(i), span() and slotOf(id). The pipeline’s fills choose the table, here through root.pipe(enemies.inject()).pipe(live.inject()):
// A helper that takes a whole row: the dead sink into the floor
Without enemies.inject(), the pipeline fails at first use or in initAsync with an error that names the accessor and the inject() it needs, such as Nothing filled `Enemy_host`: ….
Prefer getters to row(i) in hot code. row(i) copies every field, and some compilers keep loads the shader doesn’t use.
host, device, row, alive, span and slotOf are taken; a field can’t use them.
One pipeline reads one table of a definition. To read two, such as a player and its enemies, define a second.
Device rows bind read-write in compute pipelines and read-only in render pipelines. WebGPU forbids read-write storage in vertex stages. Writing device rows in a render pipeline fails with readonly buffers cannot be mutated. Host rows are read-only everywhere.
A boid kernel reads neighbours while other threads move them. Some neighbours may have moved this step and others may not. First copy the fields neighbours need in a separate dispatch. Have the steering kernel read only that copy:
/** A fish as its neighbours see it. */
const
constSeen:d.WgslStruct<{
position:d.Vec3f;
velocity:d.Vec3f;
}>
A fish as its neighbours see it.
Seen=
import d
d.
struct<{
position:d.Vec3f;
velocity:d.Vec3f;
}>(props:{
position:d.Vec3f;
velocity:d.Vec3f;
}):d.WgslStruct<{
position:d.Vec3f;
velocity:d.Vec3f;
}>
export struct
Creates a struct schema that can be used to construct GPU buffers.
Ensures proper alignment and padding of properties (as opposed to a d.unstruct schema).
The order of members matches the passed in properties object.
A module that reads rows takes getters in its options. Pipe the table’s inject() into each getter. The module receives no table handle, and its factory defines shaders that call those getters:
interface
interfaceTrailsOptions
TrailsOptions{
readonly
TrailsOptions.capacity: number
capacity:number;
readonly
TrailsOptions.alive: (i:number)=> boolean
alive:(
i: number
i:number)=>boolean;
readonly
TrailsOptions.position: table.Getter<d.Vec3f>
position:
import table
table.
interfaceGetter<T>
Reads a field in a shader. Its type carries the field schema, so a u32 getter cannot pass for
an f32 getter.
Getter<
import d
d.
export Vec3f
Type of the d.vec3f object/function: vector data type schema/constructor
Vec3f>;
readonly
TrailsOptions.width: table.Getter<d.F32>
width:
import table
table.
interfaceGetter<T>
Reads a field in a shader. Its type carries the field schema, so a u32 getter cannot pass for
an f32 getter.
Getter<
import d
d.
export F32
32-bit float schema representing a single WGSL f32 value.
Applies a configuration transform to the slots and accessors this function reads,
as root.pipe does to a pipeline's.
pipe(
constrows:Inject
rows)});// species is a u32
Error ts(2322) ― Type 'Getter<U32>' is not assignable to type 'Getter<F32>'.
Types of property '[schemaOf]' are incompatible.
Type 'U32 | undefined' is not assignable to type 'F32 | undefined'.
Type 'U32' is not assignable to type 'F32'.
Types of property 'type' are incompatible.
Type '"u32"' is not assignable to type '"f32"'.
table.Getter keeps a field’s schema in its type, through pipe too, so a u32 field doesn’t pass for an f32, though both are numbers in TypeScript.
You can pass any shader function. (i) => Enemy.alive(i) && Enemy.species(i) === BOSS selects some rows. A procedural source needs no fill. Wrap your function to pipe a fill into it: tgpu.fn((i: number) => { … }).pipe(rows).
enemies.live() returns the table’s capacity, its filled alive getter and a span function. Spread them into options for the neighbour grid, live lists, picker or state machines: liveLists.create(root, { ...enemies.live(), draws }). Each dispatches only up to the span.
The CPU doesn’t know a device field’s value. To apply a relative change such as “take 5 damage”, send a command to the kernel that owns the field. A host-only table holds the commands:
const
constHits:table.TableDef<"Hits",{
victim:d.U32;
amount:d.F32;
},{}>
Hits=
import table
table.
functiondefine<"Hits",{
victim:d.U32;
amount:d.F32;
},{}>(name:"Hits",parts:{
host?:{
victim:d.U32;
amount:d.F32;
}|undefined;
device?:{}|undefined;
}):table.TableDef<"Hits",{
victim:d.U32;
amount: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('Hits',{
host?:{
victim:d.U32;
amount: d.F32;
}|undefined
host:{
victim:d.U32
victim:
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.
@exampleconst value = u32(); // 0
@exampleconst value = u32(7); // 7
@exampleconst value = u32(3.14); // 3
@exampleconst value = u32(-1); // 4294967295
@exampleconst value = u32(-3.1); // 0
u32,
amount:d.F32
amount:
import d
d.
constf32:d.F32
export f32
A schema that represents a 32-bit float value. (equivalent to f32 in WGSL)
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,
constHits:table.TableDef<"Hits",{
victim:d.U32;
amount:d.F32;
},{}>
Hits,{
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.
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.
@exampleconst value = u32(); // 0
@exampleconst value = u32(7); // 7
@exampleconst value = u32(3.14); // 3
@exampleconst value = u32(-1); // 4294967295
@exampleconst value = u32(-3.1); // 0
u32(0);
let h:number
h<
constHits:table.TableDef<"Hits",{
victim:d.U32;
amount:d.F32;
},{}>
Hits.
span: TgpuGenericFn
()=> number
Returns the highest live slot plus one.
span();
let h:number
h++){
if(
constHits:table.TableDef<"Hits",{
victim:d.U32;
amount:d.F32;
},{}>
Hits.
alive: TgpuGenericFn
(i:number)=> boolean
Tests whether slot i holds a row. Returns false beyond the span.
alive(
let h:number
h)&&
constEnemy:table.TableDef<"Enemy",{
species:d.U32;
},{
position:d.Vec3f;
health:d.F32;
}>
Enemy.
slotOf: TgpuGenericFn
(id:number)=> number
Extracts a slot from a row id, such as an id stored in a host field. Does not check the
generation.
slotOf(
constHits:table.TableDef<"Hits",{
victim:d.U32;
amount:d.F32;
},{}>
Hits.
victim:table.Getter
(i:number)=> number
victim(
let h:number
h))===
consti:number
i)
constenemy:{
position:d.v3f;
health:number;
}
enemy.
health: number
health-=
constHits:table.TableDef<"Hits",{
victim:d.U32;
amount:d.F32;
},{}>
Hits.
amount:table.Getter
(i:number)=> number
amount(
let h:number
h);
}
});
Use one thread per row to scan the commands. A thread per command can lose an update when two commands hit the same row, and WGSL has no float atomics.
The kernel does rows × commands checks. This suits a few hundred commands per frame. For more, sort them by target or use fixed-point atomics.
Rows can’t be added on the GPU; particles keep a ring of their own.
No struct-of-arrays layout, and no packed fields such as rgba8 in a u32.
A table uses three of the eight storage bindings WebGPU guarantees per stage, for host rows, device rows and live bits.
slotOf(id) reads the id mask from the uniform the table binds for its span, so a shader that reads ids binds it too.
Two definitions given one name share their ids’ brand.
A device part of more than 32 fields marks its 32nd and later fields with one bit, so a write to any of them uploads to the end of the row.
The instances renderer passes a whole row to each of an instance kind’s hooks per vertex and per fragment. Prefer field getters in hot shader code to avoid unnecessary loads.