Rendered from AUTHORING.md at commit 26de7be8. The package is imported here by its 0.2.0 name, typeshade.
Migrating a GLSL shader
After this page you can take the GLSL construct in front of you, look up how it is spelled in the DSL, and see what that spelling turns into on each target. The rest of this guide is ordered for someone writing a new shader. This page is ordered for someone holding a working GLSL ES 3.00 shader and asking what each line becomes.
The construct table
A construct here is one piece of GLSL source you have to account for: a uniform block, a
preprocessor branch, an #include, an extension directive. Find the row, follow the
spelling to the page that explains it, and read the note for what changes.
| GLSL construct | DSL spelling | WGSL result | Notes |
|---|---|---|---|
uniform Block { … } that this module owns | uniformStruct | @group/@binding var<uniform> | the std140 layout comes from reflect(), so no offset is counted by hand |
uniform float u_x; that the host prelude already declares | externVar | the same reference, spelled per target | emits nothing, and lands in reflect().requires |
uniform float u_x; that this module declares and the host owns | hostUniform | @group/@binding var<uniform> | GLSL emits a loose default-block uniform instead of a block, and reflect() marks it 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 to field on the IR. The default 'std140-block' keeps the block. WGSL keeps the block either way, because a host bind group is one unit |
| a function the host provides | externFn | the same call on both targets | typed at the call site, with no declaration emitted |
#ifdef FEATURE where this module decides | a builder parameter and a plain if | no preprocessor | the losing arm is never built, so its bindings are never declared |
#ifdef FEATURE where the host decides | variantFamily | one module per point in the matrix | emitGuarded generates the #if ladder for a GLSL host that owns the define, and every arm is byte-identical to the standalone variant. For a ladder that goes inside an #include, emitGuardedFragment returns the same ladder with the preamble as data |
| a variant that changes only a value | overrideConst with overrideValues | override plus pipeline constants | building a separate variant for this multiplies pipelines for nothing |
#include "helper.glsl" | emitGlslFragment and emitFragment | a module fragment the host concatenates | the header comes back as preamble, as data |
| a statement-level variant slot inside one module | composeModule with placeholder | the same | statement slots only, so it does not replace an #include |
precision highp … on one declaration | the precision option on hostUniform | nothing, since WGSL has no precision qualifiers | the stage preamble is the default. This option is for a fragment composed into a host program |
usampler2D and isampler2D | texture2duT and texture2diT | texture_2d<u32> and texture_2d<i32> | the sampler precision line is emitted for you |
#extension … : require | enables | enable …; | fails closed with SD0030 on a backend whose profile has no row. That page also says which capabilities survive on WGSL |
| comparing two emits after an optimizer pass | semanticDiff | the same | compares IR and reflection, so folding and renaming do not drown the diff. Declare the production plugins as transforms and their rewrites classify into explained |
A block this module owns is the most common first row. uniformStruct takes the WGSL type
name, the slot, and the field map, and gives back typed field access:
import { mat4x4fT, uniformStruct, vec2fT } from 'typeshade'
// uniform Camera { mat4 u_matrix; vec2 u_viewport_px; } u_camera;const camera = uniformStruct( 'Camera', { group: 0, binding: 0, as: 'u_camera' }, { u_matrix: mat4x4fT, u_viewport_px: vec2fT },)
const mvp = camera.field.u_matrixBuiltin values
A builtin is a value the hardware supplies to a stage. The DSL’s vocabulary for them is
WGSL’s, typed as the closed union WgslBuiltinName, so a gl_* spelling or a typo is a
tsc error that names the union. Each backend then spells the id its own way.
| 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 and WGSL framebuffer space is top-left, so flip per target, or derive a y-symmetric value, before consuming .y |
gl_VertexID | builtin('vertex_index', u32T) | GLSL wraps the read as uint(gl_VertexID), since the DSL types it u32 and GLSL’s is int |
gl_InstanceID | builtin('instance_index', u32T) | the same uint() wrap |
gl_FrontFacing | builtin('front_facing', boolT) | |
gl_FragDepth | builtin('frag_depth', f32T) as the return attribute | |
gl_PointSize and gl_PointCoord | unsupported on both writers | point size caps vary per vendor, and WebGPU point primitives are always one pixel. Expand an instanced quad in the vertex stage and interpolate a @location(n) corner uv |
float mod(x, y) | the free function mod() | that is floor-mod. .mod() and % are trunc-mod, which is WGSL’s semantics and spells portably on GLSL too. Pick by the semantics you mean on negative operands |
A fragment stage reads the framebuffer coordinate through the same position builtin the
vertex stage writes:
import { builtin, f32, fn, vec4, vec4fT } from 'typeshade'
const fsCoord = fn( 'fs_coord', { pos: builtin('position', vec4fT) }, (p) => vec4(p.pos.x.mul(0.001), p.pos.y.mul(0.001), f32(0), f32(1)), { stage: 'fragment', retAttr: '@location(0)' },)Declarations the host owns
Four of the rows above are about the same question, which is who declares a symbol and who
owns the memory behind it. externVar is for a symbol the host prelude already declares. It
emits nothing, gives you a typed reference, and takes a per-target spelling so a move to a
host that exposes the value differently is a map change. externFn forward-declares a
callable whose body is defined elsewhere and linked in at emit, so the call is typed and no
declaration is emitted. hostUniform and hostBlock are for symbols this module declares
while the host owns the storage, so they do emit a declaration and mark it owner: 'host' in
reflection.
hostBlock also carries the choice a GLSL host forces, since a prelude provides either a
std140 block or loose uniforms and the wrong one will not link:
import { hostBlock, mat4x4fT, vec2fT } from 'typeshade'
const camera = hostBlock( 'CameraUniforms', { group: 0, binding: 0, as: 'u_camera' }, { u_matrix: mat4x4fT, u_viewport_px: vec2fT }, { glsl: 'loose' },)
camera.field.u_matrix // emits `u_matrix` on GLSL, `u_camera.u_matrix` on WGSLThe rewrite from u_camera.u_matrix to u_matrix happens on the IR before emit, so it
cannot corrupt an unrelated substring, it survives minify(), and reflect() still
describes what was emitted. A loose block’s members must be scalars, vectors or matrices, and
hostBlock throws SD0016 as you declare one that is not. Their names must also not collide
with another loose block’s, since flattening puts them all in one namespace; that one is
caught when the GLSL writer runs, with SD0030 naming both blocks.
Targeting WebGL2 only
Nothing on this page narrows what the GLSL writer can express. The neutral names are
spellings, and several of the rules behind them exist to make WebGL2 output more defined:
round emits roundEven, and float % emits a trunc-mod that GLSL ES 3.00 compiles.
For a GLSL construct the neutral surface does not model,
rawStmt accepts a payload for one target only, and
b.raw takes the same payload inside a fn() body. The point-size row above is the case
this is for: the builtin has no neutral spelling, and a WebGL2-only build can still write
it. The GLSL writer splices the payload verbatim, and the WGSL writer, if it ever runs on
that module, fails closed on that statement:
import { f32, f32T, fn } from 'typeshade'
const sized = fn('sized', {}, f32T, (_p, b) => { b.raw({ glsl: 'gl_PointSize = 4.0;' }) return f32(1)})That is the shape to reach for when you exclude WebGPU today and may want it later. What a raw costs the rest of the module is on Raw statements.