X-GIS

Concept

Tiles & sources.

X-GIS reads three kinds of source — PMTiles archives, vector tiles (MVT), and GeoJSON. Each frame it picks the tiles the current camera can actually resolve, then uploads their geometry into shared GPU arenas that evict by byte budget, not tile count.

Three source formats

The format is chosen from the URL — a .pmtiles archive opens the PMTiles loader, while a .json / .tilejson manifest and a {z}/{x}/{y} tile template both feed the vector-tile (MVT) loader. GeoJSON is handed straight to the tessellator.

01

PMTiles archives

A PMTiles source opens a single-file archive, reads its header — zoom range, bounds, and the vector-layer list — and fetches each tile by z/x/y with range requests against that one file. No per-tile HTTP endpoint and no separate manifest are required.
02

Vector tiles (MVT) over TileJSON or XYZ

A TileJSON manifest, or a bare .mvt / .pbf XYZ template, feeds the Mapbox-Vector-Tile path. A template with no manifest is wrapped in a minimal synthetic TileJSON so the rest of the pipeline never has to know whether a real manifest existed.
03

GeoJSON tessellated to GPU meshes

Inline GeoJSON is tessellated to GPU meshes up front: polygons triangulated with earcut, line strings expanded to stroke geometry, and GeometryCollection features flattened one level (matching Mapbox / MapLibre). The output is a packed float vertex array plus a 32-bit index array.

Selecting tiles per view

Nothing loads the whole pyramid. Every frame, X-GIS works out which tiles the current camera can actually resolve and asks only for those.

04

Screen-space error picks the level of detail

The default selector measures screen-space error — roughly, how many pixels of error a tile would add if drawn in place of its more-detailed children. It subdivides while that error exceeds the target pixel budget and the max zoom hasn't been hit, so far-away horizon tiles stop subdividing on their own without a pitch-band kludge.
05

Recomputed each frame, capped for safety

Selection is keyed to the camera and canvas, so it re-runs whenever the view changes and is memoised across the layers that share a source within a frame. A hard cap on emitted tiles is a safety net against a degenerate camera (deep zoom at extreme pitch) blowing the count up.

The GPU arena

Selected tiles don't each get their own GPU buffer. Their geometry is uploaded into a small set of large shared buffers — linear arenas — so the draw loop binds each arena once and draws every tile from its own slice.

06

One shared buffer, bump-allocated

Each GPUArena backs one large GPU buffer — in the default build a 128 MB vertex arena and a 64 MB index arena (extruded tiles add a separate z-attribute arena). Each tile's geometry is written at a bump-allocated offset, 4-byte aligned for WebGPU's setVertexBuffer / setIndexBuffer requirement. This replaces a buffer-per-tile pool and is what makes a single bound-once-then-draw-per-slice loop possible.
07

Freed slots reused by exact footprint

When a tile is freed its offset goes onto a free list keyed by its exact aligned size, so a reused slot can never overrun its neighbour and pan / zoom keeps re-filling the same offsets. When the live set genuinely outgrows the buffer, compaction relocates it into a larger one rather than failing outright.

Byte-aware eviction

An arena fills on bytes, not on a tile count — a few large globe or extruded tiles can exhaust it long before any count limit is reached. Eviction is therefore driven by the byte high-water mark.

08

Trigger at 75%, drain to 60%

Eviction fires when the arena crosses its high-water mark (75 %) of capacity and drains least-recently-used tiles until live bytes fall back under the low-water mark (60 %). A count-only trigger used to early-return while the byte-sized arena overflowed and crashed the render loop; the byte signal closed that gap.
09

Visible and skeleton tiles are protected

The tiles needed for the current frame — plus their fallback ancestors and a permanent low-zoom skeleton base — are protected and never evicted, even under pressure. Everything else is fair game in roughly least-recently-used order; a returning camera simply re-fetches what was dropped.

Untrusted-input guards

Tiles and GeoJSON often come from a third-party host, so the loaders treat their bytes as untrusted.

10

Decompression-bomb and ingest caps

A PMTiles tile that decompresses past the per-tile byte ceiling is dropped — it renders empty and the map stays alive instead of a tiny gzip blob inflating to hundreds of megabytes and exhausting the arena. At the GeoJSON entry point an ingest budget caps the feature and vertex count before tessellation allocates, so a hostile FeatureCollection can't OOM the tab.

Where it is going

Roadmap · not yet shipped

These are planned, not built. They are on the roadmap, not in the engine today:

  • 3D tiles — streamed 3D mesh content as a source type.
  • Terrain — elevation tiles draped on the surface.
  • Polar-cap tile synthesis — generating cap tiles beyond the Web-Mercator ±85° band that the pyramid leaves empty.

Tracked in the repo ROADMAP.md ↗.

See it live

Real PMTiles and vector sources render in the examples gallery. To turn a Mapbox / MapLibre style or a tile source into an .xgis program, use the in-browser convert tool.

Specifications

See also