diff --git a/packages/engine/src/services/browserManager.test.ts b/packages/engine/src/services/browserManager.test.ts index 08af34802..e6b52d746 100644 --- a/packages/engine/src/services/browserManager.test.ts +++ b/packages/engine/src/services/browserManager.test.ts @@ -54,6 +54,36 @@ describe("buildChromeArgs browser GPU mode", () => { }); }); +describe("buildChromeArgs compositor determinism flags", () => { + const base = { width: 1920, height: 1080 }; + + it("includes compositor determinism flags in screenshot mode", () => { + const args = buildChromeArgs({ ...base, captureMode: "screenshot" }); + expect(args).toContain("--run-all-compositor-stages-before-draw"); + expect(args).toContain("--disable-threaded-animation"); + expect(args).toContain("--disable-threaded-scrolling"); + expect(args).toContain("--enable-surface-synchronization"); + expect(args).toContain("--disable-checker-imaging"); + expect(args).toContain("--disable-image-animation-resync"); + expect(args).toContain("--disable-new-content-rendering-timeout"); + }); + + it("excludes beginFrame-exclusive flags in screenshot mode", () => { + const args = buildChromeArgs({ ...base, captureMode: "screenshot" }); + expect(args).not.toContain("--deterministic-mode"); + expect(args).not.toContain("--enable-begin-frame-control"); + }); + + it("includes both compositor and beginFrame-exclusive flags in beginframe mode", () => { + const args = buildChromeArgs({ ...base, captureMode: "beginframe" }); + expect(args).toContain("--run-all-compositor-stages-before-draw"); + expect(args).toContain("--disable-threaded-animation"); + expect(args).toContain("--enable-surface-synchronization"); + expect(args).toContain("--deterministic-mode"); + expect(args).toContain("--enable-begin-frame-control"); + }); +}); + describe("resolveBrowserGpuMode", () => { beforeEach(() => { _resetAutoBrowserGpuModeCacheForTests(); diff --git a/packages/engine/src/services/browserManager.ts b/packages/engine/src/services/browserManager.ts index 714b9a938..8e0182f88 100644 --- a/packages/engine/src/services/browserManager.ts +++ b/packages/engine/src/services/browserManager.ts @@ -77,13 +77,21 @@ let pooledCaptureMode: CaptureMode = "screenshot"; // Preserve the producer-era export so re-export shims keep the same public API. export const ENABLE_BROWSER_POOL = DEFAULT_CONFIG.enableBrowserPool; -// Flags only meaningful when Chrome's compositor is driven by -// HeadlessExperimental.beginFrame. If we fall back to screenshot mode they -// must be stripped — `--enable-begin-frame-control` in particular makes the -// compositor wait for frames we'll never send, producing blank screenshots. -const BEGINFRAME_ONLY_FLAGS = new Set([ +// Flags that ONLY work with HeadlessExperimental.beginFrame and must be +// stripped in screenshot mode. `--enable-begin-frame-control` pauses the +// compositor until explicit beginFrame calls — in screenshot mode this +// produces blank captures. `--deterministic-mode` freezes the internal +// clock, which can stall page-load timers in screenshot mode. +const BEGINFRAME_EXCLUSIVE_FLAGS = new Set([ "--deterministic-mode", "--enable-begin-frame-control", +]); + +// Compositor determinism flags safe for BOTH beginframe and screenshot modes. +// Without these, Chrome's compositor (especially Metal on Apple Silicon) +// accumulates state drift over sustained frame captures, producing vertical +// shifts after ~12s of rendering. Applied unconditionally in buildChromeArgs. +const COMPOSITOR_DETERMINISM_FLAGS = [ "--disable-new-content-rendering-timeout", "--run-all-compositor-stages-before-draw", "--disable-threaded-animation", @@ -91,10 +99,10 @@ const BEGINFRAME_ONLY_FLAGS = new Set([ "--disable-checker-imaging", "--disable-image-animation-resync", "--enable-surface-synchronization", -]); +]; function stripBeginFrameFlags(args: string[]): string[] { - return args.filter((a) => !BEGINFRAME_ONLY_FLAGS.has(a)); + return args.filter((a) => !BEGINFRAME_EXCLUSIVE_FLAGS.has(a)); } /** @@ -431,19 +439,14 @@ export function buildChromeArgs( "--disable-features=AudioServiceOutOfProcess,IsolateOrigins,site-per-process,Translate,BackForwardCache,IntensiveWakeUpThrottling", ]; - // BeginFrame flags — only when using chrome-headless-shell on Linux + // Compositor determinism flags — safe for all capture modes. Prevents + // Metal compositor drift on Apple Silicon that causes vertical frame + // shifts after sustained screenshot captures. + chromeArgs.push(...COMPOSITOR_DETERMINISM_FLAGS); + + // BeginFrame-exclusive flags — only when using chrome-headless-shell on Linux if (options.captureMode !== "screenshot") { - chromeArgs.push( - "--deterministic-mode", - "--enable-begin-frame-control", - "--disable-new-content-rendering-timeout", - "--run-all-compositor-stages-before-draw", - "--disable-threaded-animation", - "--disable-threaded-scrolling", - "--disable-checker-imaging", - "--disable-image-animation-resync", - "--enable-surface-synchronization", - ); + chromeArgs.push("--deterministic-mode", "--enable-begin-frame-control"); } if (gpuDisabled) {