Click a slot to change its kind or free it. Each kind starts at a fixed offset, encoded in separate pipelines or supplied through firstInstance so one pipeline can draw every kind.
A table keeps rows in stable slots, leaving holes when rows are removed. Drawing over every slot still does vertex work for those holes. With half the slots dead, the pass takes 1.5 to 2 times as long.
Live lists list each group’s live slots on the GPU every frame and write each group’s instance count into an indirect draw, so the CPU never counts them.
import{
import liveLists
liveLists}from'@typegpu/kit';
const
constBlock:table.TableDef<"Block",{
shape:d.U32;
position:d.Vec3f;
},{}>
Block=
import table
table.
functiondefine<"Block",{
shape:d.U32;
position:d.Vec3f;
},{}>(name:"Block",parts:{
host?:{
shape:d.U32;
position:d.Vec3f;
}|undefined;
device?:{}|undefined;
}):table.TableDef<"Block",{
shape:d.U32;
position:d.Vec3f;
},{}>
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('Block',{
host?:{
shape:d.U32;
position: d.Vec3f;
}|undefined
host:{
shape:d.U32
shape:
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.
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,
constBlock:table.TableDef<"Block",{
shape:d.U32;
position:d.Vec3f;
},{}>
Block,{
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.
Creates a list of live slots and an indirect draw per group. Each group reserves capacity
slots. A shader finds rows with live.slot(group, i) using a captured group index. With
firstInstance, it can use live.instanceSlot(instanceIndex) instead. Throws if the groups,
draws or workgroup size are invalid, the slot buffer exceeds maxStorageBufferBindingSize, or
firstInstance lacks its required feature.
@param ― options.alive whether a slot holds a live row, carrying the fills of what it reads, such
as a table's from rows.live(), spread into the options with its capacity
@param ― options.draws one draw per group
@returns ― live lists you run every frame and destroy
Maps a live slot to its group in a shader, such as with a table field's getter. Skips rows
whose group is out of range. Default every live row belongs to group 0.
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();
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.
Rewrites the live slots and instance counts. Record it after the table's flush.
compact(
constcompute:TgpuComputePass
compute);// each group's live slots, then its instance count
constcompute:TgpuComputePass
compute.
TgpuComputePass.end(): void
Completes the recording of this compute pass
end();
const
constpass:TgpuRenderPass
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.
Draws group into the pass with the pipeline set on it. Uses an indexed draw when the group's
draw is indexed.
draw(
constpass:TgpuRenderPass
pass,
constSOLID:0
SOLID);// drawIndirect, or drawIndexedIndirect for an indexed draw
constpass:TgpuRenderPass
pass.
TgpuRenderPass.end(): void
Completes the recording of this render pass
end();
});
The vertex shader reads its row’s slot with live.slot(SOLID, instanceIndex), in a pipeline created from root.pipe(blocks.inject()).pipe(live.inject()). The pipeline captures SOLID as a constant, including the start of that group’s list, so the draw keeps firstInstance at 0. This works on every device and requires one pipeline per group.
Group by mesh or pipeline, such as fish species. Rows that share both stay one group and branch on a field, as the platformer’s walkers and bats do.
Without group, every live row is in group 0; rows whose group is out of range aren’t drawn.
setDraw(group, draw) swaps a group’s draw, such as for a new mesh, and keeps its count.
blocks.live() returns the table’s capacity, its alive getter with inject() piped in and its span. Without alive, every slot is live, and the types refuse a span.
alive can be any shader function with fills for the resources it reads. (i) => Block.alive(i) && Block.shape(i) !== HIDDEN selects some table rows. Particles test their ages. Wrap a function of your own in tgpu.fn to pipe it. The neighbour grid takes its alive, position and itemOf the same way.
compact tests every slot up to the span it reads as it records, or capacity without one. A row added after compact records shows from the next frame.
Drawing through the list costs the same as drawing dense rows.
With firstInstance: true, each group’s draw starts at its own region. The shader finds its row from the instance index alone. One pipeline draws every group, so adding a group compiles no pipeline.
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,
constBlock:table.TableDef<"Block",{
shape:d.U32;
position:d.Vec3f;
},{}>
Block,{
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.
Creates a list of live slots and an indirect draw per group. Each group reserves capacity
slots. A shader finds rows with live.slot(group, i) using a captured group index. With
firstInstance, it can use live.instanceSlot(instanceIndex) instead. Throws if the groups,
draws or workgroup size are invalid, the slot buffer exceeds maxStorageBufferBindingSize, or
firstInstance lacks its required feature.
@param ― options.alive whether a slot holds a live row, carrying the fills of what it reads, such
as a table's from rows.live(), spread into the options with its capacity
@param ― options.draws one draw per group
@returns ― live lists you run every frame and destroy
Maps a live slot to its group in a shader, such as with a table field's getter. Skips rows
whose group is out of range. Default every live row belongs to group 0.
Provides one draw per group. Requires 1 to 256 draws. Default an empty draw for each of
groups, for lists that only dispatch.
draws:[{
indexCount: number
indexCount:36},{
indexCount: number
indexCount:240,
firstIndex?: number |undefined
firstIndex:36,
baseVertex?: number |undefined
baseVertex:24}],
ListOptions.firstInstance?: boolean |undefined
Uses firstInstance to start each draw at its group's region. Shaders can then find rows with
live.instanceSlot(instanceIndex), and one pipeline can draw every group. Throws without the
device feature indirect-first-instance, because those draws otherwise draw nothing without
reporting an error. Default false.
Set the pipeline on the pass once, then draw each group with live.draw(pass, 0) and live.draw(pass, 1). create throws without indirect-first-instance because these indirect draws otherwise produce nothing without an error. Check root.device.features and fall back to a pipeline per group when the feature is absent.
compact also writes indirect dispatch arguments for each group’s live rows. live.each(group, work) runs a 'use gpu' function once for each of the group’s live slots, in workgroups of workgroupSize threads, 64 by default. An update to the walkers then skips holes and bats:
Creates work that runs the shader function work(slot) for each live row of group, in no
defined order. Its pipeline includes inject(); pipe in what work reads.
each(
constWALKERS:1
WALKERS,(
slot: number
slot)=>{
'use gpu';
// … move Block slot `slot`
})
.
Work.pipe(fill: Inject): liveLists.Work
Adds fill to the work pipeline, such as a table's inject(), and returns the work.
Fills the definition's accessors with this table's buffers.
inject());
constlive:liveLists.LiveLists
live.
LiveLists.compact(pass: TgpuComputePass): void
Rewrites the live slots and instance counts. Record it after the table's flush.
compact(
constcompute:TgpuComputePass
compute);
constwalk:liveLists.Work
walk.
Work.dispatch(pass: TgpuComputePass): void
Records the work over the rows the last compact or finish listed.
dispatch(
constcompute:TgpuComputePass
compute);
For a kernel of your own, live.dispatch(pass, group) dispatches the pipeline set on the pass over the group’s rows. Its threads return at or beyond live.count(group), the group’s live row count, and find their rows with live.slot(group, i). Pass the group as a captured constant.
A kernel that already visits every row, such as a simulation step, can list rows as it goes with live.list(group, i) and skip the separate pass of compact. Every thread of a workgroup must call it once, outside any branch, because the workgroup lists together. A row it doesn’t list passes a group of groups or above. Record live.finish(pass) after the kernel to write the counts. Machines list rows by state this way. Without draws, groups sets the number of groups for lists that only dispatch:
Creates a list of live slots and an indirect draw per group. Each group reserves capacity
slots. A shader finds rows with live.slot(group, i) using a captured group index. With
firstInstance, it can use live.instanceSlot(instanceIndex) instead. Throws if the groups,
draws or workgroup size are invalid, the slot buffer exceeds maxStorageBufferBindingSize, or
firstInstance lacks its required feature.
@param ― options.alive whether a slot holds a live row, carrying the fills of what it reads, such
as a table's from rows.live(), spread into the options with its capacity
@param ― options.draws one draw per group
@returns ― live lists you run every frame and destroy
create(
constroot:TgpuRoot
root,{
ListOptions.capacity: number
Sets the number of slots, including dead slots. Indices range from 0 to capacity - 1.
capacity:
constCOUNT:4096
COUNT,
ListOptions.groups?: number |undefined
Sets the number of groups, from 1 to 256. With draws, it must equal their number. Default
the number of draws, or 1.
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(1);// not listed
if(
consti:number
i<
constCOUNT:4096
COUNT&&
functionheat(i:number):number
heat(
consti:number
i)>0.5)
let group:number
group=0;
constburning:liveLists.LiveLists
burning.
LiveLists.list: (group:number,i:number)=>void
Lists row i in group from a kernel that already visits the rows, in place of compact.
Every invocation of the workgroup must call it once, outside any branch, with a group of
groups or above for a row it doesn't list, because it synchronises the workgroup. Record
finish after the kernel.
Writes the counts of the rows listed with list into the draws and dispatches, and empties
the lists for the next listing. Record it after the kernels that list.
At most 256 groups are allowed. The slot list uses groups × capacity × 4 bytes and must fit one storage binding, 128 MiB unless the device requests a higher limit. create throws if either limit is exceeded.
The order within a group changes between frames, so overlapping see-through instances may shimmer. Draw see-through groups in a stable order of your own.