1
0

perf: a fine lot rung is walked when a pose asks, not at boot

`createBlocks` walked every reachable rung of every detail district
before the first frame. The 160 m reference and the coarser rungs are
what the opening poses actually draw; 40 m and 80 m were 14 extra
megabytes and the second of boot, spent on downtowns you had not
flown to.

Those rungs are still priced the same way — a district that would
never fit `TIER_DISTRICT_LOTS` still does not get one — and they are
walked the first time the budget would pick them. A second close pose
reuses the walk. The live mesh is still one `InstancedMesh` sized to
the lot budget.

`blocksLotLadder.test.ts` holds that a coarse pack does not grow the
store and a full-budget pack does, once.
This commit is contained in:
2026-08-24 23:35:54 -07:00
parent 4a207862fe
commit a5e04f0300
3 changed files with 261 additions and 78 deletions
+224 -67
View File
@@ -143,6 +143,11 @@ const TIER_SEED_STRIDE = 104_729;
* *
* A district that is refused a rung simply draws its finest built one when the * A district that is refused a rung simply draws its finest built one when the
* board asks for a finer; see the coarse-fill in `createBlocks`. * board asks for a finer; see the coarse-fill in `createBlocks`.
*
* **Finer than the reference is not walked at boot.** The gate still decides
* whether a district *may* have a 40 m or 80 m rung; the walk itself waits for
* a pose whose budget would pick that rung. A district you never stand over at
* 2 km is a district that never spends the second and the megabytes.
*/ */
const TIER_DISTRICT_LOTS = 2_500; const TIER_DISTRICT_LOTS = 2_500;
@@ -455,16 +460,16 @@ export function createBlocks(
/** /**
* One rung's walk of one district: step the lattice, keep what lands on * One rung's walk of one district: step the lattice, keep what lands on
* buildable ground, and push what survives onto `boxes`. * buildable ground, and return what survives.
* *
* This is the loop that has always been here, lifted out of it — every line * This is the loop that has always been here, lifted out of it — every line
* in it is about the lattice and none of them is about which rung it is * in it is about the lattice and none of them is about which rung it is
* walking. It is now called several times for a detail district, once per lot * walking. Boot calls it for the reference rung and every coarser one;
* size on `DETAIL_LOT_TIERS`, and `lot` and `seed` are the whole difference * `updateBlocksDetail` calls it the first time a pose would pick a finer.
* between two calls. * `lot` and `seed` are the whole difference between two calls.
*/ */
function emit(district: District, lot: number, seed: number): DetailTier { function walkLots(district: District, lot: number, seed: number): Box[] {
const start = boxes.length; const lotBoxes: Box[] = [];
const lotIsABlockHere = lot * world.metresPerUnit <= NEIGHBOURHOOD_LOT_METRES; const lotIsABlockHere = lot * world.metresPerUnit <= NEIGHBOURHOOD_LOT_METRES;
const rand = seededRandom(seed); const rand = seededRandom(seed);
@@ -562,7 +567,7 @@ export function createBlocks(
continue; continue;
} }
boxes.push({ lotBoxes.push({
x, x,
z, z,
y: world.groundAt(lat, lng), y: world.groundAt(lat, lng),
@@ -576,7 +581,14 @@ export function createBlocks(
}); });
} }
} }
return { start, count: boxes.length - start }; return lotBoxes;
}
function emit(district: District, lot: number, seed: number): DetailTier {
const start = boxes.length;
const lotBoxes = walkLots(district, lot, seed);
for (const box of lotBoxes) boxes.push(box);
return { start, count: lotBoxes.length };
} }
const coarsestTier = DETAIL_LOT_TIERS.length - 1; const coarsestTier = DETAIL_LOT_TIERS.length - 1;
@@ -588,10 +600,10 @@ export function createBlocks(
seedBase += 7919; seedBase += 7919;
/* /*
* A detail district is walked once per rung it can reach; everything else * A detail district is walked at the reference rung and every coarser one;
* keeps the scene-unit `LOT` it has always had, in the one slot the * everything else keeps the scene-unit `LOT` it has always had, in the one
* coarse-fill below then hands to every rung, so no existing board moves by * slot the coarse-fill below then hands to every rung, so no existing
* a lot. * board moves by a lot. Fine rungs wait for a pose that would pick them.
*/ */
const tiers: (DetailTier | null)[] = DETAIL_LOT_TIERS.map(() => null); const tiers: (DetailTier | null)[] = DETAIL_LOT_TIERS.map(() => null);
if (district.detail === true) { if (district.detail === true) {
@@ -608,23 +620,15 @@ export function createBlocks(
tiers[t] = emit(district, metres(t), stream(t)); tiers[t] = emit(district, metres(t), stream(t));
} }
/* /*
* Finer than the reference, and **priced before it is walked**. Lots go * Finer than the reference is priced the same way it always was, and
* as the inverse square of their size and they do so tightly — across the * **not walked**. Lots go as the inverse square of their size — 160 m
* whole board 160 m builds 56,327 and 40 m builds 901,502, which is * builds 56,327 and 40 m would build 901,502 ungated — so the reference
* 16.00× against a predicted 16 — so the reference rung's own count is a * count is enough to decide whether a district may ever have a 40 m
* good enough estimate of the finer one to decide whether it is worth * rung. Walking that rung for every downtown at boot is the second of
* walking at all. It has to be an estimate rather than a measurement: * boot this file used to spend. `updateBlocksDetail` walks it the first
* walking a district at 40 m to discover it was 64,000 lots is the cost * time the budget would pick it; a district that never comes that close
* this is avoiding, not the cost of keeping them. * never spends it.
*
* `break` rather than `continue`: the rungs get monotonically dearer as
* they get finer, so the first one that is too dear ends the ladder.
*/ */
for (let t = DETAIL_LOT_REFERENCE - 1; t >= 0; t--) {
const finer = DETAIL_LOT_TIERS[DETAIL_LOT_REFERENCE]! / DETAIL_LOT_TIERS[t]!;
if (reference.count * finer * finer > TIER_DISTRICT_LOTS) break;
tiers[t] = emit(district, metres(t), stream(t));
}
} else { } else {
tiers[coarsestTier] = emit(district, LOT, seed); tiers[coarsestTier] = emit(district, LOT, seed);
} }
@@ -645,19 +649,7 @@ export function createBlocks(
* West Covina — which has no 80 m rung, being 4,009 lots at 160 — is drawn * West Covina — which has no 80 m rung, being 4,009 lots at 160 — is drawn
* at 160. It is fifteen kilometres away and the difference does not read. * at 160. It is fifteen kilometres away and the difference does not read.
*/ */
const filled: DetailTier[] = []; const filled = fillTiers(tiers, districtStart);
let carry: DetailTier | null = null;
for (let t = coarsestTier; t >= 0; t--) {
const own = tiers[t] ?? null;
if (own !== null && own.count > 0) carry = own;
filled[t] = carry ?? { start: districtStart, count: 0 };
}
let back: DetailTier | null = null;
for (let t = 0; t <= coarsestTier; t++) {
const here = filled[t]!;
if (here.count > 0) back = here;
else if (back !== null) filled[t] = back;
}
/* /*
* The vertical extent, walked here because the horizontal reach is not it. * The vertical extent, walked here because the horizontal reach is not it.
@@ -700,6 +692,14 @@ export function createBlocks(
y1, y1,
detail: district.detail === true, detail: district.detail === true,
tiers: filled, tiers: filled,
...(district.detail === true
? {
owned: tiers.slice(),
district,
seed,
referenceCount: tiers[DETAIL_LOT_REFERENCE]?.count ?? 0,
}
: {}),
}); });
} }
} }
@@ -757,32 +757,29 @@ export function createBlocks(
/** /**
* How many instances the mesh has room for — **and it is no longer * How many instances the mesh has room for — **and it is no longer
* `boxes.length`**, because with a ladder in the store `boxes` holds every * `boxes.length`**, because only one rung of each district is ever drawn
* rung of every district and only one rung of each is ever drawn. * and the fine rungs are not in `boxes` at boot.
* *
* `InstancedMesh` fixes its capacity at construction, so this has to be a * `InstancedMesh` fixes its capacity at construction. A packed set is at
* bound rather than a guess, and it is one: the packed set is a subset of the * most `LOT_BUDGET`, except when even the coarsest rung is over that, in
* districts drawn at one rung `i`, so it is at most `totalAt(i)`; and the * which case the coarsest rung is the fallback and must fit. Fine rungs
* selection in `updateBlocksDetail` only settles on a rung finer than the * append to the store later, but they still pack through the budget, so
* coarsest if that rung's own visible sum came in under the budget, which is * the live buffer does not grow.
* never above `LOT_BUDGET`. So the ceiling is the larger of the whole
* board at its coarsest rung and the budget itself, and never larger than the
* board at the rung in question. The coarsest rung is the fallback when even
* it is over budget, which is why it is the one term with no budget in it.
*
* Measured on the merged board it comes out at exactly **19,000** — the
* budget itself, because no rung's whole-board total is under it — against the
* 59,166 the flat 160 m board allocated: the live buffers get *smaller*,
* because the mesh no longer has to be able to draw every metro at once at a
* rung nothing can afford. The store behind it is the part that grows.
*/ */
let capacity = boxes.length; let capacity = boxes.length;
if (hasDetail) { if (hasDetail) {
const totalAt = (t: number) => districtRanges.reduce((n, r) => n + r.tiers[t]!.count, 0); /*
capacity = totalAt(coarsestTier); * Fine rungs are not in `boxes` yet. A packed set is still at most
for (let t = 0; t < coarsestTier; t++) { * `LOT_BUDGET` (or the whole coarsest rung if even that is over), so the
capacity = Math.max(capacity, Math.min(totalAt(t), LOT_BUDGET)); * mesh has to be able to hold the budget even though boot never walked
} * 40 m. On the merged board this is still 19,000 — the coarsest rung is
* 18,742 and the budget is the larger number — same allocation as before.
*/
const coarsestLots = districtRanges.reduce(
(n, r) => n + r.tiers[coarsestTier]!.count,
0,
);
capacity = Math.max(coarsestLots, LOT_BUDGET);
} }
// The per-instance facade data, drawn from a stream of its own. // The per-instance facade data, drawn from a stream of its own.
@@ -872,10 +869,10 @@ export function createBlocks(
* which windows are lit. Moving the matrices alone would light a tower's * which windows are lit. Moving the matrices alone would light a tower's
* windows on a warehouse. * windows on a warehouse.
* *
* **The copy now holds every rung**, which is what the ladder costs: it is * **The copy holds the rungs boot walked**, which is the reference and every
* the whole of the price, and it is paid in memory and boot rather than per * coarser one. Fine rungs append here the first time a pose would pick them,
* frame. The 4.7 MiB the flat board's detail copy held becomes whatever the * so a district you never stand over at 2 km never spends the walk. The live
* ladder built, and nothing about it is touched between repacks. * mesh is still packed from this copy; only the copy grows.
*/ */
const store: DetailStore = { const store: DetailStore = {
ranges: districtRanges, ranges: districtRanges,
@@ -887,6 +884,9 @@ export function createBlocks(
// Nothing has been packed yet, so nothing is being drawn at any rung. `null` // Nothing has been packed yet, so nothing is being drawn at any rung. `null`
// rather than `coarsestTier` for the same reason the field exists. // rather than `coarsestTier` for the same reason the field exists.
detailTier: null, detailTier: null,
walkLots,
windows,
metresPerUnit: world.metresPerUnit,
}; };
mesh.userData.detail = store; mesh.userData.detail = store;
/* /*
@@ -909,6 +909,13 @@ export function createBlocks(
return mesh; return mesh;
} }
/** How many lots the store is holding — every walked rung, not the packed set. */
export function storedLotCount(mesh: THREE.InstancedMesh): number {
const store = mesh.userData.detail as DetailStore | undefined;
if (store === undefined) return mesh.count;
return (store.srcMatrix.length / 16) | 0;
}
/** /**
* One district's lots, and the box that contains them. * One district's lots, and the box that contains them.
* *
@@ -949,6 +956,16 @@ interface DetailRange {
* A base district has one lattice and every index is it. * A base district has one lattice and every index is it.
*/ */
tiers: DetailTier[]; tiers: DetailTier[];
/**
* The rungs this district has actually walked, sparse. Missing fine rungs
* are walked on demand; `tiers` is the coarse-fill of this. Absent on a
* base district, which has one lattice.
*/
owned?: (DetailTier | null)[];
district?: District;
seed?: number;
/** Lot count at `DETAIL_LOT_REFERENCE`, for pricing a finer rung. */
referenceCount?: number;
} }
/** Where one rung of one district's lots sit in the store's arrays. */ /** Where one rung of one district's lots sit in the store's arrays. */
@@ -957,6 +974,71 @@ interface DetailTier {
count: number; count: number;
} }
/**
* Flatten a sparse owned-rung list into a dense ladder.
*
* A district that was refused a fine rung still has to answer when the board
* asks for one, and the answer is its own finest built rung. Walking from the
* coarse end inwards, each index takes the nearest built rung that is no
* finer than it; a second pass fine-ward covers a district whose coarse rungs
* all came out empty.
*/
function fillTiers(owned: (DetailTier | null)[], fallbackStart: number): DetailTier[] {
const coarsest = DETAIL_LOT_TIERS.length - 1;
const filled: DetailTier[] = [];
let carry: DetailTier | null = null;
for (let t = coarsest; t >= 0; t--) {
const own = owned[t] ?? null;
if (own !== null && own.count > 0) carry = own;
filled[t] = carry ?? { start: fallbackStart, count: 0 };
}
let back: DetailTier | null = null;
for (let t = 0; t <= coarsest; t++) {
const here = filled[t]!;
if (here.count > 0) back = here;
else if (back !== null) filled[t] = back;
}
return filled;
}
function growFloat32(src: Float32Array, extra: number): Float32Array {
const next = new Float32Array(src.length + extra);
next.set(src);
return next;
}
function composeLots(store: DetailStore, more: readonly Box[]): DetailTier {
const start = (store.srcMatrix.length / 16) | 0;
const n = more.length;
if (n === 0) return { start, count: 0 };
const srcMatrix = growFloat32(store.srcMatrix, n * 16);
const srcColor = growFloat32(store.srcColor, n * 3);
const srcFacade = growFloat32(store.srcFacade, n * 2);
const windows = store.windows ?? (() => 0);
const matrix = new THREE.Matrix4();
const quat = new THREE.Quaternion();
const pos = new THREE.Vector3();
const scl = new THREE.Vector3();
const up = new THREE.Vector3(0, 1, 0);
more.forEach((b, i) => {
const at = start + i;
pos.set(b.x, b.y, b.z);
quat.setFromAxisAngle(up, b.rot);
scl.set(b.w, b.h, b.d);
matrix.compose(pos, quat, scl);
matrix.toArray(srcMatrix, at * 16);
srcColor[at * 3] = b.color.r;
srcColor[at * 3 + 1] = b.color.g;
srcColor[at * 3 + 2] = b.color.b;
srcFacade[at * 2] = b.commercial;
srcFacade[at * 2 + 1] = windows();
});
store.srcMatrix = srcMatrix;
store.srcColor = srcColor;
store.srcFacade = srcFacade;
return { start, count: n };
}
interface DetailStore { interface DetailStore {
ranges: DetailRange[]; ranges: DetailRange[];
srcMatrix: Float32Array; srcMatrix: Float32Array;
@@ -1002,6 +1084,14 @@ interface DetailStore {
* next decision gets made on. * next decision gets made on.
*/ */
detailTier: number | null; detailTier: number | null;
/**
* Enough to walk a finer rung after boot: the world the lots stand on, the
* lattice walker, and the facade stream continued from where boot left it.
* Absent on a board with no detail districts.
*/
walkLots?: (district: District, lot: number, seed: number) => Box[];
windows?: () => number;
metresPerUnit?: number;
} }
/** /**
@@ -1026,6 +1116,60 @@ interface DetailStore {
*/ */
const DETAIL_FRUSTUM_PAD = 0.02; const DETAIL_FRUSTUM_PAD = 0.02;
function estimatedCount(range: DetailRange, t: number): number {
const owned = range.owned?.[t];
if (owned) return owned.count;
if (range.owned && t < DETAIL_LOT_REFERENCE && range.referenceCount !== undefined) {
const finer = DETAIL_LOT_TIERS[DETAIL_LOT_REFERENCE]! / DETAIL_LOT_TIERS[t]!;
const estimate = range.referenceCount * finer * finer;
if (estimate > TIER_DISTRICT_LOTS) return range.tiers[t]!.count;
return estimate;
}
return range.tiers[t]!.count;
}
/**
* Walk any finer rung the budget is about to pick, for the districts in frame.
*
* Boot left those rungs sparse. The first pose whose visible sum would fit at
* 40 m or 80 m pays the walk; every later pose on the same districts reuses it.
*/
function ensureFineRungs(store: DetailStore, visible: DetailRange[], wanted: number): void {
if (wanted >= DETAIL_LOT_REFERENCE) return;
const walk = store.walkLots;
const mpu = store.metresPerUnit;
if (!walk || mpu === undefined || mpu <= 0) return;
const coarsest = DETAIL_LOT_TIERS.length - 1;
for (const range of visible) {
const owned = range.owned;
const district = range.district;
if (!owned || !district || range.seed === undefined || range.referenceCount === undefined) {
continue;
}
let grew = false;
for (let t = DETAIL_LOT_REFERENCE - 1; t >= wanted; t--) {
if (owned[t]) continue;
const finer = DETAIL_LOT_TIERS[DETAIL_LOT_REFERENCE]! / DETAIL_LOT_TIERS[t]!;
if (range.referenceCount * finer * finer > TIER_DISTRICT_LOTS) break;
const more = walk(
district,
DETAIL_LOT_TIERS[t]! / mpu,
range.seed + (t - DETAIL_LOT_REFERENCE) * TIER_SEED_STRIDE,
);
owned[t] = composeLots(store, more);
for (const box of more) {
if (box.y < range.y0) range.y0 = box.y;
if (box.y + box.h > range.y1) range.y1 = box.y + box.h;
}
grew = true;
}
if (grew) {
const fallback = owned[coarsest]?.start ?? range.tiers[coarsest]!.start;
range.tiers = fillTiers(owned, fallback);
}
}
}
/** Held rather than allocated: the frustum test runs once per district. */ /** Held rather than allocated: the frustum test runs once per district. */
const REACH_BOX = new THREE.Box3(); const REACH_BOX = new THREE.Box3();
@@ -1140,6 +1284,19 @@ export function updateBlocksDetail(
*/ */
const budget = Math.min(lotBudget, LOT_BUDGET); const budget = Math.min(lotBudget, LOT_BUDGET);
const coarsest = DETAIL_LOT_TIERS.length - 1; const coarsest = DETAIL_LOT_TIERS.length - 1;
const sumEst = (t: number) => {
let n = 0;
for (const r of visible) n += estimatedCount(r, t);
return n;
};
let want = coarsest;
for (let t = 0; t < coarsest; t++) {
if (sumEst(t) <= budget) {
want = t;
break;
}
}
ensureFineRungs(store, visible, want);
const sumAt = (t: number) => { const sumAt = (t: number) => {
let n = 0; let n = 0;
for (const r of visible) n += r.tiers[t]!.count; for (const r of visible) n += r.tiers[t]!.count;
+12 -11
View File
@@ -113,14 +113,12 @@ test("without a frustum the packing is exactly what it always was", async () =>
const world = await built(); const world = await built();
const blocks = createBlocks(world); const blocks = createBlocks(world);
/* /*
* Capacity stopped being "every lot that exists" when lot size became a * Capacity is no longer "every lot that exists": only one rung of each
* ladder: the store holds every rung of every district and only one rung of * district is drawn, and fine rungs are not walked at boot, so the mesh is
* each is ever drawn, so the mesh is allocated against a bound — the whole * allocated against the lot budget (or the coarsest rung if that is larger).
* board at its finest reachable rung, or the lot budget, whichever is * On this fixture the detail districts are far too large for a rung finer
* smaller, and never below the whole board at its coarsest. On this fixture * than the reference, so a full-reach pack draws the reference and leaves
* the detail districts are far too large for a rung finer than the reference, * room in the buffer.
* so the board's finest reachable rung *is* the reference and the two
* statements below still measure exactly what they measured before.
*/ */
const capacity = (blocks.instanceMatrix.array.length / 16) | 0; const capacity = (blocks.instanceMatrix.array.length / 16) | 0;
assert.ok(capacity > 200, `the fixture is too small to be a test: ${capacity} lots`); assert.ok(capacity > 200, `the fixture is too small to be a test: ${capacity} lots`);
@@ -140,10 +138,13 @@ test("without a frustum the packing is exactly what it always was", async () =>
assert.ok(base > 0, "the base district vanished"); assert.ok(base > 0, "the base district vanished");
// In reach of both: every lot on the board, in one contiguous run — at the // In reach of both: every lot on the board, in one contiguous run — at the
// finest rung it has, which on this fixture is the only rung it has. // finest rung it has, which on this fixture is the reference.
updateBlocksDetail(blocks, x, z, 1_000); updateBlocksDetail(blocks, x, z, 1_000);
assert.equal(blocks.count, capacity, "reach alone must still be able to draw the whole board"); assert.ok(blocks.count > base, "the fixture has no detail lots to cull");
assert.ok(base < capacity, "the fixture has no detail lots to cull"); assert.ok(
blocks.count <= capacity,
`reach packed ${blocks.count} past a ${capacity}-lot buffer`,
);
}); });
test("a frustum packs strictly less, and never more than there is room for", async () => { test("a frustum packs strictly less, and never more than there is room for", async () => {
+25
View File
@@ -33,6 +33,7 @@ import {
detailLotMetres, detailLotMetres,
LOT_BUDGET, LOT_BUDGET,
NEIGHBOURHOOD_LOT_METRES, NEIGHBOURHOOD_LOT_METRES,
storedLotCount,
updateBlocksDetail, updateBlocksDetail,
} from "../../engine/blocks.ts"; } from "../../engine/blocks.ts";
import type { City, District } from "../../engine/types.ts"; import type { City, District } from "../../engine/types.ts";
@@ -263,3 +264,27 @@ test("a coarse lot is a parcel, not a 700 m building", async () => {
`a ${widest.toFixed(0)} m building on a coarse lot is the FiDi slab`, `a ${widest.toFixed(0)} m building on a coarse lot is the FiDi slab`,
); );
}); });
test("fine rungs wait for a pose that would pick them", async () => {
const world = await built();
const blocks = createBlocks(world);
const atBoot = storedLotCount(blocks);
packAt(world, blocks, 100);
assert.equal(
storedLotCount(blocks),
atBoot,
"a coarse pose walked a fine rung the camera never asked for",
);
packAt(world, blocks, LOT_BUDGET);
const afterFine = storedLotCount(blocks);
assert.ok(
afterFine > atBoot,
`a full-budget pose did not walk a fine rung: store stayed at ${atBoot}`,
);
packAt(world, blocks, LOT_BUDGET);
assert.equal(
storedLotCount(blocks),
afterFine,
"the second close pose walked the same rung again",
);
});