Skip to content

@typegpu/kittable

Tables

Per-object rows with separate buffers for CPU and GPU writes, so uploads preserve kernel updates.

kit.table

module

table.create(root, def, { capacity, history, … }) → HistoryTable

table.create(root, def, { capacity, … }) → Table

table.rows(def, part, capacity) → Rows

table.define(name, parts) → TableDef

rootborrows
the root
deftakes
the definition its shaders were written against, from table.define
HistoryTablereturns
a table you destroy
Tablereturns
a table you destroy
deftakes
the definition, from table.define
parttakes
'host' or 'device'
capacitytakes
how many rows the array holds
Rowsreturns
an array schema of the part's rows
nametakes
a label for ids and WGSL
partstakes
the host fields the CPU writes and the device fields the GPU writes
TableDefreturns
a definition whose getters shaders read and that table.create allocates
types
PropsRowsRowIdGetterGettersTableDefAnyTableDefRowOfFieldInputValuesHostFieldDeviceFieldFieldsLiveTableHistoryTable
Optionscreate · 3 fields
capacity

number

Sets the table capacity for its lifetime.

defaults?

Values<T>= none

Sets values for fields omitted by add.

history?

boolean= false

Keeps last frame's rows, which code compiled with history.previously reads, such as a drawing's motion.

records
  • flushencoder

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
const Enemy: 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
.
function define<"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
.
const u32: 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.

@example const value = u32(); // 0

@example const value = u32(7); // 7

@example const value = u32(3.14); // 3

@example const value = u32(-1); // 4294967295

@example const value = u32(-3.1); // 0

u32
,
tint: d.Vec4f
tint
:
import d
d
.
const vec4f: d.Vec4f
vec4f
,
radius: d.F32
radius
:
import d
d
.
const f32: 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.

@example const value = f32(); // 0

@example const value = f32(1.23); // 1.23

@example const 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
.
const vec3f: d.Vec3f
vec3f
,
velocity: d.Vec3f
velocity
:
import d
d
.
const vec3f: d.Vec3f
vec3f
,
health: d.F32
health
:
import d
d
.
const f32: 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.

@example const value = f32(); // 0

@example const value = f32(1.23); // 1.23

@example const value = f32(true); // 1

f32
}, // by kernels
});
const
const enemies: 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
.
function create<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;
}>> (+1 overload)

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
(
const root: TgpuRoot
root
,
const Enemy: 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,
Options<TableDef<"Enemy", { species: U32; tint: Vec4f; radius: F32; }, { position: Vec3f; velocity: Vec3f; health: F32; }>>.defaults?: table.Values<table.TableDef<"Enemy", {
species: d.U32;
tint: d.Vec4f;
radius: d.F32;
}, {
position: d.Vec3f;
velocity: d.Vec3f;
health: d.F32;
}>> | undefined

Sets values for fields omitted by add. Without defaults, every field starts at zero. Default none.

defaults
: {
tint?: ArrayLike<number> | d.InferInput<d.Vec4f> | undefined
tint
: [1, 1, 1, 1],
radius?: number | undefined
radius
: 0.5,
health?: number | undefined
health
: 10 },
});
const
const id: table.RowId<"Enemy">
id
=
const enemies: table.Table<table.TableDef<"Enemy", {
species: d.U32;
tint: d.Vec4f;
radius: d.F32;
}, {
position: d.Vec3f;
velocity: d.Vec3f;
health: d.F32;
}>>
enemies
.
Table<TableDef<"Enemy", { species: U32; tint: Vec4f; radius: F32; }, { position: Vec3f; velocity: Vec3f; health: F32; }>>.add(values?: table.Values<table.TableDef<"Enemy", {
species: d.U32;
tint: d.Vec4f;
radius: d.F32;
}, {
position: d.Vec3f;
velocity: d.Vec3f;
health: d.F32;
}>> | undefined): table.RowId<"Enemy">

Adds a row in the lowest free slot. Uses defaults for omitted fields. Throws when the table is full.

