Skip to content

fix(engine): preserve composition root background in alpha exports - #3976

Merged
miguel-heygen merged 5 commits into
mainfrom
fix-alpha-root-background-clear
Oct 2, 2026
Merged

miguel-heygen merged 5 commits into
mainfrom
fix-alpha-root-background-clear

Conversation

@miga-heygen

@miga-heygen miga-heygen commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Summary

initTransparentBackground() prepares a page for alpha capture (webm, mov ProRes 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 forced html, body and 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> with background: rgb(0,0,255) rendered blue in mp4 but decoded as rgba(0,0,0,0) in webm.

Fix

  • initTransparentBackground(page, { clearCompositionRoot }): by default it clears only html/body; clearCompositionRoot: true also clears every [data-composition-id] element (top-level and nested).
  • The only callers that pass true are the two HDR layered stages (captureHdrStage, captureHdrHybridLoop), which already call the helper explicitly right after initializeSession(). That explicit call is the single owner of the decision; the engine's internal calls (for png capture sessions) use the default. No new CaptureOptions field.
  • The injected <style> is rewritten on a repeat call instead of insert-once. This is load-bearing: when an HDR layered session captures png (alpha output with layered shader transitions), the engine's internal call runs first with the default, and the stage's explicit clearCompositionRoot: true call 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

  • 3 unit tests in packages/engine/src/services/screenshotService.test.ts: default clears only html/body; clearCompositionRoot: true adds [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.
  • New fixture transparency-regression-root-bg plus a root-bg check in transparency-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 with root-bg: pixel (10,10) expected fully opaque (alpha=255), got rgba(0,0,0,0).
  • HDR layered behaviour is unchanged (root backgrounds still cleared there); the hdr-regression fixture in the regression shards covers it.
  • tsc --noEmit clean in packages/engine and packages/producer; oxlint and oxfmt --check clean on touched files.

miga-heygen and others added 4 commits October 2, 2026 05:21
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
miguel-heygen force-pushed the fix-alpha-root-background-clear branch from 29e0d81 to f1843b7 Compare October 2, 2026 12:27
@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 1216 (base branch 1216), smooth 862 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (1)

Unstable (1)

  • crop-none-px-r0-root-z200: tracking 0.02, pressJump 0, drop 0.04, reload 0.04, render 0.03, renderKey -, undo false, teleport true / tracking 0.02, pressJump 0, drop 0.04, reload 0.04, render 0.03, renderKey -, undo true, teleport true / tracking 0.02, pressJump 0, drop 0.04, reload 0.04, render 0.03, renderKey -, undo true, teleport true

@miguel-heygen
miguel-heygen merged commit d7285e4 into main Oct 2, 2026
161 of 162 checks passed
@miguel-heygen
miguel-heygen deleted the fix-alpha-root-background-clear branch October 2, 2026 13:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants