Skip to content

Getting started

Install the kit, draw a first frame and host it as a scene, in a project of your own.

TypeGPU Kit is a set of GPU modules on TypeGPU. Each borrows your root and records into your passes. You create the device, run the loop and submit the commands. This page sets up a web project, draws a frame and hands it to a host.

In a Vite project, install the kit, TypeGPU and its noise and colour packages, then Vite, the TypeGPU bundler plugin, tsover in place of TypeScript, and WebGPU’s types:

npm i @typegpu/kit typegpu @typegpu/noise @typegpu/color
npm i -D vite unplugin-typegpu typescript@npm:tsover@6.0.2 @webgpu/types

The plugin compiles your 'use gpu' functions for the GPU. Add it to Vite:

vite.config.ts
import typegpu from 'unplugin-typegpu/vite';
import { defineConfig } from 'vite';
export default defineConfig({ plugins: [typegpu()] });

Give TypeScript WebGPU’s types, and the disposables kit objects implement for using:

tsconfig.json
{
"compilerOptions": {
"types": ["@webgpu/types", "vite/client"],
"lib": ["ES2024", "DOM", "DOM.Iterable", "ESNext.Disposable"]
}
}

The page needs a canvas and the script that draws into it:

index.html
<canvas style="display: block; width: 100vw; height: 100vh"></canvas>
<script type="module" src="/src/index.ts"></script>

The script asks TypeGPU for a root, its handle on the GPU. A surface configures the canvas for the root and keeps its drawing buffer the size of its layout. A fragment function colours every pixel of a triangle that covers the canvas, and a frame loop gives each frame an encoder and submits it when your function returns:

The frame this section draws. Turn on Glow to add a bloom.
src/index.ts
import {
import frames
frames
} from '@typegpu/kit';
import * as
import web
web
from '@typegpu/kit/web';
import {
import common
common
,
import d
d
,
import std
std
,
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
} from 'typegpu';
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
();
const
const surface: web.Surface
surface
=
import web
web
.
function surface(root: TgpuRoot, canvas: HTMLCanvasElement, options?: web.SurfaceOptions): web.Surface

Configures a canvas for the root. Matches its drawing buffer to the layout size or the fixed options.size. Throws if the fixed size is invalid.

@param ― canvas the canvas it configures and sizes

@returns ― a surface you draw into and destroy

