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
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.
Opaque handles hide the backend
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.
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.
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.
Reflection by name, not by order
getUniformBlockIndex /
getUniformLocation) — so a
multi-resource group binds correctly regardless of declaration order.
An un-named entry falls back to binding by order.
Storage buffers become data textures
storage buffer is emulated as a
2D-tiled R32F data texture: element
i is read in GLSL with
texelFetch at
(i % W, i / W).
Fail-closed: never a silently wrong frame
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.
A live frame on WebGL2 — ?forcegl2=1
?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
Problem on this page?