OBC OpenBikeComputer / docs

The rendering pipeline

The renderer converts streamed map data into a 240×320-pixel frame. It uses fixed memory and does not allocate heap memory.

Shared render path

obc-render is a no_std crate. The simulator and device use the same geometry code.

Everything device-specific lives at two seams obc-render one no_std code path · zero-alloc scene · viewport · bg → DrawTarget where pixels go color_fn RGB565 → pixel obc-sim RGB222 framebuffer 64 colours device LS021B7DD02 panel 64 colours (RGB222)
The renderer receives a map scene, a pixel target, and a color conversion function.

The render call receives these inputs:

InputPurpose
MapSceneSupplies styles, LOD data, candidates, geometry, and diagnostics.
ViewportDefines camera position, scale, rotation, and panel size.
RenderConfigSelects per-frame presentation options.
DrawTargetReceives pixels.
Color functionConverts RGB565 styles to target pixels.
RenderScratchSupplies all per-frame work buffers.

The Reader adapter streams OBCM chunks through MapScene. The interface does not expose file offsets or cache slots.

Frame stages

One frame, start to finish map + route bytes 1 Project camera → px 2 Pick LOD for this zoom 3 Quadtree cull visible chunks 4 Priority decode fill the buffers 5 Painter sort by z-index 6 Rasterise fill + stroke 7 Overlays route · you panel
A frame selects visible data, draws it in z-order, and adds overlays.

The renderer performs these stages:

  1. Project map coordinates to screen coordinates.
  2. Select a level of detail.
  3. Find visible chunks.
  4. Select and decode features.
  5. Sort selected features by paint order.
  6. Rasterize polygons and lines.
  7. Draw route and rider overlays.

Projection

The Viewport stores camera position, zoom, latitude correction, and rotation.

Ground · µdeg camera P Δ (lon,lat) Screen · px N P ① Δ vs camera ② × cos(lat) ③ rotate ④ × zoom ⑤ round
Projection keeps the camera delta precise. It then corrects longitude, rotates, scales, and rounds.

to_screen keeps the camera delta as an integer before conversion to f32. It then corrects longitude, rotates, scales, and rounds.

to_map applies the inverse transform. Panning and viewport bounds use this operation.

Level of detail

An OBCM file contains pre-simplified level-of-detail (LOD) tiers. Each tier specifies its maximum meters per pixel.

LOD pyramid — coarse to fine coarse fine LOD 0 · coarsest covers any zoom LOD 1 good to ≤ 16 m/px LOD 2 good to ≤ 4 m/px LOD 3 · finest good to ≤ 1 m/px this view 0.5 m/px
The renderer selects the finest LOD that supports the current meters-per-pixel value.

The renderer selects the finest supported tier. The selection depends on zoom and latitude.

The selection does not depend on display size. Equal geographic views select the same tier on all hosts.

Visible chunks

Each LOD stores geometry in chunks. A quadtree indexes the chunks by geographic bounds.

Descend only where the view reaches NW skipped SW skipped NE SE view root NW NE SW SE visited leaf → a chunk to draw skipped — bbox misses the view a high bit marks a branch; a sentinel marks an empty leaf
The quadtree walk prunes nodes outside the viewport. It streams candidates from intersecting leaves.

The reader descends only into nodes that intersect the viewport. It streams candidates from each nonempty leaf.

The walk limits recursion depth and rejects backward child references. These checks protect the device from invalid map data.

The walk does not limit the total number of visible chunks. The next stage applies the global feature budget.

Feature selection

Dense views can exceed the frame buffers. Each style supplies a retention priority from 1 through 4.

Priority 1 has the highest retention priority. The z-index does not affect retention.

A 256-bit style mask removes hidden styles before geometry decode. The terrain-layer setting uses this mask.

Pass B — the source resolves each opaque winner token win skip skip win skip skip win end read head jumps offset → offset → re-decoded winners (coral) cost coordinate math; the features between them cost only a pointer add.
Pass A stores candidate metadata and an opaque token. Pass B decodes selected candidates.

Selection uses two passes:

  • Pass A stores style, bounds, size, and an opaque source token.
  • An in-memory selection admits candidates against point and ring budgets.
  • Pass B decodes only admitted candidates into caller-owned buffers.

A full candidate can evict a lower-priority candidate. The decision applies across all visible chunks.

Lowest priority is dropped — globally, by construction P1sea·land·motorway P2major roads P3minor roads P4buildings·detail frame buffer (fixed) P1 P2 P3 FULL P4 — dropped the buffer filled before priority 4 fit — exactly the right things to lose
Selection keeps high-priority candidates within the fixed point and ring budgets.

The renderer drops an invalid or oversized feature as one unit. It does not publish partial geometry.

Each selected feature becomes a compact span. The span references points and rings in the frame buffers.

RenderStats reports budget drops, decode failures, malformed data, source failures, cache activity, and stage time.

Paint order

The renderer sorts spans by z-index and collection sequence.

Priority controls feature retention. The z-index controls paint order. Collection sequence gives deterministic order for equal z-index values.

Polygon fill

