Skip to content

fix(engine): disable GPU compositing on SwiftShader for all capture modes - #3062

Closed
miga-heygen wants to merge 1 commit into
mainfrom
fix/alpha-band-ghosting
Closed

fix(engine): disable GPU compositing on SwiftShader for all capture modes#3062
miga-heygen wants to merge 1 commit into
mainfrom
fix/alpha-band-ghosting

Conversation

@miga-heygen

Copy link
Copy Markdown
Contributor

Summary

  • Move --disable-gpu-compositing from the BeginFrame-only block to all capture modes when browserGpuMode === "software"
  • Add PRODUCER_EXTRA_CHROME_ARGS env var for passing additional Chrome flags during debugging

Problem

SwiftShader's GPU compositor retains stale transformed layers across frames, producing phantom duplicate content at exactly renderHeight/4 vertical offsets. The artefact appears in both BeginFrame and Page.captureScreenshot capture modes — the existing comment documents this ("both BeginFrame and Page.captureScreenshot read the stale surface"), but the --disable-gpu-compositing workaround was gated to BeginFrame mode only.

Software-GPU renders are clamped to screenshot mode by the forceScreenshot invariant (applyConcreteGpuScreenshotClamp), so the workaround never applied to the most common SwiftShader capture path.

Root cause

buildChromeArgs placed --disable-gpu-compositing inside the if (options.captureMode !== "screenshot") block (line 914–926). This was originally scoped to BeginFrame because BeginFrame drives the compositor step-by-step (where stale retention is deterministic), while screenshot mode was considered "good enough" because the compositor runs freely. Issue #3049 proves that SwiftShader's compositor also retains stale layers in screenshot mode, with a different pattern: band duplication at renderHeight/4 rather than sequential-frame yoyo persistence.

Fix

Hoist --disable-gpu-compositing out of the BeginFrame block so it fires for any browserGpuMode === "software" session, regardless of capture mode. Hardware-GPU captures are unaffected.

The PRODUCER_EXTRA_CHROME_ARGS escape hatch (space-separated env var) lets users inject Chrome flags without wrapping the binary — the original issue reporter specifically called this out as needed for debugging.

Validation

Closes #3049

…odes (#3049)

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
@miga-heygen
miga-heygen force-pushed the fix/alpha-band-ghosting branch from 7b6e31e to 484d19b Compare August 6, 2026 05:30
@miguel-heygen

Copy link
Copy Markdown
Collaborator

Closing this. The premise does not hold, and the change measurably degrades software renders.

The one true part

--disable-gpu-compositing really was unreachable in practice. browserGpuMode === "software" forces screenshot capture (packages/engine/src/config.ts), and the flag was gated on BeginFrame, so only the pinned software-beginframe-yoyo-compositor fixture ever exercised it. That observation is correct.

Why the fix is wrong

The stated root cause is falsified. This PR claims SwiftShader retains stale layers in screenshot mode too. I rendered packages/producer/tests/software-beginframe-yoyo-compositor/src in software screenshot mode on main (25 fps, png-sequence, --no-browser-gpu --workers 1). Frames 254 and 255, the exact window where the #2550 ghost badge appears on the unfixed BeginFrame path, are clean. Two output badges, no third. That is the one composition where stale-layer retention is known to reproduce, and it does not reproduce on the screenshot path.

The existing comment does not say what the PR body says it does. It says both readback methods see the stale surface, which was measured inside a BeginFrame session. #2521's evidence was BeginFrame only.

The flag makes software output worse, not better. Same transparent composition (1280x1144, static SVG line primitives, alpha), rendered software vs a hardware-GPU render of the same file as ground truth. Frame 30:

build pixels differing max channel delta fully opaque pixels with wrong RGB
main, software 1.61% 12 0
this PR, software 2.53% 255 2698

main's software path already matches hardware to within antialiasing noise. Adding --disable-gpu-compositing changes how SwiftShader rasterizes thin strokes and glyph edges, and pushes the output away from ground truth. That is a fidelity regression on the alpha path, which is the path #3049 is about.

CI caught the same thing. png-sequence failed 60 of 60 frames. That suite is the strict RGBA byte identity gate for transparent renders. main was green on the same code base minutes earlier.

No reproduction. The validation section lists unit tests over buildChromeArgs and nothing that exercises #3049. We do not ship capture-path changes without a repro.

What #3049 actually looks like

The reporter's own evidence points away from the compositor: compositor flags had no effect, static elements duplicate, it survives Chrome 131 through Chrome 152, and merging the SVG <line> primitives into a single <path> makes it disappear. That is a content-dependent paint or raster signature, not layer retention.

I built a matching composition (240 static SVG lines, transparent background, yoyo timeline) and could not reproduce the banding on either build. Follow-up is on the issue: we need the reporter's composition before guessing again.

Salvageable

PRODUCER_EXTRA_CHROME_ARGS is a reasonable ask and the reporter called for it explicitly. Worth landing on its own, separate from any capture-path change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants