Skip to content

Contributing

Development Setup

# Install Rust (nightly recommended for WASM SIMD)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Clone and build
git clone <repo-url>
cd wgpu-triangle
make build

Code Quality

Pre-commit Hooks

The project has pre-commit hooks that run cargo fmt and cargo clippy before each commit. Make sure your code passes both:

make fmt    # Auto-format
make lint   # Run clippy

Testing

# Run all tests (~560, offline)
make test

# Run a specific test
cargo test test_interpolate_linear

# Run tests with output
cargo test -- --nocapture

Test conventions:

  • Tests are embedded in source files via #[cfg(test)] modules
  • Mock external dependencies (HTTP, GPU)
  • Test edge cases: empty inputs, invalid data, boundary values
  • One assertion per test when practical

HTTP fixtures

Tests never touch the network. Anything that fetches (style, TileJSON, tiles, sprites, glyphs) goes through fixture_http_client::for_tests(), which serves committed responses from tests/fixtures/http/{host}/{path}. API keys are dropped from the file names and replaced by FIXTURE_KEY in the bodies. A request with no fixture gets a 404 and a warning naming the URL.

Re-record after a style or tile change, or when a test needs new tiles:

NIMBUS_RECORD=1 WOOSMAP_KEY=… cargo test

This fetches every URL live and rewrites the fixtures it touched. Delete tests/fixtures/http first for a clean set. The glyphs and sprites are the ones in the maps repository's assets/, copied here so a checkout needs nothing else.

Benchmarks

Criterion.rs benchmarks cover the CPU-hot paths:

Benchmark Module
mvt_decode Protobuf parsing
tessellation Polygon triangulation
collision Label placement
expressions Style expression evaluation
extraction Geometry extraction pipeline
shape (nimbus-shaper) Complex-script shaping and SDF rasterisation, one label per script

They read the committed tiles and style in benches/fixtures/ (cargo run --bin record-fixtures refreshes them).

make bench          # Run all benchmarks
make bench-report   # Open HTML report in browser

Scene benchmark

bench_render renders fixed scenes headless from the HTTP fixtures and measures each frame the way a host draws it (update_frame, encode, submit, wait for the GPU):

Scene What it draws
pan flat z14 Montpellier, panning back and forth
zoom Montpellier, zooming between z12 and z17
buildings tilted z16 Montpellier, buildings and shadows, orbiting
models the Eiffel Tower and the car (needs make models), orbiting
globe the globe at z3, turning
scripts z15 Cairo, Arabic labels through the complex shaper
night tilted z15.5 Montpellier at midnight

For each scene it reports p50, p95 and max of every phase (poll, extract, tile_data, prepare, render, gpu_wait, whole frame), and memory: the heap's peak and what stays live (a counting global allocator), allocations per frame, the bytes the GPU device holds (Metal's currentAllocatedSize; wgpu reports nothing on Metal), and the tiles cached. target/bench/scenes.json holds it all.

make bench-scenes                       # all scenes, 120 frames each
SCENE=buildings,night FRAMES=300 make bench-scenes
make bench-baseline                     # save criterion + scene baselines
make bench-check                        # fail on a regression

The baselines live in benches/baselines/:

  • memory.json is committed. Memory is nearly deterministic, so it is checked tightly: 5% by default, twice that for the heap peak, which depends on how the worker threads interleave. Each figure is the median of three runs. A scene whose tolerance is null is reported but not gated: zoom and scripts load tiles while they are measured, and what they hold depends on when the tiles land.
  • cpu-<host>.json is per machine and gitignored. Phases fail past 10% plus 0.05 ms at p50; criterion benchmarks past 10% on the mean.
  • Timing is not counted when either run was on a busy machine (load above half the CPU count). bench-baseline will not save a CPU baseline then.

Re-run make bench-baseline after a change that is meant to move the numbers, and commit memory.json with it.

Profiling

Instruments records any scene from the command line, and opens the trace:

SCENE=buildings make profile-cpu     # Time Profiler
SCENE=models make profile-alloc      # Allocations
SCENE=night make profile-gpu         # Metal System Trace: per-pass GPU time
SCENE=pan make profile-heap          # DHAT: dhat-heap.json, for dh_view

FRAMES sets how long it records (2000 by default). The profiling build keeps its symbols in a .dSYM and is built with legacy symbol mangling, in target/instruments: Instruments cannot demangle v0, nightly's default.

Coverage

# Generate LCOV coverage report
make coverage

# Run SonarQube analysis (requires local SonarQube instance)
make sonar

Target: ≥80% coverage, zero bugs, zero vulnerabilities, zero code smells.

Architecture Guidelines

  • Extract-Prepare-Render: all new rendering features should follow this pattern
  • Free functions for geometry emission to avoid borrow conflicts
  • Tile-relative Mercator encoding for all vertex positions
  • Storage buffers for per-label/per-tile data (not per-vertex attributes)
  • [profile.dev.package."*"] opt-level = 2: dependencies are optimized even in debug builds

Module Ownership

Area Key files
Rendering pipeline src/renderer/
Tile management src/tile_loader/, src/tile_coords.rs
Style evaluation src/style_eval/, src/style.rs, src/expressions/
Text/icons src/glyph_atlas.rs, src/text_shaper.rs, src/sprite.rs, src/collision.rs
Platform bridges src/ffi.rs (iOS), src/web.rs (WASM)
Shaders shaders/*.wgsl