Skip to content

@typegpu/kit/testingtesting

Testing

Check what a module records in plain Node, then compile its WGSL on a real device.

kit.testing

tool

recordingRoot(options?) → RecordingRoot

offscreenSurface(root, { width, height, format, … }) → OffscreenSurface

steppedSchedule() → SteppedSchedule

RecordingRootreturns
the root, a log of what it saw, and the bytes it holds
rootborrows
any root, recording or real
OffscreenSurfacereturns
a surface you draw into and destroy
SteppedSchedulereturns
a schedule to give animate or a host
types
RecordingLogRecordingCountsRecordingRootOffscreenSurfaceSteppedSchedule
RecordingOptionsrecordingRoot · 2 fields
features?

readonly string[]= none

Sets the device features reported by the recording root, such as 'timestamp-query'.

limits?

Partial<Record<keyof GPUSupportedLimits, number>>= the WebGPU default limits

Overrides device limits, such as a small maxStorageBufferBindingSize to reach a size guard.

OffscreenSurfaceOptionsoffscreenSurface · 4 fields
width

number

Sets the texture width in pixels.

height

number

Sets the texture height in pixels.

format

GPUTextureFormat

Sets the texture format, as a canvas preferred format would.

pixelRatio?

number= 1

Sets the reported pixelRatio, as for a canvas on a phone.

owns

a texture

An offscreen surface's, replaced when it resizes.

lifecycle

destroy()[Symbol.dispose]

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.

A GPU module can record a dispatch in the wrong pass, submit unexpectedly or generate invalid WGSL. recordingRoot() lets a test inspect recorded commands in Node. It creates a TypeGPU root on a stand-in device that stores WGSL and logs commands without executing them.

import {
function recordingRoot(options?: RecordingOptions): RecordingRoot

Creates a recording WebGPU test double under a TypeGPU root. TypeGPU resolves shaders to WGSL. The double keeps the WGSL and records commands without compiling or executing shaders. Use it to check resources, pipelines and recording order without a GPU. Queue writes take effect, and submitted buffer copies and clears update bytes, so tests can check uploads and readbacks.

Throws for invalid mapping usage, out-of-bounds or unaligned copies and writes, missing resource usage flags, writes to destroyed buffers, and mapping in the wrong state. Also throws for non-attachment render targets, bind groups with incompatible resource usages, finishing an encoder with an open pass, and submitting destroyed resources. Throws at the call, where a device would report most of these errors later. Reports WebGPU's default limits and records render bundles, which replay into the pass that executes them. A device member it lacks throws an error that names it.

@returns ― the root, a log of what it saw, and the bytes it holds

recordingRoot
} from '@typegpu/kit/testing';
const {
const root: TgpuRoot
root
, log,
const bytes: (buffer: GPUBuffer | TgpuBuffer<BaseData>) => Uint8Array

Returns the buffer bytes after queued writes and submitted copies. Changes to the returned array change the buffer.

bytes
} =
function recordingRoot(options?: RecordingOptions): RecordingRoot

Creates a recording WebGPU test double under a TypeGPU root. TypeGPU resolves shaders to WGSL. The double keeps the WGSL and records commands without compiling or executing shaders. Use it to check resources, pipelines and recording order without a GPU. Queue writes take effect, and submitted buffer copies and clears update bytes, so tests can check uploads and readbacks.

Throws for invalid mapping usage, out-of-bounds or unaligned copies and writes, missing resource usage flags, writes to destroyed buffers, and mapping in the wrong state. Also throws for non-attachment render targets, bind groups with incompatible resource usages, finishing an encoder with an open pass, and submitting destroyed resources. Throws at the call, where a device would report most of these errors later. Reports WebGPU's default limits and records render bundles, which replay into the pass that executes them. A device member it lacks throws an error that names it.

@returns ― the root, a log of what it saw, and the bytes it holds

recordingRoot
();
const log: RecordingLog
  • shaders: the WGSL of every shader module, in creation order.
  • events: begin-compute, dispatch, end-compute, begin-render, draw, copy-buffer, submit and more.
  • state: set-pipeline and set-bind-group for each setPipeline and setBindGroup a pass receives, in order. A draw with the same pipeline as the draw before it adds none.
  • writes: every queue.writeBuffer, in order, with its buffer, offset and size in bytes.
  • buffers, textures, views, bindGroups, samplers, querySets and pipelines: how many of each the device created, and counts() all seven at once. submissions: the queue submissions.
  • alive(): the buffers, textures and query sets created and not yet destroyed, as [Buffer "label"], in creation order.
  • bytes(buffer): a TypeGPU buffer’s or GPUBuffer’s contents after the queue writes and the copies and clears submitted so far. It returns the bytes themselves, so a test can write into them what a kernel would have, such as a counter.
  • TypeGPU makes GPU objects the first time something uses them, so the counts grow when a module first records, not when you create it.
  • recordingRoot({ features: ['timestamp-query'] }) reports device features, for code that branches on them.
  • Resolution errors, including missing fills, throw as usual. The recording device also throws immediately for errors a GPU reports later. These include missing usage flags, writes or copies out of bounds or not aligned to 4 bytes, a second pending mapAsync, submitting with an open pass and submitting a destroyed buffer.
it<object>(name: string | Function, fn?: TestFunction<object> | undefined, options?: number): void (+1 overload)

Defines a test case with a given name and test function. The test function can optionally be configured with test options.

@param ― name - The name of the test or a function that will be used as a test name.

@param ― optionsOrFn - Optional. The test options or the test function if no explicit name is provided.

@param ― optionsOrTest - Optional. The test function or options, depending on the previous parameters.

@throws ― {Error} If called inside another test function.

@example

// Define a simple test
it('adds two numbers', () => {
expect(add(1, 2)).toBe(3);
});

@example

// Define a test with options
it('subtracts two numbers', { retry: 3 }, () => {
expect(subtract(5, 2)).toBe(3);
});

it
('records steps and a draw into the caller’s passes', async () => {
const {
const root: TgpuRoot
root
,
const log: RecordingLog
log
} =
function recordingRoot(options?: RecordingOptions): RecordingRoot

Creates a recording WebGPU test double under a TypeGPU root. TypeGPU resolves shaders to WGSL. The double keeps the WGSL and records commands without compiling or executing shaders. Use it to check resources, pipelines and recording order without a GPU. Queue writes take effect, and submitted buffer copies and clears update bytes, so tests can check uploads and readbacks.

Throws for invalid mapping usage, out-of-bounds or unaligned copies and writes, missing resource usage flags, writes to destroyed buffers, and mapping in the wrong state. Also throws for non-attachment render targets, bind groups with incompatible resource usages, finishing an encoder with an open pass, and submitting destroyed resources. Throws at the call, where a device would report most of these errors later. Reports WebGPU's default limits and records render bundles, which replay into the pass that executes them. A device member it lacks throws an error that names it.

@returns ― the root, a log of what it saw, and the bytes it holds

recordingRoot
();
const
const field: {
initAsync(): Promise<void>;
step(pass: TgpuComputePass): void;
draw(pass: TgpuRenderCommands): void;
}
field
=
function createFlowField(root: TgpuRoot, options: {
count: number;
step: number;
format: GPUTextureFormat;
size: {
width: number;
height: number;
};
}): {
initAsync(): Promise<void>;
step(pass: TgpuComputePass): void;
draw(pass: TgpuRenderCommands): void;
}
createFlowField
(
const root: TgpuRoot
root
, {
count: number
count
: 1000,
step: number
step
: 1 / 60,
format: GPUTextureFormat
format
: 'bgra8unorm',
size: {
width: number;
height: number;
}
size
: {
width: number
width
: 64,
height: number
height
: 32 },
});
await
const field: {
initAsync(): Promise<void>;
step(pass: TgpuComputePass): void;
draw(pass: TgpuRenderCommands): void;
}
field
.
function initAsync(): Promise<void>
initAsync
();
expect<number>(actual: number, message?: string): Assertion<void, number> (+1 overload)
expect
(
const log: RecordingLog
log
.
RecordingLog.pipelines: number
pipelines
).
JestAssertion<void, number>.toBe: <number>(expected: number) => void

Checks that a value is what you expect. It calls Object.is to compare values. Don't use toBe with floating-point numbers.

@example expect(result).toBe(42); expect(status).toBe(true);

toBe
(3);
const
const target: TgpuTexture<{
size: [64, 32];
format: "bgra8unorm";
}> & RenderFlag
target
=
const root: TgpuRoot
root
.
TgpuRoot.createTexture<number, number, number, [64, 32], "bgra8unorm", number, number, GPUTextureFormat[], GPUTextureDimension>(props: CreateTextureOptions<[64, 32], "bgra8unorm", number, number, GPUTextureFormat[], GPUTextureDimension>): TgpuTexture<{
size: [64, 32];
format: "bgra8unorm";
}>
createTexture
({
size: [64, 32]

The width, height, and depth or layer count of the texture.

size
: [64, 32],
format: "bgra8unorm"

The format of the texture.

format
: 'bgra8unorm' }).
TgpuTexture<{ size: [64, 32]; format: "bgra8unorm"; }>.$usage<["render"]>(usages_0: "render"): TgpuTexture<{
size: [64, 32];
format: "bgra8unorm";
}> & RenderFlag
$usage
('render');
const
const encoder: TgpuCommandEncoder
encoder
=
const root: TgpuRoot
root
.
TgpuRoot.createCommandEncoder(descriptor?: GPUCommandEncoderDescriptor): TgpuCommandEncoder

Creates a

TgpuCommandEncoder

for batching multiple render/compute passes (and draws within them) into a single submission.

@example

const encoder = root.createCommandEncoder();
const pass = encoder.beginRenderPass({
colorAttachments: [{ view: msaaTexture, resolveTarget: context }],
});
scenePipeline.with(pass).draw(vertexCount);
skyPipeline.with(pass).draw(3);
pass.end();
encoder.submit();

createCommandEncoder
();
const
const compute: TgpuComputePass
compute
=
const encoder: TgpuCommandEncoder
encoder
.
TgpuCommandEncoder.beginComputePass(descriptor?: TgpuComputePassDescriptor): TgpuComputePass

Begins recording a compute pass

beginComputePass
();
const field: {
initAsync(): Promise<void>;
step(pass: TgpuComputePass): void;
draw(pass: TgpuRenderCommands): void;
}
field
.
function step(pass: TgpuComputePass): void
step
(
const compute: TgpuComputePass
compute
);
const field: {
initAsync(): Promise<void>;
step(pass: TgpuComputePass): void;
draw(pass: TgpuRenderCommands): void;
}
field
.
function step(pass: TgpuComputePass): void
step
(
const compute: TgpuComputePass
compute
);
const compute: TgpuComputePass
compute
.
TgpuComputePass.end(): void

Completes the recording of this compute pass

end
();
const
const pass: TgpuRenderPass
pass
=
const encoder: TgpuCommandEncoder
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<WgslTexture<WgslTextureProps>> | TgpuTextureRenderView | GPUCanvasContext

A

GPUTextureView

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

view
:
const target: TgpuTexture<{
size: [64, 32];
format: "bgra8unorm";
}> & RenderFlag
target
} });
const field: {
initAsync(): Promise<void>;
step(pass: TgpuComputePass): void;
draw(pass: TgpuRenderCommands): void;
}
field
.
function draw(pass: TgpuRenderCommands): void
draw
(
const pass: TgpuRenderPass
pass
);
const pass: TgpuRenderPass
pass
.
TgpuRenderPass.end(): void

Completes the recording of this render pass

end
();
const encoder: TgpuCommandEncoder
encoder
.
TgpuCommandEncoder.submit(): void

Finishes the recording and submits the resulting command buffer to the device queue

submit
();
expect<string[]>(actual: string[], message?: string): Assertion<void, string[]> (+1 overload)
expect
(
const log: RecordingLog
log
.
RecordingLog.events: string[]

Lists pass, draw, dispatch and submit calls in recording order.

events
).
JestAssertion<void, string[]>.toEqual: <string[]>(expected: string[]) => void

Used when you want to check that two objects have the same value. This matcher recursively checks the equality of all fields, rather than checking for object identity.

@example expect(user).toEqual({ name: 'Alice', age: 30 });

toEqual
([
'begin-compute',
'dispatch',
'dispatch',
'end-compute',
'begin-render',
'draw',
'end-render',
'submit',
]);
expect<number>(actual: number, message?: string): Assertion<void, number> (+1 overload)
expect
(
const log: RecordingLog
log
.
RecordingLog.pipelines: number
pipelines
).
JestAssertion<void, number>.toBe: <number>(expected: number) => void

Checks that a value is what you expect. It calls Object.is to compare values. Don't use toBe with floating-point numbers.

@example expect(result).toBe(42); expect(status).toBe(true);

toBe
(3); // nothing compiled mid-frame
});

Also check log.shaders for unwanted WGSL, such as read_write storage in a vertex shader. Inspect upload sizes in log.writes or contents through bytes. Use log.submissions to check that only your frame submits:

it<object>(name: string | Function, fn?: TestFunction<object> | undefined, options?: number): void (+1 overload)

Defines a test case with a given name and test function. The test function can optionally be configured with test options.

@param ― name - The name of the test or a function that will be used as a test name.

@param ― optionsOrFn - Optional. The test options or the test function if no explicit name is provided.

@param ― optionsOrTest - Optional. The test function or options, depending on the previous parameters.

@throws ― {Error} If called inside another test function.

@example

// Define a simple test
it('adds two numbers', () => {
expect(add(1, 2)).toBe(3);
});

@example

// Define a test with options
it('subtracts two numbers', { retry: 3 }, () => {
expect(subtract(5, 2)).toBe(3);
});

it
('uploads a radius at flush, without a submit', () => {
const {
const root: TgpuRoot
root
,
const log: RecordingLog
log
,
const bytes: (buffer: GPUBuffer | TgpuBuffer<d.BaseData>) => Uint8Array

Returns the buffer bytes after queued writes and submitted copies. Changes to the returned array change the buffer.

bytes
} =
function recordingRoot(options?: RecordingOptions): RecordingRoot

Creates a recording WebGPU test double under a TypeGPU root. TypeGPU resolves shaders to WGSL. The double keeps the WGSL and records commands without compiling or executing shaders. Use it to check resources, pipelines and recording order without a GPU. Queue writes take effect, and submitted buffer copies and clears update bytes, so tests can check uploads and readbacks.

Throws for invalid mapping usage, out-of-bounds or unaligned copies and writes, missing resource usage flags, writes to destroyed buffers, and mapping in the wrong state. Also throws for non-attachment render targets, bind groups with incompatible resource usages, finishing an encoder with an open pass, and submitting destroyed resources. Throws at the call, where a device would report most of these errors later. Reports WebGPU's default limits and records render bundles, which replay into the pass that executes them. A device member it lacks throws an error that names it.

@returns ― the root, a log of what it saw, and the bytes it holds

recordingRoot
();
const
const enemies: table.Table<table.TableDef<"Enemy", {
radius: d.F32;
}, {}>>
enemies
=
import table
table
.
function create<table.TableDef<"Enemy", {
radius: d.F32;
}, {}>>(root: TgpuRoot, def: table.TableDef<"Enemy", {
radius: d.F32;
}, {}>, options: table.Options<table.TableDef<"Enemy", {
radius: d.F32;
}, {}>>): table.Table<table.TableDef<"Enemy", {
radius: d.F32;
}, {}>> (+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 Enemy: table.TableDef<"Enemy", {
radius: d.F32;
}, {}>
Enemy
, {
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
: 64 });
const
const id: table.RowId<"Enemy">
id
=
const enemies: table.Table<table.TableDef<"Enemy", {
radius: d.F32;
}, {}>>
enemies
.
Table<TableDef<"Enemy", { radius: F32; }, {}>>.add(values?: table.Values<table.TableDef<"Enemy", {
radius: d.F32;
}, {}>> | undefined): table.RowId<"Enemy">

Adds a row in the lowest free slot. Uses defaults for omitted fields. Throws when the table is full.

add
({
radius?: number | undefined
radius
: 0.5 });
const enemies: table.Table<table.TableDef<"Enemy", {
radius: d.F32;
}, {}>>
enemies
.
Table<TableDef<"Enemy", { radius: F32; }, {}>>.fields: table.Fields<table.TableDef<"Enemy", {
radius: d.F32;
}, {}>>

Provides one handle per field. Keep a handle outside a loop instead of reading fields.x inside it.

fields
.
radius: table.HostField<d.F32, "Enemy">
radius
.
function set(id: table.RowId<"Enemy">, value: number): void
set
(
const id: table.RowId<"Enemy">
id
, 2);
const enemies: table.Table<table.TableDef<"Enemy", {
radius: d.F32;
}, {}>>
enemies
.
Table<TableDef<"Enemy", { radius: F32; }, {}>>.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 radii: Float32Array<ArrayBufferLike>
radii
= new
var Float32Array: Float32ArrayConstructor
new <ArrayBufferLike>(buffer: ArrayBufferLike, byteOffset?: number, length?: number) => Float32Array<ArrayBufferLike> (+6 overloads)
Float32Array
(
const bytes: (buffer: GPUBuffer | TgpuBuffer<d.BaseData>) => Uint8Array

Returns the buffer bytes after queued writes and submitted copies. Changes to the returned array change the buffer.

bytes
(
const enemies: table.Table<table.TableDef<"Enemy", {
radius: d.F32;
}, {}>>
enemies
.
Table<TableDef<"Enemy", { radius: F32; }, {}>>.buffers: {
readonly host: (TgpuBuffer<table.Rows<{
radius: d.F32;
}>> & StorageFlag) | undefined;
readonly device: (TgpuBuffer<table.Rows<{}>> & StorageFlag) | undefined;
readonly live: TgpuBuffer<d.WgslArray<d.U32>> & StorageFlag;
readonly index: TgpuUniform<d.WgslStruct<{
span: d.U32;
mask: d.U32;
}>>;
}

Holds the rows; with history, a part's buffer holds last frame's rows from slot capacity.

buffers
.
host: (TgpuBuffer<table.Rows<{
radius: d.F32;
}>> & StorageFlag) | undefined
host
!).
Uint8Array<ArrayBufferLike>.buffer: ArrayBufferLike

The ArrayBuffer instance referenced by the array.

buffer
);
expect<number>(actual: number, message?: string): Assertion<void, number> (+1 overload)
expect
(
const radii: Float32Array<ArrayBufferLike>
radii
[
const enemies: table.Table<table.TableDef<"Enemy", {
radius: d.F32;
}, {}>>
enemies
.
Table<TableDef<"Enemy", { radius: F32; }, {}>>.slotOf(id: table.RowId<"Enemy">): number

Returns the slot for a row id. Throws if the id is stale.

slotOf
(
const id: table.RowId<"Enemy">
id
)]).
JestAssertion<void, number>.toBe: <number>(expected: number) => void

Checks that a value is what you expect. It calls Object.is to compare values. Don't use toBe with floating-point numbers.

@example expect(result).toBe(42); expect(status).toBe(true);

toBe
(2);
expect<number>(actual: number, message?: string): Assertion<void, number> (+1 overload)
expect
(
const log: RecordingLog
log
.
RecordingLog.submissions: number
submissions
).
JestAssertion<void, number>.toBe: <number>(expected: number) => void

Checks that a value is what you expect. It calls Object.is to compare values. Don't use toBe with floating-point numbers.

@example expect(result).toBe(42); expect(status).toBe(true);

toBe
(0); // flush writes through the queue and never submits
});

