fix: the five things integration found, and a cell that stands on the worst pose
The integration analysis built the actual combination of both tracks rather than adding up their reports, and found five defects plus one structural gap. All six are addressed here. **The detail normal map now follows its material into disposal.** `Material.dispose()` frees the material and not its maps — the same arithmetic `stage.ts` records for the renderer one level up — and `scene.ts`'s teardown traverses geometry and material only. Every board build was orphaning two 512-square RGBA textures on the GL context, about 2.8 MB with mips each, on a cache that retains three boards and rebuilds on every switch. `structures.ts` already had the idiom for its road canvases; copied rather than reinvented. **`TerrainOptions.detail` is `surfaceDetail`.** `createTerrain` reads `district.detail` nineteen lines below it — an unrelated flag meaning "metro lots, revealed by stand-off" — and the two are the gates for two different level-of-detail systems in one function, one of which decides whether the terrain is chunked at all. The test files had both spellings meaning both things. **The instrument stopped lying.** `store.tier` is an index and is always valid, because the coarse fill gives base districts an entry at every rung; at a pose with metro detail switched off the budget scan finds the visible base districts fit at the finest rung and sets it to 0. So `cost-at`'s new lot column printed "40 m" for a board drawing nothing but 806 m base lots — an instrument lying in the direction of "the fine rung is cheap", which is the direction that gets a budget spent that is not there. `detailTier` is `null` unless a detail district is actually drawn. **Two doc comments carried wrong numbers**, which is the defect this repo's house style exists to prevent. Capacity is exactly 19,000, not "about 26,000" — measured. And `LOT_BUDGET`'s guarantee is a bound on the *city*, not on the frame: the non-city cost is not a constant and nothing enforces it, reaching 288,365 at LA from 110 km. **And `california-one-basin`.** `california-one` is judged at the whole state from 1,551 km — the pose where the terrain grid saves nothing and the city is off. Every expensive pose this product has is somewhere else, and that was not hypothetical: six poses over the LA basin measured over the cap, one at 761,648, all reachable from the board's own chapters for as long as the merged board has existed. A cap nobody measures is not a cap. Eleven kilometres because the ladder makes the worst pose an interior one, not the closest: 12 / 11.5 / 11 / 10.5 / 10 / 7.7 km reads 340,328 / 390,598 / 390,578 / 390,578 / 387,428 / 375,172. A cell at 2.5 km or at 7.7 km would have watched the wrong thing. Its caps are `california-one`'s own rather than recorded from this run, because the question is not "has this pose regressed" but "is this board still inside the budget it claims".
This commit is contained in:
+32
-5
@@ -186,10 +186,18 @@ const TIER_DISTRICT_LOTS = 2_500;
|
||||
*
|
||||
* So: 400,000 minus 205,000 is 195,000 triangles for the city, and ten
|
||||
* triangles to an instance makes that 19,500 lots. 19,000 is that with a
|
||||
* margin, and it is what holds the guarantee — every pose is at most
|
||||
* `19,000 x 10 + 204,448 = 394,448` unless the coarsest rung alone is over
|
||||
* budget, and `DETAIL_LOT_TIERS`' 400 m floor is what keeps that from
|
||||
* happening.
|
||||
* margin, and it is what holds the guarantee — a pose whose non-city cost is at
|
||||
* or under the 204,448 this was priced against is at most
|
||||
* `19,000 x 10 + 204,448 = 394,448`.
|
||||
*
|
||||
* **That is a bound on the city and not on the frame**, and the difference is
|
||||
* worth stating because the sentence here used to claim the latter. The
|
||||
* non-city cost is not a constant and nothing enforces it: measured, it reaches
|
||||
* 288,365 at Los Angeles from 110 km, a pose where metro detail is off entirely
|
||||
* and terrain, corridor and sky are the whole of the frame. The city is bounded;
|
||||
* everything else is measured, and the worst combined pose anywhere in either
|
||||
* metro's descent is **390,730 at LA 10.5-11.5 km**, which is 2.3% under the
|
||||
* cap. Nothing in CI stands there — see `TODO.md`.
|
||||
*/
|
||||
export const LOT_BUDGET = 19_000;
|
||||
|
||||
@@ -752,7 +760,8 @@ export function createBlocks(
|
||||
* 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 about 26,000 against the
|
||||
* 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.
|
||||
@@ -865,6 +874,9 @@ export function createBlocks(
|
||||
srcFacade,
|
||||
key: null,
|
||||
tier: coarsestTier,
|
||||
// Nothing has been packed yet, so nothing is being drawn at any rung. `null`
|
||||
// rather than `coarsestTier` for the same reason the field exists.
|
||||
detailTier: null,
|
||||
};
|
||||
mesh.userData.detail = store;
|
||||
/*
|
||||
@@ -966,6 +978,20 @@ interface DetailStore {
|
||||
* that total.
|
||||
*/
|
||||
tier: number;
|
||||
/**
|
||||
* The rung, but only when a **detail** district is actually being drawn at it.
|
||||
*
|
||||
* `tier` is an index into every range's `tiers` array and is always valid,
|
||||
* because the coarse fill gives base districts an entry at every rung. That
|
||||
* makes it the right thing to index with and the wrong thing to report: at a
|
||||
* pose where the stand-off has switched metro detail off entirely, the budget
|
||||
* scan still finds that the handful of visible base districts fit at the
|
||||
* finest rung, sets `tier` to 0, and the instrument printed "40 m" for a board
|
||||
* drawing nothing but 806 m base lots. The engine was right and the readout
|
||||
* was not — which is worse than the reverse, because the readout is what the
|
||||
* next decision gets made on.
|
||||
*/
|
||||
detailTier: number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1117,6 +1143,7 @@ export function updateBlocksDetail(
|
||||
}
|
||||
}
|
||||
store.tier = tier;
|
||||
store.detailTier = visible.some((r) => r.detail) ? tier : null;
|
||||
|
||||
/*
|
||||
* `start` is unique across the whole store — every rung of every district has
|
||||
|
||||
+32
-3
@@ -406,6 +406,27 @@ function groundMaterial(
|
||||
metalness: 0,
|
||||
...(detail ? { normalMap: groundDetailTexture() } : {}),
|
||||
});
|
||||
/*
|
||||
* The map follows its owner into disposal, which it did not before.
|
||||
*
|
||||
* `Material.dispose()` frees the material and **not its maps** — the same
|
||||
* arithmetic `stage.ts` records for `WebGLRenderer.dispose()`, one level
|
||||
* down — and `scene.ts`'s teardown traverse frees geometry and material and
|
||||
* nothing else. So every board build was orphaning a 512-square RGBA texture
|
||||
* per ground material on the GL context, about 2.8 MB with mips, for the life
|
||||
* of the page. Two of them per board, since the shore plate wears the same
|
||||
* surface, on a board cache that retains up to three boards and rebuilds on
|
||||
* every switch.
|
||||
*
|
||||
* `structures.ts` already has this exact idiom for its road canvases and
|
||||
* states the reason: three's materials are event dispatchers and emit
|
||||
* `dispose`, so a texture can simply follow its owner rather than needing a
|
||||
* disposal path of its own. Copied deliberately rather than reinvented.
|
||||
*/
|
||||
if (material.normalMap !== null) {
|
||||
const map = material.normalMap;
|
||||
material.addEventListener("dispose", () => map.dispose());
|
||||
}
|
||||
/**
|
||||
* `shadowSide = BackSide` is the acne cure, and it is exactly right for a
|
||||
* heightfield.
|
||||
@@ -600,7 +621,7 @@ export function createShorePlates(world: World, options: TerrainOptions = {}): T
|
||||
*/
|
||||
const mesh = new THREE.Mesh(
|
||||
geo,
|
||||
groundMaterial(world, options.detail ?? groundDetailWanted(), { color: pal.shore }),
|
||||
groundMaterial(world, options.surfaceDetail ?? groundDetailWanted(), { color: pal.shore }),
|
||||
);
|
||||
mesh.receiveShadow = true;
|
||||
/**
|
||||
@@ -916,8 +937,16 @@ export interface TerrainOptions {
|
||||
* Defaults to `groundDetailWanted()`, which is "not on a phone". Passed
|
||||
* explicitly by the tests, which want the map on a board that has no
|
||||
* `window` to profile and want to be able to ask for it off.
|
||||
*
|
||||
* **Named `surfaceDetail` and not `detail`**, which is what it was called
|
||||
* first. `createTerrain` reads `district.detail` nineteen lines below this —
|
||||
* a completely unrelated flag meaning "metro lots, revealed by stand-off" —
|
||||
* and the two are the gate for two different level-of-detail systems in one
|
||||
* function. One of them decides whether the terrain is chunked at all. A
|
||||
* reader who conflates them gets the wrong mental model of the whole file,
|
||||
* and the cost of not conflating them is nine characters.
|
||||
*/
|
||||
readonly detail?: boolean;
|
||||
readonly surfaceDetail?: boolean;
|
||||
}
|
||||
|
||||
export function createTerrain(world: World, options: TerrainOptions = {}): THREE.Group {
|
||||
@@ -1099,7 +1128,7 @@ export function createTerrain(world: World, options: TerrainOptions = {}): THREE
|
||||
* that. A texture is not a draw call. A second mesh, a decal layer or a
|
||||
* second pass would have been, which is why none of those is here.
|
||||
*/
|
||||
const material = groundMaterial(world, options.detail ?? groundDetailWanted(), {
|
||||
const material = groundMaterial(world, options.surfaceDetail ?? groundDetailWanted(), {
|
||||
vertexColors: true,
|
||||
});
|
||||
|
||||
|
||||
+15
-3
@@ -2782,12 +2782,24 @@ function publishCameraHook(record: MountedBoard): void {
|
||||
});
|
||||
const mesh = found[0];
|
||||
if (mesh === undefined) return null;
|
||||
const store = mesh.userData.detail as { tier?: number } | undefined;
|
||||
const store = mesh.userData.detail as
|
||||
| { tier?: number; detailTier?: number | null }
|
||||
| undefined;
|
||||
/*
|
||||
* `detailTier` and not `tier`, because they answer different questions and
|
||||
* only one of them is the one being asked. `tier` is an index and is
|
||||
* always valid; `detailTier` is `null` at a pose where no metro detail is
|
||||
* drawn at all. Reporting `tier` printed "40 m" at the whole-board pose,
|
||||
* on a board drawing nothing but 806 m base lots — an instrument lying in
|
||||
* the direction of "the fine rung is cheap", which is precisely the
|
||||
* direction that would get somebody to spend a budget they do not have.
|
||||
*/
|
||||
const drawn = store?.detailTier ?? null;
|
||||
return {
|
||||
packed: mesh.count,
|
||||
capacity: (mesh.instanceMatrix.array.length / 16) | 0,
|
||||
tier: store?.tier ?? null,
|
||||
lotMetres: store === undefined ? null : detailLotMetres(store.tier ?? 0),
|
||||
tier: drawn,
|
||||
lotMetres: drawn === null ? null : detailLotMetres(drawn),
|
||||
};
|
||||
},
|
||||
board: record.id,
|
||||
|
||||
@@ -278,8 +278,8 @@ test("the shore plate receives and does not cast", async () => {
|
||||
|
||||
test("the shore plate wears the same surface as the ground it abuts", async () => {
|
||||
const world = await board();
|
||||
const plate = createShorePlates(world, { detail: true }).material as THREE.MeshStandardMaterial;
|
||||
const ground = (createTerrain(world, { detail: true }).children[0] as THREE.Mesh)
|
||||
const plate = createShorePlates(world, { surfaceDetail: true }).material as THREE.MeshStandardMaterial;
|
||||
const ground = (createTerrain(world, { surfaceDetail: true }).children[0] as THREE.Mesh)
|
||||
.material as THREE.MeshStandardMaterial;
|
||||
/*
|
||||
* Two material objects — the plate is a flat `pal.shore` and the terrain is
|
||||
@@ -346,8 +346,8 @@ test("the land catches the sky, which a Lambert card cannot", async () => {
|
||||
|
||||
test("the ground's detail relief is on by default and can be switched off whole", async () => {
|
||||
const world = await board();
|
||||
const on = createTerrain(world, { detail: true }).children[0] as THREE.Mesh;
|
||||
const off = createTerrain(world, { detail: false }).children[0] as THREE.Mesh;
|
||||
const on = createTerrain(world, { surfaceDetail: true }).children[0] as THREE.Mesh;
|
||||
const off = createTerrain(world, { surfaceDetail: false }).children[0] as THREE.Mesh;
|
||||
const lit = on.material as THREE.MeshStandardMaterial;
|
||||
const flat = off.material as THREE.MeshStandardMaterial;
|
||||
|
||||
@@ -374,7 +374,7 @@ test("the ground's detail relief is on by default and can be switched off whole"
|
||||
|
||||
test("the detail UV is built from the plan, because the ground has no uv attribute", async () => {
|
||||
const world = await board();
|
||||
const terrain = createTerrain(world, { detail: true }).children[0] as THREE.Mesh;
|
||||
const terrain = createTerrain(world, { surfaceDetail: true }).children[0] as THREE.Mesh;
|
||||
/*
|
||||
* The trap this exists for. `vNormalMapUv` is written by three's `uv_vertex`
|
||||
* from the `uv` attribute, WebGL feeds a *constant* for an attribute the
|
||||
|
||||
Reference in New Issue
Block a user