Skip to content

Map example

Specification for interactive *-map example programs: small apps that exercise language bindings and render-target integrations through a focused map demo.

  • All map, runtime, and render access from application code through the project’s language binding for that language.
  • One top-level map window with resize support.
  • Continuous map mode: runtime pumping, event draining, and repaint driven by map render events and user input.
  • Initial style URL and camera per Shared defaults.
  • Camera controls per Input.
  • Support for the three render-target modes on every graphics API the example ships (see Render-target modes).
  • Every graphics API the window toolkit and target platform can support (Vulkan, Metal, OpenGL/EGL as applicable).
  • Graceful process exit when the user closes the window.
  • Startup logging that identifies the selected render-target mode and which native render backends the loaded library supports.

A *-map program is a focused map demo. It MUST NOT include automated tests or packaging/installer UX.


ExampleBindingToolkitPlatformsBackends
examples/zig-mapZigSDL3Linux, macOS, WindowsVulkan, Metal, OpenGL
examples/rust-mapRustwinitLinux, macOS, WindowsVulkan
examples/lwjgl-mapJava FFMGLFW, LWJGLLinux, macOS, WindowsVulkan
examples/swift-mapSwiftAppKit, SwiftUImacOSMetal

  • Style URL: https://tiles.openfreemap.org/styles/bright
  • Load the style during map initialization, before the first render.
FieldValue
Centerlatitude 37.7749, longitude -122.4194 (San Francisco)
Zoom13.0
Bearing12.0 degrees
Pitch30.0 degrees

Apply with an immediate jump_to on startup.

  • Initial logical size: 960 × 640 pixels.
  • Window MUST be resizable.
  • High-DPI / Retina: derive map RenderTargetExtent from the window’s drawable size and content scale (see Viewport).
  • Runtime cache path: :memory: (in-memory).
  • Map mode: continuous (MLN_MAP_MODE_CONTINUOUS).

For owned-texture and borrowed-texture, the host-owned compositor that samples the map texture into the window swapchain MUST use a fullscreen triangle covering the viewport:

  • Vertex shader: three corners with pass-through UVs spanning the visible [0, 1] × [0, 1] texture range (large-triangle technique).
  • Fragment shader: texture(map_texture, uv) (straight copy, standard UV orientation).

SPIR-V, MSL, or GLSL source MAY differ by backend; the GPU output MUST match that pass.


The process MUST accept a render-target mode name:

ModeCLI value
Session-owned textureowned-texture
Caller-owned borrowed textureborrowed-texture
Native window surfacenative-surface

The mode is a required positional argument (for example zig-map owned-texture). There is no default mode.

On --help, print usage listing the three mode names and exit 0 before creating a window. On invalid arguments, print usage listing the three mode names and exit 1 before creating a window.

The only permitted flag is --help. Implementations MUST NOT add other CLI flags.


Every *-map example splits host responsibilities into the same logical modules. Names differ by language; boundaries MUST NOT be collapsed into a single monolithic type.

flowchart TB
subgraph shell["App shell"]
EL[Event loop]
VP[Viewport]
IN[Input]
DG[Diagnostics]
end
subgraph mapstate["Map state"]
RT[Runtime]
MP[Map]
RS[Render-target session]
end
subgraph gfx["Graphics host"]
BE[Backend context]
CP[Compositor]
SC[Presentation]
end
CLI --> shell
shell --> mapstate
mapstate --> gfx
RS -->|texture modes| CP
RS -->|native-surface| SC
ModuleResponsibility
App shellProcess entry, argument parsing, window creation, main event loop, idle pacing, shutdown ordering.
ViewportMap logical size, physical drawable size, and scale_factor for RenderTargetExtent.
Map stateOwns runtime, map, and render session; loads style and initial camera; attaches render target for the selected mode.
Render-target sessionThin wrapper over RenderSessionHandle: resize, render_update, close; dispatches by texture vs surface.
BackendHost-owned device context and window presentation for the active graphics API.
CompositorHost pass that draws a map-owned or borrowed texture into the swapchain.
InputPointer and keyboard → map camera APIs; prints control help once at startup.
DiagnosticsOptional log callback and consistent error messages on failed setup or camera commands.

Implementations SHOULD mirror this layout in the source tree (separate files or packages per module).

The backend module MUST be a discriminated implementation per render-target mode (union, sealed hierarchy, or sum type). Adding a mode or backend MUST require a localized change (new enum variant and dedicated module). Keep each graphics API and each render-target mode in its own variant or submodule rather than branching ad hoc through shared draw code.

Each backend variant implements, at minimum:

  • init / deinit
  • resize(viewport)
  • attachRenderTarget(map, viewport) → session
  • finishFrame() (window presentation upkeep each pump iteration)
  • drawTexture(session, viewport) for texture modes
  • needsRenderTargetReattachOnResize() → bool (see Resize)

