The two office stills were captioned for a building that no longer exists:
"thirty-four metres by eighteen, one storey" against a plate that is now
forty-eight by eighteen over two storeys, and a night note promising the
office "keeps its own light after the sun has gone" — which was
aspirational when it was written and is only now true.
Two new frames. `office-commons` is the double-height wing, which is the
one view that shows why the storeys are held fourteen metres apart.
`office-hangar` is Frontier Valley, and it needed a new capability: the
harness presses the office picker before choosing a chapter, because
switching buildings disposes the scene and rebuilds it, so a chapter click
issued first would land against a legend that no longer exists. Selected by
visible label and asserted, for the same reason `chapter()` asserts its
`shortLabel` — a reordered `OFFICES` table would otherwise photograph one
building and caption it with the other's name.
The office share card said "Lumbridge HQ, one floor: the rooms, the desks
and who is at them". It is the first thing a stranger sees and it was wrong
in three ways.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`launch()` asks for `--use-angle=vulkan` and gets the Radeon RX 6700 XT. On the
real film loop at 1440x900 that is 7.56 s a frame to 0.47, and about twelve
cores pegged down to less than one. A 180-frame film goes from twenty-two
minutes to ninety seconds; the three shipped reels go from over an hour to four
minutes.
The SwiftShader flag was inherited from the share-card script and its comment
said "a box with no display and no GPU". Half of that was never true. The card
has been in this machine the whole time, with the amdgpu driver and a working
RADV ICD, and Chrome reaches it with **no display server at all** — no DISPLAY,
no XDG_RUNTIME_DIR, nothing but read access to /dev/dri/renderD128, which group
`render` already grants. So the portability the flag was protecting is intact:
this still runs over ssh and under cron.
Naming the backend is the whole trick, and most of the plausible spellings are
traps. `--use-angle=gl`, `--use-gl=egl`, `--use-gl=desktop`,
`--enable-features=Vulkan` and passing no GL flags at all were each measured on
this box, and all five land on SwiftShader while reporting success. Only
`--use-angle=vulkan` and `--use-angle=gl-egl` reach the card.
So `launch()` verifies instead of assuming. `--use-angle=vulkan` is a demand,
not a preference: with the driver hidden it yields no WebGL context at all
rather than falling back, the boot curtain never lifts, and the caller sits in
`waitForFunction` until a three-minute timeout before failing with something
that looks nothing like "there is no GPU here". Reading
`UNMASKED_RENDERER_WEBGL` once per run costs about half a second and turns that
into a printed line and a slow, correct render. Both branches are tested; the
sabotaged one was verified with `VK_LOADER_DRIVERS_DISABLE`.
The pictures are the same pictures: mean absolute difference of half a level out
of 255, confined to MSAA edges and to cloud shadow that already drifts between
any two runs of the same renderer.
Also corrects every comment across the four scripts that asserted three frames a
second or twenty-two minutes a reel. Those were true when written and are not
now, and the `reducedMotion` rationale in two of them was resting on the number:
the real reason a chapter flight cannot be waited out is that it is ~40 frames
however fast they are drawn, which was twenty seconds under software GL and is a
shorter race on the GPU — still a race.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Correcting the pairing this script documents: the daylit frame goes on the
light page and the night frame on the dark one. A reader in dark mode has asked
for a dark room, and a bright noon render is a hole punched in it; the hairline
frame around each figure is what keeps a night shot from dissolving into the
page, which is the border's job rather than the sun's.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Each of the seven cameras now fires at two clocks — one daylight frame, one
around sixteen degrees below the horizon — and lumbridgecorp.com loads whichever
is the opposite of the reader's theme. A night render on that site's dark page
is a dark rectangle on a dark page: the edges dissolve and the picture stops
being an object. A daylit one on the white page has the same problem in reverse,
and the hazier frames wash out entirely.
It is also the cheapest demonstration available of the claim those pages lean on
hardest. The sun is computed from a real time rather than themed, and a reader
who doubts it can hit the light/dark toggle and watch the same city change hour
with nothing else about the geometry moving.
Shot ids lose their time of day — `soma-afternoon` is `soma`, `fidi-night` is
`fidi`, `peninsula-morning` is `peninsula` — because none of them names one hour
any more. The manifest gains a `Frame` type: `at`, `src`, `alt` and a `note`
belong to an hour, while `place` and `caption` are true of both, so a caption
cannot end up describing light that is not in the frame under it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`shots.mjs` renders the product imagery for lumbridgecorp.com/simulate: seven
frames of the running `dist/`, at chapters and times chosen rather than
defaulted, written as WebP into the sibling site checkout together with a
generated manifest that carries each picture's caption and alt text. The pages
there had described a renderer in prose for their whole life without ever
showing one, which is a strange way to sell a renderer.
The captions live next to the camera poses in this repo, not on the site, so
re-aiming a camera cannot leave a caption behind describing the old view. The
manifest emits a `ShotId` union, so a page asking for a picture that has been
renamed fails the site's typecheck instead of rendering a hole.
Along the way: the share cards were never reproducible. `scenekit.ts` eases a
chapter change over about two seconds of scene time, `stage.ts` clamps `dt` to
50 ms a frame, and SwiftShader here draws about three frames a second — so the
flight takes twenty seconds of wall clock and `capture.mjs` waited four. Every
run caught the camera at a different point over the bay, and none of them at
the chapter the key press asked for. Both scripts now open the page with
reduced motion, which is the app's own answer to "somebody clicked a name in a
list": `flyTo` sets the pose outright. `og-tera.png` is regenerated and is
Hayes Valley for the first time. The bytes still differ run to run, because
aircraft are crossing and cloud shadow is drifting; the framing no longer does.
`harness.mjs` is the static server, the SwiftShader flags, the two-hostnames-
one-dist trick and the clock shim, extracted because there are two consumers
now and two copies would have drifted apart while both claimed to photograph
the same app.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>