Authoring shaders for performance

Write WGSL so reflection can build stable layouts. Bindings should be explicit, structs should be host-shareable, and hot paths should avoid per-frame resource identity changes.

Layouts mirror what each entry point statically uses, the same way WebGPU's layout: 'auto' behaves: bindings an entry never touches are omitted, visibility covers only the stages that actually read a binding, and sampled f32 textures are declared filterable exactly when the shader samples them through a filtering sampler (textureLoad-only access stays unfilterable, keeping rgba32float readbacks valid). Mismatches fail eagerly with structured errors — VGPU-LIMIT-STORAGE-VERTEX/-FRAGMENT when a storage binding would exceed a device's per-stage limits, and VGPU-SET-TEXTURE-FILTERABILITY when a non-filterable format meets a filtering sampler — instead of surfacing as native pipeline failures.

WGSL defaults

WGSL
1
2
3
4
5
6
struct Globals {
  time: f32,
  mouse: vec2f,
  enabled: u32,
}
@group(0) @binding(0) var<uniform> globals: Globals;
  • Use u32 instead of bool in host-written uniforms; encode false/true as 0/1.
  • Put target resolution in a uniform value sourced from target.size or target.texelSize.
  • Keep imported WGSL modules binding-free. Modules may export structs/functions/constants; entry shaders own @group/@binding declarations.
  • Prefer storage buffers plus instances for many similar particles or sprites.

JavaScript defaults

TypeScript
1
2
3
4
5
6
7
const globals = uniforms(gpu, { time: 0, mouse: [0, 0], enabled: 1 });
const draw = draw(gpu, { shader: WGSL, set: { globals } });
await draw.compile(target);
frameLoop(gpu, (f) => {
  globals.set({ time: clock(gpu).time, mouse });
  f.pass({ target }, (p) => p.draw(draw));
});

Use the performance playbook before writing a new shader: bundles for static draws, compile() for pre-warm, draw.group() for many objects, uniforms(gpu) for shared state, ping-pong for iterative effects, and target-owned depth/MSAA.