Map example
Specification for interactive *-map example programs: small apps that exercise
language bindings and render-target integrations through a focused map demo.
What every example provides
Section titled “What every example provides”- 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.
What an example is not
Section titled “What an example is not”A *-map program is a focused map demo. It MUST NOT include automated tests or
packaging/installer UX.
Implementations
Section titled “Implementations”| Example | Binding | Toolkit | Platforms | Backends |
|---|---|---|---|---|
examples/zig-map | Zig | SDL3 | Linux, macOS, Windows | Vulkan, Metal, OpenGL |
examples/rust-map | Rust | winit | Linux, macOS, Windows | Vulkan |
examples/lwjgl-map | Java FFM | GLFW, LWJGL | Linux, macOS, Windows | Vulkan |
examples/swift-map | Swift | AppKit, SwiftUI | macOS | Metal |
Shared defaults
Section titled “Shared defaults”- Style URL:
https://tiles.openfreemap.org/styles/bright - Load the style during map initialization, before the first render.
Initial camera
Section titled “Initial camera”| Field | Value |
|---|---|
| Center | latitude 37.7749, longitude -122.4194 (San Francisco) |
| Zoom | 13.0 |
| Bearing | 12.0 degrees |
| Pitch | 30.0 degrees |
Apply with an immediate jump_to on startup.
Window
Section titled “Window”- Initial logical size:
960×640pixels. - Window MUST be resizable.
- High-DPI / Retina: derive map
RenderTargetExtentfrom the window’s drawable size and content scale (see Viewport).
Map and runtime
Section titled “Map and runtime”- Runtime cache path:
:memory:(in-memory). - Map mode: continuous (
MLN_MAP_MODE_CONTINUOUS).
Compositor shaders (texture modes)
Section titled “Compositor shaders (texture modes)”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.
Command-line interface
Section titled “Command-line interface”Render-target selection
Section titled “Render-target selection”The process MUST accept a render-target mode name:
| Mode | CLI value |
|---|---|
| Session-owned texture | owned-texture |
| Caller-owned borrowed texture | borrowed-texture |
| Native window surface | native-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.
Other flags
Section titled “Other flags”The only permitted flag is --help. Implementations MUST NOT add other CLI
flags.
Architecture
Section titled “Architecture”Overview
Section titled “Overview”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| SCLogical modules
Section titled “Logical modules”| Module | Responsibility |
|---|---|
| App shell | Process entry, argument parsing, window creation, main event loop, idle pacing, shutdown ordering. |
| Viewport | Map logical size, physical drawable size, and scale_factor for RenderTargetExtent. |
| Map state | Owns runtime, map, and render session; loads style and initial camera; attaches render target for the selected mode. |
| Render-target session | Thin wrapper over RenderSessionHandle: resize, render_update, close; dispatches by texture vs surface. |
| Backend | Host-owned device context and window presentation for the active graphics API. |
| Compositor | Host pass that draws a map-owned or borrowed texture into the swapchain. |
| Input | Pointer and keyboard → map camera APIs; prints control help once at startup. |
| Diagnostics | Optional 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).
Backend and mode matrix
Section titled “Backend and mode matrix”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/deinitresize(viewport)attachRenderTarget(map, viewport) → sessionfinishFrame()(window presentation upkeep each pump iteration)drawTexture(session, viewport)for texture modesneedsRenderTargetReattachOnResize() → bool(see Resize)
Lifecycle
Section titled “Lifecycle”Startup
Section titled “Startup”Order MUST be:
- Parse CLI and validate selected render mode.
- 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. - Create the window (initial size Window).
- Initialize the graphics backend for the selected mode.
- Create runtime (
:memory:cache). - Create map with extent from the initial viewport and continuous mode.
- Load style and apply initial camera.
- Attach render session for the selected mode.
- 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).
Shutdown
Section titled “Shutdown”On window close or fatal error, close resources in order:
- Finish or wait on in-flight GPU work if the backend requires it.
- Render session (compositor first when it owns GPU objects separate from the session).
- Map
- Runtime
- Graphics context and window.
Handle ownership
Section titled “Handle ownership”- 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.
Frame loop
Section titled “Frame loop”Each iteration has two phases: pump (always) and render (only when
render_pending is true).
Pump (every iteration)
Section titled “Pump (every iteration)”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.
Render (render_pending)
Section titled “Render (render_pending)”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 endRequirements:
- MUST call runtime
run_onceonce per loop iteration while the app is running. - MUST drain runtime events each iteration and set
render_pendingwhen:map_render_update_availabletargets this map (new map content to draw), ormap_render_frame_finishedtargets this map andneeds_repaintis true (continuous mode needs another frame, for example ongoing camera transitions).
- MUST set
render_pendingwhen input changes the camera. - MUST call
render_updateonly whilerender_pendingis true. - MUST clear
render_pendingafterrender_updatereturns success. - On
invalid_statefromrender_update, leaverender_pendingset 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.
Viewport
Section titled “Viewport”The viewport value MUST contain:
| Field | Meaning |
|---|---|
logical_width, logical_height | Map coordinate extent passed to MapOptions / RenderTargetExtent. |
physical_width, physical_height | Drawable pixels of the window framebuffer. |
scale_factor | Ratio 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), minimum1). - 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.
Map state
Section titled “Map state”The map state module owns the runtime, map, and render session handles plus map-specific setup.
Creation
Section titled “Creation”- 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.
Event drain
Section titled “Event drain”- Drain all pending runtime events each frame.
- Set
render_pendingfor the frame loop when either:map_render_update_availabletargets this map, ormap_render_frame_finishedtargets this map andneeds_repaintis true.
Resize API
Section titled “Resize API”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.
Render-target modes
Section titled “Render-target modes”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.
Mode comparison
Section titled “Mode comparison”| CLI value | C API concept | Compositor | Role |
|---|---|---|---|
owned-texture | Session-owned backend texture | Required | Map allocates texture, host samples it. |
borrowed-texture | Caller-owned texture borrowed by session | Required | Host allocates exportable texture; session renders into it. |
native-surface | Window presentation surface | None | Map renders directly to the window presentation target. |
Startup status lines
Section titled “Startup status lines”Startup MUST print the active mode’s CLI value and exactly one line from this table:
| CLI value | Printed line |
|---|---|
owned-texture | render target status: samples MapLibre-owned texture frames into the host swapchain |
borrowed-texture | render target status: renders into a host-owned texture, then samples it into the host swapchain |
native-surface | render target status: renders directly to the host window surface |
owned-texture
Section titled “owned-texture”- 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.
borrowed-texture
Section titled “borrowed-texture”- 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 asowned-texture. - On resize, recreate the host texture and re-attach the session (see
Resize;
needsRenderTargetReattachOnResizeistruefor this mode).
native-surface
Section titled “native-surface”- Attach with the C API surface descriptor for window presentation (see Graphics API).
render_updatepresents through the surface session directly.drawTextureMUST NOT be called for this mode.- On resize, call session
resizeand rebuild host presentation; reattach when the window toolkit supplies a new surface handle.
Resize
Section titled “Resize”- 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 returnstrueforborrowed-texturebecause the host-owned exportable texture is fixed to the viewport size: resize destroys the session, recreates the texture, and attaches again. It returnsfalseforowned-textureandnative-surface, where resize updates the swapchain or surface and calls sessionresize(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_pendingafter any resize.
Control scheme
Section titled “Control scheme”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 bearingBehavioral constants
Section titled “Behavioral constants”| Interaction | Behavior |
|---|---|
| Left drag | move_by with pointer delta in logical coordinates. |
| Right drag, or Ctrl+left drag | Adjust bearing by 0.5 × Δx degrees; adjust pitch by 0.5 × Δy degrees (same sign convention everywhere). |
| Scroll | Zoom 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 / WASD | Pan 120 logical units per key press. |
+ / - | Zoom 1.25 / 1/1.25 about viewport center. |
Q / E | Bearing ±10° with keyboard animation. |
] | Pitch +5° (clamped to [0, 60]) with animation. |
[ | Pitch −5° (clamped to [0, 60]) with animation. |
0 | Animate 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.
Diagnostics
Section titled “Diagnostics”- 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.
Graphics API
Section titled “Graphics API”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:
Vulkan
Section titled “Vulkan”- One shared Vulkan context (
VkInstance,VkDevice, queue, andVkSurfaceKHR) for compositor and render session. owned-texture: Vulkan owned-texture descriptor with those shared handles.borrowed-texture: exportableVkImageand view sized to the viewport; borrowed-texture descriptor.native-surface: surface / swapchain presentation descriptor for the windowVkSurfaceKHR.
native-surface: Metal surface descriptor for the windowCAMetalLayer.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.
OpenGL / EGL
Section titled “OpenGL / EGL”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.