Code that draws to the screen takes a Surface. offscreenSurface(root, { width, height, format }) provides one backed by a texture on a recording or real root. Tests can run the same scene frames without a canvas.

import {
function offscreenSurface(root: TgpuRoot, options: OffscreenSurfaceOptions): OffscreenSurface

Creates a surface backed by a texture for running scene frames without a canvas. Accepts a recording root or a real device. view returns one view per texture. destroy destroys the texture, and later calls to view throw.

@param ― root any root, recording or real

@returns ― a surface you draw into and destroy

offscreenSurface
,
function recordingRoot(options?: RecordingOptions): RecordingRoot

Creates a recording WebGPU test double under a TypeGPU root. TypeGPU resolves shaders to WGSL. The double keeps the WGSL and records commands without compiling or executing shaders. Use it to check resources, pipelines and recording order without a GPU. Queue writes take effect, and submitted buffer copies and clears update bytes, so tests can check uploads and readbacks.

Throws for invalid mapping usage, out-of-bounds or unaligned copies and writes, missing resource usage flags, writes to destroyed buffers, and mapping in the wrong state. Also throws for non-attachment render targets, bind groups with incompatible resource usages, finishing an encoder with an open pass, and submitting destroyed resources. Throws at the call, where a device would report most of these errors later. Reports WebGPU's default limits and records render bundles, which replay into the pass that executes them. A device member it lacks throws an error that names it.

@returns ― the root, a log of what it saw, and the bytes it holds

recordingRoot
} from '@typegpu/kit/testing';
const {
const root: TgpuRoot
root
} =
function recordingRoot(options?: RecordingOptions): RecordingRoot

Creates a recording WebGPU test double under a TypeGPU root. TypeGPU resolves shaders to WGSL. The double keeps the WGSL and records commands without compiling or executing shaders. Use it to check resources, pipelines and recording order without a GPU. Queue writes take effect, and submitted buffer copies and clears update bytes, so tests can check uploads and readbacks.

Throws for invalid mapping usage, out-of-bounds or unaligned copies and writes, missing resource usage flags, writes to destroyed buffers, and mapping in the wrong state. Also throws for non-attachment render targets, bind groups with incompatible resource usages, finishing an encoder with an open pass, and submitting destroyed resources. Throws at the call, where a device would report most of these errors later. Reports WebGPU's default limits and records render bundles, which replay into the pass that executes them. A device member it lacks throws an error that names it.

@returns ― the root, a log of what it saw, and the bytes it holds

recordingRoot
();
const
const surface: OffscreenSurface
surface
=
function offscreenSurface(root: TgpuRoot, options: OffscreenSurfaceOptions): OffscreenSurface

Creates a surface backed by a texture for running scene frames without a canvas. Accepts a recording root or a real device. view returns one view per texture. destroy destroys the texture, and later calls to view throw.

@param ― root any root, recording or real

@returns ― a surface you draw into and destroy

offscreenSurface
(
const root: TgpuRoot
root
, {
OffscreenSurfaceOptions.width: number

Sets the texture width in pixels.

width
: 64,
OffscreenSurfaceOptions.height: number

Sets the texture height in pixels.

height
: 36,
OffscreenSurfaceOptions.format: GPUTextureFormat

Sets the texture format, as a canvas preferred format would.

format
: 'bgra8unorm' });
const
const scene: {
frame(frame: Frame): void;
}
scene
= await
function createScene(root: TgpuRoot, surface: Surface): Promise<{
frame(frame: Frame): void;
}>
createScene
(
const root: TgpuRoot
root
,
const surface: OffscreenSurface
surface
);
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 scene: {
frame(frame: Frame): void;
}
scene
.
function frame(frame: Frame): void
frame
(
frame: frames.Frame
frame
)).
Frames.run(seconds: number): boolean

Records and submits one frame with a fresh Frame and encoder. Requires finite, nondecreasing seconds. Throws for an invalid time, a nested frame or an asynchronous recorder. Returns false if the loop is destroyed before or during recording. Runs all after-submit callbacks. If any throw, the frame still counts and run rethrows their errors.

