Concept
Compile pipeline.
What happens between .xgis source
and a rendered frame: how the language turns into WGSL the GPU can run.
Stages
Lexer + Parser
The lexer in
@xgis/compiler recognizes the
usual tokens (identifiers, numbers, strings, punctuation) plus
keywords reserved by the language:
source,
layer,
background,
preset,
symbol,
keyframes,
fn,
and the modifier prefix z<N>:.
The parser produces a typed AST (see
compiler/src/parser/ast.ts):
every top-level statement becomes one of
SourceStatement,
LayerStatement,
BackgroundStatement,
PresetStatement, etc.
AST → IR (Scene)
lower() walks the AST and
produces the Scene IR — a flat list of
RenderNode records, one per
layer, with utility classes already resolved into structured
fields (fill,
stroke,
opacity,
size, etc.). Presets get
expanded inline. Keyframes get attached to their referencing
layers.
The IR is what the optimizer reads and what the codegen writes from. It deliberately knows nothing about WGSL or GPU buffers — that keeps the language semantics testable in pure TypeScript.
Optimizer: constant fold + classify
Every expression is classified into one of four buckets:
| Expression | Class | Handling |
|---|---|---|
| 360 / 12 | constant | Folded to 30 at compile time |
| opacity-[interpolate(zoom, 8, 40, 16, 100)] | zoom-dependent | CPU-interpolated per frame |
| speed / 50 | clamp(4,24) | per-feature GPU | WGSL codegen, evaluated in vertex shader |
| threat_size(5) | constant (user fn) | User function inlined and folded |
Classification decides which pipeline the layer needs and which uniforms get uploaded each frame.
Codegen: paint → shader-dsl IR → WGSL
Codegen does not hand-assemble WGSL strings.
Each layer's fill / stroke / opacity paint is lowered into typed
shader-dsl IR nodes
(NodeLike<vec4<f32>>), then
emitted to WGSL by @xgis/shader-dsl's
emitExpr /
emitModule — the same DSL + emitter the
runtime's geometry shaders (polygon, line, point, raster, text) are authored in.
Because the paint is real IR, the optimizer's constant-fold / CSE / dead-code
passes apply to the emitted shader for free.
A data-driven expression — say
fill match(.continent) — routes to a
per-feature compute kernel:
compute-gen builds it as shader-dsl IR (a
matchExpr → a WGSL
switch, or a const-array
LUT past 16 arms), and the fragment shader
reads the packed result back via
unpack4x8unorm(compute_out[feat_id]).
Zoom-only paint samples a pre-baked gradient atlas; pure constants inline as
literals.
Variants are deduplicated by content fingerprint so a 50-layer scene with identical styling produces one pipeline, not 50.
The shader IR — @xgis/shader-dsl
Yes — the compiler lowers XGIS into shader-dsl partway through. The compiler and
the runtime share one shader layer:
@xgis/shader-dsl, a small typed-IR →
WGSL / GLSL compiler. The compiler turns XGIS paint into its
Expr nodes — in fact the compiler's
node-types.ts imports the
Expr /
ShaderType union straight from the
package — and the runtime authors every geometry shader in that same IR.
One emitter, one optimizer, one set of backends: a constant folds, a repeated
sub-expression CSEs, and dead code drops the same way whether the WGSL came from
a .xgis match() or a hand-written polygon shader.
WebGPU gets WGSL; the WebGL2 fallback gets GLSL ES 3.00 — from the identical IR.
Watch it live on the Compute concept page: stage 7 (render-node paint) and stage 8 (compute kernels) print the exact shader-dsl output for whatever XGIS you type. (A few legacy string-built emit paths are still being folded into the IR — an active consolidation, not the end state.)
Runtime
@xgis/runtime consumes the
emitted SceneCommands and ShaderVariants, owns the GPU device,
manages tile catalogs (PMTiles MVT / GeoJSON), and
dispatches per-frame draws. WebGPU is the production backend —
there is no Canvas 2D fallback: with no adapter the map
shows an "unavailable" notice (the host's
onWebGPUUnavailable hook)
instead of a degraded render. A WebGL2 backend implements the same
RHI
and is being brought to full-frame parity.
The runtime is the only component that touches the GPU. Compiler unit tests can fully exercise the language without a graphics context.
Problem on this page?