OpenBikeComputer
On this page

The rendering pipeline

The renderer turns streamed map data into one 240 × 320 frame. It uses fixed buffers and allocates no memory, so a dense city cannot cost more memory than open country.

Shared render path

obc-render is a no_std crate. The device, the simulator, and the browser draw with the same geometry code, so a frame on a desktop is the frame the panel shows.

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.

A render call receives the map scene, the viewport, the presentation options, a draw target, a color function, and the scratch buffers. The reader adapter streams map chunks through the scene interface, which exposes no file offsets and no cache slots, so the renderer does not know what storage it draws from.

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 Select + decode complete features 5 Painter sort by z-index 6 Rasterise fill + stroke 7 Overlays route · you panel Priority decides what fits; z-index decides paint order.
A frame selects visible data, draws it in z-order, and adds overlays.

Each stage narrows the work: the selection stages decide what is drawn, and only what survives them is decoded, projected, and rasterized.

Projection

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.

Viewport keeps the camera position, zoom, latitude correction, and rotation. The transform holds the offset from the camera as an integer before it converts to floating point, so precision does not fall away far from the origin.

Level of detail

LOD selection · illustrative thresholds 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 level whose stated scale still supports the current meters-per-pixel value. The selection uses zoom and latitude only, never the panel size, so the same geographic view selects the same level on every host.

Visible chunks

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 walks the quadtree and descends only into nodes that meet the viewport. It limits the depth and refuses a backward child reference, because a damaged map must not be able to make the device loop. It does not limit how many chunks it returns: the next stage owns the budget.

Feature selection

A dense view holds more geometry than the frame buffers. Each style carries a retention priority, and a higher-priority feature can displace a lower-priority one across the whole view. Priority decides what survives; paint order decides what covers what.

Pass B — the source resolves each opaque winner token win skip skip win skip skip win end read head jumps offset → offset → Pass B decodes the selected features. Other geometry is not decoded.
Pass A stores candidate metadata and an opaque token. Pass B decodes selected candidates.

Selection runs in two passes. The first stores only the style, bounds, size, and an opaque token for each candidate; the budget is applied in memory; the second decodes only what was admitted. Decoding is the expensive step, so nothing is decoded to be thrown away.

Feature budgets · preserve whole shapes, drop lower priorities Candidates · illustrative priorities Point budget · 12 slots P1 · 4 points P2 · 4 points P3 · 3 points P4 · 4 points P1 P1 P1 P1 P2 P2 P2 P2 P3 P3 P3 11 / 12 points used One free slot cannot hold P4. Keep the complete feature or drop it. Ring capacity is checked too. Priority decides admission across all visible chunks. Paint order is a separate sort.
The point counts and capacity are illustrative. Selection compares complete candidates across all visible chunks; higher-priority candidates can displace lower-priority ones.

A feature is dropped as one unit: the renderer never publishes part of a polygon. RenderStats reports drops, decode failures, malformed data, cache activity, and stage time, which is how a map that draws badly is diagnosed without a debugger.

Paint order

Spans are sorted by paint order, and equal values keep collection order, so a frame is deterministic.

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 filler uses the even-odd scanline rule, which handles concave rings and holes with one rule and no separate hole test.

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.

One-pixel lines go through Embedded Graphics. A wider segment becomes a convex quadrilateral filled as spans, and discs close the ends and sharp joints. Line width follows zoom, except for styles fixed at one pixel, such as contours.

A dashed line measures its pattern along the screen arc from the first point of the line, including the part the view clip removed, so the dashes stay at the same place on the map while the camera moves.

Road casing

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.

A casing is the darker outline of a road. The renderer draws every casing at the start of the road paint band and then every road fill, so a casing stays above the land fill and no casing line crosses an intersection.

Polygon outlines

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.

For the same reason, the renderer fills every polygon of a paint group before it outlines them, which keeps one shared wall between two adjacent buildings.

Map overlays

After the base map come the active route and its chevrons, the breadcrumb trail, the waypoints and the rider marker, and the status indicators, through the same stroker and filler as the map.

Frame storage and presentation

Row diff · hash each row, combine changes, write one span 1 · Rendered frame 2 · Compare row hashes 3 · Panel scan same changed changed changed same clock span skip write stop 12 rows shown; real frame: 320 320 × u32 = 1,280 bytes Other rows retained Adjacent changed rows form one span (start, count). Unchanged rows do not need a pixel write.
The presenter stores one u32 hash for each of the 320 panel rows. It combines adjacent changes into spans and sends only those rows to the panel.

The device holds one frame byte per pixel in the panel's own 64-color gamut. The presenter hashes each panel row, joins the changed rows into spans, and sends only those. A partial update is what makes the panel cheap: an unchanged row costs no transfer and no scan time. The simulator implements the same display contracts, so its output is the same frame.

A transient overlay, such as hold feedback, is not stored in the clean base frame. The presenter reads the base window, composites the overlay, and sends the result, so clearing the overlay needs no map render.

Landmark and peak photos

A photo in an article is decoded into the same resident frame as the map, in bounded steps. There is no image buffer and no resident decoder: each step writes more pixels into the frame and releases the shared work area before it presents, so a photo never delays a gesture. An interrupted photo starts again; an unreadable one leaves the text and credits available.

Source map

See system architecture for the host loop and data formats for the map format.