Even-odd rule — holes fall out for free scanline y + 0.5 x0x1x2x3 fill (x0→x1) skip (x1→x2) the hole fill (x2→x3) A hole is just another ring in the same crossing list — no special case needed.
The even-odd fill rule supports concave polygons and holes.

The polygon filler uses the even-odd scanline rule. It sorts edge crossings for each row and fills between pairs.

The filler writes clipped horizontal rectangles. It skips a row if its crossing buffer is full.

Line stroke

Clip to the view, then smooth the joints the whole route view only the clipped part is stroked butt-jointed rects (notch) + round-join / cap discs a disc (⌀ = width) at corners + run ends → smooth arc, no gaps
The stroker clips lines before rasterization. It removes subpixel duplicate points.

Embedded Graphics strokes 1-pixel lines. The renderer converts wider segments to convex quadrilaterals and fills them as spans.

Discs close run ends and sharp joints. Normal line width changes with zoom and stays from 1 through 12 pixels.

Fixed-width styles bypass the zoom scale. Contours use this style property.

Style combinations

FeatureDashedcolor2Result
LineNoNoneSolid stroke
LineNoSetRoad casing and road fill
LineYesNoneDashed stroke
LineYesSetSolid base and dashed top stroke
PolygonIgnoredSetFill and ring outline

Dashed lines use screen-space arc length after clipping. A railway style draws a solid color2 base and color dashes.

Road casing

A road casing uses color2 and adds 2 pixels to the road width. Casings run only at the finest LOD.

The casing pass goes at the z boundary — not before the frame spans · sorted (z, seq) · drawn bottom-up z ↑ · paint order ↑ water landuse buildings casings road fills ③ spans[split..) — road band, on top ② casing pass — wide color2, finest LOD split — first cased road line ① spans[0..split) — the base pass at a crossing fills continuous through the junction casing hugs the outside of each road only
The casing pass starts at the road z-band. Road fills then cover casing inside intersections.

The renderer draws casings at the start of the road z-band. It then draws all road fills.

This order keeps the casing above land fills. It also prevents casing lines inside road intersections.

Polygon outlines

A polygon with color2 receives a closed outline at the finest LOD.

Outline after every fill in the z-group, so shared walls survive per feature outline after each fill ① fill + outline A B's fill erased A's shared wall ② B's fill lands ③ one merged blob ✗ per z-group all fills, then outlines ① all fills first ② all outlines the shared wall is drawn after every fill → two crisp buildings ✓
The renderer fills every polygon in a z-group before it draws the outlines.

The renderer fills all polygons in one z-group first. It then draws all outlines in that group.

This order keeps shared walls between adjacent buildings.

Rain layer

The optional rain raster uses the gap between the ground and road z-bands. Roads, routes, markers, and UI chrome remain visible.

Only the rain-map screen requests this layer. A frame without rain uses the normal paint path.

The display path can use bilinear sampling. All weather decisions use nearest-neighbor samples from actual cells.

No-data cells do not take part in interpolation. The renderer reports when the zoom is outside the supported rain regime.

Map overlays

The renderer draws moving map content after the base map:

  1. Active route and direction chevrons
  2. Breadcrumb trail
  3. Waypoints and rider marker
  4. Map status and tool indicators

The route and breadcrumb use the shared line stroker. Markers use the shared polygon filler.

Frame storage and presentation

The device stores one RGB222 frame byte per pixel. The 240×320 frame uses 75 KiB.

Each byte has the 00_RR_GG_BB format. The framebuffer converts RGB565 pixels when it stores them.

Redraw the whole frame — push only the rows that changed ① the frame clock framebuffer screen redrew all 320 rows hash each ② the diff hash = stored → skip hash ≠ stored → span (y₀, 3) 32-bit hash per row 320×u32 = 1.28 KB store span list (start, count) ③ the push to glass 3 rows pushed, rest retained fast-forward the gate write the span stop early — rest not scanned one-minute clock tick: ~44 ms full frame → a few ms
The presenter hashes rows and sends only changed row spans to the panel.

The LS021 presenter hashes each row. It sends the changed row spans through the FLPR coprocessor.

The M33 renders the frame and publishes dirty rows. The FLPR reads shared SRAM and writes the panel wire format.

The simulator implements the same display contracts. Its final presenter writes changed rows to the host texture.

Transient overlays

A transient overlay is not stored in the clean base frame. The presenter reads the required base-frame window and composites the overlay.

Clearing the overlay presents the clean window again. It does not require a map render.

A base-frame update can exclude a live overlay region. This rule prevents overlay flicker during a map update.

Memory budgets

RenderScratch contains fixed-capacity buffers. The device initializes it in place in the shared scratch arena.

BufferPurposeCapacity
Frame pointsSelected projected vertices16,323
Frame ring lengthsSelected feature rings3,328
Candidate spansCandidate and draw records3,072
Decode pointsOne decoded feature2,048
Screen pointsOne drawn feature2,048
Scanline crossingsOne polygon row384

The board build checks the complete scratch size against its arena budget. Increasing a capacity is a device-memory decision.

Source map

See system architecture for the host loop. See data formats for the OBCM format.