From 8ac7ae9088320767c2b9d5d07fa8a66f98c36452 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Fri, 15 May 2026 10:19:22 -0700 Subject: [PATCH] fix(engine): apply compositor determinism flags in screenshot mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chrome's compositor determinism flags (--run-all-compositor-stages-before-draw, --disable-threaded-animation, --enable-surface-synchronization, etc.) were previously only applied for BeginFrame mode on Linux. In screenshot mode — used on macOS/Windows — the compositor ran without these constraints, allowing Metal on Apple Silicon to accumulate state drift over sustained frame captures. This produced vertical layout shifts after ~12 seconds of rendering, reported on M1 machines. The shifts occurred because the compositor's threaded animation and surface synchronization pipelines could race with Page.captureScreenshot, and without --run-all-compositor-stages-before-draw, pending compositor stages weren't flushed before each screenshot. Split the flags into two groups: - BEGINFRAME_EXCLUSIVE_FLAGS: --enable-begin-frame-control and --deterministic-mode, which pause the compositor or freeze the clock and must NOT be used in screenshot mode - COMPOSITOR_DETERMINISM_FLAGS: the remaining 7 flags that enforce deterministic compositor behavior and are safe for all capture modes The compositor flags are now applied unconditionally in buildChromeArgs. Closes #828 --- .../src/services/browserManager.test.ts | 30 ++++++++++++++ .../engine/src/services/browserManager.ts | 41 ++++++++++--------- 2 files changed, 52 insertions(+), 19 deletions(-) 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) {