Repository navigation
fix(engine): motion blur works over video by holding each video frame across samples - #5150
Conversation
… across samples Each output frame's injected video frame is now held for every sub-frame sample and probe of that frame, so footage stays as shot and only the layers over it blur. The refusal for sessions with a before-capture hook is removed. Fixes #5144
Edit accuracy: accurate 2059 (base branch 2059), smooth 1612 of thoseThe gate passes. Quarantined, measured but not gated (0) Unstable (1)
|
Each sample now decides which videos show at its own time, like every other layer, and only a video on screen at the output frame's time keeps that frame's picture. Before, the visible set was taken at the frame's time while sub-composition hosts were hidden at the sample's time, so the samples just before a cut showed no video and the cut frame dipped toward black. The before-capture hook takes the held time as an optional third argument, and the early injection is gone: the GPU re-render now always runs at the page's own time.
The injector copies a video's own style only when its frame changes, so the first injection of each blurred frame now runs at the frame's own time before the samples. A video sliding by its own transform no longer sits a quarter frame behind with blur on.
jrusso1020
left a comment
There was a problem hiding this comment.
Approve. The fix is small and sits at the right layer. It reuses the injector's existing "skip an unchanged frame" path (lastInjectedFrameByVideo, videoFrameInjector.ts:224), so holding a frame costs no extra decode or injection. It adds no new option or mode.
Reuse and simplicity
- The optional
heldVideoTimeonBeforeCaptureHookis the smallest change that works. Only the injector reads it. The HDR loops (captureHdrSequentialLoop,captureHdrHybridLoop,captureHdrResources) call the hook with two arguments, so their behavior is unchanged. - Removing the engine refusal is safe. Every blurred frame goes through
captureAccumulatedFrame(frameCapture.ts:4247-4251). The one route that can't accumulate,hdr_layered, is still refused inmotionBlurRoute.ts. Distributed renders can't request blur. - Merging held payloads into the active map is safe because
getActiveFramePayloadsreturns a freshMapon every call (videoFrameExtractor.ts:2547). - The pre-sample injection at
frameTime(frameCapture.ts:4150-4156) is the right fix for the transform lag the body describes. Its time is added tobeforeCaptureMs, so the perf totals stay honest.
Non-blocking notes
refreshActiveSetkeeps a forward cursor and does a full rescan whenever time goes backward (videoFrameExtractor.ts:2508). The hook now looks up the sample time and then the held time, so every sample after the frame time triggers a rescan. That's correct, and the cost is O(videos) per sample, which is small next to a screenshot. If it ever shows up in a profile, the held lookup can be done once per frame instead of once per sample.- Wording only: before a cut, the outgoing video isn't on screen at the held time, so its samples follow the sample time. They show the frame at each sample time, not the video's last frame as the "Scope notes" say. The cut-frame blend still behaves as described, and the injector test
0.98 -> /a/9pins exactly this.
Verified against the source: the injector already skips unchanged frames; resolveSessionMotionBlur no longer throws on a hook; the probes and samples all pass frameTime. I didn't run the integration test locally.
Verdict: APPROVE
Reasoning: A minimal and correct change that reuses the injector's existing dedup. The sites that call the hook without blur are unchanged, and the routes that can't blur are still refused.
— Rames
Fixes #5144.
What changes
Motion blur now works in compositions that contain a
<video>. Each output frame's video frame is held for every sub-frame sample of that frame, so the footage stays as shot (it already carries its own camera blur) and the graphics moving over it are blurred. No new option: this is simply what motion blur does over video now, and the refusal is gone.How
BeforeCaptureHook) takes an optional third argument,heldVideoTime. Motion-blur samples and the adaptive probes pass the output frame's time; a frame without blur passes nothing, so that path is unchanged.createVideoFrameInjector) decides which videos show at the sample's own time, like every other layer. For a video that is also on screen atheldVideoTime, it uses that time's frame. The injector already skips re-injecting an unchanged frame, so samples reuse the injected frame: no re-extraction, no extra decode.resolveSessionMotionBlurno longer throws when a session has a before-capture hook.Scope notes
<video>element, taken at the frame's time. Animation on a wrapper follows the samples like any other layer.Tests
frameCapture-motionBlur.test.ts: every sample and adaptive probe calls the hook at its own sub-frame time with the frame time held; a frame without blur holds nothing. The old "rejects injected video frames" case now asserts the session is accepted.videoFrameInjector.test.ts: a video on screen at both times shows the held frame (frame 15, where the sample alone would pick 14); across a cut, a sample just before it shows the outgoing scene's video.motionBlurVideo.integration.test.ts(producer, integration lane, real Chrome + ffmpeg):testsrc2video filling the frame and drifting left by its own CSS transform, with a white bar sliding across its bottom third, rendered without blur, with blur and with blur again. Every frame's video rows match the unblurred render, the bar's rows differ, and the two blurred renders are byte-identical.nested-sequential-video-local-start(two sub-composition hosts, each a full-frame video, cut at 1 s, 24 fps) with a generated clip: every non-cut frame matches the unblurred render, and the cut frame's brightness stays within 10% of it (and the unblurred cut frame is checked to have real video in it).Verified on a Linux box with real Chrome (headless shell, software GL):
frameCapture-motionBlur.test.ts,frameCapture-gpuCompletion.test.ts,videoFrameInjector.test.ts, 3 runs eachmotionBlurVideo.integration.test.ts, 3 runstsc --noEmitin engine and producer, test reachability, comment ratchetBefore / After
A lower third sliding in over generated test footage (
testsrc2, 960x540, 30 fps), frame 6, rendered to PNG. On main, the same render withmotionBlurfails at once with "[MotionBlur] sub-frame motion blur cannot run with injected video frames", so the only thing you could ship was the unblurred frame.Before
Main, blur refused, so the lower third ships unblurred (2x crop, then the full frame):
After
This branch,
motionBlur: { samplesPerFrame: 32 }. The lower third smears along its motion; the footage, including the grey bar next to it, stays exactly as shot: