#491

WGSL loader emits artifacts for WGSL that doesn't validate; errors surface late and point away from the cause

Openmatiasngfopened 1mo ago

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:

  1. Small shader (repro below): draw.compile() fails with VGPU-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, function c), so it maps to neither the source file nor the function name.
  2. 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.

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 4

The 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

  1. 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-validation does, 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.
  2. 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 fails validateReflection.
  3. Better runtime messages:
    • VGPU-COMPILE-FAILED should surface the WGSL error in message, not only in cause.
    • 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, run vgpu 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 file entry.wgsl and a line number.
  • No artifact the loader emits can fail validateReflection at runtime.
  • A VGPU-COMPILE-FAILED caused by WGSL parsing includes the parser message and the shader's source path in error.message.

Activity

0

No activity yet.