Getting started
Author a typed shader in TypeScript, then emit both backends and the pipeline reflection from the same module. No GPU is needed to author or emit — only to run the result.
Install
Today @xgis/shader-dsl is a workspace package
inside the X-GIS monorepo (not yet published to npm), so the fast path is to author inside the
checkout — new shaders live alongside the engine's own, and
import { fn, module, emitModule, reflect } from '@xgis/shader-dsl'
resolves through the workspace. Packaging it for an outside project is covered in
Shipping.
git clone https://github.com/X-GIS/X-GIS.git && cd X-GISbun install # installs the whole workspace
# Authoring + emit run straight off the TypeScript source (tsx) — no build step.# Build only when a consumer needs the compiled dist/ + .d.ts:bun run --filter '@xgis/shader-dsl' build1. Author a shader
The whole surface is plain TypeScript — typed value-expressions, control-flow combinators, and
single-source layout declarators (uniformStruct,
ioStruct). A wrong-typed field or return is a
compile error. Save it as my-shader.ts — don't worry
about every line yet (Concepts
breaks these down); here's the whole shape first.
import { fn, module, uniformStruct, ioStruct, builtin, location, vec2, vec3, vec4, sin, toF32, u32, f32T, vec2fT, vec4fT, u32T,} from '@xgis/shader-dsl'
// 1 — declare a uniform block ONCE; its std140 layout is derived, not hand-written.const U = uniformStruct('Uniforms', { group: 0, binding: 0, as: 'U' }, { time: f32T })
// 2 — the vertex→fragment interface struct.const VsOut = ioStruct('VsOut', { pos: builtin('position', vec4fT), uv: location(0, vec2fT) })
// 3 — a fullscreen-triangle vertex stage (position from the vertex index, no buffer).const vs = fn('vs', { vi: builtin('vertex_index', u32T) }, ({ vi }) => { const x = toF32(vi.bitAnd(u32(1))).mul(4).sub(1) const y = toF32(vi.shr(u32(1))).mul(4).sub(1) return VsOut.construct({ pos: vec4(x, y, 0, 1), uv: vec2(x.mul(0.5).add(0.5), y.mul(0.5).add(0.5)) })}, { stage: 'vertex' })
// 4 — a fragment stage, animated by the time uniform. Pass the ioStruct HANDLE as the// param spec ({ vo: VsOut }); the body receives a typed field proxy — vo.uv directly.const fs = fn('fs', { vo: VsOut }, ({ vo }) => { const v = sin(vo.uv.x.mul(10).add(U.field.time)) return vec4(vec3(v).mul(0.5).add(0.5), 1)}, { stage: 'fragment', retAttr: '@location(0)' })
// 5 — assemble the module. Helpers called via their handle are collected transitively,// so funcs lists only the entry stages.export const m = module({ structs: [U.struct, VsOut.decl], bindings: [U.binding], funcs: [vs, fs] })2. Emit + reflect
The same module produces both backends and its own reflection. The reflection gives the host the
std140 byte offsets to pack the uniform buffer — the exact packing the live examples use. Save this
as emit.ts next to my-shader.ts,
then run it — no GPU, no build: npx tsx emit.ts.
import { emitModule, emitGlslModule, reflect } from '@xgis/shader-dsl'import { m } from './my-shader'
console.log(emitModule(m)) // → WGSL string (WebGPU)console.log(emitGlslModule(m, 'vertex')) // → GLSL ES 3.00 vertex (WebGL2)console.log(emitGlslModule(m, 'fragment')) // → GLSL ES 3.00 fragment (WebGL2)console.dir(reflect(m), { depth: null }) // → { bindGroups, uniforms (std140), entries }3. Run the examples
The repo ships runnable examples. Print their emitted WGSL / GLSL / reflection in a terminal, or open one in the gallery to run it live on a WebGL2 canvas.
cd shader-dslnpx tsx examples/print.ts # every examplenpx tsx examples/print.ts hillshade # just one, by id