diff --git a/packages/engine/src/services/browserManager.test.ts b/packages/engine/src/services/browserManager.test.ts index cbabfbaf9..657a5d353 100644 --- a/packages/engine/src/services/browserManager.test.ts +++ b/packages/engine/src/services/browserManager.test.ts @@ -28,6 +28,25 @@ 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" }, + ); + + expect(softwareBeginFrame).toContain("--disable-gpu-compositing"); + expect(softwareScreenshot).not.toContain("--disable-gpu-compositing"); + expect(hardwareBeginFrame).not.toContain("--disable-gpu-compositing"); + }); + it("uses Metal-backed ANGLE for hardware browser GPU mode on macOS", () => { const args = buildChromeArgs({ ...base, platform: "darwin" }, { browserGpuMode: "hardware" }); expect(args).toContain("--enable-unsafe-webgpu"); diff --git a/packages/engine/src/services/browserManager.ts b/packages/engine/src/services/browserManager.ts index 942f16834..4d61d4a8a 100644 --- a/packages/engine/src/services/browserManager.ts +++ b/packages/engine/src/services/browserManager.ts @@ -686,6 +686,17 @@ export function buildChromeArgs( // 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. + if (browserGpuMode === "software") { + chromeArgs.push("--disable-gpu-compositing"); + } chromeArgs.push( "--deterministic-mode", "--enable-begin-frame-control", diff --git a/packages/producer/src/regression-harness.ts b/packages/producer/src/regression-harness.ts index 15e056819..8bc56cc17 100644 --- a/packages/producer/src/regression-harness.ts +++ b/packages/producer/src/regression-harness.ts @@ -121,6 +121,12 @@ type TestMetadata = { * guard; omit for the default screenshot/BeginFrame capture. */ experimentalFastCapture?: boolean; + /** + * Pin the browser capture path for a regression fixture. The producer's + * software-GPU default normally prefers screenshots, so BeginFrame-only + * compositor regressions must opt out explicitly to exercise that path. + */ + captureMode?: "screenshot" | "beginframe"; /** * Render-time variable overrides, equivalent to `hyperframes render * --variables ''`. Injected as `window.__hfVariables` before any @@ -390,6 +396,15 @@ function validateMetadata(meta: unknown): TestMetadata { "meta.json: 'renderConfig.experimentalFastCapture' must be a boolean (or omit for false)", ); } + if ( + rc.captureMode !== undefined && + rc.captureMode !== "screenshot" && + rc.captureMode !== "beginframe" + ) { + throw new Error( + "meta.json: 'renderConfig.captureMode' must be 'screenshot' or 'beginframe' (or omitted)", + ); + } if ( rc.variables !== undefined && (rc.variables === null || typeof rc.variables !== "object" || Array.isArray(rc.variables)) @@ -1037,7 +1052,12 @@ async function runTestSuite( // var, scoped to this suite's render so it never leaks to other suites. const useFast = suite.meta.renderConfig.experimentalFastCapture === true; const prevFast = process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE; + const captureMode = suite.meta.renderConfig.captureMode; + const prevForceScreenshot = process.env.PRODUCER_FORCE_SCREENSHOT; if (useFast) process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE = "true"; + if (captureMode) { + process.env.PRODUCER_FORCE_SCREENSHOT = captureMode === "screenshot" ? "true" : "false"; + } try { const job = createRenderJob({ fps: suite.meta.renderConfig.fps, @@ -1056,6 +1076,10 @@ async function runTestSuite( if (prevFast === undefined) delete process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE; else process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE = prevFast; } + if (captureMode) { + if (prevForceScreenshot === undefined) delete process.env.PRODUCER_FORCE_SCREENSHOT; + else process.env.PRODUCER_FORCE_SCREENSHOT = prevForceScreenshot; + } } } diff --git a/packages/producer/tests/software-beginframe-yoyo-compositor/meta.json b/packages/producer/tests/software-beginframe-yoyo-compositor/meta.json new file mode 100644 index 000000000..3018fc3fb --- /dev/null +++ b/packages/producer/tests/software-beginframe-yoyo-compositor/meta.json @@ -0,0 +1,14 @@ +{ + "name": "software-beginframe-yoyo-compositor", + "description": "Regression guard for stale SwiftShader compositor layers after a GSAP yoyo reversal. At 25 fps the unfixed BeginFrame path briefly retained the first output badge at its previous transform, painting a third ghost badge around 10.12 seconds.", + "tags": ["regression", "beginframe", "gsap"], + "minPsnr": 30, + "maxFrameFailures": 0, + "minAudioCorrelation": 0, + "maxAudioLagWindows": 1, + "renderConfig": { + "fps": 25, + "workers": 1, + "captureMode": "beginframe" + } +} diff --git a/packages/producer/tests/software-beginframe-yoyo-compositor/output/compiled.html b/packages/producer/tests/software-beginframe-yoyo-compositor/output/compiled.html new file mode 100644 index 000000000..786a7991c --- /dev/null +++ b/packages/producer/tests/software-beginframe-yoyo-compositor/output/compiled.html @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9763ae3659454f2baa08f403562e334f4b17b1610a54df20203d21b58543fc26 +size 437140 diff --git a/packages/producer/tests/software-beginframe-yoyo-compositor/output/output.mp4 b/packages/producer/tests/software-beginframe-yoyo-compositor/output/output.mp4 new file mode 100644 index 000000000..7da7e0b8a --- /dev/null +++ b/packages/producer/tests/software-beginframe-yoyo-compositor/output/output.mp4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:326ced7888acace3f38c020eb7c56a6164940b94d335603153d615f4b66dc408 +size 1843718 diff --git a/packages/producer/tests/software-beginframe-yoyo-compositor/src/index.html b/packages/producer/tests/software-beginframe-yoyo-compositor/src/index.html new file mode 100644 index 000000000..df8d4dc94 --- /dev/null +++ b/packages/producer/tests/software-beginframe-yoyo-compositor/src/index.html @@ -0,0 +1,410 @@ + + + + + + Layer Reversal Regression + + + + + + + + +
+
+ +
+
LAYER TEST
+
COMPOSITOR REVERSAL
+
+ +
+
+
+ +
+
Input Alpha
+
+
+
+ +
+
Input Beta
+
+
+
+ +
+
Input Gamma
+
+
+ +
+
+
+
+
+ +
+
+
PROCESSOR
+
+ +
+
First Output
+
Second Output
+
+ + +
+ + + +