Skip to content

@typegpu/kitliveLists

Live lists

Live rows per group, with indirect draw and dispatch arguments, written on the GPU every frame.

kit.liveLists

module

liveLists.create(root, { capacity, draws?, alive?, … }) → LiveLists

rootborrows
the root
draws?takes
one draw per group
alive?takes
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
LiveListsreturns
live lists you run every frame and destroy
types
IndirectDrawIndirectDispatcherWorkIndirectDrawerLiveLists
Optionscreate · 9 fields
capacity

number

Sets the number of slots, including dead slots.

draws?

readonly IndirectDraw[]= an empty draw for each of groups, for lists that only dispatch

Provides one draw per group.

groups?

number= the number of draws, or 1

Sets the number of groups, from 1 to 256.

group?

(i: number) => number= every live row belongs to group 0

Maps a live slot to its group in a shader, such as with a table field's getter.

firstInstance?

boolean= false

Uses firstInstance to start each draw at its group's region.

workgroupSize?

number= 64

Sets the threads per workgroup for kernels run by dispatch.

label?

string= live

Names the buffers and pipelines in captures and the GPU debug panel.

alive?

(i: number) => boolean= every slot is live

Tests whether slot i holds a live row in a shader.

span?

() => number= capacity

Returns the highest live slot plus one, read when compact records, such as a table's from live().

records
  • compactcompute
  • finishcompute
  • drawrender
  • dispatchcompute
  • each().dispatchcompute

compact after the table's flush, or finish after the kernels that list, then draw or dispatch each group from it.

owns

4 buffers ·2 pipelines

The live slots per group, their counts, the indirect draws and the indirect dispatches, and a pipeline per work.

lifecycle

initAsync()destroy()[Symbol.dispose]

injects
  • per-instance

    slot, instanceSlot and count, for the pipelines that draw or run over its groups.

binds
  • 1 storage

    slot or instanceSlot, the live slots.

  • 1 storage

    count, the indirect draws.

  • 2 storage

    list, the live slots and their counts.

requires
  • indirect-first-instanceoption

    firstInstance: true needs it; without it the live lists throw.

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
const Block: table.TableDef<"Block", {
shape: d.U32;
position: d.Vec3f;
}, {}>
Block
=
import table
table
.
function define<"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
.
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
,
position: d.Vec3f
position
:
import d
d
.
const vec3f: d.Vec3f
vec3f
} });
const
const blocks: table.Table<table.TableDef<"Block", {
shape: d.U32;
position: d.Vec3f;
}, {}>>
blocks
=
import table
table
.
function create<table.TableDef<"Block", {
shape: d.U32;
position: d.Vec3f;
}, {}>>(root: TgpuRoot, def: table.TableDef<"Block", {
shape: d.U32;
position: d.Vec3f;
}, {}>, options: table.Options<table.TableDef<"Block", {
shape: d.U32;
position: d.Vec3f;
}, {}>>): table.Table<table.TableDef<"Block", {
shape: d.U32;
position: d.Vec3f;
}, {}>> (+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 Block: 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.

capacity
: 4096 });
const
const live: liveLists.LiveLists
live
=
import liveLists
liveLists
.
function create(root: TgpuRoot, options: liveLists.Options): liveLists.LiveLists

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
(
const root: TgpuRoot
root
, {
...
const blocks: table.Table<table.TableDef<"Block", {
shape: d.U32;
position: d.Vec3f;
}, {}>>
blocks
.
Table<TableDef<"Block", { shape: U32; position: 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
(), // the table's capacity, alive and span
ListOptions.group?: ((i: number) => number) | undefined

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.

group
:
const Block: table.TableDef<"Block", {
shape: d.U32;
position: d.Vec3f;
}, {}>
Block
.
shape: table.Getter<d.U32>
shape
.
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 blocks: table.Table<table.TableDef<"Block", {
shape: d.U32;
position: d.Vec3f;
}, {}>>
blocks
.
Table<TableDef<"Block", { shape: U32; position: Vec3f; }, {}>>.inject(): Inject

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

inject
()), // a shader function from a slot to a group
ListOptions.draws?: readonly liveLists.IndirectDraw[] | undefined

Provides one draw per group. Requires 1 to 256 draws. Default an empty draw for each of groups, for lists that only dispatch.

draws
: [{
vertexCount: number
vertexCount
: 36 }, {
indexCount: number
indexCount
: 240 }], // one per group
});
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 blocks: table.Table<table.TableDef<"Block", {
shape: d.U32;
position: d.Vec3f;
}, {}>>
blocks
.
Table<TableDef<"Block", { shape: U32; position: Vec3f; }, {}>>.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
();
const
const compute: 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.

