X-GIS

Concept

One IR, two backends.

X-GIS does not hand-write WGSL. Every shader is a typed intermediate representation, and one neutral tree-walk emits it to two targets — WGSL for WebGPU and GLSL ES 3.00 for WebGL2 — through an optimizing compiler, not a transliterator.

One typed IR

The package @xgis/shader-dsl models a shader as a ModuleDecl — typed expressions and statements plus declarations (consts, structs, bindings, funcs). The target language is a final spelling decision, not the source of truth.

01

A typed expression and statement IR

Each expression node carries a ShaderType, and the authoring wrapper is phantom-typed, so a vec2 + vec3 mismatch is a TypeScript error before any GPU sees the shader. The IR covers the full surface the renderer needs — arithmetic, comparisons, intrinsics, member and index access, construction, if / for / switch, and a high-level matchExpr.
02

One neutral tree-walk, shared by every backend

A single tree-walk (emitExpr / emitStmt) renders the IR. It is target-neutral: a backend supplies only spelling — type names, literals, intrinsic names, and a handful of divergent declaration fragments. The control-flow walk is never re-implemented per target, so the two backends cannot drift on how an if or a switch is structured.

Two backends, one tree

The same module emits to two real targets. The runtime's geometry shaders — polygon, line, point, raster, and text — are authored in this IR, not as hand-written WGSL strings.

03

WGSL → WebGPU

emitModule lowers a module to a WGSL string for device.createShaderModule. This is the primary, production render path: the WebGPU pipeline compiles the WGSL the IR emits.
04

GLSL ES 3.00 → WebGL2

emitGlslModule lowers the same IR to GLSL ES 3.00. A uniform struct binding becomes a std140 UBO block whose byte offsets come from the same layout engine the host packs against; @vertex / @fragment entries become in / out varyings and a synthesised void main(). The WebGL2 RHI device compiles exactly this GLSL.
05

A capability gate that fails closed

Each backend declares its Capabilities, and a shared gate rejects any module that needs a feature the target lacks. GLSL ES 3.00 has no storage buffers, no compute, and no multisampled-texture load, so a module using one raises UnsupportedFeatureError up front — before a single line of GLSL is produced — rather than emitting code that would fail to compile on the device.

A real optimizer

The headline value of a typed IR over string templating: an optimizing compiler. The emit path runs an ordered list of correctness-preserving passes to a fixed point, so authors write plain inline expressions and the optimizer folds, prunes, and binds reuse for them.

06

The emit-time pipeline, run to a fixed point

In order: constant propagation, copy propagation, constant folding, algebraic simplification, dead-branch elimination, CSE (hoist function-top input-only repeats), statement-local CSE, loop-invariant code motion (LICM), and dead-code elimination (DCE) — iterated until the module stops changing. Both the WGSL and the GLSL backend run this same pipeline (optimize: (m) => fixpoint(m)), so the WebGL2 fragment path stops recomputing CSE-able subexpressions too.
07

Named levels — O0 / O1 / O2

Like a C compiler's -O flags: O0 is the naive lowered emit (every author-written subexpression verbatim), O1 is the bit-exact value-movers plus cleanup, and O2 is the full pipeline — the emit default. The split exists so the emit-size measurement can A/B O0 against O2 and so a debug build can read the un-optimized shader.
08

Precision-critical paths are emitted verbatim

A raw statement is opaque to every optimizer pass — the dataflow passes (const / copy propagation, dead-branch, CSE, LICM, DCE) bail out of any function that contains one, and the literal folders cannot rewrite the opaque fragment — so the path the polygon composer uses to splice the compiler's fill / stroke paint is emitted verbatim, untouched. Correctness across the rest is pinned by oracle value-equality (a pass must leave the module evaluating to identical results) and a real-GPU optimizer-parity gate.

From style to IR

The compiler lowers a map style into this same IR — the bridge from .xgis source to a GPU shader.

09

Fill and stroke are IR nodes, not strings

When the compiler specializes a layer it produces a ShaderVariant whose fillExpr and strokeExpr are typed IR nodes (NodeLike<'vec4<f32>'>), not WGSL strings. The runtime reconstructs them and the polygon composer emits them through the same tree-walk. A match() in the style becomes a matchExpr IR node, hoisted to a switch by a pre-emit pass so the rest of the emitter stays match-unaware. The live breakdown — tokens through emitted WGSL — is on the Compute paint page.

Where it is going

Roadmap · not yet shipped

These exist in the codebase but are deliberately not in the default emit path — they are implemented-but-unwired passes or a deferred runtime surface, not part of the shipped pipeline today:

  • GVN — cross-statement global value numbering completes the CSE family. Implemented, but not yet in the default pipeline: wiring it would change the byte-stable golden-WGSL snapshots, a maintainer call.
  • Whole-function tree-shaking and inlining — both passes exist but are unwired; they would prune or inline the shared projection prelude and break its deliberately byte-stable emit.
  • Full WebGL2 runtime parity — the forced-WebGL2 device renders a single-sample, isolated raster slice today. The full-frame WebGL2 path (the MSAA opaque pass and the stencil clip-mask pipeline) is deferred.
  • WebGL2 storage emulation residual — WebGL2 has no storage buffers, so the GLSL path emulates them with data textures. Only array<f32> and structs of scalar / vecN<f32> fields lower today; other element types are fail-closed.

Tracked in the repo ROADMAP.md ↗.

See it live

The shader-dsl playground compiles a gallery of example modules to both WGSL and GLSL ES 3.00 and runs the renderable ones live on your WebGL2 device — the same IR, two backends, side by side. For the IR behind compiled map styles, the Compute paint page runs the compiler in your browser and shows every stage, tokens through emitted WGSL, updating as you type.

Specifications

See also