X-GIS

Concept

Compile pipeline.

What happens between .xgis source and a rendered frame: how the language turns into WGSL the GPU can run.

Stages

.xgis source String Lexer Token[] Parser AST lower() IR (Scene) optimize() constant folding · expression classify emit() codegen() SceneCommands runtime instructions ShaderVariant[] shader-dsl IR → WGSL → Runtime + GPU (WebGPU · WebGL2 via the RHI) ←

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 / 12constantFolded to 30 at compile time
opacity-[interpolate(zoom, 8, 40, 16, 100)]zoom-dependentCPU-interpolated per frame
speed / 50 | clamp(4,24)per-feature GPUWGSL 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.