add
({
species?: number | undefined
species
: 2,
position?: ArrayLike<number> | d.InferInput<d.Vec3f> | undefined
position
: [4, 0, 1] });
const enemies: table.Table<table.TableDef<"Enemy", {
species: d.U32;
tint: d.Vec4f;
radius: d.F32;
}, {
position: d.Vec3f;
velocity: d.Vec3f;
health: d.F32;
}>>
enemies
.
Table<TableDef<"Enemy", { species: U32; tint: Vec4f; radius: F32; }, { position: Vec3f; velocity: Vec3f; health: F32; }>>.fields: table.Fields<table.TableDef<"Enemy", {
species: d.U32;
tint: d.Vec4f;
radius: d.F32;
}, {
position: d.Vec3f;
velocity: d.Vec3f;
health: d.F32;
}>>

Provides one handle per field. Keep a handle outside a loop instead of reading fields.x inside it.

fields
.
tint: table.HostField<d.Vec4f, "Enemy">
tint
.
function set(id: table.RowId<"Enemy">, x: number, y: number, z: number, w: number): void
set
(
const id: table.RowId<"Enemy">
id
, 1, 0, 0, 1);
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 enemies: table.Table<table.TableDef<"Enemy", {
species: d.U32;
tint: d.Vec4f;
radius: d.F32;
}, {
position: d.Vec3f;
velocity: d.Vec3f;
health: d.F32;
}>>
enemies
.
Table<TableDef<"Enemy", { species: U32; tint: Vec4f; radius: F32; }, { position: Vec3f; velocity: Vec3f; health: F32; }>>.flush(encoder?: TgpuCommandEncoder): void

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
// ... passes
});
const enemies: table.Table<table.TableDef<"Enemy", {
species: d.U32;
tint: d.Vec4f;
radius: d.F32;
}, {
position: d.Vec3f;
velocity: d.Vec3f;
health: d.F32;
}>>
enemies
.
Table<TableDef<"Enemy", { species: U32; tint: Vec4f; radius: F32; }, { position: Vec3f; velocity: Vec3f; health: F32; }>>.remove(id: table.RowId<"Enemy">): void

Frees the row slot and makes its id stale.

remove
(
const id: table.RowId<"Enemy">
id
);
  • 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:

Capacity Generation bits Wraps after
8 29 52 days
300 23 19 hours
4096 20 2.4 hours
2^20 12 34 seconds

enemies.fields has a handle per field, which writes the CPU copy at a fixed offset:

const
const tint: table.HostField<d.Vec4f, "Enemy">
tint
=
const enemies: table.Table<table.TableDef<"Enemy", {
species: d.U32;
tint: d.Vec4f;
radius: d.F32;
}, {
position: d.Vec3f;
velocity: d.Vec3f;
health: d.F32;
}>>
enemies
.
Table<TableDef<"Enemy", { species: U32; tint: Vec4f; radius: F32; }, { position: Vec3f; velocity: Vec3f; health: F32; }>>.fields: table.Fields<table.TableDef<"Enemy", {
species: d.U32;
tint: d.Vec4f;
radius: d.F32;
}, {
position: d.Vec3f;
velocity: d.Vec3f;
health: d.F32;
}>>

Provides one handle per field. Keep a handle outside a loop instead of reading fields.x inside it.

fields
.
tint: table.HostField<d.Vec4f, "Enemy">
tint
; // keep it outside the loop
for (const
const id: table.RowId<"Enemy">
id
of
const hit: table.RowId<"Enemy">[]
hit
)
const tint: table.HostField<d.Vec4f, "Enemy">
tint
.
function set(id: table.RowId<"Enemy">, x: number, y: number, z: number, w: number): void
set
(
const id: table.RowId<"Enemy">
id
, 1, 0, 0, 1); // components: nothing allocated
const tint: table.HostField<d.Vec4f, "Enemy">
tint
.
FieldWrite<Vec4f, "Enemy">.write(id: table.RowId<"Enemy">, value: ArrayLike<number> | d.InferInput<d.Vec4f>): void

Writes the whole field. A vector takes any array of numbers and reads its first components. Reuse typed inputs to avoid allocating.

write
(
const id: table.RowId<"Enemy">
id
, [1, 0, 0, 1]); // allocates the tuple
const tint: table.HostField<d.Vec4f, "Enemy">
tint
.
get<d.v4f>(id: table.RowId<"Enemy">, out: d.v4f): d.v4f

Copies the field into out, a vector or an array of numbers, and returns out.

get
(
const id: table.RowId<"Enemy">
id
,
import d
d
.
const vec4f: d.Vec4f
() => d.v4f (+9 overloads)
vec4f
()); // host fields read back from the CPU copy
const enemies: table.Table<table.TableDef<"Enemy", {
species: d.U32;
tint: d.Vec4f;
radius: d.F32;
}, {
position: d.Vec3f;
velocity: d.Vec3f;
health: d.F32;
}>>
enemies
.
Table<TableDef<"Enemy", { species: U32; tint: Vec4f; radius: F32; }, { position: Vec3f; velocity: Vec3f; health: F32; }>>.write(id: table.RowId<"Enemy">, values: table.Values<table.TableDef<"Enemy", {
species: d.U32;
tint: d.Vec4f;
radius: d.F32;
}, {
position: d.Vec3f;
velocity: d.Vec3f;
health: d.F32;
}>>): void

Writes values to several fields. Device field writes overwrite kernel results at flush.

write
(
const id: table.RowId<"Enemy">
id
, {
tint?: ArrayLike<number> | d.InferInput<d.Vec4f> | undefined
tint
: [1, 0, 0, 1],
health?: number | undefined
health
: 10 }); // several fields, both parts
  • 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;
  • the live bits, and the span when it changed.

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:

const
const enemies: table.HistoryTable<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
.
function create<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;
}>> & {
readonly history: true;
}): table.HistoryTable<...> (+1 overload)

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
(
const root: TgpuRoot
root
,
const Enemy: 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.

history
: true });
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 enemies: table.HistoryTable<table.TableDef<"Enemy", {
species: d.U32;
tint: d.Vec4f;
radius: d.F32;
}, {
position: d.Vec3f;
velocity: d.Vec3f;
health: d.F32;
}>>
enemies
.
HistoryTable<TableDef<"Enemy", { species: U32; tint: Vec4f; radius: F32; }, { position: Vec3f; velocity: Vec3f; health: F32; }>>.flush(encoder: TgpuCommandEncoder): void

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
const
const sinkOf: (enemy: RowOf<typeof Enemy>) => number
sinkOf
= (
enemy: {
species: number;
tint: d.v4f;
radius: number;
position: d.v3f;
velocity: d.v3f;
health: number;
}
enemy
:
type RowOf<T extends AnyTableDef> = { [K in keyof InferGPURecord<HostOf<T> & DeviceOf<T>>]: InferGPURecord<HostOf<T> & DeviceOf<T>>[K]; }

Gives the whole row type as read by shaders, for helpers that take a row value.

RowOf
<typeof
const Enemy: table.TableDef<"Enemy", {
species: d.U32;
tint: d.Vec4f;
radius: d.F32;
}, {
position: d.Vec3f;
velocity: d.Vec3f;
health: d.F32;
}>
Enemy
>) => {
'use gpu';
return
import std
std
.
function select(f: number, t: number, cond: boolean): number (+2 overloads)
export select

Returns t if cond is true, and f otherwise. Component-wise if cond is a vector.

@example select(1, 2, false) // returns 1 select(1, 2, true) // returns 2 select(vec2i(1, 2), vec2i(3, 4), true) // returns vec2i(3, 4) select(vec2i(1, 2), vec2i(3, 4), vec2b(false, true)) // returns vec2i(1, 4)

select
(
import d
d
.
function f32(v?: number | boolean): number
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.

@example const value = f32(); // 0

@example const value = f32(1.23); // 1.23

@example const value = f32(true); // 1

f32
(0),
enemy: {
species: number;
tint: d.v4f;
radius: number;
position: d.v3f;
velocity: d.v3f;
health: number;
}
enemy
.
radius: number
radius
* 2,
enemy: {
species: number;
tint: d.v4f;
radius: number;
position: d.v3f;
velocity: d.v3f;
health: number;
}
enemy
.
health: number
health
<= 0);
};
const
const vertex: TgpuVertexFn<{}, {}>
vertex
=
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
.
vertexFn: <{
vertexIndex: d.BuiltinVertexIndex;
instanceIndex: d.BuiltinInstanceIndex;
}, {
position: d.BuiltinPosition;
}>(options: {
in: {
vertexIndex: d.BuiltinVertexIndex;
instanceIndex: d.BuiltinInstanceIndex;
};
out: {
position: d.BuiltinPosition;
};
}) => TgpuVertexFnShell<{
vertexIndex: d.BuiltinVertexIndex;
instanceIndex: d.BuiltinInstanceIndex;
}, {
position: d.BuiltinPosition;
}> (+1 overload)
vertexFn
({
in: {
vertexIndex: d.BuiltinVertexIndex;
instanceIndex: d.BuiltinInstanceIndex;
}
in
: {
vertexIndex: d.BuiltinVertexIndex
vertexIndex
:
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
.
vertexIndex: d.BuiltinVertexIndex
vertexIndex
,
instanceIndex: d.BuiltinInstanceIndex
instanceIndex
:
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
.
instanceIndex: d.BuiltinInstanceIndex
instanceIndex
},
out: {
position: d.BuiltinPosition;
}
out
: {
position: d.BuiltinPosition
position
:
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
.
position: d.BuiltinPosition
position
},
})(({
vertexIndex: number
vertexIndex
,
instanceIndex: number
instanceIndex
}) => {
'use gpu';
const
const slot: number
slot
=
const live: liveLists.LiveLists
live
.
LiveLists.slot: (group: number, i: number) => number

Returns the slot of live row i in group. Pass group as a captured constant.

slot
(
const SKELETON: 0
SKELETON
,
instanceIndex: number
instanceIndex
);
const
const center: d.v3f
center
=
const Enemy: table.TableDef<"Enemy", {
species: d.U32;
tint: d.Vec4f;
radius: d.F32;
}, {
position: d.Vec3f;
velocity: d.Vec3f;
health: d.F32;
}>
Enemy
.
position: table.Getter
(i: number) => d.v3f
position
(
const slot: number
slot
); // loads one field
const
const enemy: {
species: number;
tint: d.v4f;
radius: number;
position: d.v3f;
velocity: d.v3f;
health: number;
}
enemy
=
const Enemy: table.TableDef<"Enemy", {
species: d.U32;
tint: d.Vec4f;
radius: d.F32;
}, {
position: d.Vec3f;
velocity: d.Vec3f;
health: d.F32;
}>
Enemy
.
row: TgpuGenericFn
(i: number) => {
species: number;
tint: d.v4f;
radius: number;
position: d.v3f;
velocity: d.v3f;
health: number;
}

Returns a copy of the whole row, including every field, for helpers that take a row value.

row
(
const slot: number
slot
); // the whole row
const
const corner: d.v3f
corner
=
const cube: meshes.Geometry<d.WgslStruct<{
position: d.Vec3f;
normal: d.Vec3f;
uv: d.Vec2f;
}>>
cube
.
Geometry<WgslStruct<{ position: Vec3f; normal: Vec3f; uv: Vec2f; }>>.vertexAt(this: void, i: number): {
position: d.v3f;
normal: d.v3f;
uv: d.v2f;
}
vertexAt
(
vertexIndex: number
vertexIndex
).
position: d.v3f
position
*
const enemy: {
species: number;
tint: d.v4f;
radius: number;
position: d.v3f;
velocity: d.v3f;
health: number;
}
enemy
.
radius: number
radius
;
const
const world: d.v3f
world
=
const center: d.v3f
center
+
const corner: d.v3f
corner
-
import d
d
.
const vec3f: d.Vec3f
(x: number, y: number, z: number) => d.v3f (+5 overloads)
vec3f
(0,
const sinkOf: (enemy: RowOf<typeof Enemy>) => number
sinkOf
(
const enemy: {
species: number;
tint: d.v4f;
radius: number;
position: d.v3f;
velocity: d.v3f;
health: number;
}
enemy
), 0);
return {
position: d.v4f
position
:
const viewProjection: TgpuUniform<d.Mat4x4f>
viewProjection
.
TgpuUniform<Mat4x4f>.$: d.m4x4f
$
*
import d
d
.
const vec4f: d.Vec4f
(v0: AnyNumericVec3Instance, w: number) => d.v4f (+9 overloads)
vec4f
(
const world: d.v3f
world
, 1) };
});
  • 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.

