Skip to content

Building for WASM & iOS

Nimbus is dual-target: it runs natively on desktop and in the browser via WebAssembly, with an additional iOS target via UniFFI Swift bindings.

WASM

Building

# Release build with SIMD128
make wasm

# Debug build (faster compilation, no SIMD)
make wasm-dev

The release build enables SIMD128 for vectorized geometry processing:

RUSTFLAGS='--cfg=web_sys_unstable_apis -C target-feature=+simd128' \
  wasm-pack build --target web --release --out-dir pkg

Output goes to pkg/ as an ES module ready for <script type="module">.

WASM Entry Point

src/web.rs is the WASM entry point. It:

  1. Creates a wgpu surface from an HTML <canvas> element
  2. Initializes MapRenderer with a WASM-compatible HTTP client (src/wasm_fetch.rs)
  3. Loads style, sprite, and glyph data asynchronously
  4. Drives the render loop via requestAnimationFrame

Module Workers

Tile decode and tessellation are offloaded to module workers to keep the main thread free for rendering.

graph LR
    Main[Main Thread] -->|postMessage tile bytes| Worker[Module Worker]
    Worker -->|decode MVT + tessellate| Worker
    Worker -->|postMessage vertices| Main
  • Workers use src/tile_loader/wasm_worker_entry.rs as their entry point
  • Data is serialized with postcard (compact binary, no JSON overhead)
  • WorkerTileResult is the wire type between worker and main thread
  • During initialization (before workers are ready), spawn_local is used as a fallback

WASM-Specific Optimizations

  • Global allocator: talc::TalckWasm replaces the default allocator for better WASM performance
  • Release profile: opt-level = 2 (not "s"), wasm-opt = ["-O3"]
  • Gzip decompression: Uses browser's DecompressionStream API instead of flate2
  • Async HTTP: src/wasm_fetch.rs wraps the Fetch API

Glyph Loading on WASM

Glyphs are loaded asynchronously after style application:

  1. get_glyph_info() returns the glyphs URL template + unique font stacks from resolved layers
  2. wasm_fetch::fetch_bytes() fetches PBF glyph ranges
  3. parse_pbf_glyphs() + atlas.add_glyphs() populate the atlas

iOS (UniFFI)

Architecture

src/ffi.rs exposes a Swift-friendly API via UniFFI 0.28 (proc-macro style).

#[derive(uniffi::Object)]
pub struct MapEngine {
    inner: Mutex<MapEngineInner>,
}

MapEngineInner holds the wgpu Instance/Device/Queue and optional Surface/MapRenderer. unsafe impl Send/Sync is used because wgpu types are thread-safe on Metal.

Callback Interface

The Swift side implements MapViewDelegate as a callback interface:

// UniFFI callback_interface uses Box (not Arc)
Box<dyn MapViewDelegate>

The delegate is wrapped in Arc<dyn MapViewDelegate> internally for the waker closure that signals new tile arrivals.

Surface Creation

The surface is created from a raw CAMetalLayer pointer:

create_surface_unsafe(CoreAnimationLayer(ptr))

Building the XCFramework

make xcframework

This runs build-xcframework.sh, which:

  1. Builds for aarch64-apple-darwin (macOS ARM)
  2. Builds for x86_64-apple-darwin (macOS Intel)
  3. Builds for aarch64-apple-ios (iOS device)
  4. Builds for aarch64-apple-ios-sim (iOS Simulator)
  5. Packages into an .xcframework with generated Swift bindings

The Swift package is defined in WoosMapView/Package.swift as a binary target.

MSAA Disabled

MSAA is disabled in the FFI path (sample_count = 1) because MTKView's MSAA resolve triggers a Metal texture size mismatch. sRGB correction is controlled via a uniform flag instead.