Skip to content

Rendering Pipeline

Nimbus follows an Extract-Prepare-Render pattern, separating CPU geometry work from GPU resource management and draw submission.

Three-Phase Architecture

graph LR
    A[Extract] -->|vertices + indices| B[Prepare]
    B -->|GPU buffers + bind groups| C[Render]
    C -->|draw commands| D[GPU]

Extract

MapRenderer::extract() reads loaded tile data and produces CPU-friendly ExtractedRenderData — vertex arrays and index arrays for each geometry type (fill, line, icon, text).

  • Iterates style layers (not MVT layers) for correct z-ordering
  • Tessellates polygons using earcutr (results cached in TessellationCache)
  • Generates line quads with normals in Mercator space
  • Emits icon quads (4 vertices + 6 indices per point, screen-aligned pixel offsets)
  • Shapes and wraps text, emitting glyph quads

Geometry emission functions (emit_polygon_geometry, emit_line_geometry) are free functions to avoid &mut self borrow conflicts with the tessellation cache and resolved layers.

Prepare

MapRenderer::prepare() uploads the extracted data to GPU buffers and creates bind groups.

  • GPU buffer reuse: try_reuse_gpu_buffers() checks if existing buffers have enough capacity. If so, data is written via queue.write_buffer (no GPU allocation). Otherwise, new buffers are created with mapped_at_creation: true and 1.5x overallocation.
  • mapped_at_creation on Metal means direct writes into GPU-visible memory (zero staging copies).
  • Per-label opacity is stored in a small storage buffer (2-4 KB), decoupled from vertex data.

Render

MapRenderer::render() submits draw commands in a single render pass with LoadOp::Clear (background color from the style).

Draw order within the pass:

  1. Fill geometry (polygons)
  2. Line geometry
  3. Raster tiles (if any)
  4. Icon sprites
  5. Text glyphs

Module Map

graph LR
    subgraph entry ["Entry Points"]
        main["main.rs"]
        web["web.rs"]
        ffi["ffi.rs"]
        offscreen["offscreen.rs"]
    end

    subgraph renderer ["renderer/"]
        extract["extract.rs"]
        prepare["prepare.rs"]
        symbols["symbols.rs"]
        subgraph geometry ["geometry/"]
            polygon["polygon.rs"]
            line["line.rs"]
            icons["icons.rs"]
            text_geom["text.rs"]
            coords["coords.rs"]
        end
    end

    subgraph style_system ["Style System"]
        style_eval["style_eval/"]
        expressions["expressions/"]
        style_mod["style.rs"]
        subgraph styler ["styler/"]
            styler_mod["mod.rs"]
            poi["poi.rs"]
            tree["tree.rs"]
        end
    end

    subgraph tiles ["Tile Pipeline"]
        tile_loader["tile_loader/"]
        mvt["mvt.rs"]
        tile_coords["tile_coords.rs"]
        raster["raster.rs"]
        raster_loader["raster_loader/"]
    end

    subgraph support ["Support"]
        camera["camera_transform.rs"]
        collision["collision.rs"]
        sprite["sprite.rs"]
        glyph["glyph_atlas.rs"]
        text_shaper["text_shaper.rs"]
        tess_cache["tessellation_cache.rs"]
        color["color.rs"]
    end

    entry --> renderer
    renderer --> style_system
    renderer --> tiles
    renderer --> support
    style_eval --> expressions

Dirty Flags and Debouncing

Camera pans/zooms only update the uniform buffer and tile data offsets — they never touch vertex data. Two dirty flags control what gets rebuilt:

Flag Set when Triggers
geometry_dirty Tile count changes (new tile arrives or evicted) Full extract()
camera_dirty Camera moves (pan, zoom, rotation) Uniform buffer + update_tile_data()

Extract Debounce

During active user input (zoom/pan), extract is deferred for 100ms to avoid wasted CPU work on rapidly changing tile sets. New tile arrivals always trigger an immediate extract so tiles appear without delay.

Budgeted Incremental Finalize

Large extract operations are split into 4 phases to avoid frame drops:

  1. Extract — emit geometry into staging buffers
  2. Begin finalize — start merging into ExtractedState
  3. Merge — complete the merge (old data stays intact for update_tile_data())
  4. Swap — atomic self.extracted = fs.staging (single field move, no partial state)

ExtractedState groups all 12 extracted data fields into one struct so the finalize swap is a single move — eliminating the "forgot to swap field N" class of bugs.

Shaders

Three WGSL shader files in shaders/:

File Lines Purpose
map.wgsl 179 Fill + line vertex/fragment with globe projection
icon.wgsl 232 Icon/glyph SDF + RGBA dual-mode rendering
raster.wgsl 127 Raster tile textured quad rendering

The vertex shader reconstructs world positions from tile-relative Mercator offsets plus per-tile storage buffer data, avoiding f32 precision issues at high zoom levels. See Coordinate Systems for details.