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
struct Globals {
time: f32,
mouse: vec2f,
enabled: u32,
}
@group(0) @binding(0) var<uniform> globals: Globals;- Use
u32instead ofboolin host-written uniforms; encode false/true as0/1. - Put target resolution in a uniform value sourced from
target.sizeortarget.texelSize. - Keep imported WGSL modules binding-free. Modules may export structs/functions/constants; entry shaders own
@group/@bindingdeclarations. - Prefer storage buffers plus
instancesfor many similar particles or sprites.
JavaScript defaults
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.