📐 GIS Renderer Plugin
Developer Design Document (v0.1)
0. Introduction & Project Overview
The GIS Renderer Plugin is a real-time geospatial rendering and
navigation engine that streams, renders, and augments real-world
geographic data inside interactive applications.
It is designed as a spatial runtime, not a traditional map viewer.
The system allows applications to load and interact with real-world
geography dynamically. Geographic data is streamed on demand and
rendered as raster imagery, vector map data, or elevation-based
terrain. All rendering modes share the same coordinate system and
spatial logic, ensuring consistency across zoom levels, projections,
and navigation states.
To support large geographic areas, the system uses a fixed-size
tile streaming model. Only a bounded set of tiles is kept in
memory at any time. As the camera moves, tiles entering the view are
fetched and tiles leaving the view are evicted in a deterministic way.
This keeps memory usage stable regardless of the size of the
mapped area.
Navigation is built into the core runtime rather than layered on top.
Routes are fetched from provider-agnostic APIs and parsed into
structured route data. A live Journey system tracks progress along
the route using the current user location, updating the active path,
remaining distance, estimated time, and navigation instructions
continuously.
All spatial computation—coordinate conversion, routing logic,
geocoding, and tile addressing—is handled inside the GIS core.
Latitude and longitude remain the authoritative coordinate space
throughout the system. Conversion into engine-specific world
coordinates happens only at the point of rendering to avoid precision
loss and inconsistencies.
The architecture is engine-independent and provider-
independent. The current integration targets:
Godot as the rendering and input layer
Rust as the GIS runtime responsible for streaming, navigation,
geocoding, and spatial computation
Rust is used to provide predictable memory behavior, safe
concurrency for asynchronous data fetching, and accurate
geographic calculations. Godot is responsible only for rendering, user
input, and presentation. Communication between the two is handled
through a strict FFI boundary with clear ownership and lifetime rules.
This document is a technical design specification. It describes
the system as it exists today—tile streaming, quadtree management,
navigation journeys, overlays, and asynchronous data pipelines—and
formalizes the design to support continued development and
extension.
Alright.
I’ll treat this as a proper internal engineering design doc, not
marketing fluff, not README-level, and not UE-specific.
Platform-agnostic, Godot + Rust target, but cleanly abstracted
so the renderer could be embedded elsewhere later.
I’ll also implicitly reflect the actual system you already
built (tile streaming, quadtree, navigation, overlay, async APIs)
instead of reinventing it.
1. Purpose & Scope
The GIS Renderer Plugin is a real-time geospatial rendering and
navigation engine that streams, renders, and augments real-world
geographic data inside an interactive 2D/3D scene.
It is designed as a core spatial runtime, not a map viewer.
Primary goals:
Render real-world geography (raster, vector, elevation)
interactively
Maintain constant memory usage via tile streaming
Provide navigation + geocoding as first-class primitives
Support AR-style world overlays
Remain provider-agnostic and engine-agnostic
Move heavy logic into Rust (deterministic, safe, testable)
Target integration (current):
Godot Engine (frontend / renderer)
Rust (core GIS + data + navigation engine)
2. High-Level Architecture
1 ┌──────────────────────────────┐
2 │ Host Engine (Godot) │
3 │ - Camera / Viewport │
4 │ - Mesh & Material Binding │
5 │ - Input (pan / zoom / AR) │
6 └───────────────▲──────────────┘
7 │ FFI / Bindings
8 ┌───────────────┴──────────────┐
9 │ GIS Renderer Core (Rust) │
10 │ │
11 │ ┌──────── Tile System ────┐ │
12 │ │ Quadtree / Tile IDs │ │
13 │ │ Streaming Window ││
14 │ │ Cache & Eviction ││
15 │ └────────────────────────┘ │
16 │ │
17 │ ┌──── Rendering Models ───┐ │
18 │ │ Raster Tiles │
19 │ │ Vector Tiles (MVT) │
20 │ │ Height / DEM Tiles │
21 │ └────────────────────────┘ │
22 │ │
23 │ ┌──── Navigation Engine ──┐ │
24 │ │ Route Fetching │
25 │ │ Path Flattening │
26 │ │ Journey State │
27 │ │ Instructions │
28 │ └────────────────────────┘ │
29 │ │
30 │ ┌──── Geo Intelligence ───┐ │
31 │ │ Lat/Lon ↔ Tile │
32 │ │ Geocoding / Reverse │
33 │ │ POI / Overlay Data │
34 │ └────────────────────────┘ │
35 └──────────────────────────────┘
3. Core Design Principles
3.1 Constant-Memory Streaming
World is infinite; memory is not.
Only a fixed window of tiles exists in memory.
Default strategy:
4×4 atlas grid
3×3 active viewport
Panning shifts the window; tiles are evicted and fetched
deterministically.
3.2 Data-Driven Everything
No static markers.
No baked paths.
Navigation, markers, overlays all react to live state:
user location
zoom level
provider data
journey progress
3.3 Precision First
All geographic math is double-precision.
Rendering coordinates are derived last.
No lossy projection early in the pipeline.
4. Tile System
4.1 Tile Identity
1 struct TileId {
2 zoom: u8,
3 x: u32,
4 y: u32,
5 }
Slippy-map compatible
Hashable
Parent/child relationships (quadtree)
4.2 Quadtree
Responsibilities:
Logical tile hierarchy
Cache ownership
Parent/child resolution
LOD transitions (future)
Each node:
1 struct TileNode<T> {
2 id: TileId,
3 resource: Option<T>,
4 fetched: bool,
5 children: [Option<NodeRef>; 4],
6 }
4.3 Tile Streaming Window
Inputs:
Center tile
Grid dimensions
Camera offset (fractional)
Outputs:
Deterministic list of visible TileIds
Fetch / evict decisions
This logic already exists in your system and must remain
deterministic to avoid jitter, flicker, or tile drift.
5. Rendering Modes
5.1 Static Raster Tiles
PNG / JPG
Satellite / imagery
Decoded in Rust
Uploaded as engine textures
Fallback:
Solid color tile for missing data
5.2 Vector Tiles
MVT (Mapbox Vector Tile)
Parsed in Rust
Geometry flattened into:
lines (roads)
polygons (buildings)
points (POIs)
Renderer decides:
mesh generation
styling
labels
5.3 Height / DEM Tiles
Height encoded (PNG / GeoTIFF / quantized mesh)
Converted into height arrays
Mesh generated per tile
Stitching & skirts handled at tile boundaries
6. Coordinate Conversion Engine
This is foundational and already a major solved pain point.
6.1 Supported Spaces
Lat / Lon (WGS84)
Tile Space (X/Y/Z)
Local Tile Space (fractional offsets)
Engine World Space
6.2 Core Functions
1 latlon_to_tile(lat, lon, zoom) -> TileId
2 tile_to_latlon(tile, offset) -> LatLon
3 local_to_gis(local_point, camera_state) -> LatLon
4 gis_to_local(latlon, camera_state) -> Vec2
6.3 Invariants
Center tile must always map to viewport center.
Camera offsets must be normalized against grid bounds.
Odd/even grid parity must be preserved.
Your previous debug-trace heavy work stays — it’s essential.
7. Navigation System
7.1 Route Fetching
Provider-agnostic API layer
URL built dynamically:
origin
destination
travel mode
Rust owns:
HTTP
JSON parsing
error handling
7.2 Route Data Model
1 struct GeoCoordinate {
2 lat: f64,
3 lon: f64,
4 }
5
6 struct RouteStep {
7 instruction: String,
8 distance_m: f64,
9 duration_s: f64,
10 geometry: Vec<GeoCoordinate>,
11 }
12
13 struct Route {
14 steps: Vec<RouteStep>,
15 geometry: Vec<GeoCoordinate>, // flattened
16 }
7.3 Journey System
A Journey is a live navigation session.
1 struct Journey {
2 route: Route,
3 current_index: usize,
4 remaining_distance: f64,
5 remaining_duration: f64,
6 }
Advances based on user location
Emits:
current instruction
remaining path
ETA updates
This replaces static path rendering entirely.
8. Overlay & Marker System
8.1 Marker Model
1 struct Marker {
2 lat: f64,
3 lon: f64,
4 kind: MarkerType,
5 }
Markers:
Stored in GIS space
Reprojected every frame
Auto-hidden when out of relevance
8.2 Path Rendering
Spline / polyline generated from route geometry
Clipped to local area bounds
Truncated, never “disappears”
Preserves visual continuity
(Your spline clipping solution is correct and intentional.)
9. Geocoding & World Intelligence
9.1 Forward / Reverse Geocoding
Provider API or local DB
Rust returns structured results
Engine decides presentation
9.2 Custom World Data
User-defined POIs
Metadata overlays
Reviews, ratings, tags
Used heavily in AR mode
10. AR Overlay Mode (Conceptual)
GIS world is anchor truth
Device pose is relative
Overlays projected from GIS → camera
Same marker + journey systems reused
No separate AR logic branch.
11. Error Handling & Logging
Centralized logging (already built)
Severity-aware
Engine-filterable
Safe in release builds
This is non-negotiable for a system this complex.
12. Why Rust + Godot (Explicit Rationale)
Rust:
Deterministic memory
Safe async
Testable math
No engine lifecycle footguns
Godot:
Lightweight renderer
Easy FFI
Mobile & desktop friendly
AR support without vendor lock-in
13. Non-Goals (Explicit)
Not a UI map SDK
Not a static map exporter
Not provider-locked
Not GoDot-only
Not “just tiles”
🔌 Godot ↔ Rust FFI ABI Design
0. Core Philosophy (non-negotiable)
1. Rust owns all logic and state
2. Godot owns all rendering objects
3. No shared mutable memory
4. No callbacks from Rust into Godot
5. Godot pulls snapshots; Rust never pushes
If you violate any of these, the project will rot.
1. ABI Shape (C-compatible, stable)
We expose a flat C ABI from Rust.
No Rust structs exposed directly
No lifetimes across boundary
No async across boundary
No Godot types in Rust
No Rust types in Godot
Language boundary
1 Godot (GDExtension / C++)
2 ↓
3 C ABI
4 ↓
5 Rust
2. Opaque Handles (Ownership Model)
Rust side
1 #[repr(C)]
2 pub struct RuntimeHandle {
3 ptr: *mut Runtime,
4 }
Godot never dereferences this
Godot treats it as an opaque token
Rust guarantees validity until destroyed
Godot side
1 typedef void* GIS_Runtime;
3. Lifecycle API (Minimal & Complete)
3.1 Create / Destroy
1 GIS_Runtime gis_runtime_create();
2 void gis_runtime_destroy(GIS_Runtime runtime);
Rules
create allocates all long-lived Rust state
destroy stops async tasks, drains queues, frees memory
Godot must call destroy exactly once
4. Configuration Phase (Before Start)
No hot mutation of core config.
1 void gis_runtime_set_provider(
2 GIS_Runtime runtime,
3 const char* provider_name,
4 const char* api_key
5 );
6
7 void gis_runtime_set_viewport(
8 GIS_Runtime runtime,
9 int grid_x,
10 int grid_y
11 );
Rules:
Strings are copied immediately
Caller retains ownership of strings
Configuration must happen before first update
5. Update Loop Contract (Heart of the System)
5.1 Godot → Rust (Push minimal state)
Called once per frame.
1 void gis_runtime_update(
2 GIS_Runtime runtime,
3 double delta_time,
4 double camera_lat,
5 double camera_lon,
6 double zoom,
7 double heading_deg
8 );
Rust responsibilities:
advance streaming window
advance journey state
poll async channels
update internal world state
❗ No rendering happens here
6. Snapshot Model (Pull, Don’t Push)
Godot pulls immutable snapshots every frame.
6.1 Tile Snapshot
1 typedef struct {
2 int zoom;
3 int x;
4 int y;
5 int kind; // raster / vector / height
6 const void* data;
7 int data_len;
8 } GIS_TileSnapshot;
1 int gis_runtime_get_visible_tiles(
2 GIS_Runtime runtime,
3 const GIS_TileSnapshot** out_tiles
4 );
Rules:
Rust owns memory
Snapshot valid until next update
Godot must copy data if it wants to keep it
6.2 Navigation Snapshot
1 typedef struct {
2 double lat;
3 double lon;
4 } GIS_Point;
5
6 typedef struct {
7 GIS_Point* points;
8 int count;
9 } GIS_Path;
1 bool gis_runtime_get_active_path(
2 GIS_Runtime runtime,
3 GIS_Path* out_path
4 );
Geometry already flattened
Ordered for spline / polyline rendering
Valid for one frame only
6.3 Instruction Snapshot
1 typedef struct {
2 const char* text;
3 double remaining_distance_m;
4 double remaining_time_s;
5 } GIS_NavInstruction;
1 bool gis_runtime_get_current_instruction(
2 GIS_Runtime runtime,
3 GIS_NavInstruction* out_instruction
4 );
Strings:
UTF-8
Rust-owned
Valid until next update()
7. Marker / Overlay Snapshots
1 typedef struct {
2 double lat;
3 double lon;
4 int kind;
5 } GIS_Marker;
1 int gis_runtime_get_visible_markers(
2 GIS_Runtime runtime,
3 const GIS_Marker** out_markers
4 );
Markers:
Always stored in GIS space
Godot projects them into world/AR
8. Memory Rules (READ THIS TWICE)
8.1 Allocation Rules
Resource Owner
Runtime Rust
Tile data Rust
Route geometry Rust
Strings Rust
Meshes Godot
Textures Godot
8.2 Lifetime Rules
All snapshot pointers:
invalidated on next update()
Godot must copy if persistence is needed
Rust never frees memory mid-frame
8.3 Threading Rules
All ABI calls are single-threaded
Rust async tasks communicate via channels
Godot thread never blocks on Rust async
9. Error Handling Strategy
No panics across FFI.
1 int gis_runtime_get_last_error(
2 GIS_Runtime runtime,
3 const char** out_message
4 );
Errors are sticky until queried
Godot can log or surface them
Rust continues running unless fatal
10. Minimal Godot Frame Pseudocode
1 void _process(double dt) {
2 gis_runtime_update(
3 runtime,
4 dt,
5 camera_lat,
6 camera_lon,
7 zoom,
8 heading
9 );
10
11 // tiles
12 const GIS_TileSnapshot* tiles;
13 int count = gis_runtime_get_visible_tiles(runtime, &tiles);
14 render_tiles(tiles, count);
15
16 // navigation
17 GIS_Path path;
18 if (gis_runtime_get_active_path(runtime, &path)) {
19 render_path(path);
20 }
21
22 // instruction
23 GIS_NavInstruction instr;
24 if (gis_runtime_get_current_instruction(runtime, &instr)) {
25 ui_show([Link]);
26 }
27 }
No callbacks.
No surprises.
No UB.
11. Why This ABI Will Survive
Godot can crash → Rust still clean
Rust can restart runtime → Godot unaffected
Async never leaks
No lifetime ambiguity
No engine lock-in
This is exactly how engines like Unity DOTS, Mapbox Native, and
game physics runtimes do it internally.
🦀 Rust Module Layout
1. Crate Topology
This should not be one monolithic crate.
1 gis/
2 ├── crates/
3 │ ├── gis-core/ # Pure GIS math + types (NO async, NO IO)
4 │ ├── gis-tiles/ # Tile streaming, quadtree, cache
5 │ ├── gis-net/ # HTTP, providers, async fetchers
6 │ ├── gis-nav/ # Routing, journeys, navigation logic
7 │ ├── gis-geo/ # Geocoding, POIs, world intelligence
8 │ ├── gis-runtime/ # Orchestrator / facade used by engines
9 │ ├── gis-ffi/ # Godot <-> Rust boundary
10 │ └── gis-log/ # Logging & diagnostics
11 │
12 └── [Link] # Workspace
Rule:
Anything that touches the network → async → gis-net
Anything that touches the engine → gis-ffi
Anything that does math → gis-core
Anything that coordinates systems → gis-runtime
2. Core Crates (Deep Dive)
2.1 gis-core (Zero-dependency foundation)
NO async. NO IO. NO engine.
1 gis-core/
2 ├── [Link]
3 ├── geo/
4 │ ├── [Link]
5 │ ├── tile_id.rs
6 │ ├── [Link]
7 │ └── [Link]
8 ├── math/
9 │ ├── [Link]
10 │ └── [Link]
11 └── traits/
12 ├── [Link]
13 └── render_model.rs
Responsibilities
LatLon , TileId , GeoBounds
Mercator math
Tile ↔ lat/lon conversions
Traits only, no implementations
1 pub struct LatLon {
2 pub lat: f64,
3 pub lon: f64,
4 }
5
6 pub struct TileId {
7 pub zoom: u8,
8 pub x: u32,
9 pub y: u32,
10 }
This crate must be:
deterministic
testable
engine-agnostic
2.2 gis-tiles (Streaming + quadtree)
1 gis-tiles/
2 ├── [Link]
3 ├── quadtree/
4 │ ├── [Link]
5 │ ├── [Link]
6 │ └── [Link]
7 ├── cache/
8 │ ├── [Link]
9 │ └── [Link]
10 ├── streaming/
11 │ ├── [Link]
12 │ └── [Link]
13 └── resources/
14 ├── [Link]
15 ├── [Link]
16 └── [Link]
Responsibilities
Tile window calculation (3×3, 4×4, etc.)
Cache eviction
Tile lifecycle state:
requested
in-flight
ready
evicted
1 pub enum TileState {
2 Empty,
3 Fetching,
4 Ready,
5 }
❗ Important
gis-tiles does not fetch data.
It only asks for tiles.
Fetching is injected.
2.3 gis-net (Async, providers, HTTP)
1 gis-net/
2 ├── [Link]
3 ├── http/
4 │ ├── [Link]
5 │ └── [Link]
6 ├── providers/
7 │ ├── [Link]
8 │ ├── [Link]
9 │ └── [Link]
10 ├── fetchers/
11 │ ├── [Link]
12 │ ├── [Link]
13 │ ├── [Link]
14 │ └── [Link]
15 └── decode/
16 ├── [Link]
17 ├── [Link]
18 └── [Link]
Responsibilities
Async HTTP
Provider-specific URL building
Decoding network payloads → raw data models
1 #[async_trait]
2 pub trait TileProvider {
3 async fn fetch_raster(&self, id: TileId) -> Result<RasterTile>;
4 }
Uses:
tokio
reqwest
serde
Nothing engine-facing.
2.4 gis-nav (Navigation engine)
1 gis-nav/
2 ├── [Link]
3 ├── route/
4 │ ├── [Link]
5 │ ├── [Link]
6 │ └── [Link]
7 ├── journey/
8 │ ├── [Link]
9 │ └── [Link]
10 └── instructions/
11 └── [Link]
Responsibilities
Route parsing
Geometry flattening
Journey state machine
1 pub struct Journey {
2 route: Route,
3 cursor: usize,
4 }
No rendering.
No HTTP.
Pure logic + async entry points.
2.5 gis-geo (Geocoding & POIs)
1 gis-geo/
2 ├── [Link]
3 ├── geocode/
4 │ ├── [Link]
5 │ └── [Link]
6 ├── poi/
7 │ ├── [Link]
8 │ └── [Link]
9 └── overlay/
10 └── [Link]
Supports:
External geocoding APIs
Local POI DBs
AR overlay metadata
3. gis-runtime (The brain)
This is the only crate the engine talks to.
1 gis-runtime/
2 ├── [Link]
3 ├── state/
4 │ ├── [Link]
5 │ ├── [Link]
6 │ └── [Link]
7 ├── systems/
8 │ ├── tile_system.rs
9 │ ├── nav_system.rs
10 │ └── overlay_system.rs
11 └── [Link]
Responsibilities
Hold long-lived state
Glue tiles + nav + geo together
Emit plain data outputs for renderer
1 pub struct Runtime {
2 tile_system: TileSystem,
3 nav_system: NavSystem,
4 }
This is where:
tile requests are scheduled
async results are applied
world state advances
4. gis-ffi (Godot boundary)
1 gis-ffi/
2 ├── [Link]
3 ├── api/
4 │ ├── [Link]
5 │ ├── [Link]
6 │ └── [Link]
7 ├── types/
8 │ ├── [Link]
9 │ └── [Link]
10 └── [Link]
Rules
No internal structs leak
Engine sees opaque handles
Rust owns memory
1 #[repr(C)]
2 pub struct RuntimeHandle(*mut Runtime);
Godot:
calls update(dt)
pulls snapshot data
never touches internals
5. Ownership Boundaries (Critical)
5.1 What Rust Owns
Tile cache
Route data
Navigation state
Geo math
Async tasks
5.2 What Engine Owns
Meshes
Materials
Textures
UI widgets
5.3 Data Flow Direction
1 Network → Rust → Plain Data → Engine → GPU
Never:
1 Engine → Rust → GPU
6. Async Model (Very Important)
6.1 Single Tokio Runtime
Created once (inside gis-runtime )
Shared via handles
6.2 Async Boundaries
Layer Async?
gis-core ❌
gis-tiles ❌
gis-net ✅
Layer Async?
gis-nav ⚠️ (fetch only)
gis-runtime ⚠️ (orchestration)
gis-ffi ❌
6.3 Pattern Used
Command → async task → channel → apply on tick
1 // async task
2 tokio::spawn(async move {
3 let tile = provider.fetch_raster(id).await?;
4 [Link](Event::TileReady(tile));
5 });
Runtime:
drains channel during update()
applies changes deterministically
No async inside render loop.
7. Why This Layout Will Scale
You can:
swap providers
swap engines
add offline routing
add LOD tiles
without touching core math
without rewriting FFI
without async bleeding into rendering