Order MUST be:

  1. Parse CLI and validate selected render mode.
  2. Read and log the loaded library’s supported native render backends from mln_supported_render_backend_mask(), then validate that the loaded native library supports the graphics backend(s) this binary targets; fail fast with a readable message if not.
  3. Create the window (initial size Window).
  4. Initialize the graphics backend for the selected mode.
  5. Create runtime (:memory: cache).
  6. Create map with extent from the initial viewport and continuous mode.
  7. Load style and apply initial camera.
  8. Attach render session for the selected mode.
  9. Print startup information:
    • active render-target mode CLI value
    • active render-target status line
    • control help

On failure after partial setup, release already-created handles in reverse order (session → map → runtime → graphics).

On window close or fatal error, close resources in order:

  1. Finish or wait on in-flight GPU work if the backend requires it.
  2. Render session (compositor first when it owns GPU objects separate from the session).
  3. Map
  4. Runtime
  5. Graphics context and window.
  • One runtime per process (owner thread drives run_once / pump).
  • One map per runtime for the demo.
  • One live render session per map at a time.
  • Map configuration (style, camera) uses the map handle; render-target extent and present use the render session.

Each iteration has two phases: pump (always) and render (only when render_pending is true).

While polling, handle resize (reattach the render target when required) and input (may set render_pending). Toolkits that use callbacks or timers instead of a single poll API MUST run one pump iteration per display refresh tick (for example an NSTimer on swift-map).

sequenceDiagram
participant EL as Event loop
participant RT as Runtime
participant BE as Backend
EL->>EL: Poll window + input events
EL->>RT: run_once()
EL->>RT: drain events → may set render_pending
EL->>BE: finishFrame()

finishFrame() runs every pump iteration: swapchain or surface upkeep, resize handling, and present hooks as required by the host graphics API.

sequenceDiagram
participant EL as Event loop
participant RS as Render session
participant CP as Compositor
participant BE as Backend
EL->>RS: render_update()
alt texture mode
RS-->>EL: map texture / frame
EL->>CP: draw into swapchain
CP->>BE: present
else native-surface
RS->>BE: present via surface session
end

Requirements:

  • MUST call runtime run_once once per loop iteration while the app is running.
  • MUST drain runtime events each iteration and set render_pending when:
    • map_render_update_available targets this map (new map content to draw), or
    • map_render_frame_finished targets this map and needs_repaint is true (continuous mode needs another frame, for example ongoing camera transitions).
  • MUST set render_pending when input changes the camera.
  • MUST call render_update only while render_pending is true.
  • MUST clear render_pending after render_update returns success.
  • On invalid_state from render_update, leave render_pending set and continue the pump loop (no frame was drawn yet).
  • SHOULD idle-sleep briefly when an iteration makes no progress (event poll, render, or runtime work).

Texture modes: after a successful render_update, MUST run the compositor pass to copy the map texture into the window swapchain before present.


The viewport value MUST contain:

FieldMeaning
logical_width, logical_heightMap coordinate extent passed to MapOptions / RenderTargetExtent.
physical_width, physical_heightDrawable pixels of the window framebuffer.
scale_factorRatio between physical and logical sizes (content scale / pixel density).

Derivation rules:

  • Read logical and physical sizes from the window toolkit after creation and on every resize / backing-scale change.
  • Compute logical dimensions from physical size and scale when the toolkit only exposes physical pixels (use ceil(physical / scale), minimum 1).
  • Log viewport changes at informational level with field labels logical=… physical=… scale=….

Pass logical_* and scale_factor to map creation, session attach, and session resize.


The map state module owns the runtime, map, and render session handles plus map-specific setup.

  • Create runtime with :memory: cache.
  • Create map with current viewport extent and continuous mode.
  • Load style URL.
  • Apply initial camera.
  • Delegate render-session attachment to the backend for the CLI-selected mode.
  • Drain all pending runtime events each frame.
  • Set render_pending for the frame loop when either:
    • map_render_update_available targets this map, or
    • map_render_frame_finished targets this map and needs_repaint is true.

Expose resize(viewport) that forwards to the render-target session. For texture modes, also resize the compositor. When the backend reports needsRenderTargetReattachOnResize, expose resizeWithReattachedTarget(viewport, backend) that destroys the session, resizes backend-owned textures/surfaces, and re-attaches.


Three modes MUST be modeled in every example’s architecture (CLI parsing, backend discriminant, and attach paths). Each example MUST implement all three modes on every graphics API the example binary exposes.

