The WGSL loader emits an artifact for WGSL that doesn't validate; the error only surfaces when a pipeline compiles, with a confusing message
Packages: @vgpu/wgsl@0.6.0-rc.1 (loader-webpack, used through wgslTurbopackRule()), vgpu@0.6.0-rc.1 (shader artifact validation, pipeline compile errors)
Severity: developer experience. Shader mistakes reach production builds and fail only at runtime. The error message usually points away from the real problem.
Summary
A call with the wrong number of arguments into an imported WGSL function compiles through the webpack/Turbopack loader without any error or warning, in both development and minify: true builds. The loader emits a prepared ShaderSource v2 artifact. Its reflection was computed from source that no WGSL validator accepts.
The mistake surfaces later, in one of two ways depending on the shader:
- Small shader (repro below):
draw.compile()fails withVGPU-COMPILE-FAILED: WebGPU pipeline compilation failed.and a generic fix ("Check WGSL, pipeline bindings, device limits…").- The real message is only in
error.cause:too few arguments in call to 'c', expected 5, got 4. - It is reported against the minified code (
:1:317, functionc), so it maps to neither the source file nor the function name.
- The real message is only in
- Larger shader (a PBR module with several sampled textures passed through function parameters): creating the draw throws before any compile.
- The error is
Invalid ShaderSource field 'reflection.entryPoints[1].samplingPairs[4].texture': reference is not a sampled texture. - The reflection paired a sampler with an argument that wasn't a texture at all.
- The message names neither the shader file nor the WGSL error. It reads like a vgpu bug, not a shader typo.
- The error is
vgpu check <file> --require-validation catches the same file immediately, with the exact message: too few arguments in call to 'shade', expected 5, got 4.
Reproduction
Two files.
lib.wgsl:
export fn shade(color: vec3f, env: texture_2d<f32>, envSampler: sampler, lamps: texture_2d<f32>, flag: bool) -> vec3f {
let a = textureSampleLevel(env, envSampler, vec2f(0.5), 0.0).rgb;
let b = textureSampleLevel(lamps, envSampler, vec2f(0.5), 0.0).rgb;
return select(color * a, color * b, flag);
}entry.wgsl:
import { shade } from "./lib.wgsl";
@group(0) @binding(0) var env: texture_2d<f32>;
@group(0) @binding(1) var envSampler: sampler;
@group(0) @binding(2) var lamps: texture_2d<f32>;
@vertex fn vs_main(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f {
return vec4f(f32(i & 1u), f32(i >> 1u), 0.0, 1.0);
}
// The `lamps` argument was forgotten when `shade` gained it.
@fragment fn fs_main() -> @location(0) vec4f {
return vec4f(shade(vec3f(1.0), env, envSampler, false), 1.0);
}Script:
import { readFileSync, writeFileSync } from 'node:fs';
import loader from '@vgpu/wgsl/loader-webpack';
import { draw, target } from 'vgpu';
import { init } from 'vgpu/node';
const entry = new URL('./entry.wgsl', import.meta.url).pathname;
const code = await new Promise((ok, fail) => {
const result = loader.call({ resourcePath: entry, getOptions: () => ({ minify: true }),
async: () => (error, output) => (error ? fail(error) : ok(output)), addDependency() {} }, readFileSync(entry, 'utf8'));
if (typeof result === 'string') ok(result);
});
console.log('loader: emitted', code.length, 'bytes, no error'); // <- no diagnostic
writeFileSync('./entry.artifact.mjs', code);
const shader = (await import('./entry.artifact.mjs')).default;
const gpu = await init();
const d = draw(gpu, { shader, vertexCount: 3, label: 'repro' }); // succeeds
try { await d.compile(target(gpu, { size: [4, 4] })); }
catch (error) { console.log(error.code, error.message, '|', error.cause?.message); }Observed output:
loader: emitted 2234 bytes, no error
VGPU-COMPILE-FAILED WebGPU pipeline compilation failed. | Error while parsing WGSL: :1:317 error: too few arguments in call to 'c', expected 5, got 4The emitted reflection lists only one sampling pair (env/envSampler); the second textureSampleLevel in shade is silently dropped from it. In larger shaders the shifted arguments produce a pair whose "texture" is another kind of binding. That pair then trips validateReflection in shader-source-snapshot.js at draw creation.
Why it matters
The loader is the only place every shader of an app passes through at build time. A production build can ship a shader that can't compile. The failure then shows up on the first draw that uses it, which may sit behind a rarely visited screen, and the message hides both the file and the cause. We only found ours because an end-to-end test happened to open that screen.
Suggested fix
- Fail the build on invalid WGSL. The loader already resolves the import graph and reflects the final shader, so it could validate the linked output, as
vgpu check --require-validationdoes, and throw with source-mapped file, line and function names. If full validation is too costly, at least check call arity and argument types for exported functions it inlines; the reflection pass already walks those calls. An opt-out option is fine. - Make reflection fail rather than emit inconsistent metadata. When an argument bound to a
texture_*parameter isn't a texture (or a sampler parameter isn't a sampler), the loader should throw (e.g.VGPU-WGSL-REFLECT-ARGUMENT) instead of emitting a sampling pair that later failsvalidateReflection. - Better runtime messages:
VGPU-COMPILE-FAILEDshould surface the WGSL error inmessage, not only incause.- It should name the shader artifact (source path or
resourceProvenance, plus the draw label). - For minified artifacts, include the unminified function name when known.
- The
Invalid ShaderSource field …error should say "the shader artifact is inconsistent, runvgpu check <file> --require-validation" and name the file.
Acceptance criteria
- The repro's loader call rejects (or reports a diagnostic in the Turbopack/webpack error channel) with
too few arguments in call to 'shade', the fileentry.wgsland a line number. - No artifact the loader emits can fail
validateReflectionat runtime. - A
VGPU-COMPILE-FAILEDcaused by WGSL parsing includes the parser message and the shader's source path inerror.message.
Activity
0No activity yet.