Rendered from AUTHORING.md at commit d894fc0. The package is imported here by its 0.1.0 name, typeshade.
Migrating a GLSL shader
§1–§11 are organised for someone authoring greenfield. This section answers the
question a MIGRATING consumer actually asks: my GLSL does X — what is the DSL spelling,
and does it survive on WGSL?
It exists because that lookup failed in practice. A real migration re-solved several problems the DSL already solved, purely because the feature was filed under a name nobody knew to search for. Every row below is a thing that was rebuilt by hand at least once.
| GLSL construct | DSL spelling | WGSL result | Notes |
|---|---|---|---|
uniform Block { … } (ours) | uniformStruct (§4) | @group/@binding var<uniform> | std140 layout comes from reflect(); never hand-count offsets |
uniform float u_x; (host prelude declares it) | externVar | the same reference, spelled per target | emits NOTHING; lands in reflect().requires |
uniform float u_x; (we declare it, host owns it) | hostUniform | @group/@binding var<uniform> | GLSL emits a LOOSE default-block uniform, not a block; reflect() marks owner: 'host' |
| a whole BLOCK the host owns | hostBlock | one @group/@binding var<uniform> | glsl: 'loose' flattens it to one uniform per member and rewrites blk.field → field ON THE IR; 'std140-block' (default) keeps the block. WGSL keeps the block either way — a host bind group is one unit |
| a host-provided FUNCTION | externFn | same call, per-target spelling | typed at the call site; no declaration emitted |
#ifdef FEATURE — we decide | a builder parameter and a plain if (§11) | no preprocessor | preferred: the losing arm is never built, so its bindings are never declared |
#ifdef FEATURE — the host decides | variantFamily | one module per point in the matrix | emitGuarded generates the #if ladder for a GLSL host that owns the define; every arm is byte-identical to the standalone variant. For a ladder that goes inside an #include, use emitGuardedFragment — same ladder, preamble returned as data |
| a variant that changes only a VALUE | overrideConst + overrideValues (§11) | override + pipeline constants | do NOT specialise: that multiplies pipelines for nothing |
#include "helper.glsl" | emitGlslFragment / emitFragment | a module fragment the host concatenates | returns preamble as DATA; never strip a header with a regex |
| a statement-level variant slot inside one module | composeModule + placeholder (§1) | same | statement slots only — it contributes no consts/structs/bindings, so it is not an #include substitute |
precision highp … on one declaration | the precision option on hostUniform | n/a (WGSL has no precision qualifiers) | the stage preamble is the default; this is for a fragment composed into a host program |
usampler2D / isampler2D | texture2duT / texture2diT (§4) | texture_2d<u32> / texture_2d<i32> | the sampler precision line is emitted for you |
#extension … : require | enables (§10) | enable …; | fails closed (SD0030) on a backend whose capProfile has no row |
| comparing two emits after an optimizer pass | semanticDiff | same | compares IR + reflection, so folding and renaming do not drown the diff; declare your prod plugins as transforms and their rewrites classify into explained instead of the fail-able buckets (§8) |
The builtin-value vocabulary — gl_* name → WGSL id
builtin(name, type)’s vocabulary is WGSL’s, typed as WgslBuiltinName — a gl_*
spelling or a typo is a tsc error naming the union. This lookup exists because the
reverse direction failed in practice: a GLSL-minded author reached for frag_coord
(accepted by the GLSL writer at the time), and the module died only when the WGSL
writer ran.
Upgrading from a build where frag_coord emitted fine? It did — on GLSL only. That
alias was retired precisely because of the asymmetry: a module authored against it
compiled on WebGL2 and failed on WGSL, so the trap only sprang on the target you were less
likely to be testing. Both writers now reject it with the same remedy — spell it
builtin('position', vec4fT) on the fragment input. The rename is spelling-only: GLSL
still reads gl_FragCoord and the emitted bytes do not move.
| GLSL global | DSL spelling | Notes |
|---|---|---|
gl_Position | builtin('position', vec4fT) on the VERTEX output | writes gl_Position on GLSL |
gl_FragCoord | builtin('position', vec4fT) on a FRAGMENT input | reads gl_FragCoord on GLSL. Mind the y-origin: GL window space is bottom-left, WGSL framebuffer space top-left — flip per target (or derive y-symmetric) before consuming .y |
gl_VertexID | builtin('vertex_index', u32T) | GLSL wraps the read as uint(gl_VertexID) (the DSL types it u32, GLSL’s is int) |
gl_InstanceID | builtin('instance_index', u32T) | same uint() wrap |
gl_FrontFacing | builtin('front_facing', boolT) | |
gl_FragDepth | builtin('frag_depth', f32T) as the return attr | |
gl_PointSize / gl_PointCoord | — unsupported on BOTH writers | not a WGSL gap being imposed on GLSL: point sprites have per-vendor size caps, and the map dropped them for instanced quad expansion (map/src/shaders/dsl/point.ts) — the emit error’s remedy says the same |
float mod(x, y) | the free fn mod() (floor-mod) | .mod()/% is TRUNC-mod (WGSL semantics) and now spells portably on GLSL too — pick by the semantics you mean on negatives |
Targeting WebGL2 only? Nothing above narrows what the GLSL writer can express — the
neutral names are spellings, not capabilities, and several of this vocabulary’s rules
exist to make WebGL2 output MORE defined (round emits roundEven; float % emits a
trunc-mod GLSL ES 3.00 actually compiles). For GLSL-only constructs the neutral surface
does not model, rawStmt (§1) accepts a { glsl }-only payload: the GLSL writer splices
it verbatim, and if the WGSL writer ever runs on that module it fails CLOSED (SD0030)
naming every site to port — the deliberate shape for a consumer who excludes WebGPU
today but may not forever.
Does it survive on WGSL?
capabilityMatrix([wgslBackend, glslEs300Backend]) answers that per capability, derived
from the backends’ own capProfiles rather than transcribed — so it cannot go stale
against them. §10 explains the three support classes it reports; SD0030’s hint points
back here when an emit fails closed.
Two honesty notes a reader needs before trusting a row:
- Support is not reachability.
f16,subgroupsandmultiviewhave profile rows and emit their directives, and NONE of them can be authored today — there is no f16 scalar, no subgroup intrinsic, and neithernum_viewsnorgl_ViewID_OVR. The matrix reports what the profile says;capability-reachability.test.tsis where the allowlist of known-unreachable caps lives, with a reason each. - A missing row is a hard stop, by design. It is not a hint to work around: emit throws rather than writing source the driver would reject.