run
(0);
  • setSize({ width, height }) replaces the texture, as a canvas does on a new layout; targets that follow the surface rebuild at their next fit(). It takes the same fixed size as a canvas surface’s setSize.
  • surface.texture can be copied from, so a test on a real GPU can read its pixels.
  • pixelRatio is 1 unless you give one, such as pixelRatio: 3 for a scene that draws hairlines on a phone.
  • It pauses and resumes like a canvas surface, so a test can stage a scene on it or host one.

steppedSchedule() lets a test run animation frames requested by a host or animate. step(0, 16) runs one at 0 ms and the next at 16 ms. waiting reports whether a frame is requested.

const
const display: SteppedSchedule
display
=
function steppedSchedule(): SteppedSchedule

Creates a schedule stepped by hand. Each time passed to step runs the last requested animation frame, as requestAnimationFrame would at that time.

@returns ― a schedule to give animate or a host

steppedSchedule
();
const
const steps: number[]
steps
: number[] = [];
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
, ({
dt: number

Reports seconds since the previous submitted frame. The first frame has a dt of zero.

dt
}) =>
const steps: number[]
steps
.
Array<number>.push(...items: number[]): number

Appends new elements to the end of an array, and returns the new length of the array.

@param ― items New elements to add to the array.

push
(
dt: number

Reports seconds since the previous submitted frame. The first frame has a dt of zero.

dt
));
const
const animation: frames.Animation
animation
=
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
, {
AnimateOptions.schedule?: frames.Schedule | undefined

Supplies animation frames. Use runtimes.vsync(worker) on a React Native worker runtime, or a schedule you step by hand in a test. Default requestAnimationFrame.

schedule
:
const display: SteppedSchedule
display
});
const display: SteppedSchedule
display
.
SteppedSchedule.step(...milliseconds: number[]): void

Runs a requested frame for each given time, in milliseconds. Does nothing for times with no requested frame.

step
(0, 16, 32); // steps is [0, 0.016, 0.016]
const animation: frames.Animation
animation
.
Animation.destroy(): void

Stops requesting frames, even when called from a frame. Keeps the loop alive. Also [Symbol.dispose].

destroy
();
const display: SteppedSchedule
display
.
SteppedSchedule.waiting: boolean

Reports whether a requested frame is waiting to run or be cancelled.

waiting
; // false: a destroyed animation asks for no frame

Node and dawn.node have no createImageBitmap, so a mesh pool can’t decode a glTF model’s images and add throws. Give the scene a decode option that it hands to meshPool.create, and pass a stub from the test, such as a white pixel for every image; the page leaves it out:

import {
function offscreenSurface(root: TgpuRoot, options: OffscreenSurfaceOptions): OffscreenSurface

Creates a surface backed by a texture for running scene frames without a canvas. Accepts a recording root or a real device. view returns one view per texture. destroy destroys the texture, and later calls to view throw.

@param ― root any root, recording or real

@returns ― a surface you draw into and destroy

offscreenSurface
,
function recordingRoot(options?: RecordingOptions): RecordingRoot

Creates a recording WebGPU test double under a TypeGPU root. TypeGPU resolves shaders to WGSL. The double keeps the WGSL and records commands without compiling or executing shaders. Use it to check resources, pipelines and recording order without a GPU. Queue writes take effect, and submitted buffer copies and clears update bytes, so tests can check uploads and readbacks.

Throws for invalid mapping usage, out-of-bounds or unaligned copies and writes, missing resource usage flags, writes to destroyed buffers, and mapping in the wrong state. Also throws for non-attachment render targets, bind groups with incompatible resource usages, finishing an encoder with an open pass, and submitting destroyed resources. Throws at the call, where a device would report most of these errors later. Reports WebGPU's default limits and records render bundles, which replay into the pass that executes them. A device member it lacks throws an error that names it.

@returns ― the root, a log of what it saw, and the bytes it holds

recordingRoot
} from '@typegpu/kit/testing';
const {
const root: TgpuRoot
root
} =
function recordingRoot(options?: RecordingOptions): RecordingRoot

Creates a recording WebGPU test double under a TypeGPU root. TypeGPU resolves shaders to WGSL. The double keeps the WGSL and records commands without compiling or executing shaders. Use it to check resources, pipelines and recording order without a GPU. Queue writes take effect, and submitted buffer copies and clears update bytes, so tests can check uploads and readbacks.

Throws for invalid mapping usage, out-of-bounds or unaligned copies and writes, missing resource usage flags, writes to destroyed buffers, and mapping in the wrong state. Also throws for non-attachment render targets, bind groups with incompatible resource usages, finishing an encoder with an open pass, and submitting destroyed resources. Throws at the call, where a device would report most of these errors later. Reports WebGPU's default limits and records render bundles, which replay into the pass that executes them. A device member it lacks throws an error that names it.

@returns ― the root, a log of what it saw, and the bytes it holds

recordingRoot
();
const
const surface: OffscreenSurface
surface
=
function offscreenSurface(root: TgpuRoot, options: OffscreenSurfaceOptions): OffscreenSurface

Creates a surface backed by a texture for running scene frames without a canvas. Accepts a recording root or a real device. view returns one view per texture. destroy destroys the texture, and later calls to view throw.

@param ― root any root, recording or real

@returns ― a surface you draw into and destroy

offscreenSurface
(
const root: TgpuRoot
root
, {
OffscreenSurfaceOptions.width: number

Sets the texture width in pixels.

width
: 64,
OffscreenSurfaceOptions.height: number

Sets the texture height in pixels.

height
: 36,
OffscreenSurfaceOptions.format: GPUTextureFormat

Sets the texture format, as a canvas preferred format would.

format
: 'bgra8unorm' });
const
const white: {
width: number;
height: number;
pixels: Uint8Array<ArrayBuffer>;
}
white
= {
width: number
width
: 1,
height: number
height
: 1,
pixels: Uint8Array<ArrayBuffer>
pixels
: new
var Uint8Array: Uint8ArrayConstructor
new (elements: Iterable<number>) => Uint8Array<ArrayBuffer> (+6 overloads)
Uint8Array
([255, 255, 255, 255]) };
const
const scene: {
frame(frame: Frame): void;
}
scene
= await
function createScene(root: TgpuRoot, surface: Surface, options?: {
decode?: images.Decode;
}): Promise<{
frame(frame: Frame): void;
}>
createScene
(
const root: TgpuRoot
root
,
const surface: OffscreenSurface
surface
, {
decode?: images.Decode | undefined
decode
: async () =>
const white: {
width: number;
height: number;
pixels: Uint8Array<ArrayBuffer>;
}
white
});

