1
0

feat: boards load before you ask for them

The owner asked for one map, with detail arriving in the background rather than
behind a wait. The interface already said one place (4bd8481: one ladder, no tab
strip, no boot card). This is the world catching up: on a desktop both metros are
now resident within seconds of landing, and the switch that used to be a build
becomes a fog dip over a cache hit.

**Measured, three runs each, California -> The Bay after an 8 s dwell:**
the longest blocking task falls from **521 ms to 58 ms** (before 700/501/521,
after 58/58/61) and long tasks from 2 to 1.

**And the cost, stated rather than omitted.** Over ten seconds sitting on the
state board: 601 frames -> ~571, p95 frame interval 18.0 -> 18.7 ms, and **one
long task of ~500 ms that did not exist before**. The build is not chunked yet,
so this moves a freeze from the moment of interaction to a moment nobody asked
for. That is a real trade and the follow-on that removes it is chunking the
build. `PREFETCH_ENABLED` in main.ts turns the whole lane off in one line.

A parked board draws nothing: 371 draws per frame before the lane lands a board
and 371 after, patched at the GL entry points. The performance budget reproduces
every cell to the digit.

**Three defects found on the way, two of them latent for a while.**

1. `environmentRig` had no idea what off-stage meant. `createScene` calls
   `apply()` unconditionally, two hundred lines above the `present` check, and
   the rig holds one probe per kind: on a key miss it convolves a new one,
   disposes the one the visible board is using, and repoints every applied scene
   at the replacement. A board built ahead of the camera is observed at its own
   centre — about forty minutes of apparent solar time across the state — so its
   key differs by construction. The board on screen would have changed colour
   because something invisible finished loading. Harmless until today only
   because every build was followed within ~800 ms by the swap that presented it.
   Fixed with `ApplyOptions.offstage`; the regression test was written first and
   failed first.

2. `mounting` was assigned and never cleared. Harmless while the only reader was
   the next `mountCity` wanting something to abort. It stopped being harmless the
   moment a second lane asked "is a foreground build in flight?", because the
   honest answer after the first mount of the session was permanently yes — the
   background lane armed exactly never, and the only symptom was a feature that
   silently did nothing. Found by measuring, not by reading.

3. `buildBoard`'s `onProgress` ends in `bootProgress`, which raises the switch
   pill. A prefetch would have put "Building The Bay Area... terrain 42%" over a
   visitor who did nothing — the exact chrome that removing the tab strip was
   for. `quiet` closes it.

**The policy is pure and lives with the eviction policy it has to agree with.**
`prefetchTarget` and `canAdmit` in boards.ts take scalars, never a camera, so
boards.ts keeps CONTRACT section 1's no-DOM/no-WebGL/no-three promise and
prefetchPolicy.test.ts can assert against the real pack bounds. Two regimes,
because collapsing them was the first version's mistake: with free room the gate
is simply "is there a slot", since the common path is to land on the state at
1,551 km and click a metro, and on that path a proximity trigger fires never;
with the cache full — the handheld case, capacity two with California pinned —
proximity is the only thing that justifies an eviction. The discs are asserted
non-overlapping against the shipped bounds (92 + 146 km of reach across a 314 km
gap), which is the covering-set argument residentCapacity already rests on.

**Also fixed, and separately load-bearing: reconcile's flag parser lied.** It
prefix-matched, so `?reconcile=palette` selected nothing and produced an empty
set — indistinguishable from the flag being absent. Every photograph taken to
judge a rule could have been a photograph of the unreconciled board with no way
to tell. Exact names now, with a warning that says "the flag is NOT off".

And two consumers were reading the raw packs beside a reconciled World:
`buildLadder` derives every rung's stand-off from `focus`, and `createMinimap`
was handed `entry.city` next to `handle.world`. Both now read `world.city`.
Latent with the flag off — `reconciledCity` returns by identity — which is what
kept it alive: it corrupts the measurement rather than announcing itself.

