Emitting and reflection
On this page

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 helpers
f.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 struct
r.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 variant
r.requiredFeatures // the capabilities a host must have active before it creates a pipeline
r.requiredLanguageFeatures // the WGSL language features the browser must implement
r.requires // host-provided globals the module references and does not declare

uniforms 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 reads
reach.fns // the call-graph closure from those entries, the entries included

owner 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.

Edit this page Report a problem