The scene calls meshPool.create(root, { capacity: 32, decode: options.decode }). A model whose images are already { width, height, pixels } needs no decode, so a test can also swap each image for the white pixel before add.

Nothing should allocate during a frame. Run a couple of frames so TypeGPU’s first-use objects exist, then take the counts, run more and compare:

scene.test.ts
it<object>(name: string | Function, fn?: TestFunction<object> | undefined, options?: number): void (+1 overload)

Defines a test case with a given name and test function. The test function can optionally be configured with test options.

@param ― name - The name of the test or a function that will be used as a test name.

@param ― optionsOrFn - Optional. The test options or the test function if no explicit name is provided.

@param ― optionsOrTest - Optional. The test function or options, depending on the previous parameters.

@throws ― {Error} If called inside another test function.

@example

// Define a simple test
it('adds two numbers', () => {
expect(add(1, 2)).toBe(3);
});

@example

// Define a test with options
it('subtracts two numbers', { retry: 3 }, () => {
expect(subtract(5, 2)).toBe(3);
});

it
('creates nothing in a steady frame', async () => {
const {
const root: TgpuRoot
root
,
const log: RecordingLog
log
} =
function recordingRoot(options?: RecordingOptions): RecordingRoot

Creates a recording WebGPU test double under a TypeGPU root. TypeGPU resolves shaders to WGSL. The double keeps the WGSL and records commands without compiling or executing shaders. Use it to check resources, pipelines and recording order without a GPU. Queue writes take effect, and submitted buffer copies and clears update bytes, so tests can check uploads and readbacks.

Throws for invalid mapping usage, out-of-bounds or unaligned copies and writes, missing resource usage flags, writes to destroyed buffers, and mapping in the wrong state. Also throws for non-attachment render targets, bind groups with incompatible resource usages, finishing an encoder with an open pass, and submitting destroyed resources. Throws at the call, where a device would report most of these errors later. Reports WebGPU's default limits and records render bundles, which replay into the pass that executes them. A device member it lacks throws an error that names it.

@returns ― the root, a log of what it saw, and the bytes it holds

recordingRoot
();
const
const surface: OffscreenSurface
surface
=
function offscreenSurface(root: TgpuRoot, options: OffscreenSurfaceOptions): OffscreenSurface

Creates a surface backed by a texture for running scene frames without a canvas. Accepts a recording root or a real device. view returns one view per texture. destroy destroys the texture, and later calls to view throw.

@param ― root any root, recording or real

@returns ― a surface you draw into and destroy

offscreenSurface
(
const root: TgpuRoot
root
, {
OffscreenSurfaceOptions.width: number

Sets the texture width in pixels.

width
: 64,
OffscreenSurfaceOptions.height: number

Sets the texture height in pixels.

height
: 36,
OffscreenSurfaceOptions.format: GPUTextureFormat

Sets the texture format, as a canvas preferred format would.

format
: 'bgra8unorm' });
const
const scene: {
frame(frame: Frame): void;
}
scene
= await
function createScene(root: TgpuRoot, surface: Surface): Promise<{
frame(frame: Frame): void;
}>
createScene
(
const root: TgpuRoot
root
,
const surface: OffscreenSurface
surface
);
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 scene: {
frame(frame: Frame): void;
}
scene
.
function frame(frame: Frame): void
frame
(
frame: frames.Frame
frame
));
const loop: frames.Frames
loop
.
Frames.run(seconds: number): boolean

Records and submits one frame with a fresh Frame and encoder. Requires finite, nondecreasing seconds. Throws for an invalid time, a nested frame or an asynchronous recorder. Returns false if the loop is destroyed before or during recording. Runs all after-submit callbacks. If any throw, the frame still counts and run rethrows their errors.

run
(0);
const loop: frames.Frames
loop
.
Frames.run(seconds: number): boolean

Records and submits one frame with a fresh Frame and encoder. Requires finite, nondecreasing seconds. Throws for an invalid time, a nested frame or an asynchronous recorder. Returns false if the loop is destroyed before or during recording. Runs all after-submit callbacks. If any throw, the frame still counts and run rethrows their errors.

run
(1 / 60);
const
const before: RecordingCounts
before
=
const log: RecordingLog
log
.
RecordingLog.counts(): RecordingCounts

Returns counts of created objects. Steady frames should leave these counts unchanged.

counts
();
const loop: frames.Frames
loop
.
Frames.run(seconds: number): boolean

Records and submits one frame with a fresh Frame and encoder. Requires finite, nondecreasing seconds. Throws for an invalid time, a nested frame or an asynchronous recorder. Returns false if the loop is destroyed before or during recording. Runs all after-submit callbacks. If any throw, the frame still counts and run rethrows their errors.

run
(2 / 60);
const loop: frames.Frames
loop
.
Frames.run(seconds: number): boolean

Records and submits one frame with a fresh Frame and encoder. Requires finite, nondecreasing seconds. Throws for an invalid time, a nested frame or an asynchronous recorder. Returns false if the loop is destroyed before or during recording. Runs all after-submit callbacks. If any throw, the frame still counts and run rethrows their errors.

run
(3 / 60);
expect<RecordingCounts>(actual: RecordingCounts, message?: string): Assertion<void, RecordingCounts> (+1 overload)
expect
(
const log: RecordingLog
log
.
RecordingLog.counts(): RecordingCounts

Returns counts of created objects. Steady frames should leave these counts unchanged.

counts
()).
JestAssertion<void, RecordingCounts>.toEqual: <RecordingCounts>(expected: RecordingCounts) => void

Used when you want to check that two objects have the same value. This matcher recursively checks the equality of all fields, rather than checking for object identity.

@example expect(user).toEqual({ name: 'Alice', age: 30 });

toEqual
(
const before: RecordingCounts
before
);
});
  • A frame after a resize creates the new targets and the bind groups that read them, so take the counts once the size has settled.
  • Counts include only GPU objects. They omit JS allocations such as the handle returned by .with(...). Bind groups at setup, then set the pipeline on the pass during a frame.