surface
(
const root: TgpuRoot
root
,
var document: Document

window.document returns a reference to the document contained in the window.

MDN Reference

document
.
ParentNode.querySelector<"canvas">(selectors: "canvas"): HTMLCanvasElement | null (+4 overloads)

Returns the first element that is a descendant of node that matches selectors.

MDN Reference

querySelector
('canvas')!);
const
const time: TgpuUniform<d.F32>
time
=
const root: 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
.
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 rings: TgpuFragmentFn<{
uv: d.Vec2f;
}, d.Vec4f>
rings
=
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
.
fragmentFn: <{
uv: d.Vec2f;
}, d.Vec4f>(options: {
in: {
uv: d.Vec2f;
};
out: d.Vec4f;
}) => TgpuFragmentFnShell<{
uv: d.Vec2f;
}, d.Vec4f> (+1 overload)
fragmentFn
({
in: {
uv: d.Vec2f;
}
in
: {
uv: d.Vec2f
uv
:
import d
d
.
const vec2f: d.Vec2f
vec2f
},
out: d.Vec4f
out
:
import d
d
.
const vec4f: d.Vec4f
vec4f
})(({
uv: d.v2f
uv
}) => {
'use gpu';
const
const wave: number
wave
=
import std
std
.
function sin(value: number): number (+1 overload)
export sin
sin
(
import std
std
.
length<d.v2f>(value: d.v2f): number (+1 overload)
export length
length
(
uv: d.v2f
uv
- 0.5) * 40 -
const time: TgpuUniform<d.F32>
time
.
TgpuUniform<F32>.$: number
$
* 3) * 0.5 + 0.5;
return
import d
d
.
const vec4f: d.Vec4f
(x: number, y: number, z: number, w: number) => d.v4f (+9 overloads)
vec4f
(0.2, 0.6, 1, 1) *
const wave: number
wave
;
});
const
const drawing: TgpuRenderPipeline<d.Vec4f>
drawing
=
const root: TgpuRoot
root
.
WithBinding.createRenderPipeline<{}, {}, {
uv: import("node_modules/typegpu/data/wgslTypes").Vec2f;
}, d.Vec4f>(descriptor: TgpuRenderPipeline<in Targets = never>.DescriptorBase & {
attribs?: {} | undefined;
vertex: TgpuVertexFn<{}, {
uv: import("node_modules/typegpu/data/wgslTypes").Vec2f;
}> | ((input: AutoVertexIn<InferGPURecord<AttribRecordToDefaultDataTypes<{}>>>) => AutoVertexOut<AnyAutoCustoms>);
fragment: TgpuFragmentFn<{
uv: import("node_modules/typegpu/data/wgslTypes").Vec2f;
} & Record<string, AnyFragmentInputBuiltin>, d.Vec4f> | ((input: AutoFragmentIn<...>) => d.v4f | (AnyAutoCustoms & Partial<...>));
targets?: TgpuColorTargetState;
}): TgpuRenderPipeline<...> (+2 overloads)
createRenderPipeline
({
vertex: TgpuVertexFn<{}, {
uv: import("node_modules/typegpu/data/wgslTypes").Vec2f;
}> | ((input: AutoVertexIn<InferGPURecord<AttribRecordToDefaultDataTypes<{}>>>) => AutoVertexOut<AnyAutoCustoms>)
vertex
:
import common
common
.
const fullScreenTriangle: TgpuVertexFn<{}, {
uv: import("node_modules/typegpu/data/wgslTypes").Vec2f;
}>
export fullScreenTriangle

A vertex function that defines a single full-screen triangle out of three points.

@example

import { common } from 'typegpu';
const pipeline = root.createRenderPipeline({
vertex: common.fullScreenTriangle,
fragment: yourFragmentShader,
});
pipeline.draw(3);

fullScreenTriangle
,
fragment: TgpuFragmentFn<{
uv: import("node_modules/typegpu/data/wgslTypes").Vec2f;
} & Record<string, AnyFragmentInputBuiltin>, d.Vec4f> | ((input: AutoFragmentIn<InferGPURecord<{
uv: import("node_modules/typegpu/data/wgslTypes").Vec2f;
}>>) => d.v4f | (AnyAutoCustoms & Partial<InferGPURecord<{
readonly $fragDepth: import("node_modules/typegpu/builtin").BuiltinFragDepth;
readonly $sampleMask: import("node_modules/typegpu/builtin").BuiltinSampleMask;
}>>))
fragment
:
const rings: TgpuFragmentFn<{
uv: d.Vec2f;
}, d.Vec4f>
rings
,
targets?: TgpuColorTargetState
targets
: {
format?: GPUTextureFormat | undefined

The

GPUTextureFormat

of this color target. The pipeline will only be compatible with

GPURenderPassEncoder

s which use a

GPUTextureView

of this format in the corresponding color attachment.

@default ― navigator.gpu.getPreferredCanvasFormat()

format
:
const surface: web.Surface
surface
.
Surface.format: GPUTextureFormat
format
},
});
const
const loop: frames.Frames
loop
=
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 time: TgpuUniform<d.F32>
time
.
TgpuBufferBindingBase<F32>.write(data: number, options?: BufferWriteOptions): void
write
(
frame: frames.Frame
frame
.
Frame.time: number

Reports seconds since the first submitted frame.

time
);
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: GPUTextureView | (ColorTextureConstraint & RenderFlag) | TgpuTextureView<d.WgslTexture<WgslTextureProps>> | TgpuTextureRenderView | GPUCanvasContext

A

GPUTextureView

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

view
:
const surface: 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
) } });
const pass: TgpuRenderPass
pass
.
TgpuRenderCommands.setPipeline(pipeline: TgpuRenderPipeline): void

Sets the current

TgpuRenderPipeline

for subsequent draw calls

setPipeline
(
const drawing: TgpuRenderPipeline<d.Vec4f>
drawing
);
const pass: TgpuRenderPass
pass
.
TgpuRenderCommands.draw(vertexCount: number, instanceCount?: number, firstVertex?: number, firstInstance?: number): void
draw
(3);
const pass: TgpuRenderPass
pass
.
TgpuRenderPass.end(): void

Completes the recording of this render pass

end
();
});
import frames
frames
.
function animate(loop: Pick<frames.Frames, "run"> & Partial<Pick<frames.Frames, "seconds">>, options?: frames.AnimateOptions): frames.Animation
export animate

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
(
const loop: frames.Frames
loop
);

Start Vite, then open the page in a WebGPU browser:

npx vite

Blue rings ripple out from the middle of the canvas. How the kit works takes this frame apart line by line.

Move what you draw into a scene: a file that exports createScene(root, surface) and returns its frame and destroy. A scene holds no loop or page code, so it runs unchanged on the web, on React Native and in a test.

