Repository navigation
fix(core): pair a staged scene copy's video with its own render frame - #4791
jrusso1020 merged 1 commit into
Conversation
Page-side shader transitions clone the outgoing and incoming scenes, ids included, into staging layers. Color grading grades the copied <video> too, and findInjectedRenderFrame looked its frame up with getElementById, which returns the live scene's <img>. The live and the copied grading entries then both kept their canvas right after that one <img>, moving each other on every mutation, and the loop blocked the page: a graded <video> inside a transition stopped the render at the transition's first frame. Look through the element's following siblings first, where the injector puts the frame, and fall back to the id lookup only when it isn't there.
jrusso1020
left a comment
There was a problem hiding this comment.
Approving at 9d60ac64, adding to the existing approval at this head. Clear write-up, and a fix sized to the cause.
Can the sibling-first lookup change the answer when ids are unique? No. renderFrameSibling.ts:57-61 now requires document.getElementById(frameId) to be non-null before looking at siblings, and only accepts a sibling whose id === frameId. With unique ids, that sibling is the getElementById result, so the returned element is identical. When the frame isn't in the document (preview), it still returns null right away, as before. The answer can only differ when the id is repeated, which is the staged-copy case.
The four callers
media.ts:35,568andmediaProxy.ts:72only use the result as a boolean. Non-null still requires the id to resolve in the document, so their answers change only in the duplicate case, and there only in which<img>gets picked.video-texture-compat.ts:30-41already triesvideo.nextElementSiblingwith the__render_frame__class before calling the helper. So the helper now also covers a frame that isn't the immediate sibling, which is consistent with what it already did.colorGrading.ts:2476(findRenderFrameImage) is the caller that was actually wrong. Pairing the copy with the live<img>is what made the two grading canvases trade places inkeepCanvasAboveSource.
Tests. I ran renderFrameSibling.test.ts at this head (all pass). I then restored renderFrameSibling.ts from main and ran it again: resolves a staged copy of a scene to the copy's own frame (#3994) fails, as the PR says. I couldn't run the colorGrading.test.ts case locally because of stale dependencies. From reading it, the 100-move cap on insertBefore and the < 10 moves assertion are the right shape to catch the ping-pong without hanging.
CI at this head: 41 checks pass and none fail. 18 were still pending (Windows jobs and the studio shards) when I checked.
On the two follow-ups: both are worth sending.
prepareCompositereviving the native<video>(engineModePageComposite.ts:299-302). Forcingopacity/visibilityon every[data-start]in the copy also un-hides a<video>that the injector hid behind its__render_frame__. After the clip's window ends, the ungraded render then shows the clip's first frame while the graded one shows nothing. Skipping<video>s that have a render-frame sibling makes both paths agree. Small change, real correctness fix.--no-page-side-compositingrenders a graded video black.getEffectiveOpacity(videoFrameInjector.ts:580) multiplies in the element's own opacity, which color grading sets to 0 on the source<video>. So the stacking query drops the clip. It's already broken on main, and ignoring the element's own opacity when it carriesdata-hf-color-grading-source-hiddenmatches whatinjectVideoFramesBatchdoes.
Verdict: APPROVE
Reasoning: The lookup is provably unchanged for unique ids, it fixes the duplicate-id pairing that stalled the render, and the regression test fails on main as claimed.
— Rames Jusso
What
A graded
<video>inside a page-side shader transition no longer stalls the render.findInjectedRenderFramenow looks for the render frame among the element's following siblings before falling back todocument.getElementById.Why
During a transition, the page-side compositor clones the outgoing and incoming scenes, ids included, into staging layers. Color grading picks up the copied
<video>and grades it too, and it finds the frame to draw from throughfindInjectedRenderFrame, which usedgetElementById. With the id repeated, the copy got the live scene's<img>. Both grading entries then kept moving their canvas right after that same<img>(keepCanvasAboveSource), each move re-triggering the other through the MutationObserver, which starves the page. The capture stops advancing at the transition's first frame, as in the report.Related work
Fixes #3994
How
The injector always inserts the frame right after its
<video>(insertBefore(img, video.nextSibling)), and the staged copy carries its own copy of it, so the element's following siblings are the right place to look first. The id lookup stays as the fallback, and when the frame isn't in the document at all (preview) it returns right away, as before. For everything that callsfindInjectedRenderFrame(color grading, media sync, the media proxy, the WebGPU video texture shim) the answer only changes when an id is duplicated.Test plan
renderFrameSibling.test.ts: a staged copy of a scene resolves to its own frame (fails onmain), a frame past a node inserted between them, and a frame that isn't a sibling is still found by id.colorGrading.test.ts: a graded video plus a staged copy with the same ids; each canvas has to end up right after its own frame. Onmainthe canvases keep trading places (the test caps it at 100 moves and fails); here it settles after a couple.packages/core3736 passed,packages/shader-transitions57 passed.tsc, lint, format, the comments check, the comment ratchet andfallow auditare clean.mainstops at 653/1200 with--workers 2, same as the report (I stopped it after 3 minutes).--workers 2, 53 s with 1 worker and 25 s with 4.data-color-grading,mainand this branch produce the same 1200 frames (identical framemd5).Two related things I found, not changed here
Happy to send either of these as a follow-up if you'd like them:
prepareCompositeforce-shows every[data-start]element in the copy, which also revives the hidden native<video>, a fresh element sitting at time 0. Skipping<video>s that have a__render_frame__sibling in that loop is a two-line change, with a unit test inengineModePageComposite.test.ts. I tried it on a copy and from 2.0 s on both renders measure the same, frame by frame.--no-page-side-compositingrenders a graded<video>black during the transition (luma 16 against about 140 ungraded), onmaintoo. InqueryElementStacking,getEffectiveOpacitycounts theopacity: 0 !importantthat grading puts on the source<video>, soselectDomLayerShowIdsdrops the clip and the scene buffer stays empty. Ignoring the element's own opacity when it carriesdata-hf-color-grading-source-hidden, asinjectVideoFramesBatchalready does, is about three lines, with a unit test on the stacking query. On a patched copy the video shows up with the grade applied.