Shader diagnostics and fix-its

Use these messages as the self-correction map for generated shader code. Prefer fixing the shader/binding shape over suppressing errors.

VGPU-RESOLVE-MODULE-BINDING

WGSL modules must be pure helpers. A module may export structs, functions, constants, and types, but it must not declare @group(...) @binding(...) variables. Move bindings to the entry shader:

WGSL
1
2
3
// noise.wgsl
export struct NoiseConfig { seed: u32 }
export fn noise(p: vec2f, cfg: NoiseConfig) -> f32 { return f32(cfg.seed) * 0.0; }
WGSL
1
2
3
// entry.wgsl
import { NoiseConfig, noise } from "./noise.wgsl";
@group(0) @binding(0) var<uniform> cfg: NoiseConfig;

VGPU-SHADER-SOURCE-INVALID

main API (vgpu) shader arguments are either a WGSL string or a loader ShaderSource { version: 1, wgsl }. If importing .wgsl returns a URL/object without version and wgsl, configure @vgpu/wgsl/loader-vite, @vgpu/wgsl/loader-webpack, or pass a raw WGSL string.

TypeScript
1
2
import shader from "./shader.wgsl";
const draw = draw(gpu, { shader });

Stage storage limits: VGPU-LIMIT-STORAGE-VERTEX / VGPU-LIMIT-STORAGE-FRAGMENT

Symptom: creating a draw reports that the selected vertex or fragment entry uses more storage buffers than the device grants for that stage.

Cause: bindings statically reached by the selected entry point count against maxStorageBuffersInVertexStage or maxStorageBuffersInFragmentStage (falling back to maxStorageBuffersPerShaderStage when a stage-specific property is unavailable). Unused declarations and resources used only by another stage do not count.

Fix: if the adapter supports it, request the reported count through init({ requiredLimits: { maxStorageBuffersInVertexStage: count } }) or the fragment sibling. For vertex data, prefer geometry(gpu, ...) vertex streams where possible. Otherwise reduce the number of storage buffers reached by that stage. The error detail includes stage, entryPoint, count, limit, and the { name, group, binding } bindings that were counted.

Missing binding: VGPU-R1-BINDING-NEVER-SET

Every reflected binding must be set by name or covered by a claimed group. Do not rely on globals or implicit buffers.

TypeScript
1
2
const effect = effect(gpu, WGSL);
effect.set({ params: { time: clock(gpu).time }, tex: target.color, samp: sampler(gpu) });

Ownership flip: VGPU-R1-OWNERSHIP-FLIP

The first set() decides ownership. Plain JS values are lib-owned and updated in place. Resources (Uniform, storage, textures, samplers, bind groups) are user-owned. Do not switch the same binding from JS value to resource later.

TypeScript
1
2
3
4
// Pick one from the start:
wave.set({ params: { time: 0 } });     // lib-owned
// or
wave.set({ params: sharedUniform });   // user-owned

Bool host-shareable layouts

Rule of thumb: treat every bool host-shareable uniform as a u32 in WGSL. WGSL bool is not a stable host-shareable uniform field for JS packing. Use u32 and encode booleans as 0 or 1.

WGSL
1
struct Params { enabled: u32 }

Bundle stale

VGPU-R3-BUNDLE-STALE means a bundle was recorded for a different render signature or an old bind-group/resource identity. A bundle survives resizing the target it draws onto when formats/depth/sample count match; re-record after resource identity changes, including sampling a resized target. Plain JS set() updates are safe because buffers are written in place.

Manual bind-group claims

VGPU-R4-GROUP-CLAIMED, VGPU-R4-GROUP-INCOMPATIBLE, and VGPU-R4-GROUP-VALIDATION all point to manual bind-group ownership. Build the bind group with draw.layout(group) or draw.layout(group, { dynamicOffsets: true }), call draw.group(group, bindGroup), and send dynamic offsets through p.draw(draw, { offsets }).

Compute aliasing

VGPU-R1-STORAGE-ALIASING means a writable storage buffer is bound as both source and destination. Use pingPongStorage(gpu) and swap after dispatch.