X-GIS

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.

Terminal window
git clone https://github.com/X-GIS/X-GIS.git && cd X-GIS
bun 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' build

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

Terminal window
cd shader-dsl
npx tsx examples/print.ts # every example
npx tsx examples/print.ts hillshade # just one, by id