A kernel writes through a reference from Enemy.device, and reads through the getters:

const
const STEP: number
STEP
= 1 / 60;
const
const crowd: TgpuComputeFn<{
gid: d.BuiltinGlobalInvocationId;
}>
crowd
=
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 Enemy: table.TableDef<"Enemy", {
species: d.U32;
tint: d.Vec4f;
radius: d.F32;
}, {
position: d.Vec3f;
velocity: d.Vec3f;
health: d.F32;
}>
Enemy
.
alive: TgpuGenericFn
(i: number) => boolean

Tests whether slot i holds a row. Returns false beyond the span.

alive
(
const i: number
i
)) return; // also false past the span
const
const speed: number
speed
= 2 /
const Enemy: table.TableDef<"Enemy", {
species: d.U32;
tint: d.Vec4f;
radius: d.F32;
}, {
position: d.Vec3f;
velocity: d.Vec3f;
health: d.F32;
}>
Enemy
.
radius: table.Getter
(i: number) => number
radius
(
const i: number
i
); // a host field: small enemies are fast
const
const enemy: {
position: d.v3f;
velocity: d.v3f;
health: number;
}
enemy
=
const Enemy: table.TableDef<"Enemy", {
species: d.U32;
tint: d.Vec4f;
radius: d.F32;
}, {
position: d.Vec3f;
velocity: d.Vec3f;
health: d.F32;
}>
Enemy
.
device: TgpuAccessor<table.Rows<{
position: d.Vec3f;
velocity: d.Vec3f;
health: d.F32;
}>>

Provides GPU-written rows with read-write access in compute pipelines and read-only access in render pipelines.

device
.
TgpuAccessor<Rows<{ position: Vec3f; velocity: Vec3f; health: F32; }>>.$: {
position: d.v3f;
velocity: d.v3f;
health: number;
}[]
$
[
const i: number
i
]; // a reference: writes go to the buffer
const enemy: {
position: d.v3f;
velocity: d.v3f;
health: number;
}
enemy
.
velocity: d.v3f
velocity
=
import std
std
.
mix<d.v3f>(e1: d.v3f, e2: d.v3f, e3: number): d.v3f (+2 overloads)
export mix
mix
(
const enemy: {
position: d.v3f;
velocity: d.v3f;
health: number;
}
enemy
.
velocity: d.v3f
velocity
,
const toPlayer: (position: d.v3f) => d.v3f
toPlayer
(
const enemy: {
position: d.v3f;
velocity: d.v3f;
health: number;
}
enemy
.
position: d.v3f
position
) *
const speed: number
speed
, 0.1);
const enemy: {
position: d.v3f;
velocity: d.v3f;
health: number;
}
enemy
.
position: d.v3f
position
+=
const enemy: {
position: d.v3f;
velocity: d.v3f;
health: number;
}
enemy
.
velocity: d.v3f
velocity
*
const STEP: number
STEP
;
});

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
const Seen: 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.

@example const CircleStruct = d.struct({ radius: d.f32, pos: d.vec3f });

@param ― props Record with string keys and TgpuData values, each entry describing one struct member.

struct
({
position: d.Vec3f
position
:
import d
d
.
const vec3f: d.Vec3f
vec3f
,
velocity: d.Vec3f
velocity
:
import d
d
.
const vec3f: d.Vec3f
vec3f
});
const
const seen: TgpuMutable<d.WgslArray<d.WgslStruct<{
position: d.Vec3f;
velocity: d.Vec3f;
}>>>
seen
=
const root: TgpuRoot
root
.
TgpuRoot.createMutable<d.WgslArray<d.WgslStruct<{
position: d.Vec3f;
velocity: d.Vec3f;
}>>>(typeSchema: d.WgslArray<d.WgslStruct<{
position: d.Vec3f;
velocity: d.Vec3f;
}>>, initial?: ((buffer: TgpuBuffer<NoInfer<d.WgslArray<d.WgslStruct<{
position: d.Vec3f;
velocity: d.Vec3f;
}>>>>) => void) | d.InferInput<NoInfer<d.WgslArray<d.WgslStruct<{
position: d.Vec3f;
velocity: d.Vec3f;
}>>>> | undefined): TgpuMutable<d.WgslArray<d.WgslStruct<{
position: d.Vec3f;
velocity: d.Vec3f;
}>>> (+1 overload)

