API reference
A selected tour of the surface exported from @xgis/shader-dsl.
See the barrel
and examples for the rest.
Module & functions
fn(name?, params, ret?, body, opts?) Declare a function. name is optional (auto _fn{n}); pass a ShaderType in the ret slot to PIN the return type, or omit it to infer from the body’s return. A param spec is a ShaderType, an IO field spec, or an ioStruct / structDecl HANDLE — a handle param arrives in the body as a typed field proxy (no .of() re-assertion). opts.stage = "vertex" | "fragment" | "compute" makes it a pipeline entry; opts.retAttr / workgroupSize for bare-return / compute. The returned handle is CALLABLE — prefer calling it directly, in the object form f({ a, b }), which type-checks argument names + types.
module({ structs, bindings, funcs, consts? }) Assemble a module from its declarations. funcs is an array (order preserved) or a key-named record; fns called through their handle are collected transitively (define-before-use), so funcs lists only the entry points.
externFn(name, params, ret) A typed CALL-ONLY handle for a fn whose body is defined elsewhere (forward declaration) — call it now, f({ a, b }) / f(a, b), and the body links in at emit. Use it when the callee cannot be an importable FnHandle at the caller’s load time.
callFn(name, ret, …args) Low-level string call that externFn emits under the hood. Prefer calling the FnHandle directly (myFn(a, b)) or an externFn: a bare string name is an out-of-band contract — a typo compiles, then fails at WGSL link.
Layout declarators (single source of truth)
Declare a layout ONCE; the struct, the binding, the access node, and typed field access are all derived — they cannot drift.
uniformStruct(name, { group, binding, as }, fields) A uniform block + its binding. .field.x is typed access; a field can be arrayOf(handle, n) → .field.arr.at(i) gives typed element access. .struct / .binding go into module().
ioStruct(name, fields) A vertex/fragment IO struct. .decl, .type, .of(node).field, .var(name?) (a typed var proxy; .$ is the raw node), .construct({…}).
structDecl(name, fields) A plain / storage-element struct.
storageBuffer(name, element, { group, binding, access }) An array<Element> storage buffer; .at(i) is the typed element accessor.
resource(name, type, { group, binding }) A non-struct bound resource (texture / sampler).
builtin(name, type) · location(n, type, interpolate?) IO field specs: @builtin(name) / @location(n).
Values & constructors
f32(v) i32(v) u32(v) bool(v) Scalar literals (take a JS number / boolean).
vec2/vec3/vec4(...) · vec2u vec2i Vector constructors; accept nodes, numbers, and sub-vectors (vec4(rgb, 1)).
Var(name?, init) · Var(type, init?) · Let(name?, value) Local bindings. Var → a MUTABLE Node (.assign allowed); Let and params → ReadonlyNode (.assign is a tsc error). Name a Let to force a CSE-stable named temp.
constExpr(name, type, value) · arrayLit(elem, …items) A module-scope const from a value node, and an array literal (pairs with arrayT) — the declared way to author non-scalar consts.
toF32(x) toI32(x) toU32(x) Scalar casts (emit float()/int()/uint() on GLSL).
Operators (methods on a value node)
.add .sub .mul .div .neg() Arithmetic, with WGSL vec∘scalar broadcast — including scalar-node × vec-node (f32 · vec3<f32> → vec3<f32>). A vec2∘vec3 mismatch is a TS error.
.mod(y) TRUNC-mod (WGSL %) — integer-only on GLSL ES 3.00 floats. For a PORTABLE float modulo use the free mod(x, y) below (floor-mod, both targets).
.lt .gt .le .ge .eq .ne Comparisons → bool.
.and .or · .bitAnd .bitOr .bitXor .shl .shr Logical (bool) and bitwise (u32/i32).
.x .y .z .w · .rgb .xy .xyz … · .swizzle("…") Component access and swizzles. .swizzle("rgb") infers its result key from the component string (elem-typed; works on u32/i32 vecs too).
.assign(v) · .select(a, b) lvalue store; condition ? a : b (on a bool node).
Built-in functions
sin cos tan asin acos atan atan2 Trigonometry.
exp log log2 pow sqrt Exponential / power.
floor ceil fract abs sign min max clamp mix smoothstep Common math / interpolation.
mod(x, y) Portable FLOOR-mod (x − y·⌊x/y⌋), identical on WGSL + GLSL; negatives wrap into [0, y). THE float modulo — reach for it over the .mod method, which is trunc-mod and invalid on GLSL ES 3.00 floats.
length dot radians degrees fwidth Geometry, angle conversion, screen-space derivative (GPU-only).
textureSample textureLoad textureDimensions Texture access (GLSL fuses sampler into the combined sampler2D).
bitcastU32 pack4x8unorm unpack4x8unorm Bit-level helpers.
Control flow
If(cond, () => …).elif(cond, …).else(…) Conditional statement blocks. A body may return value for an early return.
Loop(init, i => cond, i => body, step?) C-style for over the innermost scope; the body receives the typed counter Node. Optional leading name; read positions accept ReadonlyNode.
Return(value?) · ReturnIf(cond, value) · Break() · Continue() · Discard() Explicit / guard-clause returns and loop control. Discard() kills the fragment (GLSL discard).
reduce(init, i0, i => cond, (acc, i) => next, step?) Immutable loop-fold: the body RETURNS the next accumulator (no Var + assign at the call site); materialises the var + loop internally, byte-identical to the hand form. Returns the accumulator Node.
when(c, () => a, () => b) · when([[c, () => e]], () => fallback) Immutable value-by-CONDITION dispatch — var + if/elif/else, NOT select (only the taken arm runs). The blessed conditional expression; supersedes ifExpr/condExpr.
Switch(scrut).case(n, body).default(body?) Chainable switch STATEMENT builder; lowers to a real WGSL switch. Forward-declare a Var(default), then assign inside the case arms.
enumU32({ A: 0, … }) · matchEnum(s, E, { A, … }) Exhaustive integer dispatch: matchEnum requires ONE arm per enum member — omit one and tsc errors, so adding a member surfaces every un-handled site. Lowers to matchExpr (byte-identical).
matchExpr(scrutinee, cases, default) The low-level multi-arm dispatch matchEnum / Switch lower to. Prefer matchEnum when the scrutinee is an enum, for exhaustiveness.
Types
f32T i32T u32T boolT Scalar types.
vec2fT vec3fT vec4fT · vec2uT vec3uT vec4uT · vec2iT vec4iT Vector types.
mat4x4fT · texture2dfT texture2dMsfT · arrayT(elem) structT(name) Matrix, texture, and composite types.
f64T · vec2f64T vec3f64T vec4f64T Emulated double precision (df64, ~48-bit) — declare a value f64T and the SAME operators/built-ins work; f64(v) lifts a literal, toF64 / toF32 convert. The host writes 1.0 to the auto-injected _fp64 guard binding, and splitF64(x) packs a JS number into the hi/lo attribute pair. See the fp64 examples.
Emit & reflect
emitModule(m, opts?) → WGSL string (WebGPU). Byte-stable. opts is the optional { plugins } bag below.
emitGlslModule(m, "vertex" | "fragment", opts?) → GLSL ES 3.00 string (WebGL2). Throws on compute / SSBO / MSAA-load. Accepts the same { plugins } bag.
EmitOptions — { plugins?: EmitPlugin[] } A Vite/Webpack-style plugin bag. An EmitPlugin is { name, transformIR?, transformText? }; hooks fire staged across all plugins — every transformIR (on the LOWERED module, must be deterministic) before assembly, then every transformText on the string. The core knows nothing about the implementations.
emitModuleWithReflection(m, backend) → { code, reflection } (code byte-identical to emitModule). Dev tooling — import from @xgis/shader-dsl/dev (#740 R2b).
reflect(m) → { bindGroups, uniforms (std140), storage (std430), entries }. Read-only over the IR.
wgslLayout(struct, "std140" | "std430") The standalone offset engine reflect() uses.
Production emit — @xgis/shader-dsl/emit-prod
Ship-time plugins, composed Vite/Webpack-style: emitModule(m, { plugins: [mangle(), minify()] }). On their OWN subpath — your bundler minifies the JS, these do the same for the shader strings it can never reach, and a runtime-emit consumer that never imports the subpath bundles zero bytes of them. The plain emit stays byte-identical.
mangle({ renames? }) → EmitPlugin The rename plugin: helper fn names, plain struct names, module consts (including the injected df64_* library) become _f0/_S0/_k0. Deterministic per module, so the two GLSL stage emits always agree on shared names and programs link. Pass a Map as renames to receive authored → emitted names — the shader "source map" for decoding production driver logs and GPU captures.
minify() → EmitPlugin The compaction plugin. Token-safe by construction (WGSL/GLSL have no string literals; # directives keep their own line). minifyShaderText(code) is the raw function it wraps, for a string you already hold. No semantic change — the CI gate compiles AND pixel-compares obfuscated output on real Tint + ANGLE.
inline() → EmitPlugin Call-graph flattening (obfuscation): inlines every safely-inlinable helper at all its call sites so those functions vanish — single-return helpers by substitution, and LINEAR multi-statement helpers (a let/var prelude + one return) by lifting their statements into the caller. Leaves the df64 library, entry points, recursive fns, and control-flow helpers intact. NOT a size win — a multi-call helper is duplicated per site; the point is removing structure. Opt-in; place before mangle(): { plugins: [inline(), ...obfuscate()] }.
obfuscate({ renames? }) → EmitPlugin[] The standard preset, [mangle(opts), minify()]. Spread into { plugins }. inline() is deliberately NOT in the preset (it can grow bytes) — add it explicitly when you want the call graph gone too.
never renamed (the ABI boundary) Entry-point names (WebGPU entryPoint refers to them) · binding names incl. the _fp64 guard (hosts resolve them by name) · binding-struct names (the GLSL UBO block tag, getUniformBlockIndex) · struct field names (std140 packing + GLSL varyings link vertex↔fragment BY NAME). Reflection-driven hosts bind unchanged.
Validation passes
validate() is the emit-time gate on the main entry. The lint / capability analysis tooling moved to the @xgis/shader-dsl/dev subpath (#740 R2b) — dev-time only, never on the emit path.
validate(m) Type/structure validation — the emit-time gate (main entry).
lintModule(m) · requiredCaps(m) · assertCaps(caps, backend) The 21-rule lint engine and capability analysis (compute the caps a module needs; assert a backend supports them). Import from @xgis/shader-dsl/dev.