map-from-scratch · git:20261027.231c915 · 2026-10-27 · sha256 7c02977bd1eef5e9
map-from-scratch git:20261027.231c915A
Immutable. This exact content never changes and is served at /api/v1/blob/7c02977bd1eef5e9.
---
name: map-from-scratch
description: Build the smallest, fastest map renderer for one request ("I want to do X, so make me a map"), almost from scratch, drawing on the map engines collected in ref-repos, and prove it with measurements. Use when someone wants a map front end for a specific purpose, data set, device or performance target, rather than a general-purpose map engine.
argument-hint: "[the request: what the map is for, on what device, with what data]"
---
# /map-from-scratch: a renderer made for one request
A general map engine pays for every feature it might need. A renderer made for
one request pays only for that request. This skill writes such a renderer in
`examples/<nn>-<slug>/` of the map-from-scratch repository, defines it in
`spec.yaml`, measures it with the harness, and keeps changing it until the
numbers say it is small and fast enough for what was asked.
Repository: `~/Workspaces/repos/__yuiseki/map-from-scratch` (below: the repo).
Everything runs in Docker through `bin/mfs`; the host needs only docker and,
for GPU examples, the nvidia container runtime. The repo is GPL-3.0-or-later.
## Rules
- One request, one renderer. Do not grow an earlier example into a shared
engine. Copy the closest example and cut it down.
- Learn from the engines in `ref-repos/` (MapLibre GL JS and Native, Cesium,
deck.gl, OpenLayers, QGIS, Mapnik, Organic Maps, ortho-earth and ~40 more;
`ref-repos/clone.sh` fetches them). Read how they solve the sub-problem in
front of you, then write only the part this request needs. Code under GPL-3,
GPL-2.0-or-later, LGPL or a permissive licence may be adapted with a comment
naming the source file; GPL-2.0-only code (GeoServer, parts of Navit) may be
read but not copied. Check the repository's own licence files.
- No external dependencies at run time, except earcut (vendored in
`vendor/earcut`) for polygon triangulation. Platform APIs (Canvas 2D,
WebGL2, WebGPU, EGL/GLES, X11, libc) are not dependencies. A general engine
may appear only under `baselines/`, to measure against.
- Do the work at bake time, not at run time: projection, simplification,
triangulation, draw order, quantisation and compression all happen in
`bake.ts`. The renderer receives data laid out for exactly what it draws.
- Every requirement becomes a budget in `spec.yaml`, or says why it is not
measured. The harness refuses a definition that leaves one out.
- Every bench has functional checks, defined in `spec.yaml` and reported by
id, that read back what reached the screen. Fast numbers from a renderer
that draws nothing are worthless, and this has happened
(references/pitfalls.md).
## Procedure
1. **Read the request into `spec.yaml` `requirements`.** Quote the request.
One requirement per need, in the request's words, each with the number it
becomes and why (60 Hz → 16.7 ms; "page under 150 KB" → `total_gzip_bytes`).
Write `architecture_selection.not_doing` before any code: what the renderer
will not do (3D, tiles, labels, rotation, …) is where most savings come
from. The format of every section is in references/spec-yaml.md.
2. **Choose runtime and API from the requirements**, not from habit, and
write the choice and the rejected alternatives in
`architecture_selection`:
| When the request says | Start from |
| --- | --- |
| embed in a page, tiny, no libraries, a few dozen shapes | Canvas 2D + Path2D (01) |
| phone browser, pinch, tap to identify | WebGL2, on-demand drawing, id-buffer picking (02) |
| animate over time, scrub | WebGL2, all data on the GPU, one uniform per frame (03) |
| millions of points, recount per view | WebGL2 float target + additive blend + GPU reduction (04) |
| desktop, no browser, instant start | C + EGL + GLES3, instanced segments, program binary cache (05) |
| microcontroller, no FPU, little RAM | freestanding C, integers, strip rendering, data in flash (06) |
| thousands of points, hover for a name, whole page size-capped | WebGL2 stencil fill, CPU brute-force hover (07) |
WebGPU is a valid choice for a renderer, but in this harness headless
Chromium only gets SwiftShader for WebGPU, so a WebGPU example cannot be
measured on the GPU here yet. Say so if you choose it.
3. **Design the data structure for the request, too.** The format the data
arrives in is often the real bottleneck. Decide what the renderer reads
first and most often (a view, a time step, a zoom level), and lay the
bytes out so that one or a few HTTP Range requests fetch exactly that:
index first, then blocks ordered by space (Hilbert, Morton) or time,
quantised, delta-coded, columnar where it helps. Read the ideas in
`ref-repos/maplibre__maplibre-tile-spec` (MLT: columnar layout, FastPFOR,
dictionary and FSST strings, pre-tessellation) and
`ref-repos/kenjiyoshidahome2026-bit__ortho-earth/packages/geopbf`
(GPL-3), and the PMTiles spec. Measure the new structure against reading
the original format for the same views (bytes, requests, time to first
frame under `benchmarks.network`), and record both in
`performance_knowledge`. Bake-time tools: `tools/sources.ts`,
`tools/geo.ts`, `tools/pmtiles.ts`, the `duckdb` CLI (spatial),
`ogr2ogr`, `osmium`. Local copies: z.yuiseki.net/static is
`/sata_hdd_24tb/www/html/static/`, Hugging Face datasets of yuiseki are
under `/sata_hdd_24tb/hf_data/__published/`.
4. **Look it up in the know-how, then in ref-repos.** For each sub-problem of
the design (line joins, polygon fill, tile decoding, label placement,
picking, projection, globe camera), first read the matching know-how skill
in `.agents/skills/<topic>/SKILL.md`: decisions, trade-offs and reversals
mined from the issues, pull requests and commit logs of the 48 engines,
each claim with its source. Topics: 3d-buildings-and-meshes,
3d-tiles-and-hlod, animation-and-camera, antialiasing-and-blending,
api-design-and-extensibility, data-range-requests, draw-order-and-depth,
float-precision, gpu-memory-and-batching, label-placement,
line-tessellation, mobile-and-power, picking-and-hit-testing,
point-rendering-and-clustering, polygon-triangulation,
projection-and-globe, raster-tiles, simplification-and-generalization,
startup-and-bundle-size, style-and-expressions, symbols-and-icons,
terrain-and-elevation, testing-and-benchmarking, text-rendering,
tile-cache-and-loading, tile-lod-and-overzoom, vector-tile-encoding,
worker-threading; and yuiseki-open-data for the data itself. Then search
`ref-repos/` (`rg -l` by concept, then read the few files that implement
it) for the code, and note in `performance_knowledge` or a code comment
which engine's approach you took or rejected and why.
5. **Scaffold** by copying the closest example to `examples/<nn>-<slug>/`:
`spec.yaml`, `REQUEST.md` (the narrative: design and what was learnt),
`bake.ts`, the renderer, and for browser examples `index.html` that loads
`main.ts`, plus a `bench.ts` that `main.ts` imports only under `?bench`.
Everything is TypeScript: Node runs it directly, the static server strips
the types for the browser, and `bin/mfs test` type-checks it with tsc
(strict, erasable syntax only, `.ts` import specifiers). Count in
`chosen.code` exactly what a user downloads. Shared bake helpers are in `tools/` (`sources.ts` for raw data,
converted again when its settings change; `geo.ts` for projections,
simplification, triangulation, clipping); add to them with tests.
6. **Bake**: `bin/mfs bake <nn>`. Print counts (features, vertices, bytes)
and look at them: an outlier bounding box or a 10x larger file than
expected is a bug in the bake, not a fact about the data. Write the steps
in `datasets.preparation`.
7. **Write the renderer**, smallest first: get one frame on screen, then add
interaction. Patterns that worked are in references/patterns.md; the
browser plumbing (DPR-aware ResizeObserver, pointer to device pixels,
one pending rAF) is easiest to copy from 02 or 07.
8. **Write the bench**: first frame (say exactly when it starts and stops in
`benchmarks.scenarios`; do not count loading the bench code), a camera
path (`zoomPath` and `benchFrames` in `harness/page/bench.ts`, or the C
equivalent), fixed views for the cheap and expensive ends
(`benchFrames(gl, n, draw, {prefix: 'whole_'})` reports `whole_frame_ms`),
interaction costs, and every check of `verification.checks` as
`emit({check: id, pass, detail})`. Report the GPU (`info.gpu`).
9. **Measure**: `bin/mfs bench <nn>`, then look at
`results/<id>/screenshot.png`. Before believing a number, ask what it
measures (references/pitfalls.md); note the machine's load, which moves
time budgets far more than GPU time. Change one thing at a time; for a
parameter sweep pass it through `MFS_PAGE_QUERY` and compare runs.
10. **Record** what was learnt in `performance_knowledge` (finding, evidence
with numbers, when it applies), including dead ends and corrected mistakes.
11. **Optionally compare** with a general engine under `baselines/` (see
`baselines/b02-maplibre`): same data, same viewport, same camera path.
12. **Commit** when `bin/mfs test` passes and `bin/mfs bench <nn>` passes all
budgets and checks. `bin/mfs report` prints every example's latest numbers
for the README.
## Harness reference
- `bin/mfs image | test | list | bake <id> | build <id> | bench <id|all> [-n N] | report | serve [port] | probe [lifetime]`
- `spec.yaml` (references/spec-yaml.md) is read by `harness/lib/spec.ts`;
the harness takes runtime, page or command, viewport, throttle, iterations,
bake and build commands, shipped files, budgets and check ids from it.
- Renderers report with one JSON object per line prefixed `@mfs `
(native: stdout; browser: `window.__mfsEmit`): `{"metric":name,"value":x}`,
`{"metric":name,"values":[…]}`, `{"mark":name}` (native; timed from fork),
`{"check":id,"pass":bool,"detail":…}`, `{"info":{…}}`, `{"error":msg}`,
`{"done":true}`.
- Results go to `results/<id>/` (latest.json with budgets and checks,
timestamped runs, screenshot, chrome.log). They are not committed.
- Browser examples are published on GitHub Pages
(https://yuiseki.github.io/map-from-scratch/) by `tools/deploy-pages.sh`,
which builds the site (`tools/build-site.ts`), checks every page on it
(`bin/mfs bench <id> --site`) and pushes `gh-pages`. Pages must work as
plain static files with relative paths (no COOP/COEP there), and every
data file must stay under 100 MB.