Skip to main content

Architecture

This page explains how tile57 turns an ENC chart — a cell, in the spec's vocabulary — into vector tiles, how the codebase is layered, and the memory design that keeps it small.

The pipeline

A chart flows through these stages, all inside the engine. Both source formats converge on the same S-101 feature + attribute records — a native S-101 dataset produces them directly; an S-57 chart is adapted into them:

S-101 ENC (.000) S-57 ENC cell (.000)
│ decode 8211 │ decode 8211 src/iso8211/ (iso8211)
▼ ▼
S-100 spatial + feature S-57 feature + geometry src/s57/ (s57)
records model
│ assemble to the │ adapt S-57 → S-101 src/s101/ native.zig
│ S-101 model │ features / adapter.zig
└───────────┬─────────────┘

S-101 feature + attribute records
│ apply S-101 portrayal src/portray/ + embedded Lua 5.4
▼ (vendor/S-101_Portrayal-Catalogue)
portrayal instruction stream
│ parse the instruction stream src/s101/ instructions.zig

scene generation src/scene/ (project + clip + draw calls)

render Surface src/render/surface.zig
├─► tile surfaces: MVT / MLT encode + PMTiles src/tiles/
│ + MapLibre style.json + portrayal assets src/style/, src/sprite/
└─► pixel surfaces: PNG raster · vector PDF · terminal text (src/render/)
  1. Decode (ISO 8211). Both S-101 and S-57 charts use the ISO 8211 binary container format; the decoder reads its raw records and fields. The chart format is detected from the file's own record schema.
  2. Build the S-101 model. A native S-101 dataset (S-100 Part 10a) is read straight into S-101 feature + attribute records — its own in-band code tables already carry the S-101 class and attribute names, and its complex attributes are explicit, so no conversion is needed. An S-57 chart instead builds the S-57 feature + geometry model (depth areas, buoys, coastlines, …) and adapts it into the same S-101 records. Geometry (assembled from the vector topology) and attributes become a queryable in-memory model either way.
  3. Apply S-101 portrayal. The official IHO S-101 Portrayal Catalogue — the real Lua rule files — runs in embedded Lua 5.4 and decides how to draw each feature: symbol, colour, line style, conditional symbology. Zig implements the Host* query callbacks the rules call back into (plus a small C shim, since Lua's macros are easiest from C).
  4. Adapt the instructions. The portrayal output is turned into simple drawing primitives: filled polygons, stroked lines, symbols, patterns, soundings, text.
  5. Generate the scene. Each primitive is projected to web-mercator tile coordinates, clipped (extent 4096, buffer 64), and emitted as draw calls on a Surface — the backend contract described below.

The Surface contract

Every output format implements one vtable — Surface (src/render/surface.zig). The engine emits semantic draw calls (S-52 colour tokens, symbol names, raw depths, per-feature display metadata); what happens next depends on the listening backend: the tile surface serializes the semantics into MVT/MLT tiles for a client renderer, the pixel surface resolves them (tokens → RGB, symbol names → vector outlines) and paints PNG raster or deterministic vector PDF, and the ASCII surface lowers them to a Unicode terminal grid (with optional ANSI colour, or real pixels inline via the kitty graphics protocol). One engine, pluggable outputs — see The Rendering Engine.

Tile formats: MLT by default, MVT optional

Bakes encode MLT (MapLibre Tile) by default; MapLibre GL JS ≥ 5.12 decodes it natively via the vector source encoding option, and the generated styles carry that hint. The engine can also encode Mapbox Vector Tiles for consumers without an MLT decoder. tile57_info.tile_type reports which encoding a chart's tiles use, so a host hints its renderer correctly.

Band handoff (coverage-clipped ownership)

Overlapping charts of different compilation scales are resolved per tile to the best band. Rather than carry a coarser chart's features down into a finer band's tiles (the old smax-tagged carry-down), the compositor clips each chart to the ownership partition: at every tile the finer chart's M_COVR coverage wins the ground it holds, and a coarser chart renders only where no finer chart covers it. Band boundaries hand off without holes or double-draws, and there is no per-feature handoff tag for the style to gate.

Overscale indication (oscl)

Per S-52 §10.1.10, every contributing chart's coverage polygon is baked as an OVERSC01 vertical-line hatch tagged oscl = the chart's compilation-scale denominator. The hatch shows only while the display is finer than 1:oscl, and the style sandwiches it between the overscaled and at-scale fill passes so finer opaque data occludes a coarser chart's hatch. The show_overscale mariner toggle (default on) drives its visibility.

The Zig modules

The stages are separate Zig modules (see build.zig), most of them pure (no libc/Lua) and target-agnostic:

ModuleRole
iso8211the ISO/IEC 8211 container reader (the bottom layer; std-only)
s57the S-57 chart parser + geometry model (reads 8211 records through iso8211)
s101the S-101 catalogue, the native S-101 dataset reader, the S-57 → S-101 adapter, and the portrayal instruction stream
portraythe embedded-Lua S-101 runner (links libc)
tilesMVT + MLT encoders, gzip, the PMTiles container, web-mercator tile math
renderthe Surface contract, the resolver (colours, display gates), and the pixel machinery (Canvas, PNG, PDF, ASCII)
sceneS-57 → tile-surface scene generation + the banded ENC_ROOT baker (bake_enc.zig)
stylethe S-101 color tables and line styles, the MapLibre style.json layer set, and the S-52 mariner settings model (mariner.zig)
spritethe S-101 sprite + area-fill pattern atlases from the catalogue Symbols/AreaFills (SVG raster; links libc)

The style and sprite modules generate the S-101/S-52 portrayal assets — color tables, line styles, sprites, and patterns — from the S-101 Portrayal Catalogue. They read the catalogue bytes as input rather than importing s101, so they stay independent modules a caller can grab on their own. | engine | the pure packages re-exported as one import (the test root) | | tile57 | the curated public surface (src/tile57.zig) |

portray and sprite are the only modules that link libc, and neither is imported by the pure test build. The C ABI (src/capi.zig) is a thin shim over the same Zig API as the tile57 module.

The layering: chart / compose / style

The public surface composes the packages into high-level entry points:

  • Chart — ONE open chart, no composition. Open a baked PMTiles archive (openPmtilesPath, mmap'd; openBytes) or a live chart (openBytes on .000 bytes), then take its outputs: view renders (renderView — PNG, PDF, or a callback canvas; renderSurfaceView — world-space GPU callbacks), the cursor pick (queryPoint), the metadata getters, and — for an archive — its stored tiles verbatim through pmtilesReader(). In the C ABI: tile57_chart_tile / tile57_chart_png / tile57_chart_pdf / tile57_chart_canvas / tile57_chart_surface / tile57_chart_query. A streaming ENC_ROOT open (openPath / openCharts / openChartsStreaming) is the metadata + extraction view of raw source data (the C tile57_enc_* readers); it serves no tiles or renders.
  • Tile production — bake each chart to its own PMTiles at its compilation scale (tile57_bake_chart_bytes, which runs the banded bake engine scene/bake_enc.zig on a single chart), then a runtime compositor stitches the overlapping charts through an ownership partition and offers the SAME outputs as a chart, composed: tile57_compose_tile for any (z, x, y) on demand, tile57_compose_png / _pdf / _canvas / _surface / _query for composed views and the composed pick. Baking is strictly per-chart: one chart, one archive.
  • style.build (style/maplibre.zig) + style / sprite — generate the MapLibre style and the portrayal assets it references (tile57_style_build / tile57_bake_assets in the C ABI).

The memory design

tile57 is built to hold only its working set:

  • Lazy per-chart reads. An ENC_ROOT is opened by scanning each chart's header for a cheap spatial index (band + bbox). A chart's bytes are read and parsed only when a metadata or feature query needs them, then released under an LRU bound.
  • Ownership, not overlays. Overlapping charts of different compilation scales are resolved by the precomputed ownership partition — each tile's ground belongs to exactly one chart per band, so composing never loads every overlapping chart.
  • Streaming open. openChartsStreaming (and its on-disk driver openPath, which backs the C tile57_enc_* readers) take per-chart metadata (bbox + scale) plus a reader; a chart's bytes are read only on demand and freed on eviction. A host then holds only the working set's bytes, not the whole catalogue — the right choice for a large ENC_ROOT.
  • Per-chart bakes. Each chart bakes independently at its own compilation scale, so a bake holds a single chart's parsed data at a time — memory doesn't grow with the size of the catalogue.
  • Tile cache. Generated/decoded tiles are memoized per chart (keyed z<<48 | x<<24 | y) and released with the chart, so a long-running host bounds memory by closing charts it no longer renders.

The live-composite bake

One bake command (tile57 bake <cell.000 | ENC_ROOT> -o out/) writes the live-composite structure — per-chart tiles plus the ownership partition a runtime compositor serves them from:

out/
tiles/US5MD1MC.pmtiles one PMTiles per chart, baked at its compilation scale
tiles/US4MD81M.pmtiles (M_COVR coverage embedded in each archive's metadata)
partition.tpart the ownership partition: which chart renders which ground

There is no merged archive: any (z, x, y) tile is composed from the overlapping charts on demand (tile57_compose_tile), so re-baking one chart doesn't rewrite the whole ENC_ROOT. The portrayal assets are generated separately (tile57 assets / style); the tiles carry S-52 colour tokens, never RGB, and both halves come from the same S-101 catalogue, so they cannot drift. The tile-schema vocabulary (tile57/2) is the contract a renderer checks.

See the Tile Schema for the vector-tile layer contract.