Allocates memory on the GPU, allows passing data between host and shader. Can be mutated in-place on the GPU. For a general-purpose buffer, use

TgpuRoot.createBuffer

.

@param ― typeSchema The type of data that this buffer will hold.

@param ― initial Either initial value of the buffer, or an initializer to execute on the mapped buffer. (optional)

createMutable
(
import d
d
.
arrayOf<d.WgslStruct<{
position: d.Vec3f;
velocity: d.Vec3f;
}>>(elementType: d.WgslStruct<{
position: d.Vec3f;
velocity: d.Vec3f;
}>, elementCount: number): d.WgslArray<d.WgslStruct<{
position: d.Vec3f;
velocity: d.Vec3f;
}>> (+2 overloads)
export arrayOf

@location ― . Wrap align/size in a struct instead, e.g. d.arrayOf(d.struct({ value: d.align(16, d.u32) }), n).

arrayOf
(
const Seen: d.WgslStruct<{
position: d.Vec3f;
velocity: d.Vec3f;
}>

A fish as its neighbours see it.

Seen
, 4096));
const
const snapshot: TgpuComputeFn<{
gid: d.BuiltinGlobalInvocationId;
}>
snapshot
=
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 Fish: table.TableDef<"Fish", {
species: d.U32;
}, {
position: d.Vec3f;
velocity: d.Vec3f;
}>
Fish
.
alive: TgpuGenericFn
(i: number) => boolean

Tests whether slot i holds a row. Returns false beyond the span.

alive
(
const i: number
i
)) return;
const seen: TgpuMutable<d.WgslArray<d.WgslStruct<{
position: d.Vec3f;
velocity: d.Vec3f;
}>>>
seen
.
TgpuMutable<WgslArray<WgslStruct<{ position: Vec3f; velocity: Vec3f; }>>>.$: {
position: d.v3f;
velocity: d.v3f;
}[]
$
[
const i: number
i
] =
const Seen: d.WgslStruct
(props?: {
position: d.v3f;
velocity: d.v3f;
} | undefined) => {
position: d.v3f;
velocity: d.v3f;
}

A fish as its neighbours see it.

Seen
({
position: d.v3f
position
:
const Fish: table.TableDef<"Fish", {
species: d.U32;
}, {
position: d.Vec3f;
velocity: d.Vec3f;
}>
Fish
.
position: table.Getter
(i: number) => d.v3f
position
(
const i: number
i
),
velocity: d.v3f
velocity
:
const Fish: table.TableDef<"Fish", {
species: d.U32;
}, {
position: d.Vec3f;
velocity: d.Vec3f;
}>
Fish
.
velocity: table.Getter
(i: number) => d.v3f
velocity
(
const i: number
i
) });
});
// the steering kernel reads seen.$[k] and writes only Fish.device.$[i]
  • Two dispatches in one compute pass run in order, so the copy is whole before steering starts.
  • The copy holds only what neighbours read. In the fish tank, the neighbour grid’s sort writes it, in sorted order.

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
interface TrailsOptions
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
.
interface Getter<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
.
interface Getter<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.

