Repository navigation
fix(engine): preserve composition root background in alpha exports - #3976
Merged
Merged
Conversation
initTransparentBackground() forced html, body, AND any [data-composition-id] element (the composition root, or a nested sub-composition root) to transparent for every alpha-carrying capture session. That's correct for the HDR two-pass layered DOM pass, where the HDR video itself is the backdrop — but for a plain alpha export (MOV ProRes4444, webm+alpha) of a normal composition, an author's own background painted on the root is real, intentional content, and it was being silently dropped. Add CaptureOptions.clearCompositionRootBackground, threaded through session.options into every general-session call site (default false — preserve the root's authored background). The two HDR-layered-DOM- session call sites set it to true, keeping their existing (correct) behavior. initTransparentBackground's injected stylesheet is now idempotently rewritten rather than insert-once/skip, so a caller's choice is never silently ignored by an earlier call on the same page. Add a producer test fixture + transparency-test.ts check proving a composition root's authored background survives into alpha output, plus unit tests for both flag values and the rewrite-on-repeat-call behavior directly. Co-Authored-By: Miguel Ángel <[email protected]>
…planner transparency-regression-root-bg is exercised by transparency-test.ts directly (asserts a real alpha channel), not by the sharded regression harness, same as its sibling transparency-regression. The shard planner hard-fails on any fixture directory it doesn't recognize as either scheduled or explicitly excluded, so register it in the excluded list with the same reason as its sibling. Co-Authored-By: Miguel Ángel <[email protected]>
…ered sessions The previous commit removed the two HDR-layered-DOM-session call sites' own explicit initTransparentBackground() call, relying on initializeSession()'s internal call (gated behind session.options.format === "png") plus a new clearCompositionRootBackground session option. That was wrong: a plain HDR render's own DOM session captures "jpeg", not "png" — the final HDR MP4/HEVC output doesn't itself carry an alpha channel, only the layered-compositing step does. So initializeSession()'s internal call never fired at all for this common case, and the HDR video layer never got a transparent DOM backdrop to composite into. Reproduced directly against the hdr-regression fixture (part of the regression-shards CI suite): a real local render showed a severe PSNR collapse (~8-56dB) in the "Window B: z-order sandwich" segment, where a DOM element behind the HDR video, the video itself, and a translucent DOM overlay in front all need correct z-ordering — with the composition root's own background no longer cleared, it painted solid over the video slot instead of letting the video composite through. Confirmed via bisection: reverting to the prior commit's files reproduces 0 failed frames (passing); the removed-call version reproduces the exact failure; restoring the explicit calls fixes it again (100/100 checkpoints, 0 failed frames). Restore the explicit, unconditional call in both HDR call sites, right after initializeSession(). clearCompositionRootBackground still does its job for the general/plain-alpha-export path (frameCapture.ts), which is unaffected by this fix. Co-Authored-By: Miguel Ángel <[email protected]>
Drop the CaptureOptions.clearCompositionRootBackground field: every HDR layered DOM session already calls initTransparentBackground right after init, so that call is the one owner. Engine paths use the default, which keeps root backgrounds.
miguel-heygen
force-pushed
the
fix-alpha-root-background-clear
branch
from
October 2, 2026 12:27
29e0d81 to
f1843b7
Compare
Contributor
Edit accuracy: accurate 1216 (base branch 1216), smooth 862 of thoseThe gate passes. Quarantined, measured but not gated (1)
Unstable (1)
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
initTransparentBackground()prepares a page for alpha capture (webm,movProRes 4444,png-sequence, and the HDR layered DOM pass). Because Chrome's default-background override does not beat author CSS, it also injects a stylesheet. That stylesheet forcedhtml,bodyand every[data-composition-id]element to transparent, unconditionally.Clearing composition roots is right for the HDR layered DOM pass, where the HDR video is the backdrop and the DOM layer may only contribute foreground pixels. For a plain alpha export it is wrong: a background the author painted on the composition root (or a nested sub-composition root) is real content, and it was silently dropped. A root
<div data-composition-id>withbackground: rgb(0,0,255)rendered blue in mp4 but decoded asrgba(0,0,0,0)in webm.Fix
initTransparentBackground(page, { clearCompositionRoot }): by default it clears onlyhtml/body;clearCompositionRoot: truealso clears every[data-composition-id]element (top-level and nested).trueare the two HDR layered stages (captureHdrStage,captureHdrHybridLoop), which already call the helper explicitly right afterinitializeSession(). That explicit call is the single owner of the decision; the engine's internal calls (forpngcapture sessions) use the default. No newCaptureOptionsfield.<style>is rewritten on a repeat call instead of insert-once. This is load-bearing: when an HDR layered session capturespng(alpha output with layered shader transitions), the engine's internal call runs first with the default, and the stage's explicitclearCompositionRoot: truecall must win.Behaviour change to note: an alpha export whose composition root paints a full-frame background now comes out opaque, as authored. None of the alpha regression fixtures (
webm-transparency,distributed/{webm-vp9,mov-prores,png-sequence}) paint a root background, so their baselines are unaffected.Known exception
A webm, mov or png-sequence export that uses shader transitions still renders through the HDR layered stage, which clears the composition root on purpose (the layer must carry only foreground pixels). Such an export still comes out transparent where the root painted. mp4, gif and hls shader transitions composite in the page and keep the root background.
Test plan
packages/engine/src/services/screenshotService.test.ts: default clears onlyhtml/body;clearCompositionRoot: trueadds[data-composition-id]; a later call replaces the earlier rule. 35/35 pass. Mutation checks: with the old helper the first test fails (expected 'html,body,[data-composition-id]{backg…' to contain 'html,body{'); with insert-once restored the third test fails.transparency-regression-root-bgplus aroot-bgcheck intransparency-test.ts(renders webm, asserts the root's blue survives as opaque and the red card still renders). Ran on Linux: passes at this head; with the old helper it fails withroot-bg: pixel (10,10) expected fully opaque (alpha=255), got rgba(0,0,0,0).hdr-regressionfixture in the regression shards covers it.tsc --noEmitclean inpackages/engineandpackages/producer;oxlintandoxfmt --checkclean on touched files.