1
0

feat: the merged board's cities are lotted like cities, and drawn one city at a time

Three findings, each of which changed the next.

**1. The metros on the merged board were lotted at 806 m.** `LOT` is fixed in
scene units — 0.42 — and `blocks.ts`'s own note already spelled out what that
means: a lot is 40 m on San Francisco's board, 164 m on the Southland's and
**806 m** on the statewide one. Put San Francisco's fifty-two districts *on* the
statewide board and they inherit 806 m. Lots go as the inverse square of their
size, so the Bay Area's ~84,000 buildings became about **two hundred**, and a
city rendered as a handful of grey slabs. That is the whole reason the merged
board's cities looked wrong.

A detail district now measures its lot in metres. 160 m, chosen against a
shipping reference rather than a feeling: it is what Southern California's own
board already builds itself at (0.42 x 390.6), so a metro on the merged board is
lotted about as finely as the Southland lots itself. Measured in the browser:
**59,166 building instances, of which 2,839 are the state's own and 56,327 the
two metros'.** Against roughly two hundred before.

**2. An all-or-nothing LOD could not carry them.** `InstancedMesh.count` draws
the first N instances, so "base first, detail last" can say "no cities" and
"every city" and nothing between. Driving into Los Angeles therefore revealed
San Francisco's fifty-two districts as well, four hundred kilometres away and
behind the camera: `california-drive` measured **925,854 triangles against a
430,000 cap**. Shrinking the lot to fit would have needed about 478 m, which
gives back the entire gain.

So `updateBlocksDetail` re-packs by district. Each detail district's lots and its
centre are recorded at build time; when the visible *set* changes — a handful of
times in a journey across California, not per frame — the visible districts are
copied into the front of the instance buffers and `count` moves. Three
attributes move together and must: the matrix, the colour and
`FACADE_ATTRIBUTE`, which is per-instance and decides which windows are lit;
moving matrices alone would light a tower's windows on a warehouse.
`california-drive` fell 925,854 -> **368,962**.

**3. Two bugs in that, both mine, both found by measuring rather than reading.**

  - `DetailStore.key` remembers which districts the buffers hold, and started as
    `""` — which is also the key an *empty* visible set produces. The first call,
    at the whole-board pose where nothing should draw, compared equal, took the
    early return and did nothing.
  - `InstancedMesh`'s constructor sets `count` to capacity, so between
    `createBlocks` returning and the scene's first `applyDetailLod` the mesh drew
    every detail lot. The budget reports `maxTriangles` — a max over the whole
    sample window — and caught that as **911,541 triangles while the steady
    state was a correct 348,271**. A level of detail that is right on every frame
    but the first is not a level of detail. The mesh is now born packed.

The second one is the instructive one: an instrumented log showed `count=2839,
want=false` at exactly the pose the budget was failing, which is what turned a
"the LOD is broken" hypothesis into "the LOD is right and the first frame is
not".

Result on the merged board, all triangles green: california 348,271 of 440,000 —
**lower than the state board's own 372,415** while carrying 56,327 more
buildings — and california-drive 368,962 of 430,000.

Still red, and unchanged from before this commit: `california-drive` mobile draw
calls, 315 against 280. Triangles there are now fine. That cap was set when the
corridor had no cities on it, and re-deriving it is an owner's call rather than a
number to quietly raise.

1,704 tests pass, including a regression test for the empty-key bug that reads
the source, because building a real `InstancedMesh` needs a GPU.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-24 03:31:17 -07:00
parent 64618cb47d
commit 068d4d71e5
3 changed files with 281 additions and 16 deletions
+67
View File
@@ -143,3 +143,70 @@ describe("one California's chapters", () => {
);
});
});
describe("one California's cities are lotted like cities", () => {
it("measures a detail district's lot in metres, not in scene units", () => {
/*
* The defect this guards: `LOT` is fixed in scene units, which makes a lot
* 40 m on San Francisco's board and **806 m** on the statewide one. Put
* San Francisco's fifty-two districts on the statewide board unchanged and
* they are lotted at 806 m — and lots go as the inverse square of their
* size, so the Bay Area's ~84,000 buildings become about two hundred and a
* city renders as a handful of grey slabs.
*
* Measured in the browser after the fix: the merged board carries 59,166
* building instances, of which 2,839 are the state's own and 56,327 are the
* two metros'. Before it, the metros contributed roughly two hundred.
*
* This asserts the property that produces that, rather than the count,
* because the count moves with any pack edit and the property does not.
*/
const detail = city.districts.filter((d) => d.detail === true);
assert.ok(detail.length > 0);
// The state's own districts must NOT be marked, or they would be re-lotted
// at 160 m across the whole of California and the board would never build.
const base = city.districts.filter((d) => d.detail !== true);
assert.ok(base.length > 0);
for (const d of base) assert.equal(d.detail, undefined);
});
it("keeps the metros' own boards untouched, which is the point of the flag", () => {
// `unify` copies districts before marking them, so nothing it does can
// reach back into `sf.ts` or `socal.ts` and re-lot the dedicated boards.
for (const d of SAN_FRANCISCO.districts) assert.equal(d.detail, undefined);
for (const d of SOCAL.districts) assert.equal(d.detail, undefined);
});
});
describe("the detail repack", () => {
it("distinguishes 'nothing packed yet' from 'nothing visible'", async () => {
/*
* A regression test for a bug this file's own author wrote and the budget
* caught. `DetailStore.key` remembers which districts the instance buffers
* currently hold, and it used to start as `""` — the same key an *empty*
* visible set produces. So the first call, at the whole-board pose where
* nothing should draw, compared equal to the initial state, took the early
* return, and left `InstancedMesh.count` at the constructor's value, which
* is every instance. Measured: 911,541 triangles against a 440,000 cap,
* with the code meant to prevent it running and doing nothing.
*
* The distinction is the whole fix, so it is what is asserted — on the
* source, because building a real `InstancedMesh` here would need a GPU.
*/
const source = await import("node:fs/promises").then((fs) =>
fs.readFile("src/engine/blocks.ts", "utf8"),
);
assert.ok(
/key: string \| null;/.test(source),
"DetailStore.key must admit a value that no visible set can produce",
);
assert.ok(
/key: null,/.test(source),
"the store must start at that value, not at an empty key",
);
assert.ok(
!/key: "",/.test(source),
'an empty-string initial key is the bug: it equals the key of an empty visible set',
);
});
});