Rendered from AUTHORING.md at commit 26de7be8. The package is imported here by its 0.2.0 name, typeshade.
Layouts and resources
After this page you can declare a vertex, uniform, storage or texture layout once and read every field off that one declaration.
A layout is the agreement between a shader and the pipeline that feeds it: the fields one
stage hands to the next, the uniform block a draw call binds, the buffers and textures at
each slot. Each helper takes the layout once, as a field map or as an element type, and
returns a handle that carries whatever declarations that layout needs together with the
typed accessors for it. Because the accessors come off the same object as the declaration,
a field name or a field type you get wrong is a TypeScript error at the authoring line.
Pass the handles to module({ uses: [...] }) and the module collects whatever declarations
each one carries.
IO structs
An IO struct is a group of fields that crosses a stage boundary: a vertex stage returns
it and a fragment stage takes it as a parameter. ioStruct declares one from a name and a
field map, and every field carries a stage attribute. builtin(name) declares a value
the hardware supplies, and takes a WGSL builtin id such as 'position' or 'vertex_index',
typed as a closed union, so a name WGSL does not define is a tsc error. location(n, type)
declares a numbered slot, with an optional interpolation mode of 'flat', 'linear' or
'perspective'. An integer field gets flat without asking, because WGSL requires it for an
integer varying. 'flat' and 'perspective' are the modes both targets support; 'linear'
has no GLSL ES 3.00 form, so a module that uses it fails closed on that target.
const VsOut = ioStruct('VsOut', { pos: builtin('position', vec4fT), uv: location(0, vec2fT), vis: location(1, f32T), view_w: location(2, f32T),})
// A vertex fn returns the struct. `construct` builds the value in one expression,// so a missing or extra field is a TS error.const vs = fn( 'vs_main', { xy: location(0, vec2fT), uv: location(1, vec2fT) }, (p) => { const pos = vec4(p.xy, 0, 1) return VsOut.construct({ pos, uv: p.uv, vis: f32(1), view_w: pos.w }) }, { stage: 'vertex' },)
// Read fields off a parameter declared with the handle.const fs = fn( 'fs_main', { input: VsOut }, (p) => { If(p.input.vis.lt(0), () => { Discard() }) return vec4(p.input.uv, 0, 1) }, { stage: 'fragment' },)VsOut.var('out') is the third form. It declares a mutable var of the struct and hands back
assignable fields, for an output you build over several statements. To read fields off a
value you hold as a plain node, use VsOut.of(node).
Uniform blocks
A uniform block is a struct the host fills once per draw and every invocation reads.
uniformStruct declares the struct and its binding in one call: the WGSL type name, the
slot (group, binding, and as for the variable name), then the field map. Read a field
with .field.<name>. The result is an ordinary node, so component access and math chain
straight off it. Uniform fields are read-only in WGSL, and the handle types them that way,
so assigning to one is a tsc error.
const U = uniformStruct( 'Uniforms', { group: 0, binding: 0, as: 'u' }, { mvp: mat4x4fT, proj_params: vec4fT, raster_params: vec4fT, },)
const opacity = U.field.raster_params.xconst m = U.field.mvpU.field is an ordinary object, so destructure it once at the top of a body and read the
fields by name: const { mvp, proj_params } = U.field.
A uniform that is one scalar or vector needs no struct around it. resource declares a
single bound value and hands back .node to read it and .binding for uses:
const mode = resource('mode', u32T, { group: 0, binding: 1 })// mode.node is a ReadonlyNode<'u32'> — `@group(0) @binding(1) var<uniform> mode: u32;`resource declares textures and samplers too; see Textures and samplers.
Plain structs
structDecl declares a struct that never crosses a stage boundary: the element type of a
storage buffer, or a struct nested inside another one. Its fields are plain types with no
stage attribute, which is the whole difference from an IO struct. The handle has the same
shape, plus a positional .get(node, 'field') reader for call sites that pull many fields
off one shorthand.
export const ShapeSegment = structDecl('ShapeSegment', { kind: u32T, color_idx: u32T, flags: u32T, _pad: u32T, p0: vec2fT, p1: vec2fT, p2: vec2fT, p3: vec2fT,})Storage buffers
A storage buffer is a bound array<Element> whose length comes from the buffer the host
binds. storageBuffer declares one from its element alone, either a struct handle or a
scalar or vector type, and .at(i) is the element accessor: the typed field proxy for a
struct element, the element node itself for a scalar or vector one. access fixes the write
capability at the type level. Under 'read' the fields are read-only and assigning to one is
a tsc error; under 'read_write' they are assignable.
const segmentsB = storageBuffer('segments', ShapeSegment, { group: 0, binding: 9, access: 'read' })
const seg = segmentsB.at(i)seg.p0 // → Node<'vec2<f32>'>seg.kind // → Node<'u32'>
const featIds = storageBuffer('feat_ids', u32T, { group: 0, binding: 10, access: 'read' })featIds.at(i) // → Node<'u32'>GLSL ES 3.00 has no storage buffer object, so a read binding lowers to a data-texture fetch
at emit and the shader source stays as written. On the GLSL target the host allocates that
data texture, and its internal format has to match the sampler the lowering declares: R32F
for a float array, R32UI for array<u32>, R32I for array<i32>. Nothing checks the pairing
at runtime, so read the element off reflect() instead of tracking it separately.
That lowering only gathers, so a 'read_write' binding on the GLSL target is a build-time
error. Prefer 'read' unless the module is WGSL-only. A 'read_write' binding in a module
that also targets GLSL stays invisible until the first GLSL emit.
Textures and samplers
resource declares a bound value that has no fields of its own, which covers textures and
samplers. The type token decides what the access node accepts: texture2dfT for a 2D
texture of filtered floats, samplerT for a sampler, which is the object that carries the
filtering and wrap state. .node keeps the specific type, so a texture and a sampler passed
in the wrong order is caught by tsc.
const tex = resource('tex', texture2dfT, { group: 0, binding: 1 })const texSampler = resource('tex_sampler', samplerT, { group: 0, binding: 2 })
const c = textureSample(tex.node, texSampler.node, uv) // fragment onlyconst cv = textureSampleLevel(tex.node, texSampler.node, uv, 0) // any stageconst size = textureDimensions(tex.node) // → Node<'vec2<u32>'>, the extent in texelstextureSample takes its mip level from screen-space derivatives, which only a fragment
invocation has, so it is fragment-only and a vertex or compute use is an SD0109 lint error.
Read an explicit level with textureSampleLevel(tex, smp, uv, level), which is legal in
every stage. The derivative builtins fwidth, dpdx and dpdy are fragment-only for the
same reason and have no vertex or compute form, so precompute the quantity there and pass it
in.
Array and integer textures
A 2D array texture holds N layers behind one binding, with the layer picked on each call, so
an atlas of any depth costs one slot. Declare it with texture2dArrayfT. The same three read
functions cover it: the first argument’s type selects the array form, and the layer
argument then becomes required. A plain number layer lifts to an integer literal.
textureDimensions reports width and height only, for an array texture too, and the layer
count is the separate textureNumLayers, which returns u32; wrap it in toF32 for float
math.
An integer texture holds exact 32-bit texels, for an id map, a packed colour table or a
bitfield lookup. texture2duT and texture2diT declare the 2D forms, and
texture2dArrayuT and texture2dArrayiT the array twins. The result of a load follows the
texture’s element, vec4<u32> off an unsigned one and vec4<i32> off a signed one, so
assigning it to the wrong type is a tsc error. textureSample and textureSampleLevel
reject these types at tsc, because filtering is a weighted average and WGSL has no
sampling form for an integer texture at all. What an integer texture offers is textureLoad,
textureDimensions and textureNumLayers.
const atlas = resource('atlas', texture2dArrayfT, { group: 0, binding: 4 })const atlasSampler = resource('atlas_sampler', samplerT, { group: 0, binding: 5 })
textureSample(atlas.node, atlasSampler.node, uv, layer) // implicit LOD, fragment onlytextureSampleLevel(atlas.node, atlasSampler.node, uv, layer, level) // explicit LOD, any stagetextureLoad(atlas.node, coord, layer, level) // unfiltered texel fetchtextureNumLayers(atlas.node) // → Node<'u32'>
const ids = resource('ids', texture2duT, { group: 0, binding: 3 })
textureLoad(ids.node, coord, 0) // → Node<'vec4<u32>'>textureDimensions(ids.node) // → Node<'vec2<u32>'>, same as any other texturereflect() reports each texture binding’s dimension and element, which is what a host needs
to create a matching view and to pick the sample type for it. Getting that pairing wrong
raises nothing at runtime: a texture whose format disagrees with its sampler type is
incomplete, and a fetch on it silently returns zero.