`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>
`hero-soma` is the landing backdrop: SoMa, midnight to midnight, 180 frames at
eight minutes each so the loop closes without repeating a minute. It is the only
film shot `chrome: "bare"` — the figures keep the app's panel because its clock
is their caption, but a hero has a headline on top and a second interface under
that is two interfaces arguing. It is also the only one that runs a full 24
hours, because the page seeks it to the reader's local hour and a film starting
at 04:40 has no frame for somebody at two in the morning.
Whole-board was the first camera and was wrong: at that standoff the edge of the
terrain plate stands against the sky across the top of the frame, and midday is
a lot of pale sand.
Encoding is two-stage now — frames to a CRF 18 master, deliverables derived from
it. A time-lapse is near the worst case for inter-frame compression, because the
camera never moves but every pixel changes as the light does, and at the figure
default of CRF 22 this reel came out at 2.5 MB. Deriving from a master turned
'try CRF 28' into ten seconds instead of a twenty-two minute re-shoot, and it
keeps what ships identical to what this script makes rather than something
hand-rolled with ffmpeg the day the size became a problem. It lands at 1440w
CRF 28 (1.0 MB) plus a 960w CRF 30 (402 kB) for small screens, because <video>
has no srcset and a phone has no use for the wide encode.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`films.mjs` now writes into the sibling lumbridge-v4 checkout — the MP4s and
their posters into `apps/web/public/films/`, and a generated `films.ts` beside
the shots manifest, carrying each reel's caption, alt text and running time.
Same shape as `shots.mjs`, including the `FilmId` union that turns a page
naming a deleted reel into a typecheck failure rather than a dead <video>.
`--publish <dir>` puts an existing render in front of the site without
re-shooting it. Twenty-two minutes a reel is long enough that the alternative
would have been `cp`, and a hand-copied artefact is the thing this pipeline
exists to not have. `--frames` refuses to publish at all: a 24-frame stutter is
a rough cut, not something to put on a website by accident.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`films.mjs` renders time-lapses of the running dist/: a locked camera watching
eighteen hours pass, shadow sweeping the city and the windows coming on as the
computed sun goes down. Two ship — the Financial District and the whole board.
Real-time capture was never on the table and would have been the wrong idea
anyway. SwiftShader draws about three frames a second with no GPU, so a screen
recording is a slideshow; and a sunset takes an hour, which is not a length of
video anybody watches. So it is rendered offline the way films always have been:
set the clock, let the frame settle, expose, advance. The output is smooth 30fps
regardless of what the renderer managed while being photographed.
Two shims in `filmClock()` get that done without a reload per frame, which
matters because a reload is fifteen seconds and would put a 180-frame film at
three quarters of an hour. The skew is a live global read on every `Date` call
rather than baked in at load; and `setInterval(…, 60_000)` is compressed to
120ms, which is the load-bearing half — `main.ts` recomputes the sun on a
once-a-minute wall-clock tick, so a shifted clock otherwise sits unrendered.
Only the 60-second interval is touched, by value.
Both are capture-harness lies told to the page. The deployed bundle has no idea
this file exists.
Not wired into the site. This is the capability and two reels to look at.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>