From 484d19bfd3e13736841e6c202af19093d8991846 Mon Sep 17 00:00:00 2001 From: miguel-heygen Date: Thu, 6 Aug 2026 05:28:48 +0000 Subject: [PATCH] fix(engine): disable GPU compositing on SwiftShader for all capture modes (#3049) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SwiftShader's GPU compositor retains stale transformed layers across frames, producing phantom duplicate content at renderHeight/4 offsets in transparent renders. The --disable-gpu-compositing workaround was previously gated to BeginFrame capture mode only, but the same artefact affects Page.captureScreenshot on SwiftShader — and software-GPU renders are clamped to screenshot mode by the forceScreenshot invariant. Move --disable-gpu-compositing outside the BeginFrame-only block so it applies to all capture modes when browserGpuMode is "software". Also adds PRODUCER_EXTRA_CHROME_ARGS env var: a space-separated escape hatch for passing additional Chrome flags during debugging, so future compositor investigations do not require wrapping the chrome binary. Closes #3049 --- .../src/services/browserManager.test.ts | 21 +++++++++++-- .../engine/src/services/browserManager.ts | 31 ++++++++++++------- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/packages/engine/src/services/browserManager.test.ts b/packages/engine/src/services/browserManager.test.ts index dfa068e5d8..450c0b9caa 100644 --- a/packages/engine/src/services/browserManager.test.ts +++ b/packages/engine/src/services/browserManager.test.ts @@ -132,7 +132,7 @@ describe("buildChromeArgs browser GPU mode", () => { expect(args).not.toContain("--enable-gpu-rasterization"); }); - it("disables GPU compositing only for software BeginFrame capture", () => { + it("disables GPU compositing for all software capture modes (#2550, #3049)", () => { const softwareBeginFrame = buildChromeArgs( { ...base, captureMode: "beginframe" }, { browserGpuMode: "software" }, @@ -145,10 +145,27 @@ describe("buildChromeArgs browser GPU mode", () => { { ...base, captureMode: "beginframe", platform: "linux" }, { browserGpuMode: "hardware" }, ); + const hardwareScreenshot = buildChromeArgs( + { ...base, captureMode: "screenshot", platform: "linux" }, + { browserGpuMode: "hardware" }, + ); expect(softwareBeginFrame).toContain("--disable-gpu-compositing"); - expect(softwareScreenshot).not.toContain("--disable-gpu-compositing"); + expect(softwareScreenshot).toContain("--disable-gpu-compositing"); expect(hardwareBeginFrame).not.toContain("--disable-gpu-compositing"); + expect(hardwareScreenshot).not.toContain("--disable-gpu-compositing"); + }); + + it("appends PRODUCER_EXTRA_CHROME_ARGS when set", () => { + process.env.PRODUCER_EXTRA_CHROME_ARGS = + "--disable-partial-raster --force-gpu-mem-available-mb=4096"; + try { + const args = buildChromeArgs(base); + expect(args).toContain("--disable-partial-raster"); + expect(args).toContain("--force-gpu-mem-available-mb=4096"); + } finally { + delete process.env.PRODUCER_EXTRA_CHROME_ARGS; + } }); it("uses Metal-backed ANGLE for hardware browser GPU mode on macOS", () => { diff --git a/packages/engine/src/services/browserManager.ts b/packages/engine/src/services/browserManager.ts index 899c3498b1..759e1e7648 100644 --- a/packages/engine/src/services/browserManager.ts +++ b/packages/engine/src/services/browserManager.ts @@ -910,20 +910,21 @@ export function buildChromeArgs( chromeArgs.push(WEBGPU_FLAG); } + // SwiftShader's GPU compositor can retain a transformed layer across frames, + // producing phantom duplicate content at regular vertical offsets (HF#2550, + // HF#3049). The artefact appears in BOTH BeginFrame and Page.captureScreenshot + // capture modes: a band of page content is duplicated at exactly + // renderHeight/4 in the alpha channel, and static DOM elements (SVG lines, + // rects) are affected equally. Routing compositing through Chrome's software + // path eliminates the stale-layer retention. Hardware-GPU captures keep their + // existing compositor path. Remove this workaround once the pinned + // chrome-headless-shell includes https://issues.chromium.org/issues/535256667. + if (browserGpuMode === "software") { + chromeArgs.push("--disable-gpu-compositing"); + } + // BeginFrame flags — only when using chrome-headless-shell on Linux if (options.captureMode !== "screenshot") { - // SwiftShader's GPU compositor can retain a transformed layer for several - // sequential frames after a GSAP yoyo/reversal. The DOM and timeline are - // already at the requested time, but both BeginFrame and - // Page.captureScreenshot read the stale surface (the duplicate is present - // in the raw JPEG before encoding). Keep deterministic BeginFrame capture, - // but route compositing through Chrome's software path when the browser is - // already in software-GPU mode. Hardware-GPU and screenshot captures keep - // their existing compositor paths. Remove this workaround once the pinned - // chrome-headless-shell includes https://issues.chromium.org/issues/535256667. - if (browserGpuMode === "software") { - chromeArgs.push("--disable-gpu-compositing"); - } chromeArgs.push( "--deterministic-mode", "--enable-begin-frame-control", @@ -940,6 +941,12 @@ export function buildChromeArgs( if (gpuDisabled) { chromeArgs.push("--disable-gpu"); } + + const extraArgs = process.env.PRODUCER_EXTRA_CHROME_ARGS; + if (extraArgs) { + chromeArgs.push(...extraArgs.split(/\s+/).filter(Boolean)); + } + return chromeArgs; }