perf: compile scene shaders off-thread before the first frame
I warm the world's programs with renderer.compileAsync() at the end of the async build, with the composer's scene target bound so tone mapping and output colour space key the same programs the composer draws with, and with hidden subtrees revealed so the light count matches the frame-0 warm render. The wait is capped at 2.5 s; the warm render stays as the safety net for the post passes, texture uploads and anything a slow or extension-less driver left. Test builds report renderer.info.programs, and a browser spec pins zero program growth after frame 0. On the Intel ARL laptop the frame-0 task drops from ~1.74 s to ~0.34 s with a pixel-identical first frame; measurements in docs/overnight/compile-async-results.md.
This commit is contained in:
55
tests/browser/shader-warmup.spec.ts
Normal file
55
tests/browser/shader-warmup.spec.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { expect, test } from '../helpers/serve-dist';
|
||||
|
||||
// Issue 2 (cold-GPU first frame): the build links the world's programs with
|
||||
// renderer.compileAsync() against the composer's target, and frame 0 draws
|
||||
// everything once as a safety net. After frame 0 no program may be created:
|
||||
// each later one is a 70-280 ms hitch inside the opening shot. Test builds
|
||||
// dispatch portfolio-program-count whenever renderer.info.programs changes.
|
||||
|
||||
interface ProgramCountEvent {
|
||||
generation: number;
|
||||
frame: number;
|
||||
programs: number;
|
||||
warmup: { outcome: 'ready' | 'timeout' | 'failed'; waited: number; parallel: boolean; programs: number };
|
||||
}
|
||||
|
||||
test('compiles every scene program before the first frame and none after it', async ({ page, distUrl }) => {
|
||||
await page.addInitScript(() => {
|
||||
const events: unknown[] = [];
|
||||
(window as unknown as { __programCounts: unknown[] }).__programCounts = events;
|
||||
window.addEventListener('portfolio-program-count', (event) => {
|
||||
events.push(structuredClone((event as CustomEvent).detail));
|
||||
});
|
||||
});
|
||||
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
|
||||
await expect(page.locator('canvas.journey-canvas')).toHaveAttribute('data-lit', '', { timeout: 20_000 });
|
||||
|
||||
// The ignition hold, then the pull-away and the first chapters.
|
||||
await page.waitForTimeout(3_500);
|
||||
await page.mouse.move(200, 300);
|
||||
for (let step = 0; step < 12; step += 1) {
|
||||
await page.mouse.wheel(0, 400);
|
||||
await page.waitForTimeout(120);
|
||||
}
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const events = await page.evaluate(
|
||||
() => (window as unknown as { __programCounts: ProgramCountEvent[] }).__programCounts,
|
||||
);
|
||||
expect(events.length).toBeGreaterThan(0);
|
||||
for (const generation of new Set(events.map((event) => event.generation))) {
|
||||
const own = events.filter((event) => event.generation === generation);
|
||||
const first = own[0]!;
|
||||
// The first report comes right after frame 0.
|
||||
expect(first.frame).toBe(1);
|
||||
// The warm-up compiled the scene before frame 0 (or gave up on a slow driver).
|
||||
expect(['ready', 'timeout']).toContain(first.warmup.outcome);
|
||||
expect(first.warmup.programs).toBeGreaterThan(0);
|
||||
expect(first.programs).toBeGreaterThanOrEqual(first.warmup.programs);
|
||||
// Frame 0 adds only the post passes (9 on desktop). A warm-up keyed for
|
||||
// the wrong target or light count would add the whole scene again (~60).
|
||||
expect(first.programs - first.warmup.programs).toBeLessThanOrEqual(12);
|
||||
// No growth afterwards.
|
||||
expect(own.slice(1)).toEqual([]);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user