Files
Miguel Ángel d21883fe05 fix: recursive sub-composition inlining for depth-3+ nesting (#2660)
* fix(core): discover sub-composition hosts to a fixed point during inlining

inlineSubCompositions collected [data-composition-src] hosts from the
document once, before inlining began, then processed that fixed list in
a single flat loop. A host's inlined content could introduce new hosts
of its own (a sub-composition nesting another sub-composition), and
those were never discovered: two-level nesting worked because the
second level's host already existed in the root document, but any
third level's host only appeared inside content the single pass had
already finished walking, so its content silently never rendered.

Host discovery now runs as a work queue: after a host is inlined, the
newly inserted subtree is re-scanned for further hosts, which are
enqueued with their ancestry chain. A host whose source already appears
in its own ancestry is a circular reference and is reported through the
existing onMissingComposition channel instead of being inlined; a depth
ceiling backstops any gap in that check. Existing single- and two-level
fixtures are unaffected.

* fix(producer): assign runtime composition ids to hosts discovered mid-inline

The producer's per-instance runtime id assignment (assignBundledRuntimeCompositionIds)
ran once over the root document's initial hosts, before inlining. With
host discovery now iterating to a fixed point (previous commit), hosts
revealed inside an already-inlined sub-composition were never in that
pre-pass, so the identity map returned undefined for them: two sibling
instances of the same sub-composition, discovered mid-traversal, both
fell back to their shared authored id and clobbered each other's
variablesByComp entry.

hostIdentityMap is now a lazy map: a pre-pass cache hit returns
unchanged, a miss reads the host's authored data-composition-id,
allocates a collision-checked runtime id by scanning the live document,
writes it back, and caches it. No change to the shared inliner's call
signature. Existing single-instance and root-level reusable-template
cases (#2064) are unaffected.

* test(producer): add depth-3 nested sub-composition regression fixture

Adds a minimal three-level chain (root -> level-2 -> level-3) where the
deepest level renders a distinguishing full-frame marker, following the
existing sub-comp-* fixture pattern. Confirmed against the pre-fix
commit (735128a61) in a separate scratch checkout that the fixture fails
without the previous two commits (level-3's marker absent, ~0.4B fewer
matching pixels than the golden expects) and passes with them.

The golden reference video was captured on the team's render host
(devbox) rather than locally, to avoid PSNR drift from font/GPU
differences against whatever renders the regression suite in CI. Local
render against that golden also passes (PSNR ~24.5dB throughout, 0
failed frames of 100 checkpoints), confirming cross-machine consistency
as well.
2026-07-20 14:47:37 +02:00

13 lines
1.2 KiB
JSON

{
"name": "Nested sub-composition depth-3 inlining",
"description": "Regression test for the fix in 693768d21 (core: inlineSubCompositions now discovers [data-composition-src] hosts to a fixed point instead of a single flat pass) and fa4a7f012 (producer: hostIdentityMap now lazily assigns runtime composition ids to hosts discovered mid-inline). Before these fixes, a sub-composition hosting another sub-composition (root -> level-2 -> level-3) silently dropped level-3's content: two-level nesting always worked because the second-level host already existed in the root document before inlining began, but a third-level host only appears inside content the single pass had already finished walking, so it was never discovered or inlined. This fixture's level-3 composition contributes a large, uniquely colored and positioned marker element covering more than half the frame; if the single-pass bug is reintroduced, level-3 is dropped entirely and the rendered PSNR against the golden baseline falls well below threshold.",
"tags": ["sub-composition", "regression", "nested"],
"minPsnr": 20,
"maxFrameFailures": 10,
"minAudioCorrelation": 0.0,
"maxAudioLagWindows": 120,
"renderConfig": {
"fps": 24
}
}