CLI valueC API conceptCompositorRole
owned-textureSession-owned backend textureRequiredMap allocates texture, host samples it.
borrowed-textureCaller-owned texture borrowed by sessionRequiredHost allocates exportable texture; session renders into it.
native-surfaceWindow presentation surfaceNoneMap renders directly to the window presentation target.

Startup MUST print the active mode’s CLI value and exactly one line from this table:

CLI valuePrinted line
owned-texturerender target status: samples MapLibre-owned texture frames into the host swapchain
borrowed-texturerender target status: renders into a host-owned texture, then samples it into the host swapchain
native-surfacerender target status: renders directly to the host window surface
  • Attach with the C API owned-texture descriptor for the active graphics API.
  • Pass the host graphics context handles required by that descriptor (see Graphics API).
  • On render_update, acquire the frame/image from the session, draw via compositor, release/close the frame per the C API frame lifetime rules.
  • Host creates an exportable texture sized to the viewport (see Graphics API).
  • Attach with the borrowed-texture descriptor referencing host-owned handles.
  • On render_update, sample that texture through the same compositor path as owned-texture.
  • On resize, recreate the host texture and re-attach the session (see Resize; needsRenderTargetReattachOnResize is true for this mode).
  • Attach with the C API surface descriptor for window presentation (see Graphics API).
  • render_update presents through the surface session directly.
  • drawTexture MUST NOT be called for this mode.
  • On resize, call session resize and rebuild host presentation; reattach when the window toolkit supplies a new surface handle.

  • Subscribe to window size, framebuffer size, and display-scale / content-scale events (as available on the platform).
  • Recompute viewport; skip rendering if extent is empty.
  • needsRenderTargetReattachOnResize() is a backend method. It returns true for borrowed-texture because the host-owned exportable texture is fixed to the viewport size: resize destroys the session, recreates the texture, and attaches again. It returns false for owned-texture and native-surface, where resize updates the swapchain or surface and calls session resize (and resizes the compositor for texture modes).
  • When it returns true, use the full reattach path; otherwise resize backend, compositor (texture modes), and session in place.
  • Set render_pending after any resize.

Implementations MUST provide the following interactions and MUST print this help text once at startup:

Controls:
left drag: pan
right drag or Ctrl+left drag: rotate with X, pitch with Y
scroll: zoom at cursor
arrows or WASD: pan
+ / -: zoom at center
Q / E: rotate
] / [: pitch
0: reset pitch and bearing
InteractionBehavior
Left dragmove_by with pointer delta in logical coordinates.
Right drag, or Ctrl+left dragAdjust bearing by 0.5 × Δx degrees; adjust pitch by 0.5 × Δy degrees (same sign convention everywhere).
ScrollZoom about cursor: scale_by(2^(Δ * 0.25), anchor). Δ from the toolkit wheel event; scrolling up zooms in (use OS-adjusted deltas as reported—do not undo platform scroll inversion).
Arrow keys / WASDPan 120 logical units per key press.
+ / -Zoom 1.25 / 1/1.25 about viewport center.
Q / EBearing ±10° with keyboard animation.
]Pitch +5° (clamped to [0, 60]) with animation.
[Pitch −5° (clamped to [0, 60]) with animation.
0Animate bearing and pitch to 0 with keyboard animation.

Keyboard animated moves SHOULD use ~160 ms duration. Pointer drags use immediate move_by / jump_to / pitch_by.

On pointer down that starts a drag, cancel in-flight camera transitions before applying deltas.

Input handlers return whether the camera changed so the frame loop can set render_pending.


  • SHOULD register a native log callback during startup and clear it on shutdown.
  • On setup or camera failure, print a short message including the native status and diagnostic strings returned by the C API.
  • On startup, print the three items listed in Startup step 9.

Each example MUST expose every API below that its toolkit and platform can support, and MUST implement all three render-target modes on each exposed API. Attach descriptors and shared context handles:

  • One shared Vulkan context (VkInstance, VkDevice, queue, and VkSurfaceKHR) for compositor and render session.
  • owned-texture: Vulkan owned-texture descriptor with those shared handles.
  • borrowed-texture: exportable VkImage and view sized to the viewport; borrowed-texture descriptor.
  • native-surface: surface / swapchain presentation descriptor for the window VkSurfaceKHR.
  • native-surface: Metal surface descriptor for the window CAMetalLayer.
  • owned-texture: Metal owned-texture descriptor; shared device and layer handles required by the C API.
  • borrowed-texture: exportable Metal texture sized to the viewport; borrowed-texture descriptor.
  • native-surface: OpenGL or EGL surface descriptor for the window’s platform GL surface.
  • owned-texture: OpenGL owned-texture descriptor; shared GL context handles required by the C API.
  • borrowed-texture: exportable GL texture sized to the viewport; borrowed-texture descriptor.