F32
>;
}
export function
function createTrails(root: TgpuRoot, options: TrailsOptions): TgpuComputePipeline
createTrails
(
root: TgpuRoot
root
:
(alias) interface TgpuRoot
import TgpuRoot
TgpuRoot
,
options: TrailsOptions
options
:
interface TrailsOptions
TrailsOptions
) {
const {
const capacity: number
capacity
,
const alive: (i: number) => boolean
alive
,
const position: table.Getter<d.Vec3f>
position
,
const width: table.Getter<d.F32>
width
} =
options: TrailsOptions
options
;
const
const step: TgpuComputeFn<{
gid: d.BuiltinGlobalInvocationId;
}>
step
=
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';
if (
gid: d.v3u
gid
.
v3u.x: number
x
>=
const capacity: number
capacity
|| !
const alive: (i: number) => boolean
alive
(
gid: d.v3u
gid
.
v3u.x: number
x
)) return;
const
const p: d.v3f
p
=
const position: table.Getter
(i: number) => d.v3f
position
(
gid: d.v3u
gid
.
v3u.x: number
x
);
const
const w: number
w
=
const width: table.Getter
(i: number) => number
width
(
gid: d.v3u
gid
.
v3u.x: number
x
);
// ..
});
return
root: TgpuRoot
root
.
WithBinding.createComputePipeline<{
gid: d.BuiltinGlobalInvocationId;
}>(descriptor: TgpuComputePipeline.Descriptor<{
gid: d.BuiltinGlobalInvocationId;
}>): TgpuComputePipeline
createComputePipeline
({
compute: TgpuComputeFn<{
gid: d.BuiltinGlobalInvocationId;
}>
compute
:
const step: TgpuComputeFn<{
gid: d.BuiltinGlobalInvocationId;
}>
step
});
}
const
const rows: Inject
rows
=
const enemies: table.Table<table.TableDef<"Enemy", {
species: d.U32;
size: d.F32;
}, {
position: d.Vec3f;
velocity: d.Vec3f;
}>>
enemies
.
Table<TableDef<"Enemy", { species: U32; size: F32; }, { position: Vec3f; velocity: Vec3f; }>>.inject(): Inject

Fills the definition's accessors with this table's buffers.

inject
();
const
const trails: {
position: table.Getter<d.Vec3f>;
capacity: number;
alive: (i: number) => boolean;
span?: () => number;
}
trails
= { ...
const enemies: table.Table<table.TableDef<"Enemy", {
species: d.U32;
size: d.F32;
}, {
position: d.Vec3f;
velocity: d.Vec3f;
}>>
enemies
.
Table<TableDef<"Enemy", { species: U32; size: F32; }, { position: Vec3f; velocity: Vec3f; }>>.live(): table.Live

Returns the capacity and live test for live lists, a grid, a picker or state machines. Use liveLists.create(root, { ...coins.live(), draws }).

live
(),
position: table.Getter<d.Vec3f>
position
:
const Enemy: table.TableDef<"Enemy", {
species: d.U32;
size: d.F32;
}, {
position: d.Vec3f;
velocity: d.Vec3f;
}>
Enemy
.
position: table.Getter<d.Vec3f>
position
.
Getter<Vec3f>.pipe(transform: (cfg: Configurable) => Configurable): table.Getter<d.Vec3f>

Applies a configuration transform to the slots and accessors this function reads, as root.pipe does to a pipeline's.

pipe
(
const rows: Inject
rows
) };
function createTrails(root: TgpuRoot, options: TrailsOptions): TgpuComputePipeline
createTrails
(
const root: TgpuRoot
root
, { ...
const trails: {
position: table.Getter<d.Vec3f>;
capacity: number;
alive: (i: number) => boolean;
span?: () => number;
}
trails
,
TrailsOptions.width: table.Getter<d.F32>
width
:
const Enemy: table.TableDef<"Enemy", {
species: d.U32;
size: d.F32;
}, {
position: d.Vec3f;
velocity: d.Vec3f;
}>
Enemy
.
size: table.Getter<d.F32>
size
.
Getter<F32>.pipe(transform: (cfg: Configurable) => Configurable): table.Getter<d.F32>

Applies a configuration transform to the slots and accessors this function reads, as root.pipe does to a pipeline's.

pipe
(
const rows: Inject
rows
) });
function createTrails(root: TgpuRoot, options: TrailsOptions): TgpuComputePipeline
createTrails
(
const root: TgpuRoot
root
, { ...
const trails: {
position: table.Getter<d.Vec3f>;
capacity: number;
alive: (i: number) => boolean;
span?: () => number;
}
trails
, width:
const Enemy: table.TableDef<"Enemy", {
species: d.U32;
size: d.F32;
}, {
position: d.Vec3f;
velocity: d.Vec3f;
}>
Enemy
.
species: table.Getter<d.U32>
species
.
Getter<U32>.pipe(transform: (cfg: Configurable) => Configurable): table.Getter<d.U32>

Applies a configuration transform to the slots and accessors this function reads, as root.pipe does to a pipeline's.

pipe
(
const rows: 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
const Hits: table.TableDef<"Hits", {
victim: d.U32;
amount: d.F32;
}, {}>
Hits
=
import table
table
.
function define<"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
.
const u32: 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.

@example const value = u32(); // 0

@example const value = u32(7); // 7

@example const value = u32(3.14); // 3

@example const value = u32(-1); // 4294967295

@example const value = u32(-3.1); // 0

u32
,
amount: d.F32
amount
:
import d
d
.
const f32: 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.

@example const value = f32(); // 0

@example const value = f32(1.23); // 1.23

@example const value = f32(true); // 1

f32
} });
const
const hits: table.Table<table.TableDef<"Hits", {
victim: d.U32;
amount: d.F32;
}, {}>>
hits
=
import table
table
.
function create<table.TableDef<"Hits", {
victim: d.U32;
amount: d.F32;
}, {}>>(root: TgpuRoot, def: table.TableDef<"Hits", {
victim: d.U32;
amount: d.F32;
}, {}>, options: table.Options<table.TableDef<"Hits", {
victim: d.U32;
amount: d.F32;
}, {}>>): table.Table<table.TableDef<"Hits", {
victim: d.U32;
amount: d.F32;
}, {}>> (+1 overload)

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
(
const root: TgpuRoot
root
,
const Hits: 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.

capacity
: 256 });
const hits: table.Table<table.TableDef<"Hits", {
victim: d.U32;
amount: d.F32;
}, {}>>
hits
.
Table<TableDef<"Hits", { victim: U32; amount: F32; }, {}>>.add(values?: table.Values<table.TableDef<"Hits", {
victim: d.U32;
amount: d.F32;
}, {}>> | undefined): table.RowId<"Hits">

