X-GIS

Concept

One RHI, two backends.

X-GIS draws through a backend-agnostic Render Hardware Interface — the RHI. WebGPU is the production backend; a WebGL2 backend implements the same interface for browsers without WebGPU. Renderers target the RHI instead of a GPU device, so swapping the backend is one implementation, not a rewrite of every renderer.

The RHI

The RHI (Render Hardware Interface) is a backend-agnostic GPU surface. Through it the engine creates buffers, textures, samplers, bind groups, pipelines, and a draw pass — recording against the interface rather than calling GPUDevice or a render-pass encoder directly. A backend swap then becomes a single implementation of one interface, not an edit to every renderer.

Two implementations exist today: a WebGPU backend and a WebGL2 backend. Each capability below is true of the engine today; the designed-but-not-yet-shipped parts are marked as roadmap. In production the renderers still issue WebGPU calls directly — the RHI is the seam they are migrating onto, and the WebGL2 path proves that seam works end-to-end for raster.

WebGPU, the production backend

01

One backend-agnostic interface

RhiDevice is the single GPU contract — it creates buffers, textures, samplers, bind groups, and pipelines, and begins draw passes. It carries a positive backend marker ('webgpu' | 'webgl2') so a forced-fallback path can be asserted to have actually run on the backend it claims.
02

Opaque handles hide the backend

Resource handles — RhiBuffer, RhiTexture, RhiPipeline — are opaque: callers never see a GPUBuffer or a WebGLBuffer. The WebGPU impl wraps the native object; the WebGL2 impl wraps its GL record. The renderer above the seam stays backend-blind.
03

WebGPU is the production backend

WebGpuDevice is a thin, one-to-one set of wrappers over GPUDevice / GPURenderPassEncoder, so it adds ~zero overhead. The app boots a WebGPU device by default, which makes it the reference the WebGL2 backend is matched against.

The WebGL2 backend

The WebGL2 backend is real code, not a placeholder. It absorbs the gap between WebGPU's resource model and WebGL2's so the renderers above the seam stay unchanged.

04

A real WebGL2 backend, not a stub

WebGl2Device wraps a WebGL2RenderingContext: shaders compile from GLSL ES 3.00 (a split vertex + fragment source), uniforms bind as std140 UBOs via bindBufferBase / bindBufferRange, textures bind to texture units paired with sampler objects, and vertex attributes bind via vertexAttribPointer.
05

Reflection by name, not by order

When a bind-layout entry carries the shader's reflected name, the linked program's uniform blocks and samplers bind by name (getUniformBlockIndex / getUniformLocation) — so a multi-resource group binds correctly regardless of declaration order. An un-named entry falls back to binding by order.
06

Storage buffers become data textures

WebGL2 (ES 3.00) has no storage buffers, so a storage buffer is emulated as a 2D-tiled R32F data texture: element i is read in GLSL with texelFetch at (i % W, i / W).
07

Fail-closed: never a silently wrong frame

Anything WebGL2 cannot yet do throws instead of degrading silently — the offscreen / MRT command encoder, stencil pipeline state, the rgba16float OIT accumulation target, and a pipeline missing its GLSL source. A draw that cannot be reproduced faithfully can never quietly corrupt the picture.

Proving the fallback

The fallback is verified end-to-end, not asserted on paper.

08

A live frame on WebGL2 — ?forcegl2=1

An off-by-default boot toggle, ?forcegl2=1, builds a WebGl2Device instead of a WebGPU device and renders a real z0 raster tile through the same RasterDraper the WebGPU pilot uses. A page-readable backend marker and an end-to-end gate read the pixels back to confirm the frame actually originated on WebGL2.

That live frame is an isolated, single-sample raster slice — the proof that one renderer draws correctly across the seam. Every other primitive (points, lines, heatmaps) already has an RHI adapter wired behind a flag and exercised by per-primitive parity gates, but those adapters are off by default; flipping the whole frame onto the RHI is the roadmap below.

Render passes

The engine's render path is a fixed linear chain of passes — background → opaque → translucent → points → labels → heatmap → overdraw-compose → graphics — with the per-feature compute dispatch running as a separate pre-pass before the chain. (An OIT node is registered at its historical slot but is runtime-dead — its shouldRun is immutably false.) Each pass is a stateless object that emits its GPU commands for the frame; today those commands record into a raw WebGPU command encoder.

The RHI models that topology backend-agnostically. A RhiRenderPass is the draw scope (set a pipeline, set bind groups, set vertex buffers, draw); beginScreenPass covers the single-sample backbuffer; and an offscreen / MRT pass is described by RhiRenderPassDesc through a command encoder. The WebGPU impl maps those offscreen descriptors byte-identically to the raw descriptors each pass builds — a parity test gates the byte-identity, so adopting the seam never drifts the topology.

Per-tile clip masks ride a setStencilReference on the pass: WebGPU maps it to the native call, while WebGL2 leaves it inert and fail-closes on a stencil-configured pipeline. On WebGL2 the offscreen / MRT encoder fail-closes entirely, so the ?forcegl2=1 slice uses only the single-sample screen pass. Routing the full live chain through the RHI — so the whole frame can run on WebGL2 — is roadmap.

Where it is going

Roadmap · not yet shipped

The WebGL2 backend is real, but today it carries one renderer (raster) on a single-sample frame. These extend it to the full map — planned, not built:

  • Full-frame WebGL2 — vector tiles, lines, points, and heatmaps drawn on WebGL2. Each primitive already has an RHI adapter, but they are off by default.
  • MSAA, OIT, and picking on WebGL2 — the multi-sample, order-independent-transparency, and pick-readback topology still fail-closes on WebGL2.
  • Compute on WebGL2 — ES 3.00 has no compute shaders, so the GPU compute-paint path fail-closes; a data-texture emulation is planned.
  • @xgis/engine ↔ @xgis/map split — carving the backend-agnostic GPU engine out from the map content. An engineering plan today, not a shipped package boundary.

Tracked in the repo ROADMAP.md ↗.

See it live

Every map in the examples gallery is a live X-GIS render on the WebGPU backend — the production path through the RHI's reference implementation. The WebGL2 backend ships as a developer boot toggle (?forcegl2=1), which renders the single-sample raster slice and reads it back to prove the fallback.

Specifications

See also