@xgis/shader-dsl
Shaders, authored in TypeScript.
Write a shader once as typed TypeScript and one IR emits WGSL for WebGPU and GLSL ES 3.00 for WebGL2 — no hand-porting, no hand-derived binding offsets, and a CPU f64 oracle to prove the two targets agree. It's the shader layer behind every X-GIS map.
gradient.ts — authored
// ═══ @xgis/shader-dsl example — fullscreen gradient pass ═══//// A self-contained render pass: an oversized fullscreen triangle (no vertex buffer)// feeds a fragment stage that mixes two colours by the screen-space UV, modulated by a// uniform. Shows the std140 uniform block + the `If/elif` control-flow combinator. The// /shader-dsl site page packs `top`/`bottom`/`mix_bias` into the UBO from reflect().
import { fn, module, vec2, vec4, f32, mix, f32T, u32T, vec2fT, vec4fT, If, ioStruct, builtin, location, uniformStruct,} from '../src/index.ts'import type { ShaderExample } from './_shared.ts'
// Uniform block — std140-laid-out by reflect(). `top`/`bottom` are the two gradient// endpoints; `mix_bias` shifts the blend up or down.const U = uniformStruct( 'Uniforms', { group: 0, binding: 0, as: 'u' }, { top: vec4fT, bottom: vec4fT, mix_bias: f32T, },)
// Vertex → fragment interface struct.const VsOut = ioStruct('VsOut', { pos: builtin('position', vec4fT), uv: location(0, vec2fT),})
// Oversized fullscreen triangle (3 verts, NDC −1..3) — covers the screen from a single// non-indexed draw with no vertex buffer.const vsFull = fn( 'vs_full', { idx: builtin('vertex_index', u32T) }, (p) => { const pos = vec2(-1, -1) If(p.idx.eq(1), () => { pos.assign(vec2(3, -1)) }).elif(p.idx.eq(2), () => { pos.assign(vec2(-1, 3)) }) return VsOut.construct({ pos: vec4(pos, 0, 1), uv: vec2(pos.x.add(1).mul(0.5), pos.y.add(1).mul(0.5)), }) }, { stage: 'vertex' },)
// Fragment — vertical gradient between the two uniform colours, biased. `vo` (not `in`,// a GLSL reserved word) is the fragment input.const fsGradient = fn( 'fs_gradient', { vo: VsOut }, (p) => { const t = p.vo.uv.y.add(U.field.mix_bias) const rgb = mix(U.field.bottom.rgb, U.field.top.rgb, t) return vec4(rgb, f32(1)) }, { stage: 'fragment', retAttr: '@location(0)' },)
const gradientModule = module({ structs: [U.struct, VsOut.decl], bindings: [U.binding], funcs: [vsFull, fsGradient],})
export const gradient: ShaderExample = { id: 'gradient', title: 'Gradient pass', blurb: 'A two-colour vertical gradient with a biasable blend — the minimal render pass. `top`/`bottom`/`mix_bias` are packed into one std140 uniform block recovered from reflect().', category: 'generic', file: 'gradient-pass.ts', module: gradientModule, renderable: true, controls: { top: { kind: 'const', value: [0.16, 0.5, 0.95, 1] }, // sky blue bottom: { kind: 'const', value: [0.02, 0.03, 0.09, 1] }, // near-black mix_bias: { kind: 'slider', label: 'Mix bias', min: -0.5, max: 0.5, step: 0.01, value: 0 }, },}emitModule(m) — WGSL
struct Uniforms { top: vec4<f32>, bottom: vec4<f32>, mix_bias: f32,}
struct VsOut { @builtin(position) pos: vec4<f32>, @location(0) uv: vec2<f32>,}
@group(0) @binding(0) var<uniform> u: Uniforms;
@vertexfn vs_full(@builtin(vertex_index) idx: u32) -> VsOut { var _av0: vec2<f32> = vec2<f32>(-1.0, -1.0); if ((idx == 1u)) { _av0 = vec2<f32>(3.0, -1.0); } else if ((idx == 2u)) { _av0 = vec2<f32>(-1.0, 3.0); } return VsOut(vec4<f32>(_av0, 0.0, 1.0), vec2<f32>(((_av0.x + 1.0) * 0.5), ((_av0.y + 1.0) * 0.5)));}
@fragmentfn fs_gradient(vo: VsOut) -> @location(0) vec4<f32> { return vec4<f32>(mix(u.bottom.rgb, u.top.rgb, (vo.uv.y + u.mix_bias)), 1.0);}emitGlslModule(m, …)
gives the matching GLSL ES 3.00 for WebGL2. Nothing is authored twice.
What it gives you
One source → many targets
Author once; emit WGSL for WebGPU and GLSL ES 3.00 for WebGL2. The WGSL is byte-stable, so builds are reproducible and diff-testable.
Type-checked
Nodes carry compile-time keys — a wrong-typed field or return is a TypeScript error, not a shader-compile surprise.
Reflection
reflect(module) recovers bind groups, std140/std430 layouts, and entry signatures — the host binds without hand-derived offsets.
Optimize + lint
CSE, DCE, LICM, const-fold and auto-vars, plus a 21-rule lint engine and a capability gate.
CPU f64 oracle
Compile the same module to an f64 CPU function to cross-check the GPU result — the parity gate X-GIS rendering relies on.
Bring your own shaders
It ships the authoring + emit surface, not any app’s shaders. X-GIS authors its own map shaders through it.
Two ways in. Evaluating the design? Read Concepts — one IR, two backends, and the parity oracle. Ready to write one? Jump to Getting started and emit your first shader in a few minutes.