**What I did NOT ship, having tried it.** `roads` on by default. The rule exists
to stop California drawing 1,919 m freeways, and `city.roads` is not what draws
them: California is the one board with `roadTraffic`, so `scene.ts:816` takes the
`createFreewayWorld` branch and `createRoads` — the only reader of `Road.width` —
is never called for it. The visible corridor is a deliberate atlas glyph sized so
DRIVE mode can drive down it. Photographed at three chapters the rule moved
Downtown LA not at all (empty diff bbox), FiDi by RMSE 0.0006, California by
0.001% of pixels; the only measurable effect anywhere was -112 triangles on
Southern California. See DEFAULT_RULES for the whole argument.

1,688 + 295 tests, every gate, chapter-identity OK against the unmodified
fixture, ui-smoke PASS, budget PASS with private-request checks clean on all ten
cells.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-23 13:15:37 -07:00
parent 2ff11a385e
commit 85be0b13f8
9 changed files with 1239 additions and 31 deletions
+21 -1
View File
@@ -632,7 +632,27 @@ test("main.ts makes exactly one call into the chrome, and writes no visibility i
test("the city scene is handed the same environment rig the office is", () => {
const scene = readFileSync(path.join(ROOT, "src/engine/scene.ts"), "utf8");
const office = readFileSync(path.join(ROOT, "src/interiors/officeScene.ts"), "utf8");
assert.ok(scene.includes('options.environment?.apply(scene, state, "city")'));
/*
* The call now carries a fourth argument, and this assertion got *stronger*
* rather than looser when it did.
*
* `apply` used to be unconditional everywhere it appeared, which is what let a
* board being built off-stage dispose the visible board's probe and repoint it
* — the rig holds one per kind and rebuilds on a key miss, and a board built
* ahead of the camera is observed at its own centre, so its key differs by
* construction. So the thing worth asserting is not merely that the city hands
* its lighting to the rig; it is that every clock-driven apply is *guarded* by
* whether anyone is looking. A future edit that drops the guard to "simplify"
* fails here.
*/
assert.ok(
scene.includes('options.environment?.apply(scene, state, "city", { offstage: !presented })'),
"the city's clock-driven apply lost its off-stage guard",
);
assert.ok(
!/options\.environment\?\.apply\(scene, state, "city"\)/.test(scene),
"an unguarded clock-driven apply is back in scene.ts",
);
assert.ok(scene.includes("options.environment?.release(scene)"));
assert.ok(office.includes('options.environment?.apply(scene, state, "office")'));
assert.ok(office.includes("options.environment?.release(scene)"));
+235
View File
@@ -0,0 +1,235 @@
/**
* When a board loads before anybody asks for it.
*
* ## What this is for
*
* "One map" is a claim about continuity, and continuity is paid for by having
* the detail already in memory when the camera arrives. CONTRACT §1.1 already
* keeps three boards resident on a desktop and pins California; what was missing
* was anything that *fills* that residency before a visitor demands it. This is
* the policy half of that — the half with no DOM, no WebGL and no `three` in it,
* so the decision can be argued about in a test instead of in a browser.
*
* ## The two numbers, and why they are safe
*
* `PREFETCH_HANDOVER_MULTIPLE = 3` and `PREFETCH_REACH_MULTIPLE = 1.5` are not
* taste. The first puts San Francisco's trigger at 213.6 km, inside the 133 km
* dead band between California's finest authored rung (242,206 m) and the first
* Bay rung (71,200 m) — so it fires on a state-scale rung with nothing authored
* below it, and never at the 1,551 km overview. The second gives discs of 92 km
* and 146 km around two metros that are 313.9 km apart, and 92 + 146 = 238 <
* 314, so **at most one tier is ever a candidate**. Both properties are asserted
* below against the real bounds, because both are load-bearing for a two-deep
* handheld cache and both would rot silently if a pack's bounds moved.
*/
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import CALIFORNIA from "../cities/california.ts";
import SAN_FRANCISCO from "../cities/sf.ts";
import SOCAL from "../cities/socal.ts";
import { HANDOVER_STANDOFF_M } from "../engine/ladder.ts";
import {
PREFETCH_HANDOVER_MULTIPLE,
PREFETCH_REACH_MULTIPLE,
canAdmit,
prefetchTarget,
residentCapacity,
} from "../engine/boards.ts";
import type { PrefetchTier } from "../engine/boards.ts";
const TIERS: readonly PrefetchTier[] = [
{ id: "sf", handoverStandoffM: HANDOVER_STANDOFF_M.sf!, bounds: SAN_FRANCISCO.bounds },
{ id: "socal", handoverStandoffM: HANDOVER_STANDOFF_M.socal!, bounds: SOCAL.bounds },
];
const DESKTOP = residentCapacity(false);
const HANDHELD = residentCapacity(true);
/** The camera parked over each metro's own middle. */
function centre(b: PrefetchTier["bounds"]) {
return { lat: (b.minLat + b.maxLat) / 2, lng: (b.minLng + b.maxLng) / 2 };
}
const SF_MID = centre(SAN_FRANCISCO.bounds);
const SOCAL_MID = centre(SOCAL.bounds);
function ask(over: { lat: number; lng: number }, standoffM: number, extra: Partial<Parameters<typeof prefetchTarget>[0]> = {}) {
return prefetchTarget({
tiers: TIERS,
lat: over.lat,
lng: over.lng,
standoffM,
resident: ["california"],
visible: "california",
capacity: DESKTOP,
pinned: ["california"],
...extra,
});
}
describe("what the prefetcher arms on", () => {
it("loads the Bay while the camera is descending toward it", () => {
assert.equal(ask(SF_MID, HANDOVER_STANDOFF_M.sf! * 2), "sf");
});
it("loads the Southland while the camera is descending toward it", () => {
assert.equal(ask(SOCAL_MID, HANDOVER_STANDOFF_M.socal! * 2), "socal");
});
/**
* The common path, and the reason the free-room rule exists at all.
*
* A visitor lands on the state overview at 1,551 km and then clicks a metro.
* On that path a proximity trigger fires exactly never — the camera is
* hundreds of kilometres above every threshold and the next thing that happens
* is the click. A desktop has two empty slots and CONTRACT §1.1 already
* promises they may be filled, so they are.
*/
it("fills the free residency from the state overview, where the visitor actually is", () => {
assert.equal(ask(SF_MID, 1_551_000), "sf");
// And having taken one, it goes back for the other rather than stopping.
assert.equal(ask(SF_MID, 1_551_000, { resident: ["california", "sf"] }), "socal");
// Until there is nothing left to want.
assert.equal(ask(SF_MID, 1_551_000, { resident: ["california", "sf", "socal"] }), null);
});
it("never proposes the board on screen, or one already resident", () => {
assert.notEqual(ask(SF_MID, HANDOVER_STANDOFF_M.sf! * 2, { visible: "sf" }), "sf");
assert.notEqual(
ask(SF_MID, HANDOVER_STANDOFF_M.sf! * 2, { resident: ["california", "sf"] }),
"sf",
);
});
/**
* With the cache full, proximity is the only thing that justifies an eviction —
* and this is the handheld case, capacity two with California pinned. A phone
* parked on the state board must load nothing at all.
*/
it("arms nothing on a full handheld cache away from either metro", () => {
const full = {
resident: ["california", "sf"],
visible: "california",
capacity: HANDHELD,
pinned: ["california"],
};
// Redding, in the far north: nowhere near the Southland.
assert.equal(ask({ lat: 40.58, lng: -122.39 }, HANDOVER_STANDOFF_M.socal!, full), null);
// And the state overview, which is above every threshold.
assert.equal(ask(SOCAL_MID, 1_551_000, full), null);
});
it("arms on a full cache only while descending toward the tier it would evict for", () => {
const full = {
resident: ["california", "sf"],
visible: "california",
capacity: HANDHELD,
pinned: ["california"],
};
assert.equal(ask(SOCAL_MID, HANDOVER_STANDOFF_M.socal! * 2, full), "socal");
});
});
describe("the two multiples are safe against the real packs", () => {
/**
* The dead band. If the Bay's trigger were above California's finest rung the
* prefetch would fire while the visitor was still choosing a region; if it
* were below the first Bay rung it would fire too late to have helped.
*/
it("puts the Bay trigger inside the ladder's authored dead band", () => {
const trigger = HANDOVER_STANDOFF_M.sf! * PREFETCH_HANDOVER_MULTIPLE;
assert.ok(trigger > HANDOVER_STANDOFF_M.sf!, "the trigger is below the handover");
assert.ok(trigger < 242_206, `the Bay trigger ${trigger} is above California's finest rung`);
});
/**
* The covering-set argument, re-derived. Two overlapping discs would mean a
* camera position from which both metros are candidates, and on a two-deep
* handheld cache that is one board too many.
*/
it("gives the two metros discs that cannot overlap", () => {
const M = 111_320;
const reach = (b: PrefetchTier["bounds"]) => {
const mid = (b.minLat + b.maxLat) / 2;
const north = (b.maxLat - b.minLat) * M;
const east = (b.maxLng - b.minLng) * M * Math.cos((mid * Math.PI) / 180);
return (Math.hypot(north, east) / 2) * PREFETCH_REACH_MULTIPLE;
};
const gapM =
(SAN_FRANCISCO.bounds.minLat - SOCAL.bounds.maxLat) * M;
const together = reach(SAN_FRANCISCO.bounds) + reach(SOCAL.bounds);
assert.ok(
SAN_FRANCISCO.bounds.minLat > SOCAL.bounds.maxLat,
"the metro bounds now intersect — the whole residency argument rests on them not doing so",
);
assert.ok(
together < gapM,
`the prefetch discs overlap: ${Math.round(together / 1000)} km of reach across a ` +
`${Math.round(gapM / 1000)} km gap`,
);
});
it("keeps California inside every board's bounds, which is what makes it the pin", () => {
for (const metro of [SAN_FRANCISCO, SOCAL]) {
assert.ok(metro.bounds.minLat >= CALIFORNIA.bounds.minLat);
assert.ok(metro.bounds.maxLat <= CALIFORNIA.bounds.maxLat);
assert.ok(metro.bounds.minLng >= CALIFORNIA.bounds.minLng);
assert.ok(metro.bounds.maxLng <= CALIFORNIA.bounds.maxLng);
}
});
});
describe("admission never costs the board on screen", () => {
it("refuses when the only victim left is the visible board", () => {
// A handheld holding the pin plus the board being looked at is full, and the
// only thing it could drop is the thing the visitor is looking at.
assert.equal(
canAdmit({
id: "socal",
resident: ["california", "sf"],
visible: "sf",
capacity: HANDHELD,
pinned: ["california"],
}),
false,
);
});
it("admits when a board that is neither pinned nor visible can go", () => {
assert.equal(
canAdmit({
id: "socal",
resident: ["california", "sf"],
visible: "california",
capacity: HANDHELD,
pinned: ["california"],
}),
true,
);
});
it("admits freely below capacity, and is idempotent for a resident board", () => {
assert.equal(
canAdmit({ id: "sf", resident: ["california"], visible: "california", capacity: DESKTOP }),
true,
);
assert.equal(
canAdmit({ id: "sf", resident: ["california", "sf"], visible: "sf", capacity: 2 }),
true,
);
});
/** And the policy must be reflected in what `prefetchTarget` proposes. */
it("proposes nothing a full handheld cache could not take", () => {
assert.equal(
ask(SOCAL_MID, HANDOVER_STANDOFF_M.socal! * 2, {
resident: ["california", "sf"],
visible: "sf",
capacity: HANDHELD,
pinned: ["california"],
}),
null,
);
});
});
+181
View File
@@ -0,0 +1,181 @@
/**
* Everything that reads a pack must read the pack the World is drawing.
*
* ## The bug this is downstream of
*
* `World`'s constructor is `this.city = reconciledCity(city)` (`world.ts:116`),
* so the moment any reconciliation rule is on, there are **two** Californias in
* the process: the object `cities/california.ts` exported, and the rewritten one
* the engine is actually projecting, exaggerating and drawing roads from.
*
* Two consumers were reading the first one while the camera stood in the second:
*
* - `buildLadder` (`main.ts`) derives every rung's stand-off from `focus`, so
* a raw-pack ladder measures a world nobody is looking at — the Places rail
* silently disagreeing with the camera about where it is.
* - `createMinimap` was handed `entry.city` beside a reconciled `handle.world`,
* so the plan view drew a different California than the frame beside it.
*
* Neither is visible with the flag off, because `reconciledCity` returns the
* pack **by identity** when no rule is on — which is exactly what made the bug
* survive: it is latent until the day somebody turns a rule on to take a
* measurement, and then it corrupts the measurement rather than announcing
* itself.
*
* ## What is asserted
*
* Not "main.ts calls the right function" — that is a source-text assertion and
* `integration/sceneWiring.test.ts` already owns that genre. This asserts the
* *property*: for every pack and every rule, the reconciled object differs from
* the raw one in the columns that rule owns, so a consumer holding the wrong one
* is holding provably different numbers. A future consumer wired to the raw pack
* is a bug this file describes even if it cannot name the call site.
*/
import assert from "node:assert/strict";
import { after, describe, it } from "node:test";
import CALIFORNIA from "../cities/california.ts";
import SAN_FRANCISCO from "../cities/sf.ts";
import SOCAL from "../cities/socal.ts";
import { RULES, reconciledCity, setReconcile, setReconcileWarn } from "../cities/reconcile.ts";
import type { Rule } from "../cities/reconcile.ts";
import type { City } from "../engine/types.ts";
const PACKS: readonly (readonly [string, City])[] = [
["california", CALIFORNIA],
["sf", SAN_FRANCISCO],
["socal", SOCAL],
];
after(() => {
setReconcile(null);
setReconcileWarn(null);
});
/** The columns each rule owns, as the reader of a pack would see them. */
const OWNED: Readonly<Record<Rule, (city: City) => unknown>> = {
projection: (city) => city.lngScale ?? null,
exaggeration: (city) => city.verticalExaggeration,
roads: (city) => city.roads.map((road) => road.width).join(","),
ground: (city) => `${city.coastFalloff}|${JSON.stringify(city.palette ?? null)}`,
};
describe("a reconciled pack is not the pack the module exported", () => {
it("returns the very same object when nothing is on", () => {
setReconcile(false);
for (const [id, pack] of PACKS) {
assert.equal(reconciledCity(pack), pack, `${id} was copied with the flag off`);
}
});
/**
* The load-bearing one. If a rule changes nothing for every pack then a
* consumer reading the raw pack is harmless and this whole file is theatre —
* so the file has to prove it is not.
*/
it("changes the column its rule owns, for at least one pack, for every rule", () => {
for (const rule of RULES) {
setReconcile([rule]);
const moved = PACKS.filter(([, pack]) => {
const read = OWNED[rule];
return read(reconciledCity(pack)) !== read(pack);
});
assert.ok(
moved.length > 0,
`rule "${rule}" changed nothing on any pack — either the rule is dead ` +
"or OWNED is reading the wrong column",
);
}
});
it("never mutates the pack the module exported", () => {
const before = PACKS.map(([, pack]) => JSON.stringify(pack));
setReconcile(true);
for (const [, pack] of PACKS) reconciledCity(pack);
setReconcile(false);
for (const [index, [id]] of PACKS.entries()) {
assert.equal(JSON.stringify(PACKS[index]![1]), before[index], `${id} was mutated in place`);
}
});
/**
* The worker's half of the contract. `World` posts `this.city` to the terrain
* worker as a structured clone and the worker builds a second `World` from it;
* `reconciled: true` is what stops that second pass applying every rule again
* on top of itself. Without it the exaggeration rule would square.
*/
it("is idempotent, so the worker's second pass is a no-op", () => {
setReconcile(true);
for (const [id, pack] of PACKS) {
const once = reconciledCity(pack);
assert.equal(once.reconciled, true, `${id} did not mark itself reconciled`);
assert.equal(reconciledCity(once), once, `${id} was reconciled twice`);
}
});
});
describe("the rule parser", () => {
it("takes exact rule names", () => {
setReconcile(null);
for (const rule of RULES) {
// Through the public surface: setReconcile with an explicit list is what
// a node script uses, and the query-string path shares `parse`.
setReconcile([rule]);
const read = OWNED[rule];
const moved = PACKS.some(([, pack]) => read(reconciledCity(pack)) !== read(pack));
assert.ok(moved, `"${rule}" did not select itself`);
}
});
/**
* The defect: `parse` prefix-matched, so `?reconcile=palette` selected nothing
* and produced an empty set — **identical to the flag being absent**. A
* photograph taken to judge a rule was a photograph of the raw board, and
* nothing in the picture could tell you which.
*/
it("warns loudly rather than silently reading an unknown rule as off", async () => {
const said: string[] = [];
setReconcileWarn((message) => said.push(message));
setReconcile(null);
const { activeRules } = await import("../cities/reconcile.ts");
const search = { search: "?reconcile=palette" };
const globals = globalThis as { location?: unknown };
const had = "location" in globals;
const previous = globals.location;
globals.location = search;
try {
const on = activeRules();
assert.equal(on.size, 0, "an unknown rule must not select a real one");
assert.equal(said.length, 1, "an unknown rule must warn exactly once");
assert.match(said[0] ?? "", /unknown rule "palette"/);
assert.match(said[0] ?? "", /the flag is NOT off/);
} finally {
if (had) globals.location = previous;
else delete globals.location;
setReconcileWarn(null);
}
});
/**
* The other half of prefix-matching: one letter used to select a whole rule,
* which is fine until two rules share it. `p` must now select nothing.
*/
it("does not accept an abbreviation", async () => {
const said: string[] = [];
setReconcileWarn((message) => said.push(message));
setReconcile(null);
const { activeRules } = await import("../cities/reconcile.ts");
const globals = globalThis as { location?: unknown };
const had = "location" in globals;
const previous = globals.location;
globals.location = { search: "?reconcile=p" };
try {
assert.equal(activeRules().size, 0, '"p" selected a rule by prefix');
assert.equal(said.length, 1);
} finally {
if (had) globals.location = previous;
else delete globals.location;
setReconcileWarn(null);
}
});
});
+142
View File
@@ -353,3 +353,145 @@ test("the rig constructs no light of any kind", () => {
// hence matching the import statement rather than the word.)
assert.doesNotMatch(source, /^\s*import[^\n]*RoomEnvironment/m);
});
// ---- The prefetch hazard ---------------------------------------------------
/**
* A board being built off-stage must not relight the board on screen.
*
* ## The mechanism
*
* `createScene` calls `options.environment?.apply(scene, opening, "city")` at
* `scene.ts:811` — unconditionally, and two hundred lines above the
* `if (options.present !== false) stage.setScene(stageScene)` at `:1331` that
* decides whether anybody is going to look at this scene at all. `main.ts`
* builds one rig on one renderer for the life of the page, so an off-stage
* board and the visible one are the same rig.
*
* The rig caches exactly one PMREM target per kind, keyed on a fingerprint of
* the lighting. On a key **miss** it does three things, in this order: renders
* and convolves a new probe on the shared renderer, `dispose()`s the target the
* visible board is currently using, and then walks `applied` reassigning every
* scene of that kind to the new texture.
*
* That is correct and necessary when the miss comes from time passing. It is a
* defect when the miss comes from a board **nobody is looking at**: a board
* prefetched for another part of the state is observed at its own centre, which
* across California is about forty minutes of apparent solar time, so its sun
* direction — and therefore its key — *will* differ. The visible board's sky
* would change because something invisible was loaded behind it.
*
* It is harmless today only because every build is followed within about
* 800 ms by the swap that makes it the visible board. It stops being harmless
* the moment anything builds a board it does not intend to show.
*
* ## What this asserts
*
* The property, at the rig's own boundary: applying to a scene while telling the
* rig it is off-stage must not disturb any scene already applied. Written before
* the fix, and it failed before the fix, which is the only way to know a
* regression test is testing anything.
*/
test("an off-stage apply does not relight the scene already on screen", () => {
const fake = fakeRenderer();
const rig = createEnvironmentRig(fake.as());
const visible = new THREE.Scene();
rig.apply(visible, lightingState(), "city");
const wasShowing = visible.environment;
const wasTarget = targetOf(fake, visible);
assert.ok(wasShowing, "the visible board never got an environment");
// A board prefetched for elsewhere in the state: same kind, different sun.
// This is the key miss that does the damage.
const offstage = new THREE.Scene();
rig.apply(offstage, lightingState({ sun: { direction: [0.62, 0.51, -0.6], color: 0xffd9a8, intensity: 1.7 } }), "city", {
offstage: true,
});
assert.equal(
visible.environment,
wasShowing,
"building a board off-stage reassigned the visible board's environment",
);
assert.equal(
wasTarget.texture,
wasShowing,
"building a board off-stage disposed the texture the visible board is using",
);
rig.dispose();
});
/**
* The other half, and the reason the fix is a deferral rather than a refusal:
* the off-stage board must still end up correctly lit when it is presented. A
* fix that simply skipped the apply forever would give the incoming board no
* environment at all, which is the "plastic" look this rig exists to cure.
*
* Two cases, and they are different on purpose. With nothing cached there is
* nothing to borrow, so the build happens at presentation — which is correct,
* and is the same one build the board would have paid anyway. With a probe
* already cached on the same key, the off-stage board borrows it and
* presentation costs no convolution at all, which is the case that actually
* matters: it is what a prefetched board hits when the visitor arrives.
*/
test("an off-stage board is lit when it is finally presented", () => {
const fake = fakeRenderer();
const rig = createEnvironmentRig(fake.as());
const offstage = new THREE.Scene();
rig.apply(offstage, lightingState(), "city", { offstage: true });
rig.apply(offstage, lightingState(), "city");
assert.ok(offstage.environment, "a presented board has no environment");
rig.dispose();
});
test("a prefetched board borrows the cached probe and presents for free", () => {
const fake = fakeRenderer();
const rig = createEnvironmentRig(fake.as());
const visible = new THREE.Scene();
rig.apply(visible, lightingState(), "city");
const built = fake.renders;
assert.ok(built > 0, "the first apply did not build anything");
// The board built behind it, at the same hour, is the common case.
const prefetched = new THREE.Scene();
rig.apply(prefetched, lightingState(), "city", { offstage: true });
assert.equal(prefetched.environment, visible.environment, "the probe was not borrowed");
assert.equal(fake.renders, built, "an off-stage apply convolved a new probe");
// And arriving costs nothing.
rig.apply(prefetched, lightingState(), "city");
assert.equal(fake.renders, built, "presenting a cache hit convolved a new probe");
assert.ok(prefetched.environment, "the presented board lost its environment");
rig.dispose();
});
/**
* The failure mode the fix must not introduce: an off-stage board whose sun
* differs must not go dark. It borrows the cached probe — slightly wrong for its
* own hour, and invisible, and corrected the instant it is shown.
*/
test("an off-stage board at a different hour borrows rather than going unlit", () => {
const fake = fakeRenderer();
const rig = createEnvironmentRig(fake.as());
const visible = new THREE.Scene();
rig.apply(visible, lightingState(), "city");
const elsewhere = new THREE.Scene();
rig.apply(
elsewhere,
lightingState({ sun: { direction: [0.62, 0.51, -0.6], color: 0xffd9a8, intensity: 1.7 } }),
"city",
{ offstage: true },
);
assert.ok(elsewhere.environment, "an off-stage board at another hour was left with no environment");
assert.equal(elsewhere.environment, visible.environment, "it did not borrow the cached probe");
rig.dispose();
});