X-GIS

@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;
@vertex
fn 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)));
}
@fragment
fn 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);
}
One module in; byte-stable WGSL out — and 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.