Skip to content

@typegpu/kit, @typegpu/kit/debugpipelines · census

Pipeline warm-up

Compile pipelines behind a loading screen, list each stage's bindings, and find the pipelines warm-up missed.

kit.pipelines

cpu

Compiles pipelines behind a loading screen, lists a pipeline's bindings per stage, and provides blend states.

pipelines.compile(items, options?)

pipelines.bindingsOf(pipeline, options?) → Bindings

CompileOptionscompile · 2 fields
onProgress?

(fraction: number) => void= none

Receives the completed fraction whenever an item finishes compiling.

budget?

number= 8

Limits time spent starting compilations at a stretch, in milliseconds; starting an item resolves its WGSL on the calling thread.

BindingsOptionsbindingsOf · 1 field
limits?

Partial<Record<(typeof limitNames)[Overflow['kind']], number>>= the minimums WebGPU guarantees: 8 storage buffers, 12 uniform buffers, 16 textures, 16 samplers and 4 storage textures

Sets the per-stage limits to compare with, such as root.device.limits.

kind
cpu
constants
blend
types
WarmableStageBindingKindBindingOverflowBindings
Compare 1 ms and 32 ms budgets to see frames exceeding the 16.7 ms display interval. The ten simulated items take 1 to 4 ms to start and 12 to 42 ms to compile.

A pipeline compiles at first use. That frame waits while TypeGPU resolves WGSL and the browser compiles it. Compile pipelines before rendering to avoid that stall. pipelines.compile starts every compile before the frame loop runs, yielding to the page between them so a loading screen keeps drawing:

import {
import pipelines
pipelines
} from '@typegpu/kit';
await
var Promise: PromiseConstructor

Represents the completion of an asynchronous operation

Promise
.
PromiseConstructor.all<[Promise<void>, Promise<void>]>(values: [Promise<void>, Promise<void>]): Promise<[void, void]> (+1 overload)

Creates a Promise that is resolved with an array of results when all of the provided Promises resolve, or rejected when any Promise is rejected.

@param ― values An array of Promises.

@returns ― A new Promise.

all
([
const castle: {
ready: Promise<void>;
}
castle
.
ready: Promise<void>
ready
, // the model's files
import pipelines
pipelines
.
function compile(items: readonly pipelines.Warmable[], options?: pipelines.CompileOptions): Promise<void>

Starts compiling every item, yielding to the event loop past the time budget so the page's other work, such as a loading animation, keeps running. Use before the frame loop starts. Waits for every item to finish, then rejects if any failed.

@param ― items pipelines, and modules that compile their own

@returns ― a promise that resolves once every item has compiled

compile
([
const field: {
initAsync(): Promise<unknown>;
}
field
,
const final: {
initAsync(): Promise<unknown>;
}
final
], {
CompileOptions.onProgress?: ((fraction: number) => void) | undefined

Receives the completed fraction whenever an item finishes compiling. Default none.

onProgress
: (
fraction: number
fraction
) => (
const bar: HTMLProgressElement
bar
.
HTMLProgressElement.value: number

The value property of the HTMLProgressElement interface represents the current progress of the element.

MDN Reference

value
=
fraction: number
fraction
),
}),
]);
  • compile(items) calls initAsync() on each item and resolves once all of them finish. An item is a TypeGPU pipeline, or a module, which compiles all of its pipelines there.
  • Each start resolves WGSL and creates a shader module on the main thread. The browser compiles in the background. budget limits time spent starting compiles at a stretch before compile yields to the event loop, but at least one item starts. The default is 8 ms, about half a 60 Hz frame. A larger budget can make frames late without increasing compiler throughput.
  • onProgress(fraction) runs as each item finishes; every item counts the same.
  • The promise rejects after every item finishes. Multiple failures produce an AggregateError. A missing inject() fails with an error that names the fill, and a stage over its binding limit with an error that lists its bindings.
  • Wait for the promise before recording. TypeGPU throws if a pipeline’s initAsync is pending. A frame loop that starts early returns from its recorder until then. A coroutine waits with yield* coroutines.until(compiled).

WebGPU guarantees each shader stage 8 storage buffers, 12 uniform buffers, 16 textures, 16 samplers and 4 storage textures. A binding counts in every stage its layout entry is visible to, whether or not that stage reads it. TypeGPU makes most automatic bindings visible to both the vertex and the fragment stage, so a uniform only the vertex shader reads still counts in the fragment stage. A lit renderer adds up the camera, the lighting, its shadows and every lighting hook’s resources.

When a pipeline in compile fails while a stage is over its limit, the error lists that kind’s bindings by name, which TypeGPU takes from each resource’s label:

The pipeline 'figure' binds more than WebGPU guarantees per stage:
- vertex: 13 uniform buffers, over 12
- fragment: 13 uniform buffers, over 12
Its uniform buffers:
camera (group 0, binding 0; vertex, fragment)
paint (group 0, binding 7; vertex, fragment)
…

pipelines.bindingsOf(pipeline) returns the same list at any time: every entry of the pipeline’s bind group layouts with its group, binding, name, kind and the stages that count it, and over for the stages above their limits. It counts entries the shader never reads, such as the other parts of a shared layout. An external texture counts as 4 textures, a sampler and a uniform buffer. Pass { limits: root.device.limits } to compare with the device’s own limits.

To make room:

  • Fill a lighting hook on the renderers that read it, with things.renderer().pipe((cfg) => cfg.with(shading.fog, fogged)), instead of in lighting.create. A hook filled there binds its resources into every lit pipeline.
  • Pack uniforms that change together into one struct.
  • Keep per-object values in a buffer the pipeline already binds, such as an instance kind’s host fields.

kit.census

tool

Counts pipelines, their bindings per stage, and late compiles.

census.create(root, options?) → Census

rootborrows
the root
Censusreturns
counts that grow as pipelines compile, until you destroy it
types
CensusEntryCensus
Optionscreate · 1 field
limits?

BindingsOptions['limits'] & { readonly maxBindGroups?: number }= the minimums WebGPU guarantees: 4 bind groups, 8 storage buffers, 12 uniform buffers, 16 textures, 16 samplers and 4 storage textures per stage

Sets the limits to compare with, such as root.device.limits.

lifecycle

destroy()[Symbol.dispose]

A census from @typegpu/kit/debug finds what warm-up missed. Create it right after the root, in development only, and call warm() once warm-up is done:

import {
import census
census
} from '@typegpu/kit/debug';
const
const counted: census.Census
counted
=
import census
census
.
function create(root: TgpuRoot, options?: census.Options): census.Census

Wraps the device's creation methods to count shader modules, pipelines and the bindings in each pipeline's layout, until destroyed. Create it right after the root, before anything compiles, so it sees every layout.

@returns ― counts that grow as pipelines compile, until you destroy it

create
(
const root: TgpuRoot
root
); // before any compile
await
import pipelines
pipelines
.
function compile(items: readonly pipelines.Warmable[], options?: pipelines.CompileOptions): Promise<void>

Starts compiling every item, yielding to the event loop past the time budget so the page's other work, such as a loading animation, keeps running. Use before the frame loop starts. Waits for every item to finish, then rejects if any failed.

@param ― items pipelines, and modules that compile their own

@returns ― a promise that resolves once every item has compiled

compile
([
const field: {
initAsync(): Promise<unknown>;
}
field
]);
const counted: census.Census
counted
.
Census.warm(): void

Marks warm-up complete. Later synchronous compilations count as late.

warm
(); // a compile from now on is late
  • It wraps the device’s createShaderModule, createBindGroupLayout, createPipelineLayout and four pipeline methods. destroy(), or using, restores them.
  • late lists the pipelines created synchronously after warm(), which compiled mid-frame. Play through the game to catch an effect or a menu that compiles on first use.
  • Each entry of pipelines lists its pipeline’s layout entries in bindings, counted as bindingsOf counts them, its bind groups in groups, including TypeGPU’s automatic group, and the stages over a limit in over.
  • overBudget lists the pipelines with a stage over a limit or more bind groups than maxBindGroups. The limits default to the minimums WebGPU guarantees on every device.
  • modules and wgslBytes count the shader modules and their WGSL.

pipelines.blend.add adds light, for sparks, glow and bloom. pipelines.blend.over lays premultiplied colour over what is there, for a fragment that returns vec4f(rgb * a, a), and pipelines.blend.straight lays straight colour, for one that returns vec4f(rgb, a). Give any of them to a pipeline’s target:

root.createRenderPipeline({ vertex, fragment, targets: { format, blend: pipelines.blend.add } });
  • The budget covers only compilation starts. Allow time for the rest of your frame. At 120 Hz a frame lasts about 8 ms, so use a smaller budget.
  • Progress counts items, so a module of six pipelines moves the bar as much as one pipeline. Pass a module’s pipelines one by one to count each.
  • Started compiles can’t be cancelled, so a typo in the first item is reported only after the slowest compile.
  • Nothing warms a pipeline the first frames don’t use, such as an effect that fires later. On WebKit it stalls the first time it draws.
  • Only pipelines compiled through compile, directly or inside a module, fail with the binding list. A pipeline that compiles at its first draw reports the device’s error.