Skip to content

Commit b57dc13

Browse files
fix(engine): stop SwiftShader ghosting in software screenshot captures (#3096)
Apply the --disable-gpu-compositing workaround to every software capture, not just BeginFrame ones. SwiftShader's compositor re-presents stale raster for a partially invalidated layer, so successive screenshot captures accumulate copies of earlier seeks; alpha renders are forced onto the screenshot path and were the only ones left unprotected. Refreshes the byte-strict png-sequence alpha baseline for the resulting antialiasing delta (content unchanged, min PSNR 41.3 dB). Fixes #3049.
1 parent 218eff7 commit b57dc13

62 files changed

Lines changed: 162 additions & 148 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/engine/src/services/browserManager.test.ts

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { join } from "node:path";
77

88
import type { Browser, PuppeteerNode } from "puppeteer-core";
99

10+
import type { CaptureMode } from "./browserLeasePool.js";
11+
1012
import {
1113
_resetAutoBrowserGpuModeCacheForTests,
1214
_resetBrowserPoolForTests,
@@ -132,23 +134,22 @@ describe("buildChromeArgs browser GPU mode", () => {
132134
expect(args).not.toContain("--enable-gpu-rasterization");
133135
});
134136

135-
it("disables GPU compositing only for software BeginFrame capture", () => {
136-
const softwareBeginFrame = buildChromeArgs(
137-
{ ...base, captureMode: "beginframe" },
138-
{ browserGpuMode: "software" },
139-
);
140-
const softwareScreenshot = buildChromeArgs(
141-
{ ...base, captureMode: "screenshot" },
142-
{ browserGpuMode: "software" },
143-
);
144-
const hardwareBeginFrame = buildChromeArgs(
145-
{ ...base, captureMode: "beginframe", platform: "linux" },
146-
{ browserGpuMode: "hardware" },
147-
);
137+
// HF#3049: the stale-raster accumulation lives in SwiftShader's compositor,
138+
// which every capture mode reads from — so the gate is the GPU mode alone.
139+
// Swept over the whole CaptureMode union so a future mode can't quietly opt
140+
// out of the workaround the way `screenshot` did.
141+
const captureModes: CaptureMode[] = ["beginframe", "screenshot", "drawelement"];
142+
143+
it.each(captureModes)("disables GPU compositing for software %s capture", (captureMode) => {
144+
expect(
145+
buildChromeArgs({ ...base, captureMode, platform: "linux" }, { browserGpuMode: "software" }),
146+
).toContain("--disable-gpu-compositing");
147+
});
148148

149-
expect(softwareBeginFrame).toContain("--disable-gpu-compositing");
150-
expect(softwareScreenshot).not.toContain("--disable-gpu-compositing");
151-
expect(hardwareBeginFrame).not.toContain("--disable-gpu-compositing");
149+
it.each(captureModes)("leaves hardware %s capture on the GPU compositor", (captureMode) => {
150+
expect(
151+
buildChromeArgs({ ...base, captureMode, platform: "linux" }, { browserGpuMode: "hardware" }),
152+
).not.toContain("--disable-gpu-compositing");
152153
});
153154

154155
it("uses Metal-backed ANGLE for hardware browser GPU mode on macOS", () => {

packages/engine/src/services/browserManager.ts

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -910,20 +910,33 @@ export function buildChromeArgs(
910910
chromeArgs.push(WEBGPU_FLAG);
911911
}
912912

913+
// SwiftShader's GPU compositor can retain a transformed layer for several
914+
// sequential frames after a GSAP yoyo/reversal, and it re-presents stale
915+
// raster for a partially invalidated layer: content already drawn in an
916+
// earlier seek is never cleared, so successive captures accumulate copies of
917+
// it (HF#3049 — a moving SVG group smears wider on every frame, and static
918+
// siblings appear duplicated one band lower). The DOM and timeline are
919+
// already at the requested time; the defect is in the compositor surface both
920+
// BeginFrame and Page.captureScreenshot read, so it is not specific to a
921+
// capture mode — it is specific to compositing on SwiftShader.
922+
//
923+
// Routing compositing through Chrome's software path is the only mitigation
924+
// that holds: capture-side changes (fromSurface, captureBeyondViewport, a
925+
// second capture, extra rAF ticks, a 250ms settle) and every raster/tiling
926+
// flag (--disable-partial-raster, --disable-checker-imaging, --disable-zero-copy,
927+
// forced tile sizes) leave the accumulation untouched. The cost is that
928+
// SwiftShader rasterizes thin strokes and glyph edges slightly differently
929+
// (antialiased edges only — measured on HF#3049's frame 0: 96 opaque pixels
930+
// differ, by 1/255). Duplicated content in 68% of frames is the worse defect.
931+
//
932+
// Remove this workaround once the pinned chrome-headless-shell includes
933+
// https://issues.chromium.org/issues/535256667.
934+
if (browserGpuMode === "software") {
935+
chromeArgs.push("--disable-gpu-compositing");
936+
}
937+
913938
// BeginFrame flags — only when using chrome-headless-shell on Linux
914939
if (options.captureMode !== "screenshot") {
915-
// SwiftShader's GPU compositor can retain a transformed layer for several
916-
// sequential frames after a GSAP yoyo/reversal. The DOM and timeline are
917-
// already at the requested time, but both BeginFrame and
918-
// Page.captureScreenshot read the stale surface (the duplicate is present
919-
// in the raw JPEG before encoding). Keep deterministic BeginFrame capture,
920-
// but route compositing through Chrome's software path when the browser is
921-
// already in software-GPU mode. Hardware-GPU and screenshot captures keep
922-
// their existing compositor paths. Remove this workaround once the pinned
923-
// chrome-headless-shell includes https://issues.chromium.org/issues/535256667.
924-
if (browserGpuMode === "software") {
925-
chromeArgs.push("--disable-gpu-compositing");
926-
}
927940
chromeArgs.push(
928941
"--deterministic-mode",
929942
"--enable-begin-frame-control",
Lines changed: 2 additions & 2 deletions
Lines changed: 2 additions & 2 deletions
Lines changed: 2 additions & 2 deletions
Lines changed: 2 additions & 2 deletions
Lines changed: 2 additions & 2 deletions
Lines changed: 2 additions & 2 deletions
Lines changed: 2 additions & 2 deletions
Lines changed: 2 additions & 2 deletions

0 commit comments

Comments
 (0)