engine
Why we built a GPU-first map engine
X-GIS compiles a declarative style language into optimized WebGPU shaders — through a real compiler and a typed shader IR. Here's the shape of the system and why each layer exists.
By the X-GIS team 2 min read
Most web maps push work to the CPU: parse a style, walk features every frame, build vertex buffers in JavaScript. X-GIS takes the opposite stance — the GPU is the runtime, and the style is a program we compile.
A style is source code
You write declarative .xgis:
layer continents { source: countries | fill match(.CONTINENT) { "Africa" -> amber-600, "Asia" -> rose-500, _ -> gray-400 }}That isn’t interpreted per frame. A real compiler — lexer → parser → IR →
optimizer → codegen — turns it into GPU programs at load time. Constants fold to
literals. Data-driven paint (match, interpolate) becomes a per-feature
compute kernel. Zoom-dependent paint becomes a pre-baked gradient atlas. The
per-frame cost collapses to “bind and draw.”
One shader layer, two backends
Every shader — the geometry pipelines and the compute kernels — is authored in a
typed shader IR (@xgis/shader-dsl), not hand-written WGSL strings, and the
compiler lowers your paint expressions into that same IR — so a match() in
your style and a hand-written polygon shader fold and dedupe together, and the
one IR emits both WGSL (WebGPU) and GLSL ES 3.00 (the WebGL2 fallback). The
compiler-pipeline post is the
anatomy of that path.
Built for a globe
The target isn’t a flat slippy map — it’s a 3D globe with real geodesy, and that
forces a precision problem the flat-map engines never hit. A surface point in
ECEF coordinates runs to the earth’s radius: 6,378,137 m. Store that in a
32-bit float — a 24-bit mantissa — and the representable grid near the surface is
6378137 / 2²⁴ ≈ 0.38 m. A building drawn straight from f32 ECEF snaps and
jitters on a ~0.4 m lattice, right at the scale you’re looking at it. So position
is never a raw f32: each vertex carries an ECEF hi + lo float pair relative to
a per-tile center (the extra low word buys back the lost mantissa bits), and depth
is logarithmic — so a building at street level and a continent at orbit both stay
crisp in the same frame.
What’s next
This blog will go deep on each layer — the compiler passes, the shader IR and its optimizer, the projection/globe math, the WebGPU render graph, and the performance work (GPU arenas, compute scheduling) that keeps it smooth. The Concepts docs cover the architecture; this blog covers the decisions behind it — one subsystem, and the problem that shaped it, at a time.
Read next
compiler
6 min read
The .xgis compiler pipeline: from style text to GPU programs
How a declarative map style becomes GPU work: lexer → parser → a Scene IR → optimization passes → codegen that emits typed shader-dsl IR (never WGSL strings), per-feature compute kernels for match(), and a gradient atlas for zoom-dependent paint.
shader-dsl
6 min read
Your bundler minifies everything except your shaders
Emitted WGSL/GLSL ships to gl.shaderSource with its authored vocabulary intact — no JS minifier reaches it. A Vite/Webpack-style emit-plugin pass (mangle + minify) that compacts and obfuscates the shader text, the ABI boundary it must never cross, and why compile-and-link is not enough to trust it.
shader-dsl
6 min read
Emulated double precision in the shader DSL
f64 and vecN<f64> as first-class shader-dsl types, emulated as two-f32 double-float pairs — same authoring syntax as f32, one lowering pass, ~48 significand bits.