Rendered from AUTHORING.md at commit 26de7be8. The package is imported here by its 0.2.0 name, typeshade.
Emitting and reflection
After this page you can turn a module into WGSL, into both GLSL stages, or into a fragment a host composes into its own program, and you can read the pipeline metadata a host binds from.
Emitting turns the IR a module holds into source text for one target. Reflection is the other half of the same module: a description of what the shader expects, as data, so the code that allocates buffers, builds bind groups and sets a vertex layout reads it off the module instead of repeating it by hand. Both are read-only over the IR, so calling either one leaves the module you pass exactly as it was.
Emitting WGSL
emitModule(m) returns the whole module as one WGSL string: the module’s consts, structs,
bindings and functions, with each entry point carrying its stage attribute. It runs the
pre-emit checks first, so a module that does not validate, or that needs a capability the
target lacks, throws a coded error at the emit call.
import { emitModule, emitModuleAt, emitIdentity } from 'typeshade'
const wgsl = emitModule(m)
// The same module at an explicit optimization level. 'O2' is what emitModule runs and is// byte-identical to it; 'O0' is the lowered module with no optimizer pass.const naive = emitModuleAt(m, 'O0')
// A one-line identity for one emit configuration, for a build to compare:emitIdentity('wgsl') // 'wgsl;parens=full;fp64=float;plugins=-#069d5f95'An emit is a configuration as well as a module. Parenthesis mode, the f64 flavor, pinned
override values and the plugin chain all change the emitted bytes, and two files produced
by two of those configurations look like an unexplained diff. emitIdentity(target, opts)
folds the configuration into a readable summary plus a digest of it. Write it beside an
artifact you commit and compare it after a build: equal stamps mean the same configuration
produced both, and different stamps name the axis that moved.
Emitting GLSL ES 3.00
A WebGL2 program is two shaders compiled separately, so the GLSL backend spells one stage
per call. emitGlslStages(m) returns both stages of one module and pays the shared
lowering once for the pair.
import { emitGlslModule, emitGlslStages } from 'typeshade'
const { vertex, fragment } = emitGlslStages(m)
// One stage on its own, byte-identical to that member of the pair.const fs = emitGlslModule(m, 'fragment')
// Omit the stage for the whole module in one string. It carries every entry point, so it// is for reading and diffing and does not compile as a stage.const whole = emitGlslModule(m)Each string carries its own #version 300 es line, the precision preamble, and only the
declarations that stage reaches. The two stages still agree on every shared name, because
the lowering is deterministic, so the pair links. emitGlslStages also takes
vertexEntry and fragmentEntry for a module that carries several entry points in one
stage, and naming them keeps the single lowering. GLSL has one main() per stage, so such a
stage with no entry named is refused, by emitGlslStages and emitGlslModule alike, and
compile() reports it as a TS8015 warning with glsl left undefined. The GLSL calls take the WGSL emit
options plus their own; the GlslEmitOptions reference lists them.
Module fragments
A module fragment is a piece of a program: the declarations and helpers with no version
header and, by default, no entry point, for a host that owns the final program and pastes
ours into it. It is what to reach for where a GLSL codebase would write #include.
emitGlslFragment(m, stage) returns one for GLSL and emitFragment(m) for WGSL, both in
the same shape.
import { emitFragment, emitGlslFragment } from 'typeshade'
const f = emitGlslFragment(m, 'fragment')
f.source // 'struct VsOut {\n vec4 pos;\n …', the declarations and helpersf.preamble // ['#version 300 es', 'precision highp float;', 'precision highp int;']f.declares // { functions, structs, bindings, consts, overrides, entryPoints }f.requires // [], the symbols this fragment calls and does not define
// Keep the entry points, and the WGSL twin:const w = emitFragment(m, { entryPoints: true })The preamble comes back as data so the composer merges and de-duplicates those lines across
every fragment it assembles. Nothing is dropped, so nobody has to strip a header with a
regular expression. declares is the manifest of what source defines, which a composer
can check for a collision before it concatenates, and requires lists what the host’s own
prelude has to supply. Entry points are excluded unless you ask for them, and they are
listed in declares.entryPoints either way, because they still decide the stage scope:
which helpers, structs and bindings the fragment carries is what that stage needs. A
fragment runs the same pre-emit checks a whole-module emit runs.
What reflect returns
reflect(m) describes a module as target-neutral data. Target-neutral means reflect
takes only a module, with no backend argument, so one reflection describes the WGSL emit
and the GLSL emit of that module at once.
import { reflect } from 'typeshade'
const r = reflect(m)
r.bindGroups // [{ group: 0, entries: [{ group: 0, binding: 0, name: 'U', space: 'uniform',// resourceKind: 'uniform-buffer', owner: 'module',// structName: 'Uniforms', stages: ['fragment'] }] }]r.uniforms // [{ name: 'Uniforms', size: 32, align: 16, fields: [// { name: 'time', type: 'f32', offset: 0, align: 4, size: 4 }, … ] }]r.storage // std430 layouts for a storage binding declared directly as a structr.vertex // { attributes: [{ name, location, type, offset }], arrayStride }r.entries // [{ name: 'vs', stage: 'vertex', inputs: ['u32'], output: 'struct:VsOut', io }, …]r.overrides // the pipeline constants a host supplies per variantr.requiredFeatures // the capabilities a host must have active before it creates a pipeliner.requiredLanguageFeatures // the WGSL language features the browser must implementr.requires // host-provided globals the module references and does not declareuniforms is std140 and storage is std430, the two byte layouts the targets use for a
uniform block and for a storage buffer. Each one gives every field its offset, alignment and
size, plus the struct’s own size already rounded up to its alignment, so that size is the
stride for an array of the struct as well. A binding declared with storageBuffer is an
array of the element type you pass, so it appears in bindGroups and storage has no entry
for it. To get that element’s std430 stride, or the layout of any structDecl handle you
hold on its own with no module around it, call wgslLayout on the handle’s decl, as in
wgslLayout(ShapeSegment.decl, 'std430') for the struct
Layouts and resources declares. vertex is
undefined for a module whose vertex entry takes no location parameters.
Binding from reflection
A host walks bindGroups and creates one resource per entry. Three fields carry the
decision.
import { reachFrom, reflect, stageOf } from 'typeshade'
for (const group of reflect(m).bindGroups) { for (const e of group.entries) { if (e.owner === 'host') continue // the surrounding renderer allocates this one e.resourceKind // 'uniform-buffer' | 'storage-buffer' | 'texture' | 'sampler' e.stages // ['fragment'], ordered vertex, fragment, compute }}
// The same reachability question for an entry set you choose yourself:const reach = reachFrom( m, m.funcs.filter((f) => stageOf(f) === 'fragment'),)reach.bindings // Set { 'U' }, the binding names that stage readsreach.fns // the call-graph closure from those entries, the entries includedowner says who owns the resource. It is 'module' when the module declares the binding
and the host allocates it from this reflection, and 'host' when the host that owns the
pipeline declares it and its layout is the authority. The list stays complete under both,
because a host still has to know about a binding it owns. resourceKind says what to create, and a texture entry
also carries textureDim and textureElem, the two axes a view needs, which
Layouts and resources covers from the
authoring side, and sampleType, the word its bind group layout takes. A depth texture is
'depth' and an integer one 'uint' or 'sint'. An f32 texture is 'float' where a call
gives it a sampler, and 'unfilterable-float' where the program only loads, measures or counts
it, which is what a 32-bit float texture such as r32float needs. stages says which stages
reach the binding, which is the visibility mask a
WebGPU bind group layout entry requires and the per-stage assignment a WebGL2 host makes for
uniform block points and texture units.
The bind groups include a binding a lowering injects as well as the ones you declared. The
fp64 lowering adds a texture binding named _fp64, the guard
texture that page describes, to a module whose emulated helpers read it, and a host that
binds from this reflection binds it without knowing that. Pass reflect the same fp64Flavor the emit will get, so the reflection
describes the program that will run.