src/scene.ts
import type {
(alias) interface Frame
import Frame

Represents one frame being recorded. Each run creates a separate object, so modules can compare frames by identity.

Frame
,
(alias) interface Surface
import Surface

Provides a destination for frames, such as a canvas from web.surface or native.surface, or a texture from offscreenSurface in a test. Pass the frame to view for code that works with every destination.

Surface
} from '@typegpu/kit';
import type {
(alias) interface TgpuRoot
import TgpuRoot
TgpuRoot
} from 'typegpu';
export async function
function createScene(root: TgpuRoot, surface: Surface): Promise<{
frame(frame: Frame): void;
destroy(): void;
}>
createScene
(
root: TgpuRoot
root
:
(alias) interface TgpuRoot
import TgpuRoot
TgpuRoot
,
surface: Surface
surface
:
(alias) interface Surface
import Surface

Provides a destination for frames, such as a canvas from web.surface or native.surface, or a texture from offscreenSurface in a test. Pass the frame to view for code that works with every destination.

Surface
) {
// The uniform, fragment function and pipeline from index.ts
return {
function frame(frame: Frame): void
frame
(
frame: Frame
frame
:
(alias) interface Frame
import Frame

Represents one frame being recorded. Each run creates a separate object, so modules can compare frames by identity.

Frame
) {
// The body of the frame loop from index.ts
},
function destroy(): void
destroy
() {},
};
}

web.host creates the root, surface and loop, runs the scene and reports its first error. Its handle’s destroy() tears them down in order:

src/index.ts
import * as
import web
web
from '@typegpu/kit/web';
import {
function createScene(root: TgpuRoot, surface: Surface): Promise<{
frame(frame: Frame): void;
destroy(): void;
}>
createScene
} from './scene.ts';
import web
web
.
function host<{
frame(frame: Frame): void;
destroy(): void;
}>(createScene: web.CreateScene<Record<never, never>, {
frame(frame: Frame): void;
destroy(): void;
}>, hosting: web.HostOptions<{
frame(frame: Frame): void;
destroy(): void;
}>): web.Hosted<{
frame(frame: Frame): void;
destroy(): void;
}> (+1 overload)

Runs a scene on a canvas with a root, surface, frame loop and animation. destroy tears them down in order. Use send to control the scene or onFrame to read it. If scene options are optional, call host(createScene, { canvas }). Throws if both device and root are supplied.

@param ― createScene the scene file's createScene

@param ― options the scene's third argument

@param ― hosting the canvas, the device or root, and what the page runs beside the scene

@returns ― a hosted scene you send closures to and destroy

host
(
function createScene(root: TgpuRoot, surface: Surface): Promise<{
frame(frame: Frame): void;
destroy(): void;
}>
createScene
, {
HostOptions<S extends Scene>.canvas: HTMLCanvasElement

Supplies the canvas for the scene.

canvas
:
var document: Document

window.document returns a reference to the document contained in the window.

MDN Reference

document
.
ParentNode.querySelector<"canvas">(selectors: "canvas"): HTMLCanvasElement | null (+4 overloads)

Returns the first element that is a descendant of node that matches selectors.

MDN Reference

querySelector
('canvas')! });

The picture stays the same. The hosting page covers controlling the scene from the page, input, and running it on React Native.

The packages ship built JavaScript with their 'use gpu' functions already transformed, and declarations. The plugin transforms your own 'use gpu' functions. Vector operators in them, a * b, type-check only under tsover, TypeGPU’s fork of TypeScript. It replaces typescript, so a project on another TypeScript version moves to tsover’s.

A few entries need optional peers:

Import Needs
text’s font baker, @typegpu/kit/bake and typegpu-kit-bake-font @pmndrs/glyph and Node 24.2 or later, at build time. On React Native, add rgba to Metro’s assetExts for the atlas.
@typegpu/kit/native, /runtimes, /scenes and /scenes/react React, React Native, react-native-webgpu 0.20.0 or later and react-native-worklets 0.13.0 or later.
@typegpu/kit/testing/dawn webgpu (dawn.node) and vitest. With pnpm 11, add webgpu: false to allowBuilds. Its build script only clears macOS’s quarantine flag.
@typegpu/kit/testing/warnings vitest, for the warning checks in a test setup.

The packages ship built JavaScript, so leave them to Vite’s dependency prebundling, and add no resolve.dedupe. Excluding them from optimizeDeps while TypeGPU stays prebundled loads two copies of TypeGPU: the page warns “Found duplicate TypeGPU version”, then fails on its first schema.

The kit ships a guide for coding agents, the skill skills/typegpu-kit/SKILL.md. It lists the kit’s rules, such as one encoder per frame, uniforms split by when they change, fixed steps and @typegpu/noise, and links to these pages as Markdown in the package’s docs/, with every module’s contract and the examples’ source. The site serves each page as Markdown too, at its address with .md for the trailing slash, and lists them in llms.txt at the site’s root. These name the kit version they describe and send agents in a project with the kit installed to its docs/.

  • TanStack Intent points the project’s agent config at the skills its packages ship. Run it once:

    npx @tanstack/intent@latest install
  • Or point to the skill from the project’s AGENTS.md:

    AGENTS.md
    Read node_modules/@typegpu/kit/skills/typegpu-kit/SKILL.md before writing GPU code.