feat: relief that ramps with the camera, and an instrument to cost a pose
`verticalExaggeration` is derived per board so the tallest blended peak
fills a fixed fraction of the board framed whole — 15.00 for California,
5.78 for the Bay Area, 3.41 for the Southland. Each is right, and while a
board could only be looked at from its own stand-off one number was all a
board could want. The merged California is the first that can be flown
from 1,551 km to 1.9 km, and 15x is what put the camera inside Twin Peaks
at the FiDi rung: 2.19 units of drawn hill against a 2.26-unit stand-off.
So a pack may now declare `nearVerticalExaggeration` — what it wants when
the camera is *in* it — and `unify.ts` sets it to the largest figure
either merged metro asked for, which is San Francisco's 5.78. Largest
rather than mean or smallest: both metros are known to read well at their
own number, and picking the smaller would flatten the Bay to suit a basin
four hundred kilometres away.
It is applied as one `scale.y` on a new `ground` group rather than by
rebuilding anything. Every height in that group came from the same
exaggeration, so multiplying restates all of them consistently — a lot
placed on a hillside is still on it, a bridge deck still clears the
water, a berth is still at the quay. That is the property the alternative
does not have: re-deriving placements against new ground is exactly the
blocker TODO.md names for the terrain quadtree, and this sidesteps it
rather than solving it. The quadtree will still have to solve it.
The sky is deliberately outside the group — clouds, precipitation,
migration, live aircraft, satellites, Starlink. Their altitudes are facts
about the atmosphere, not about the terrain, and an aeroplane that sinks
when the hills flatten draws the wrong thing. The visible consequence is
that traffic stands further off the ground as the ramp flattens it,
because it always was that far off in true metres.
The ramp is driven from stand-off, not altitude. Altitude is measured
against the ground and this moves the ground, so an altitude trigger is a
feedback loop; stand-off belongs to the controls alone. Log-interpolated,
because the rungs are spaced logarithmically — 1,551, 501, 256, 95, 45,
24, 8.3, 7.7 km — and a linear ramp spends its whole travel between the
two widest and none across the eight that matter.
Also: `scripts/cost-at.mjs`, which costs an arbitrary pose with
`performance-budget.mjs`'s own patched GL counters, so its numbers are
comparable rather than nearly comparable. The budget harness only ever
visits the poses a board is judged on, which is the right instrument for
"did this regress" and useless for "how much room is there at the bottom
of the descent". First run, merged board, at SF HQ:
400 km 355,759 tris / 414 draws
120 km 351,019 / 327
45 km 332,969 / 265
7.7km 330,426 / 339
2.5km 329,392 / 334
The Bay Area board at that last pose draws 2,307,964. Ours is flat across
three orders of magnitude of stand-off, which is not a budget being spent
carefully — it is a board with no frustum culling doing any work, paying
for the whole state's heightfield while looking at one city block.
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* What one pose actually costs, in triangles and draw calls.
|
||||
*
|
||||
* `scripts/performance-budget.mjs` measures the poses a board is *judged* on and
|
||||
* refuses to move its caps. That is the right instrument for "did this change
|
||||
* regress the product" and the wrong one for "how much room is there at the
|
||||
* bottom of the descent" — it never goes there, because no budget cell does.
|
||||
*
|
||||
* node scripts/cost-at.mjs --url "/?city=california&one=1" \
|
||||
* --lat 37.7897 --lng -122.3972 --standoffs 200000,60000,20000,7700,2000
|
||||
*
|
||||
* The instrumentation is `performance-budget.mjs`'s, deliberately: the same four
|
||||
* patched GL entry points and the same triangle arithmetic, so a number from
|
||||
* here is comparable with a number from there rather than nearly comparable.
|
||||
*
|
||||
* Camera aiming goes through `__teraCamera.seek`, which takes true metres, so
|
||||
* one stand-off list reads the same on a 94 m board and a 1,919 m one.
|
||||
*/
|
||||
|
||||
import { spawn } from "node:child_process";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { createServer } from "node:net";
|
||||
import { chromium } from "playwright";
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const flag = (name, fallback) => {
|
||||
const at = args.indexOf(name);
|
||||
return at < 0 ? fallback : args[at + 1];
|
||||
};
|
||||
|
||||
const PORT = await new Promise((resolve, reject) => {
|
||||
const probe = createServer();
|
||||
probe.once("error", reject);
|
||||
probe.listen(0, "127.0.0.1", () => {
|
||||
const { port } = probe.address();
|
||||
probe.close(() => resolve(port));
|
||||
});
|
||||
});
|
||||
const server = spawn(
|
||||
new URL("../node_modules/.bin/vite", import.meta.url).pathname,
|
||||
["preview", "--port", String(PORT), "--strictPort"],
|
||||
{ detached: true, stdio: ["ignore", "pipe", "pipe"] },
|
||||
);
|
||||
const shutdown = () => {
|
||||
try { process.kill(-server.pid, "SIGTERM"); } catch { /* already gone */ }
|
||||
};
|
||||
process.on("exit", shutdown);
|
||||
const bound = await new Promise((resolve) => {
|
||||
let seen = "";
|
||||
const settle = setTimeout(() => resolve(null), 30000);
|
||||
server.stdout.on("data", (c) => {
|
||||
seen += String(c);
|
||||
const m = /http:\/\/(?:localhost|127\.0\.0\.1):(\d+)/.exec(seen);
|
||||
if (m) { clearTimeout(settle); resolve(Number(m[1])); }
|
||||
});
|
||||
server.stderr.on("data", (c) => { seen += String(c); });
|
||||
});
|
||||
if (bound !== PORT) {
|
||||
console.error(`cost-at: preview bound ${bound}, not ${PORT}`);
|
||||
process.exit(1);
|
||||
}
|
||||
{
|
||||
const served = await fetch(`http://localhost:${PORT}/index.html`).then((r) => r.text());
|
||||
const onDisk = readFileSync(new URL("../dist/index.html", import.meta.url), "utf8");
|
||||
const bundle = (html) => /src="([^"]*\/assets\/[^"]+\.js)"/.exec(html)?.[1] ?? null;
|
||||
if (bundle(served) === null || bundle(served) !== bundle(onDisk)) {
|
||||
console.error("cost-at: that port is not serving this dist/");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const browser = await chromium.launch({
|
||||
channel: "chrome",
|
||||
args: ["--use-gl=angle", "--use-angle=vulkan", "--enable-unsafe-swiftshader", "--ignore-gpu-blocklist"],
|
||||
});
|
||||
const page = await browser.newPage({ viewport: { width: 1600, height: 1000 } });
|
||||
|
||||
/** `performance-budget.mjs`'s counters, restated so the numbers are comparable. */
|
||||
await page.addInitScript(() => {
|
||||
const state = { calls: 0, tris: 0 };
|
||||
globalThis.__teraCost = state;
|
||||
const triangleCount = (mode, count) =>
|
||||
mode === 4 ? count / 3 : mode === 5 || mode === 6 ? Math.max(0, count - 2) : 0;
|
||||
const patch = (proto, countAt, instancesAt = null) => (method) => {
|
||||
if (!proto) return;
|
||||
const original = proto[method];
|
||||
if (!original) return;
|
||||
proto[method] = function (...v) {
|
||||
state.calls += 1;
|
||||
const instances = instancesAt === null ? 1 : Number(v[instancesAt]) || 0;
|
||||
state.tris += triangleCount(Number(v[0]), Number(v[countAt]) || 0) * instances;
|
||||
return original.apply(this, v);
|
||||
};
|
||||
};
|
||||
for (const proto of [globalThis.WebGLRenderingContext?.prototype, globalThis.WebGL2RenderingContext?.prototype]) {
|
||||
patch(proto, 2)("drawArrays");
|
||||
patch(proto, 1)("drawElements");
|
||||
patch(proto, 2, 3)("drawArraysInstanced");
|
||||
patch(proto, 1, 4)("drawElementsInstanced");
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto(`http://localhost:${PORT}${flag("--url", "/")}`, { waitUntil: "networkidle", timeout: 60000 });
|
||||
await page.waitForTimeout(Number(flag("--wait", "11000")));
|
||||
try { await page.getByText(/^Skip$/).first().click({ timeout: 2500 }); await page.waitForTimeout(1000); } catch { /* not shown */ }
|
||||
|
||||
const lat = Number(flag("--lat", "37.7897"));
|
||||
const lng = Number(flag("--lng", "-122.3972"));
|
||||
const standoffs = String(flag("--standoffs", "200000,60000,20000,7700,2000")).split(",").map(Number);
|
||||
|
||||
console.log(`cost-at: ${flag("--url", "/")} at ${lat}, ${lng}`);
|
||||
console.log(" standoff triangles draws");
|
||||
for (const standoffM of standoffs) {
|
||||
const placed = await page.evaluate((pose) => {
|
||||
const cam = globalThis.__teraCamera;
|
||||
if (!cam || typeof cam.seek !== "function") return null;
|
||||
return { board: cam.board, ...cam.seek(pose) };
|
||||
}, { lat, lng, standoffM });
|
||||
if (placed === null) {
|
||||
console.error("cost-at: no __teraCamera hook on this build");
|
||||
break;
|
||||
}
|
||||
// Let the detail LOD settle: the repack is driven from the frame loop and a
|
||||
// measurement taken on the frame the camera moved is a measurement of the
|
||||
// previous pose.
|
||||
await page.waitForTimeout(2500);
|
||||
// One frame's worth, sampled over several so a single odd frame cannot decide it.
|
||||
const sample = await page.evaluate(async () => {
|
||||
const s = globalThis.__teraCost;
|
||||
const frames = [];
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const c0 = s.calls, t0 = s.tris;
|
||||
await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
|
||||
frames.push({ calls: s.calls - c0, tris: s.tris - t0 });
|
||||
}
|
||||
frames.sort((a, b) => a.tris - b.tris);
|
||||
return frames[Math.floor(frames.length / 2)];
|
||||
});
|
||||
const km = (standoffM / 1000).toFixed(1).padStart(8);
|
||||
console.log(` ${km} km ${String(Math.round(sample.tris / 2)).padStart(9)} ${String(Math.round(sample.calls / 2)).padStart(5)}`);
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
shutdown();
|
||||
Reference in New Issue
Block a user