A module’s destroy() frees every buffer, texture and query set it made, as its card’s owns lists. Take log.alive() before the module is made, run some frames, destroy it and compare:

glow.test.ts
it<object>(name: string | Function, fn?: TestFunction<object> | undefined, options?: number): void (+1 overload)

Defines a test case with a given name and test function. The test function can optionally be configured with test options.

@param ― name - The name of the test or a function that will be used as a test name.

@param ― optionsOrFn - Optional. The test options or the test function if no explicit name is provided.

@param ― optionsOrTest - Optional. The test function or options, depending on the previous parameters.

@throws ― {Error} If called inside another test function.

@example

// Define a simple test
it('adds two numbers', () => {
expect(add(1, 2)).toBe(3);
});

@example

// Define a test with options
it('subtracts two numbers', { retry: 3 }, () => {
expect(subtract(5, 2)).toBe(3);
});

it
('frees what it made when destroyed', () => {
const {
const root: TgpuRoot
root
,
const log: RecordingLog
log
} =
function recordingRoot(options?: RecordingOptions): RecordingRoot

Creates a recording WebGPU test double under a TypeGPU root. TypeGPU resolves shaders to WGSL. The double keeps the WGSL and records commands without compiling or executing shaders. Use it to check resources, pipelines and recording order without a GPU. Queue writes take effect, and submitted buffer copies and clears update bytes, so tests can check uploads and readbacks.

Throws for invalid mapping usage, out-of-bounds or unaligned copies and writes, missing resource usage flags, writes to destroyed buffers, and mapping in the wrong state. Also throws for non-attachment render targets, bind groups with incompatible resource usages, finishing an encoder with an open pass, and submitting destroyed resources. Throws at the call, where a device would report most of these errors later. Reports WebGPU's default limits and records render bundles, which replay into the pass that executes them. A device member it lacks throws an error that names it.

@returns ― the root, a log of what it saw, and the bytes it holds

recordingRoot
();
const
const surface: OffscreenSurface
surface
=
function offscreenSurface(root: TgpuRoot, options: OffscreenSurfaceOptions): OffscreenSurface

Creates a surface backed by a texture for running scene frames without a canvas. Accepts a recording root or a real device. view returns one view per texture. destroy destroys the texture, and later calls to view throw.

@param ― root any root, recording or real

@returns ― a surface you draw into and destroy

offscreenSurface
(
const root: TgpuRoot
root
, {
OffscreenSurfaceOptions.width: number

Sets the texture width in pixels.

width
: 64,
OffscreenSurfaceOptions.height: number

Sets the texture height in pixels.

height
: 36,
OffscreenSurfaceOptions.format: GPUTextureFormat

Sets the texture format, as a canvas preferred format would.

format
: 'bgra8unorm' });
const
const before: string[]
before
=
const log: RecordingLog
log
.
RecordingLog.alive(): string[]

Lists buffers, textures and query sets that have not been destroyed, in creation order, as [Buffer "label"]. Compare the list before createScene and after scene.destroy() to detect leaks.

alive
();
const
const glow: {
record(frame: Frame): void;
destroy(): void;
}
glow
=
function createGlow(root: TgpuRoot, surface: Surface): {
record(frame: Frame): void;
destroy(): void;
}
createGlow
(
const root: TgpuRoot
root
,
const surface: OffscreenSurface
surface
);
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 glow: {
record(frame: Frame): void;
destroy(): void;
}
glow
.
function record(frame: Frame): void
record
(
frame: frames.Frame
frame
));
for (let
let i: number
i
= 0;
let i: number
i
< 10;
let i: number
i
++)
const loop: frames.Frames
loop
.
Frames.run(seconds: number): boolean

Records and submits one frame with a fresh Frame and encoder. Requires finite, nondecreasing seconds. Throws for an invalid time, a nested frame or an asynchronous recorder. Returns false if the loop is destroyed before or during recording. Runs all after-submit callbacks. If any throw, the frame still counts and run rethrows their errors.

run
(
let i: number
i
/ 60);
const glow: {
record(frame: Frame): void;
destroy(): void;
}
glow
.
function destroy(): void
destroy
();
expect<string[]>(actual: string[], message?: string): Assertion<void, string[]> (+1 overload)
expect
(
const log: RecordingLog
log
.
RecordingLog.alive(): string[]

Lists buffers, textures and query sets that have not been destroyed, in creation order, as [Buffer "label"]. Compare the list before createScene and after scene.destroy() to detect leaks.

alive
()).
JestAssertion<void, string[]>.toEqual: <string[]>(expected: string[]) => void

Used when you want to check that two objects have the same value. This matcher recursively checks the equality of all fields, rather than checking for object identity.

@example expect(user).toEqual({ name: 'Alice', age: 30 });

toEqual
(
const before: string[]
before
);
});
  • Run frames first: TypeGPU makes most objects on first use, and a resize makes new targets.
  • The surface is borrowed, so it stays alive, and is in before.
  • What a library leaves to the garbage collector shows here too. A library can leave first-use buffers; take before after a warm-up frame.
  • A scene needn’t pass this check. What it drops, the garbage collector frees.

A recording root resolves WGSL without compiling it. Add *.test.gpu.ts tests on dawn.node to catch compiler errors and check shader results. Create their roots with gpuRoot() from @typegpu/kit/testing/dawn:

compile.test.gpu.ts
it<object>(name: string | Function, fn?: TestFunction<object> | undefined, options?: number): void (+1 overload)

Defines a test case with a given name and test function. The test function can optionally be configured with test options.

@param ― name - The name of the test or a function that will be used as a test name.

@param ― optionsOrFn - Optional. The test options or the test function if no explicit name is provided.

