One frame written in plain TypeGPU, then piece by piece with the kit, and the six rules every module follows.
TypeGPU Kit is a set of modules for TypeGPU. Your program keeps the GPU device, the loop and every command. Each module takes over one piece you would otherwise write by hand, such as textures that follow the canvas size, a glow or a pool of particles.
This page writes one frame in plain TypeGPU, then moves it onto the kit one piece at a time. Each step introduces one of the six rules every module follows, yours included.
The frame draws rings that ripple out from the middle of the canvas. A small program on the GPU, a kernel, moves the ripple forward each frame, and a fragment function colours every pixel from it.
The frame this page builds. Turn on Glow to add the bloom from A module borrows.
TypeGPU starts from a root, which holds the GPU device and creates everything else. The program asks it for two pieces of GPU memory and two pipelines, each a GPU function compiled together with the fixed state it needs:
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)
'use gpu' marks a function TypeGPU compiles for the GPU. Inside one, +, -, * and / work on vectors such as d.vec2f.
A uniform holds a few values that are the same for every pixel. The CPU writes it. A mutable buffer is memory that kernels write. A shader reads either through .$.
configureContext connects the canvas to the root’s device. Its format and a pipeline’s both default to the browser’s preferred format.
common.fullScreenTriangle is TypeGPU’s vertex function for one triangle that covers the canvas, and hands each pixel its uv, from 0 to 1 across the canvas.
Compiling a pipeline takes time, so a program creates its pipelines once, before the first frame.
The GPU doesn’t run commands as they are called. An encoder records them. Work goes into passes: a compute pass runs kernels, and a render pass draws into textures. A submit sends what the encoder recorded to the GPU, which runs it in order:
A larger program also needs a loop with time, textures that follow the canvas size, effects such as a glow, and thousands of objects. The kit’s modules provide those.
The kit’s frames runs the loop part of frame(). It creates the encoder, calls your function with it, and submits once your function returns. web.surface configures the canvas and follows its size. The passes stay yours:
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(
constroot:TgpuRoot
root,(
frame:frames.Frame
frame)=>{
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.
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.
Runs the loop on each scheduled frame. Time advances by at most maxDt per frame, never steps
back, and stops while the surface is paused. Destroying the animation keeps the loop alive.
Another animation on the same loop continues from its recorded time. A frame error stops the
animation.
@param ― loop the loop to run on each unpaused frame. Continues from its seconds if it ran before
@returns ― an animation you destroy to stop the frames
animate(
constloop:frames.Frames
loop);// runs the loop at every animation frame
frame holds the encoder, the time in seconds and dt, the seconds since the last frame.
The first rule: your program owns the device, the loop, the encoder, the passes and the submit. No module submits during a frame. Each records into a pass or an encoder you hand it, so the GPU runs the frame in the order of your calls.
A glow makes the bright rings bleed light. It needs the frame drawn into a texture with more range than the canvas, a high dynamic range (HDR) texture, which a blur then reads. Three modules cover it:
targets holds a set of textures that follow the canvas size. fit() replaces them when the size changes.
bloom blurs the bright parts of a texture into a glow texture.
finish, the final pass, brings HDR colour into the canvas’s range and adds the glow.
Creates render targets that follow a size. Replaces the whole set when the size changes. Put
targets needed only sometimes in their own set and create that set when needed. Throws for
transient targets with sampling, storage or mip levels, for multisampled targets with storage or
mip levels, and for layered targets that are multisampled, mipped or have more layers than the
device allows.
@param ― source the size it follows, such as a surface
@param ― specs a format and options per target, by name
@param ― options.resolution the fraction of each side drawn, which changes without allocating
@returns ― a set of targets you fit every frame and destroy
Creates bloom from a chain of half-size textures. Each level downsamples with 13 taps, then a
tent filter adds it to the next larger level. The result is mip 0 of output, at half the
source size. With region, passes draw and sample only that region; changing the region
allocates nothing.
@allocates ― on resize: a new chain and the bind groups of its passes; so does a new source
@owns ― targets, a sampler, 3 pipelines, 2 bind groups and a buffer per level: the chain of glow
levels, the settings, a group for each pass, and the region each pass reads
@records ― record after whatever wrote the source; then output holds the glow.
@param ― options.source the HDR picture, read at every record
@param ― options.region the part of the source that holds the picture, read at every record
@returns ― a bloom you record every frame and destroy
Provides the HDR picture: a rendered picture, such as aa, or a function returning a
texture, such as () => screen.textures.hdr. record reads it on every call, so the texture
can be rebuilt.
Creates the final pass of a frame. It reads the source, adds glow, tone maps, grades, applies
the vignette, encodes and dithers. It draws one triangle into the caller pass.
@allocates ― on resize: a bind group for a new source or glow
@owns ― 2 buffers, a sampler, a pipeline, 2 bind groups: the settings, the regions it reads, and a
group each for the source and the glow
@records ― record into a pass of its own on the view you give, usually the canvas's, or draw
into the caller's pass, after the bloom's record.
@param ― options.source the picture, rendered or a texture, read at every draw
@param ― options.region the part of the source that holds the picture, read at every draw
@param ― options.glow a bloom, or a glow of your own, added before tone mapping
@param ― options.exposure an adapted exposure, read on the GPU
@returns ― a final pass you draw every frame and destroy
create(
constroot:TgpuRoot
root,{
Options.format: GPUTextureFormat
Sets the output format, such as the canvas format.
Provides the picture at any size: a rendered picture, such as aa or blur, or a function
returning a texture, such as () => screen.textures.hdr. draw reads it on every call, so
the texture can be rebuilt.
source:
consthdr:()=>targets.Texture<{
readonly format:"rgba16float";
readonly sampled:true;
}>
hdr,
Options.glow?:finish.Rendered|undefined
Adds a bloom, or a glow of your own such as an emission target, before tone mapping. draw
reads its output and region on every call. Default none.
Rebuilds all textures if the source size changed, destroying the old textures. Updates
regions for the current resolution. Returns true after a rebuild so callers can update bind
groups that hold old textures. Call before recording passes that use the targets. Throws after
destroy.
fit();
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.
Opens a pass on view, such as surface.view(frame), draws into it and ends it. A
viewport draws the picture into that rectangle and clears the rest, transparent with
alpha: 'premultiplied' and black otherwise. Optional timer writes measure the pass.
record(
frame:frames.Frame
frame,
constsurface:web.Surface
surface.
Surface.view(frame: frames.Frame): GPUTextureView
Returns a view of the current canvas texture, created once per texture. Applies pending size
changes first, which a pass using context would skip.
view(
frame:frames.Frame
frame));
});
Each module came from a factory, create(root, options). A factory allocates what the module needs and returns a handle:
Resources, as TypeGPU objects you read and write. glow.settings is a uniform, so glow.settings.patch({ threshold: 0.8 }) changes the threshold. glow.output is the glow texture, which your own passes can sample.
Recording methods, such as glow.record(frame), which record into what you pass them.
'use gpu' functions your shaders can call, on modules that have them, such as particles’ emit.
destroy(), which frees what the module made and nothing it borrowed. glow.destroy() frees the glow’s textures, and the screen’s stay.
The second rule: a module borrows. It takes the root and the resources you hand it, keeps no global state, and destroys only what it made, so two glows share nothing. A recording method takes the smallest piece of the frame it needs: a compute or render pass, or the frame or its encoder when it opens passes of its own. A method that eases over time reads dt from the frame.
The figure shows a larger frame: a flow field, particles, a readback and a glow, each recording into the one encoder. Turn a module off to see what it recorded.
Turn a module off to remove its calls or passes. Point at a resource to follow it from the writing pass to its readers.
Next to plain WebGPU, TypeGPU and a renderer such as three.js, the kit keeps TypeGPU’s shaders and schemas and leaves you the passes and the submit:
Creating GPU objects during a frame costs time and memory the frame can’t spare, and a pipeline compiled during a frame stalls it. So the modules allocate when you create them, compile in initAsync, and only record during frames:
awaitPromise.all([
advancing.initAsync(),
drawing.initAsync(),
glow.initAsync(),
final.initAsync(),
]);
After that a steady frame creates no GPU objects at all. The figure runs a scene on a recording root, which counts what the device creates without a GPU. Run a frame twice and the counts stay put.
WGSL in log.shaders
Press frame again to see that a steady frame allocates nothing, with changed counters highlighted. Reset and run a frame before initAsync() to see compilation on first use. steps sets how many fixed steps a frame records.
The third rule: nothing is hidden. Each module’s page opens with a card generated from its source, which states its costs. A test runs every module and checks the card against what it does:
allocates names anything a module creates after setup, such as the bind group a video feed makes for each camera frame it copies.
owns lists what destroy() frees.
binds counts the storage buffers and uniforms its shaders add. WebGPU guarantees 8 storage buffers per shader stage, so the counts tell you what fits in one pipeline.
records says where its work goes and in which order.
injects names the accessors it fills, which the next section explains.
submits setup marks the few calls that submit on their own, such as meshPool.add without an encoder, so you make them while loading.
The ripple can become a module too. Then the colouring shader no longer captures phase itself: it reads the ripple through an accessor, a placeholder that a pipeline fills with a real resource when it is created. The module fills its accessor in inject():
exportfunction
functioncreateRipple(root:TgpuRoot):{
heightAt:(uv:d.v2f)=>number;
step:(pass:TgpuComputePass)=>void;
inject:()=>Inject;
}
createRipple(
root: TgpuRoot
root:
(alias)interfaceTgpuRoot
importTgpuRoot
TgpuRoot){
const
constphase:TgpuMutable<d.F32>
phase=
root: TgpuRoot
root.
TgpuRoot.createMutable<d.F32>(typeSchema: d.F32, initial?: number |((buffer:TgpuBuffer<NoInfer<d.F32>>)=>void)|undefined): TgpuMutable<d.F32>(+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.
constf32:d.F32
export f32
A schema that represents a 32-bit float value. (equivalent to f32 in WGSL)
Interface representing its WGSL vector type counterpart: vec2f or vec2.
A vector with 2 elements of type f32
v2f)=>{
'use gpu';
return
import std
std.
functionsin(value:number):number(+1overload)
export sin
sin(
import std
std.
length<d.v2f>(value: d.v2f): number(+1 overload)
export length
length(
uv:d.v2f
uv-
import d
d.
constvec2f:d.Vec2f
(xy:number)=>d.v2f(+3 overloads)
vec2f(0.5))*40-
constripple:TgpuAccessor<d.F32>
ripple.
TgpuAccessor<F32>.$: number
$)*0.5+0.5;
},
step:(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(...).
Fills module accessors for pipelines created from the configuration it is piped into. Modules
return it from inject(), as TypeGPU caches do. Use root.pipe(table.inject()).
The fourth rule: modules connect through TypeGPU’s own accessors, slots and pipe, never through a registry. The kit’s modules connect this way everywhere:
Every renderer reads the camera through one accessor, cameras.current. A scene picks its camera with .pipe(view.inject()).
Any kernel piped with sparks.inject() can start particles in sparks with sparks.emit.
Connecting modules covers the kinds of accessor, callbacks and resources that are replaced.
A pipeline is compiled from its code, and a value captured into the code is part of it. Ten ripples with their centres captured into the shader would compile ten pipelines. With the centres in a buffer, one pipeline draws all ten, and an eleventh needs one more row and no new pipeline:
// One pipeline per ripple: each centre is part of a shader's code
Calls a defined callback function on each element of an array, and returns an array that contains the results.
@param ― callbackfn A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array.
@param ― thisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.
map((
centre:d.v2f
centre)=>(
uv:d.v2f
uv:
import d
d.
export v2f
Interface representing its WGSL vector type counterpart: vec2f or vec2.
A vector with 2 elements of type f32
v2f)=>{
'use gpu';
return
import std
std.
functionsin(value:number):number(+1overload)
export sin
sin(
import std
std.
length<d.v2f>(value: d.v2f): number(+1 overload)
export length
length(
uv:d.v2f
uv-
centre:d.v2f
centre)*40);
});
// One pipeline for every ripple: the centres are data
Allocates memory on the GPU, allows passing data between host and shader.
Read-only on the GPU, optimized for large 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)
A real scene draws more than colour. TAA needs to know where each pixel was last frame, picking needs an id at every pixel, and the depth decides what is in front. Every renderer would have to write all of these.
Instead, passes.create describes once what each target holds. A renderer hands over a drawing: what each corner carries to its pixels, what a pixel looks like, and how to draw. Here is the ripple as a drawing:
import{
import passes
passes}from'@typegpu/kit';
const
constSeen:d.WgslStruct<{
color:d.Vec4f;
}>
Seen=
import d
d.
struct<{
color:d.Vec4f;
}>(props:{
color:d.Vec4f;
}):d.WgslStruct<{
color:d.Vec4f;
}>
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.
@param ― props Record with string keys and TgpuData values,
each entry describing one struct member.
struct({
color:d.Vec4f
color:
import d
d.
constvec4f:d.Vec4f
vec4f});// what a pixel looks like
const
constscene:passes.Passes<{
readonly hdr:typeofpasses.color;
}>
scene=
import passes
passes.
functioncreate<{
readonly hdr:{
readonly format:"rgba16float";
readonly sampled:true;
};
},{
readonly hdr:typeofpasses.color;
}>(root:TgpuRoot,screen:targets.Targets<{
readonly hdr:{
readonly format:"rgba16float";
readonly sampled:true;
};
}>,writes:{
readonly hdr:typeofpasses.color;
},options?:passes.TargetOptions<{
readonly hdr:{
readonly format:"rgba16float";
readonly sampled:true;
};
}>|undefined):passes.Passes<{
readonly hdr:typeofpasses.color;
}>(+1overload)
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
create(
constroot:TgpuRoot
root,
constscreen:targets.Targets<{
readonly hdr:{
readonly format:"rgba16float";
readonly sampled:true;
};
}>
screen,{
hdr:typeofpasses.color
hdr:
import passes
passes.
constcolor:typeofpasses.color
Marks the target that receives each renderer's colour.
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.
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.
add({
...
import passes
passes.
constfullScreen:{
Vertex:d.WgslStruct<{
position:d.Vec4f;
uv:d.Vec2f;
}>;
at:(vertexIndex:number,_instanceIndex:number)=>{
position:d.v4f;
uv:d.v2f;
};
}
Provides Vertex and at for a renderer that covers the screen with one triangle at depth 0.
Draw it with draw(3). Use { ...passes.fullScreen, surfaceOf, color, draw }.
fullScreen,// one triangle over the screen, carrying uv
Computes data at a pixel from the interpolated vertex. position is the pixel position.
Required for descriptions that write colour and for depthOf.
surfaceOf:(
corner:{
position:d.v4f;
uv:d.v2f;
}
corner:
import d
d.
typeInfer<T>=Textends{
readonly[$repr]:inferTRepr;
}?TRepr:T
export Infer
Extracts the inferred representation of a resource.
For inferring types as seen by the GPU, see
InferGPU
@exampletype A = Infer // => number
type B = Infer<WgslArray> // => number[]
type C = Infer<Atomic> // => number
Infer<typeof
import passes
passes.
constfullScreen:{
Vertex:d.WgslStruct<{
position:d.Vec4f;
uv:d.Vec2f;
}>;
at:(vertexIndex:number,_instanceIndex:number)=>{
position:d.v4f;
uv:d.v2f;
};
}
Provides Vertex and at for a renderer that covers the screen with one triangle at depth 0.
Draw it with draw(3). Use { ...passes.fullScreen, surfaceOf, color, draw }.
Records a draw into the pass, on which the description has set the pipeline it built. That
pipeline binds indices if supplied. drawIndexed throws without an index buffer. A group
the drawing sets with pass.setBindGroup stays for later drawings in the pass that read its
layout, so give such a layout to one drawing.
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:frames.Frame
frame,[
constwater:passes.Drawn
water]));
scene.add builds the pipeline that writes every target the description lists. Add a motion target to the description and the same drawing writes motion too.
The sixth rule: scenes are described once, and every renderer, the kit’s included, hands over a drawing. Scenes and passes covers outputs, shadows and other views.
The kit has no scene graph, ECS, physics, required asset format or engine loop. Parent-child transforms, game state and input stay in your code. A host, such as web.host or React Native’s useScene, runs a scene for you, and the scene still records its own frame.
Modules return TypeGPU resources, and most fit in one source file. You can edit a module or write your own and record both into the same encoder. The kit also runs on a device it shares with three.js or vgpu: Beside other renderers passes buffers, textures and images between them.