encoder
.
TgpuCommandEncoder.beginComputePass(descriptor?: TgpuComputePassDescriptor): TgpuComputePass

Begins recording a compute pass

beginComputePass
();
const live: liveLists.LiveLists
live
.
LiveLists.compact(pass: TgpuComputePass): void

Rewrites the live slots and instance counts. Record it after the table's flush.

compact
(
const compute: TgpuComputePass
compute
); // each group's live slots, then its instance count
const compute: TgpuComputePass
compute
.
TgpuComputePass.end(): void

Completes the recording of this compute pass

end
();
const
const pass: 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.

encoder
.
TgpuCommandEncoder.beginRenderPass(descriptor: TgpuRenderPassDescriptor): TgpuRenderPass

Begins recording a render pass. Attachment views accept TypeGPU textures, texture views and canvas contexts, next to raw

GPUTextureView

s.

beginRenderPass
({
TgpuRenderPassDescriptor.colorAttachments?: ColorAttachment | readonly (ColorAttachment | null)[] | undefined
colorAttachments
: {
ColorAttachment.view: (ColorTextureConstraint & RenderFlag) | GPUTextureView | TgpuTextureView<d.WgslTexture<WgslTextureProps>> | TgpuTextureRenderView | GPUCanvasContext

A

GPUTextureView

describing the texture subresource that will be output to for this color attachment.

view
} });
const pass: TgpuRenderPass
pass
.
TgpuRenderCommands.setPipeline(pipeline: TgpuRenderPipeline): void

Sets the current

TgpuRenderPipeline

for subsequent draw calls

setPipeline
(
const solid: TgpuRenderPipeline<never>
solid
);
const live: liveLists.LiveLists
live
.
LiveLists.draw(pass: liveLists.IndirectDrawer, group: number): void

Draws group into the pass with the pipeline set on it. Uses an indexed draw when the group's draw is indexed.

draw
(
const pass: TgpuRenderPass
pass
,
const SOLID: 0
SOLID
); // drawIndirect, or drawIndexedIndirect for an indexed draw
const pass: 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.

import {
import liveLists
liveLists
} from '@typegpu/kit';
const
const root: TgpuRoot
root
= await
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
.
init: (options?: InitOptions) => Promise<TgpuRoot>

Requests a new GPU device and creates a root around it. If a specific device should be used instead, use

@see ― initFromDevice. *

@example

When given no options, the function will ask the browser for a suitable GPU device.

const root = await tgpu.init();

@example

If there are specific options that should be used when requesting a device, you can pass those in.

const adapterOptions: GPURequestAdapterOptions = ...;
const deviceDescriptor: GPUDeviceDescriptor = ...;
const root = await tgpu.init({ adapter: adapterOptions, device: deviceDescriptor });

init
({
device?: (GPUDeviceDescriptor & {
optionalFeatures?: Iterable<GPUFeatureName>;
}) | undefined
device
: {
optionalFeatures?: Iterable<GPUFeatureName> | undefined
optionalFeatures
: ['indirect-first-instance'] },
});
const
const blocks: table.Table<table.TableDef<"Block", {
shape: d.U32;
position: d.Vec3f;
}, {}>>
blocks
=
import table
table
.
function create<table.TableDef<"Block", {
shape: d.U32;
position: d.Vec3f;
}, {}>>(root: TgpuRoot, def: table.TableDef<"Block", {
shape: d.U32;
position: d.Vec3f;
}, {}>, options: table.Options<table.TableDef<"Block", {
shape: d.U32;
position: d.Vec3f;
}, {}>>): table.Table<table.TableDef<"Block", {
shape: d.U32;
position: d.Vec3f;
}, {}>> (+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 Block: 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.

capacity
: 4096 });
const
const live: liveLists.LiveLists
live
=
import liveLists
liveLists
.
function create(root: TgpuRoot, options: liveLists.Options): liveLists.LiveLists

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
(
const root: TgpuRoot
root
, {
...
const blocks: table.Table<table.TableDef<"Block", {
shape: d.U32;
position: d.Vec3f;
}, {}>>
blocks
.
Table<TableDef<"Block", { shape: U32; position: 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
(),
ListOptions.group?: ((i: number) => number) | undefined

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.

group
:
const Block: table.TableDef<"Block", {
shape: d.U32;
position: d.Vec3f;
}, {}>
Block
.
shape: table.Getter<d.U32>
shape
.
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 blocks: table.Table<table.TableDef<"Block", {
shape: d.U32;
position: d.Vec3f;
}, {}>>
blocks
.
Table<TableDef<"Block", { shape: U32; position: Vec3f; }, {}>>.inject(): Inject

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

inject
()),
// two meshes in one index buffer
ListOptions.draws?: readonly liveLists.IndirectDraw[] | undefined

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.

firstInstance
: true,
});
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: <{
instance: d.BuiltinInstanceIndex;
}, {
position: d.BuiltinPosition;
}>(options: {
in: {
instance: d.BuiltinInstanceIndex;
};
out: {
position: d.BuiltinPosition;
};
}) => TgpuVertexFnShell<{
instance: d.BuiltinInstanceIndex;
}, {
position: d.BuiltinPosition;
}> (+1 overload)
vertexFn
({
in: {
instance: d.BuiltinInstanceIndex;
}
in
: {
instance: d.BuiltinInstanceIndex
instance
:
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
},
})(({
instance: number
instance
}) => {
'use gpu';
const
const slot: number
slot
=
const live: liveLists.LiveLists
live
.
LiveLists.instanceSlot: (instanceIndex: number) => number

Returns the slot drawn by instanceIndex. Requires firstInstance: true. A shader that calls it without that option fails to resolve.

instanceSlot
(
instance: number
instance
); // no group anywhere
return {
position: d.v4f
position
:
import d
d
.
const vec4f: d.Vec4f
(v0: AnyNumericVec3Instance, w: number) => d.v4f (+9 overloads)
vec4f
(
const Block: table.TableDef<"Block", {
shape: d.U32;
position: d.Vec3f;
}, {}>
Block
.
position: table.Getter
(i: number) => d.v3f
position
(
const slot: number
slot
), 1) };
});

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:

const
const walk: liveLists.Work
walk
=
const live: liveLists.LiveLists
live
.
LiveLists.each(group: number, work: (slot: number) => void): liveLists.Work

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
(
const WALKERS: 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.

pipe
(
const blocks: table.Table<table.TableDef<"Block", {
shape: d.U32;
position: d.Vec3f;
}, {}>>
blocks
.
Table<TableDef<"Block", { shape: U32; position: Vec3f; }, {}>>.inject(): Inject

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

inject
());
const live: liveLists.LiveLists
live
.
LiveLists.compact(pass: TgpuComputePass): void

Rewrites the live slots and instance counts. Record it after the table's flush.

compact
(
const compute: TgpuComputePass
compute
);
const walk: liveLists.Work
walk
.
Work.dispatch(pass: TgpuComputePass): void

Records the work over the rows the last compact or finish listed.

dispatch
(
const compute: 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:

const
const burning: liveLists.LiveLists
burning
=
import liveLists
liveLists
.
function create(root: TgpuRoot, options: liveLists.Options): liveLists.LiveLists

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
(
const root: TgpuRoot
root
, {
ListOptions.capacity: number

Sets the number of slots, including dead slots. Indices range from 0 to capacity - 1.

capacity
:
const COUNT: 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.

groups
: 1 });
const
const spread: TgpuComputeFn<{
gid: d.BuiltinGlobalInvocationId;
}>
spread
=
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
;
let
let group: number
group
=
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
(1); // not listed
if (
const i: number
i
<
const COUNT: 4096
COUNT
&&
function heat(i: number): number
heat
(
const i: number
i
) > 0.5)
let group: number
group
= 0;
const burning: 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.

list
(
let group: number
group
,
const i: number
i
);
});
const
const spreading: TgpuComputePipeline
spreading
=
const root: TgpuRoot
root
.
WithBinding.createComputePipeline<{
gid: d.BuiltinGlobalInvocationId;
}>(descriptor: TgpuComputePipeline.Descriptor<{
gid: d.BuiltinGlobalInvocationId;
}>): TgpuComputePipeline
createComputePipeline
({
compute: TgpuComputeFn<{
gid: d.BuiltinGlobalInvocationId;
}>
compute
:
const spread: TgpuComputeFn<{
gid: d.BuiltinGlobalInvocationId;
}>
spread
});
const compute: TgpuComputePass
compute
.
TgpuComputePass.setPipeline(pipeline: TgpuComputePipeline): void

Sets the current

TgpuComputePipeline

for subsequent dispatches

setPipeline
(
const spreading: TgpuComputePipeline
spreading
);
const compute: TgpuComputePass
compute
.
TgpuComputePass.dispatchWorkgroups(x: number, y?: number, z?: number): void
dispatchWorkgroups
(
const COUNT: 4096
COUNT
/ 64);
const burning: liveLists.LiveLists
burning
.
LiveLists.finish(pass: TgpuComputePass): void

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.

finish
(
const compute: TgpuComputePass
compute
);
  • 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.