Adds a row in the lowest free slot. Uses defaults for omitted fields. Throws when the table is full.

add
({
victim?: number | undefined
victim
:
const id: table.RowId<"Enemy">
id
,
amount?: number | undefined
amount
: 5 }); // an enemy's id, stored as a u32
// after the frame: hits.clear()
// The kernel that owns `health`, created from root.pipe(enemies.inject()).pipe(hits.inject()):
const
const damage: TgpuComputeFn<{
gid: d.BuiltinGlobalInvocationId;
}>
damage
=
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 Enemy: table.TableDef<"Enemy", {
species: d.U32;
}, {
position: d.Vec3f;
health: d.F32;
}>
Enemy
.
alive: TgpuGenericFn
(i: number) => boolean

Tests whether slot i holds a row. Returns false beyond the span.

alive
(
const i: number
i
)) return;
const
const enemy: {
position: d.v3f;
health: number;
}
enemy
=
const Enemy: table.TableDef<"Enemy", {
species: d.U32;
}, {
position: d.Vec3f;
health: d.F32;
}>
Enemy
.
device: TgpuAccessor<table.Rows<{
position: d.Vec3f;
health: d.F32;
}>>

Provides GPU-written rows with read-write access in compute pipelines and read-only access in render pipelines.

device
.
TgpuAccessor<Rows<{ position: Vec3f; health: F32; }>>.$: {
position: d.v3f;
health: number;
}[]
$
[
const i: number
i
];
for (let
let h: number
h
=
import d
d
.
function u32(v?: number | boolean): number
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.

@example const value = u32(); // 0

@example const value = u32(7); // 7

@example const value = u32(3.14); // 3

@example const value = u32(-1); // 4294967295

@example const value = u32(-3.1); // 0

u32
(0);
let h: number
h
<
const Hits: 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 (
const Hits: 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
) &&
const Enemy: 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
(
const Hits: table.TableDef<"Hits", {
victim: d.U32;
amount: d.F32;
}, {}>
Hits
.
victim: table.Getter
(i: number) => number
victim
(
let h: number
h
)) ===
const i: number
i
)
const enemy: {
position: d.v3f;
health: number;
}
enemy
.
health: number
health
-=
const Hits: 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.