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.
This commit is contained in:
Miguel Ángel
2026-08-07 15:18:52 -07:00
committed by GitHub
parent 218eff7d36
commit b57dc13cb6
62 changed files with 162 additions and 148 deletions
@@ -7,6 +7,8 @@ import { join } from "node:path";
import type { Browser, PuppeteerNode } from "puppeteer-core";
import type { CaptureMode } from "./browserLeasePool.js";
import {
_resetAutoBrowserGpuModeCacheForTests,
_resetBrowserPoolForTests,
@@ -132,23 +134,22 @@ describe("buildChromeArgs browser GPU mode", () => {
expect(args).not.toContain("--enable-gpu-rasterization");
});
it("disables GPU compositing only for software BeginFrame capture", () => {
const softwareBeginFrame = buildChromeArgs(
{ ...base, captureMode: "beginframe" },
{ browserGpuMode: "software" },
);
const softwareScreenshot = buildChromeArgs(
{ ...base, captureMode: "screenshot" },
{ browserGpuMode: "software" },
);
const hardwareBeginFrame = buildChromeArgs(
{ ...base, captureMode: "beginframe", platform: "linux" },
{ browserGpuMode: "hardware" },
);
// HF#3049: the stale-raster accumulation lives in SwiftShader's compositor,
// which every capture mode reads from — so the gate is the GPU mode alone.
// Swept over the whole CaptureMode union so a future mode can't quietly opt
// out of the workaround the way `screenshot` did.
const captureModes: CaptureMode[] = ["beginframe", "screenshot", "drawelement"];
expect(softwareBeginFrame).toContain("--disable-gpu-compositing");
expect(softwareScreenshot).not.toContain("--disable-gpu-compositing");
expect(hardwareBeginFrame).not.toContain("--disable-gpu-compositing");
it.each(captureModes)("disables GPU compositing for software %s capture", (captureMode) => {
expect(
buildChromeArgs({ ...base, captureMode, platform: "linux" }, { browserGpuMode: "software" }),
).toContain("--disable-gpu-compositing");
});
it.each(captureModes)("leaves hardware %s capture on the GPU compositor", (captureMode) => {
expect(
buildChromeArgs({ ...base, captureMode, platform: "linux" }, { browserGpuMode: "hardware" }),
).not.toContain("--disable-gpu-compositing");
});
it("uses Metal-backed ANGLE for hardware browser GPU mode on macOS", () => {
+25 -12
View File
@@ -910,20 +910,33 @@ export function buildChromeArgs(
chromeArgs.push(WEBGPU_FLAG);
}
// SwiftShader's GPU compositor can retain a transformed layer for several
// sequential frames after a GSAP yoyo/reversal, and it re-presents stale
// raster for a partially invalidated layer: content already drawn in an
// earlier seek is never cleared, so successive captures accumulate copies of
// it (HF#3049 — a moving SVG group smears wider on every frame, and static
// siblings appear duplicated one band lower). The DOM and timeline are
// already at the requested time; the defect is in the compositor surface both
// BeginFrame and Page.captureScreenshot read, so it is not specific to a
// capture mode — it is specific to compositing on SwiftShader.
//
// Routing compositing through Chrome's software path is the only mitigation
// that holds: capture-side changes (fromSurface, captureBeyondViewport, a
// second capture, extra rAF ticks, a 250ms settle) and every raster/tiling
// flag (--disable-partial-raster, --disable-checker-imaging, --disable-zero-copy,
// forced tile sizes) leave the accumulation untouched. The cost is that
// SwiftShader rasterizes thin strokes and glyph edges slightly differently
// (antialiased edges only — measured on HF#3049's frame 0: 96 opaque pixels
// differ, by 1/255). Duplicated content in 68% of frames is the worse defect.
//
// 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",