@param ― optionsOrTest - Optional. The test function or options, depending on the previous parameters.

@throws ― {Error} If called inside another test function.

@example

// Define a simple test
it('adds two numbers', () => {
expect(add(1, 2)).toBe(3);
});

@example

// Define a test with options
it('subtracts two numbers', { retry: 3 }, () => {
expect(subtract(5, 2)).toBe(3);
});

it
('runs the flow field on a real device', async () => {
const
const root: GpuRoot
root
= await
function gpuRoot(options?: GpuRootOptions): Promise<GpuRoot>

Creates a root on a dawn.node device. Call this inside a test. At test completion, it fails on uncaptured WebGPU errors not consumed by takeErrors and destroys the device. Calling root.destroy() leaves device destruction to this cleanup. The first call creates the Dawn instance.

@param ― options.features features the device must have, which throw when the adapter lacks one

@returns ― a root whose device the test's end destroys

gpuRoot
();
const
const surface: OffscreenSurface
surface
=
function offscreenSurface(root: TgpuRoot, options: OffscreenSurfaceOptions): OffscreenSurface

Creates a surface backed by a texture for running scene frames without a canvas. Accepts a recording root or a real device. view returns one view per texture. destroy destroys the texture, and later calls to view throw.

@param ― root any root, recording or real

@returns ― a surface you draw into and destroy

offscreenSurface
(
const root: GpuRoot
root
, {
OffscreenSurfaceOptions.width: number

Sets the texture width in pixels.

width
: 64,
OffscreenSurfaceOptions.height: number

Sets the texture height in pixels.

height
: 36,
OffscreenSurfaceOptions.format: GPUTextureFormat

Sets the texture format, as a canvas preferred format would.

format
: 'bgra8unorm' });
const
const scene: {
frame(frame: Frame): void;
}
scene
= await
function createScene(root: TgpuRoot, surface: Surface): Promise<{
frame(frame: Frame): void;
}>
createScene
(
const root: GpuRoot
root
,
const surface: OffscreenSurface
surface
); // Dawn compiles the WGSL here
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: GpuRoot
root
, (
frame: frames.Frame
frame
) =>
const scene: {
frame(frame: Frame): void;
}
scene
.
function frame(frame: Frame): void
frame
(
frame: frames.Frame
frame
)).
Frames.run(seconds: number): boolean

Records and submits one frame with a fresh Frame and encoder. Requires finite, nondecreasing seconds. Throws for an invalid time, a nested frame or an asynchronous recorder. Returns false if the loop is destroyed before or during recording. Runs all after-submit callbacks. If any throw, the frame still counts and run rethrows their errors.

run
(0); // and validates the frame here
}); // fails here on any uncaptured WebGPU error, and destroys the device
  • gpuRoot({ features }) throws when the adapter lacks one of features, so a test never runs a fallback it didn’t mean to. optionalFeatures are requested where the adapter has them, limits are the device’s required limits, and dawn holds flags for the Dawn instance, such as 'enable-dawn-features=allow_unsafe_apis'.
  • Call it inside a test: when the test ends, any uncaptured WebGPU error fails it, and the device is destroyed. A test that expects errors takes them first with takeErrors(), which waits for the queue and returns the errors so far:
expect<string>(actual: string, message?: string): Assertion<void, string> (+1 overload)
expect
((await
const root: GpuRoot
root
.
GpuRoot.takeErrors(): Promise<string[]>

Waits for the queue and returns pending uncaptured WebGPU errors, then clears them. Use this for expected errors in a test.

takeErrors
()).
Array<string>.join(separator?: string): string

Adds all the elements of an array into a string, separated by the specified separator string.

@param ― separator A string used to separate one element of the array from the next in the resulting string. If omitted, the array elements are separated with a comma.

join
('\n')).
JestAssertion<void, string>.toMatch: (expected: string | RegExp) => void

Check that a string matches a regular expression.

@example expect(message).toMatch(/hello/); expect(greeting).toMatch('world');

toMatch
(/bindGroupLayoutCount \(5\)/);
  • On a machine without a GPU, such as a CI runner, gpuRoot({ dawn: ['backend=null'] }) uses Dawn’s null backend. It compiles WGSL and validates every call at the default limits, and executes nothing, so a test that reads results needs a real adapter.
  • GPU tests run one file at a time. They catch WGSL compiler errors, binding and format limits, and incorrect shader results. To count pipelines, events and submissions on a GPU, use gpu-debug’s instrument.
  • Both levels load @typegpu/kit/testing/warnings, which fails any test during which TypeGPU warned, such as about >> on a u32. To let some through, a setup file of your own calls failOnWarnings({ allow: [/deprecated/] }) instead and disposes of what it returns in afterAll, which puts console.warn back.

For @typegpu/kit/testing/dawn, install its optional peers, webgpu for dawn.node and vitest, as dev dependencies. @typegpu/kit/testing/warnings needs only vitest. pnpm 11 stops an install at a build script it hasn’t been told about, and webgpu has one, which only clears macOS’s quarantine flag from a downloaded dawn.node. Allow it, or deny it:

pnpm-workspace.yaml
allowBuilds:
webgpu: false

Run both levels from one config:

vitest.config.ts
import typegpu from 'unplugin-typegpu/vite';
import { defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [typegpu()],
test: {
setupFiles: ['@typegpu/kit/testing/warnings'],
projects: [
{ extends: true, test: { name: 'unit', include: ['tests/**/*.test.ts'] } },
{
extends: true,
test: {
name: 'gpu',
include: ['tests/**/*.test.gpu.ts'],
pool: 'forks',
fileParallelism: false, // one GPU file at a time
},
},
],
},
});
  • A recording root keeps no texture contents, so a test that reads pixels runs on gpuRoot().
  • A recording root reports WebGPU’s default limits. Pass recordingRoot({ limits }) to reach a size guard. A device method it lacks throws, naming the method; run that test on gpuRoot().
  • Maps on a recording root complete in a microtask. To test code that depends on readback taking several frames, use a stand-in that delays mapping.