The contract every TypeGPU Kit module follows, and how to write one.
Every module has a factory, create(root, options), that allocates resources and returns them with shader functions and recording methods. How the kit works introduces the rules modules follow. This page covers the details you rely on when using or writing one.
Follow resources from creation to teardown. The first use creates GPU buffers and bind groups; later frames allocate nothing, destroy() frees four buffers, pipelines and groups release when unreferenced, and the borrowed surface remains.
initAsync() compiles every pipeline. It is optional; without it the first recording compiles, which stalls that frame and is slow on WebKit.
A few setup calls submit, marked submits setup on their card: meshPool.add(shape) and meshes.bake given no encoder or pass. Call them while loading, or pass the frame’s encoder where supported.
destroy() frees the resources listed under owns on the card. It leaves borrowed resources alive. Calling it twice is safe. [Symbol.dispose] is the same function, so using destroys a module at the end of its block on the web. The card’s lifecycle shows whether a module has destroy(). A drawing built by a description has its own drawn.destroy().
After destroy(), a method that would put a destroyed resource into GPU work throws an error that names the module, such as “The live lists were destroyed.” That covers inject(), recording methods (compact, sort, pick), writes (a camera’s write) and getters that hand out GPU objects (a target set’s views, a surface’s view). A table throws on every use.
Methods that only read CPU state or perform optional work return without an error. frames.run returns false, capture records nothing, take and latest return undefined, and a camera’s update writes nothing. A loop that ticks once more after its scene is torn down ends without an error.
The garbage collector frees what a dropped scene made. On a device that outlives the scene, such as one a gallery shares, destroy frees the large things sooner: targets that follow the screen, large buffers, models and the modules the scene made. Leave small uniforms, samplers, bind groups and pass descriptions to the collector. owner() collects each thing as you make it, so destroy needn’t list them again:
import{
functionowner(message?:string):Owner
Collects what a scene or module makes, to free it all in one call, and guards its methods. Pass
each thing through it as you make it, and return its destroy as your own. Take a resource that
is replaced during the module's life, such as a texture that follows the canvas size, as one
cleanup that frees the current one. Methods that only read CPU state or do optional work, such
as capture, take or a camera's update, should return early on destroyed instead of
calling usable.
@param ― message what usable throws, naming the module: "The table was destroyed."
@returns ― an owner you pass each thing to and destroy
owner}from'@typegpu/kit';
const
constown:Owner
own=
functionowner(message?:string):Owner
Collects what a scene or module makes, to free it all in one call, and guards its methods. Pass
each thing through it as you make it, and return its destroy as your own. Take a resource that
is replaced during the module's life, such as a texture that follows the canvas size, as one
cleanup that frees the current one. Methods that only read CPU state or do optional work, such
as capture, take or a camera's update, should return early on destroyed instead of
calling usable.
Describes the targets and outputs for scene passes. With a surface such as the canvas, writes
only each renderer's colour. With a target set, writes colour into the target marked color,
depth into the target marked depth, and other outputs into their named targets. Multisampled
targets resolve according to resolve. Targets with layers draw one pass per layer, and every
target the description draws into needs the same layers. add builds a renderer's drawing.
begin starts a pass. pipe supplies fills such as a camera.
@param ― screen the surface, or the targets, which give the formats; each one the description
names is drawn into
@param ― writes what fills each target, by its key in screen
@returns ― a description you destroy, which holds the pipelines it builds
Adds fill after existing fills and returns a description of the same passes. Piped fills
take precedence over a drawing's own. For example, passes.create(…).pipe(view.inject()) uses
the camera from view.
pipe(
constview:cameras.OrbitCamera
view.
CameraBase.inject(): Inject
Fills cameras.current for the pipelines created from the configuration it is piped into.
inject());
const
consttime:TgpuUniform<d.F32>
time=
constroot:TgpuRoot
root.
TgpuRoot.createUniform<d.F32>(typeSchema: d.F32, initial?: number |((buffer:TgpuBuffer<NoInfer<d.F32>>)=>void)|undefined): TgpuUniform<d.F32>(+1 overload)
Allocates memory on the GPU, allows passing data between host and shader.
Read-only on the GPU, optimized for small data. 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)
createUniform(
import d
d.
constf32:d.F32
export f32
A schema that represents a 32-bit float value. (equivalent to f32 in WGSL)
Can also be called to cast a value to an f32.
@exampleconst value = f32(); // 0
@exampleconst value = f32(1.23); // 1.23
@exampleconst value = f32(true); // 1
f32);
const
constsparks:particles.Particles<d.WgslStruct<{
position:d.Vec3f;
age:d.F32;
velocity:d.Vec3f;
life:d.F32;
color:d.Vec4f;
size:d.F32;
seed:d.F32;
}>>
sparks=
constown:Owner
<particles.Particles<d.WgslStruct<{
position:d.Vec3f;
age:d.F32;
velocity:d.Vec3f;
life:d.F32;
color:d.Vec4f;
size:d.F32;
seed:d.F32;
}>>>(item:particles.Particles<d.WgslStruct<{
position:d.Vec3f;
age:d.F32;
velocity:d.Vec3f;
life:d.F32;
color:d.Vec4f;
size:d.F32;
seed:d.F32;
}>>)=>particles.Particles<d.WgslStruct<{
position:d.Vec3f;
age:d.F32;
velocity:d.Vec3f;
life:d.F32;
color:d.Vec4f;
size:d.F32;
seed:d.F32;
}>>
Takes item to free at destroy and returns it. Passes undefined through. Frees a fixed
resource, such as one from root.createUniform, through its buffer, and runs a cleanup
function. After destroy, frees item at once.
Creates a GPU particle pool. CPU bursts and kernel emit calls allocate ring slots. Fixed
steps move particles, and live lists list only live particles for drawing.
@injects ― per-instance emit and the pool, for kernels piped with it
@binds ― 2 storage: emit, the pool and its ring counter
@binds ― 1 storage: alive or particle, the pool
@binds ― 1 storage, 1 uniform: drawnAt, the pool and the last compact's ahead
@owns ― 6 buffers, 2 pipelines, liveLists: the pool, its ring counter, the burst queue and three
uniforms; the live lists list the live particles. Each description drawing is added to builds
its pipeline.
@records ― spawn once a submission before compact, step once per fixed step, compact once
after the steps, then the draw a description added drawing as; clear whenever you like.
@param ― camera the camera the quads face unless place says otherwise; the callbacks read it too
@param ― options.update a 'use gpu' function; it and the other callbacks carry the fills of what
they read beside the camera, such as tgpu.fn(rise).pipe(noise.inject())
Sets the length of a step in seconds, such as a fixed-step clock's sim.step. Each step
advances by it.
step:
constsim:clock.FixedStep
sim.
FixedStep.step: number
Sets the step duration in seconds. A new value applies from the next advance: unstepped time
carries over, capped at maxSteps new steps, and time keeps the seconds already stepped.
Particles and machines take its value when created and keep it. Throws unless the value is
finite and above 0.
Builds a pipeline for drawing that writes every described output and returns its draw
methods. If the drawing cannot supply the fields an output needs, type checking fails with
that output's name. Use without to leave it out or instead to replace it.
Provides the particle drawing for pass descriptions, as in scene.add(system.drawing). It
supplies colour, world position and corner data to outputs, tests depth without writing it
and draws the list from the last compact. The description initAsync also compiles system
kernels.
drawing)];
const
constscene:{
view:cameras.OrbitCamera;
frame(frame:Frame):void;
destroy:()=>void;
}
scene={
view:cameras.OrbitCamera
view,
functionframe(frame:Frame):void
frame(
frame: Frame
frame:
(alias)interfaceFrame
importFrame
Represents one frame being recorded. Each run creates a separate object, so modules can
compare frames by identity.
Begins a pass, draws drawn or each drawing in a list in order, then ends the pass. Reuse the
list to avoid allocating each frame. Call begin yourself when you need to choose drawings
per frame. Records nothing while enabled is false.
record(
frame: Frame
frame,
constdrawn:passes.Drawn[]
drawn);
},
destroy:()=>void
destroy:
constown:Owner
own.
Owner.destroy(): void
Frees everything it took, the last taken first, once; later calls do nothing. When one throws,
frees the rest and then rethrows. Also [Symbol.dispose].
destroy,
};
own(thing) returns thing. It takes anything with destroy(), and a fixed resource such as a uniform, which it frees through its buffer. It passes undefined through, for a part you make only with a setting on.
own(() => …) runs the function at destroy. Take a texture that a resize replaces this way, as one function that frees the current texture, rather than each texture as it is made.
destroy frees the last thing first, so a module goes before the ones it borrows. Calling it twice is safe.
Something taken after destroy is freed at once, such as a model that finishes loading after the scene closed.
Leave out what the scene borrows: the root, the surface and the host’s resources.
light.shadow.record(encoder, casters);// opens and ends its own depth pass
constpass=scene.begin(frame);
terrain.draw(pass);// what scene.add(drawing) built
embers.draw(pass);
pass.end();
A renderer returns a drawing. A description of passes builds it with scene.add(drawing), which returns a handle with draw(pass).
step runs one fixed step and can run several times in a pass. Compute work that runs once per frame has a specific name, such as live lists’ compact or the particles’ spawn.
Bind a pipeline’s groups at setup, then set it on the pass and draw through the pass. Each .with(...) returns another object, and a draw through a different object from the previous draw’s sets the pipeline, groups and buffers again:
pipeline.with(group).with(pass).draw(3);// in the frame: a new object and all state on every draw
constbound=pipeline.with(group);// at setup
pass.setPipeline(bound);// in the frame
pass.draw(3);// a later draw with the same pipeline sets nothing
A pass.setBindGroup or setVertexBuffer between draws makes the next draw set everything again.
A group that follows a replaced texture comes from memo, and the pipeline bound to it can too, as finish’s is.
A module sets only its pipeline on the caller’s pass. It makes no other state changes such as pass.setBindGroup there, because later draws would inherit them. Your frame code can make those changes.
draw takes a render pass or a render bundle encoder. pass.executeBundles([bundle]) replays the recorded draws in one call, which saves per-draw JS work on Hermes:
A bundle replays fixed draw calls while buffer contents can change. Record it again when the draw calls or resources change. Instances need this when a mesh gets its first instance or its textures arrive. Finish needs it when bloom replaces its output.
A method that sets a viewport or a scissor can’t go in a bundle. It takes a pass, or the encoder and opens its own passes, as bloom’s record does.
This module damps a buffer of velocities. Its rate is a setting; its step length is fixed at creation:
import{
functionowner(message?:string):Owner
Collects what a scene or module makes, to free it all in one call, and guards its methods. Pass
each thing through it as you make it, and return its destroy as your own. Take a resource that
is replaced during the module's life, such as a texture that follows the canvas size, as one
cleanup that frees the current one. Methods that only read CPU state or do optional work, such
as capture, take or a camera's update, should return early on destroyed instead of
calling usable.
Dispatch either by binding TypeGPU pipelines to it
(pipeline.with(pass).dispatchWorkgroups(...)), or proxy-style via
pass.setPipeline(pipeline) followed by pass.dispatchWorkgroups(...).
Call end() when done recording.
TgpuComputePass,type
(alias)interfaceTgpuRoot
importTgpuRoot
TgpuRoot}from'typegpu';
const
constDampingSettings:d.WgslStruct<{
rate:d.F32;
}>
DampingSettings=
import d
d.
struct<{
rate:d.F32;
}>(props:{
rate:d.F32;
}):d.WgslStruct<{
rate:d.F32;
}>
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.
Collects what a scene or module makes, to free it all in one call, and guards its methods. Pass
each thing through it as you make it, and return its destroy as your own. Take a resource that
is replaced during the module's life, such as a texture that follows the canvas size, as one
cleanup that frees the current one. Methods that only read CPU state or do optional work, such
as capture, take or a camera's update, should return early on destroyed instead of
calling usable.
@param ― message what usable throws, naming the module: "The table was destroyed."
@returns ― an owner you pass each thing to and destroy
owner('The damping was destroyed.');
const
constsettings:TgpuUniform<d.WgslStruct<{
rate:d.F32;
}>>
settings=
constown:Owner
<TgpuUniform<d.WgslStruct<{
rate:d.F32;
}>>>(item:TgpuUniform<d.WgslStruct<{
rate:d.F32;
}>>)=> TgpuUniform<d.WgslStruct<{
rate:d.F32;
}>>
Takes item to free at destroy and returns it. Passes undefined through. Frees a fixed
resource, such as one from root.createUniform, through its buffer, and runs a cleanup
function. After destroy, frees item at once.
Allocates memory on the GPU, allows passing data between host and shader.
Read-only on the GPU, optimized for small data. 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)
Takes item to free at destroy and returns it. Passes undefined through. Frees a fixed
resource, such as one from root.createUniform, through its buffer, and runs a cleanup
function. After destroy, frees item at once.
Immediately resolves the pipeline, then awaits device.createComputePipelineAsync().
NOTE: it is not necessary to initialize pipelines manually.
initAsync(),
functionstep(pass:TgpuComputePass):void
step(
pass: TgpuComputePass
pass:
(alias)interfaceTgpuComputePass
importTgpuComputePass
A compute pass recording into a
TgpuCommandEncoder
.
Dispatch either by binding TypeGPU pipelines to it
(pipeline.with(pass).dispatchWorkgroups(...)), or proxy-style via
pass.setPipeline(pipeline) followed by pass.dispatchWorkgroups(...).
Call end() when done recording.
TgpuComputePass){
constown:Owner
own.
Owner.usable(): void
Throws the owner's message if it is destroyed. Call first in methods that use GPU resources,
including inject, recording methods, writes and getters that return GPU objects.
An intrinsic object that provides basic mathematics functionality and constants.
Math.
Math.ceil(x: number): number
Returns the smallest integer greater than or equal to its numeric argument.
@param ― x A numeric expression.
ceil(
constcount:number
count/64));
},
destroy:()=>void
destroy:
constown:Owner
own.
Owner.destroy(): void
Frees everything it took, the last taken first, once; later calls do nothing. When one throws,
frees the rest and then rethrows. Also [Symbol.dispose].
destroy,
[
var Symbol:SymbolConstructor
Symbol.
SymbolConstructor.dispose: typeofSymbol.dispose
A method that is used to release resources held by an object. Called by the semantics of the using statement.
dispose]:
constown:Owner
own.
Owner.destroy(): void
Frees everything it took, the last taken first, once; later calls do nothing. When one throws,
frees the rest and then rethrows. Also [Symbol.dispose].
Schemas and shader functions that capture no instance’s resources live at module scope.
The kit’s owner takes each resource as it is made and frees them all once at destroy. Its usable() throws its message after destruction, so calling it in step keeps a dispatch from binding destroyed buffers.
Validation and allocation happen in the factory; recording methods only record.
A test builds it on recordingRoot and checks what it records.
A package exports each module as a namespace. The module’s file calls its factory create and its options Options, and the package entry re-exports the file:
index.ts
// damping.ts: export function create(…), export interface Options, export type Damping
export*as damping from'./damping.ts';
exporttype{ Damping }from'./damping.ts';
Call damping.create(root, options) and name the result for what it holds, as in const drag = damping.create(...). frames.create and bloom.create follow the same convention. A module in your own program can stay a plain createDamping.