fix(producer): capture page-side shader composites via screenshot#1656
Conversation
9e86215 to
5671c30
Compare
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
Reviewed at 5671c30d. Tight, well-scoped fix — root-cause writeup matches the diff and the verification trail is concrete.
Verdict matrix
| Item | Status |
|---|---|
| Fix shape | ✅ PREEMPT (narrow) — gated to usePageSideCompositingForTransitions only |
| Performance impact | ✅ No regression — screenshot path only fires on the already-narrow page-side compositing path; default BeginFrame untouched |
| Colorspace / scaling | ✅ No drift — page-side compositing already required forceScreenshot to honor the prepare/resolve handshake; PR makes that requirement explicit instead of silently violating it on Linux/headless-shell |
| Probe-session discard | ✅ Safe — captureStreamingStage accepts `probeSession: CaptureSession |
| Observability | ✅ updateCaptureObservability({ forceScreenshot }) + captureMode auto-flip + [Render] Recreating capture session… log + capture_strategy checkpoint includes both forceScreenshot and usePageSideCompositing |
| Tests | ✅ Both helpers have happy + inactive coverage; security fixture fix is a clean sandbox-nesting improvement |
| Cross-cutting | ✅ Mutation happens at renderOrchestrator.ts:1404 before any capture stage runs, so chunked-encode + streaming-encode both inherit correct mode |
Notes
Probe-discard ordering looks right. The pre-existing workerCount > 1 discard at renderOrchestrator.ts:1327 runs before the new compositing discard at L1389-1403. If parallel capture already nulled probeSession, the new block correctly no-ops the discard branch (hasProbeSession=false → helper returns false) but still installs the pre-head script and flips captureForceScreenshot. Two independent reasons to discard, composed cleanly.
Test fixture fix is genuinely load-bearing. The pre-PR join(workDir, \"..\", \"..\", \"etc\", \"passwd\") would have written onto the host root if isPathInside() ever regressed silently — tmpdir()/../../etc/passwd resolves to /etc/passwd. New sandbox-rooted shape (sandboxRoot/work/inner → escape target sandboxRoot/etc/passwd) keeps the test contained. Worth the 4 extra lines.
Nit
renderOrchestrator.ts:1393,1406— passingusePageSideCompositing: trueas a hardcoded literal inside theif (usePageSideCompositingForTransitions)branch is slightly redundant. Either drop the param (helpers become single-input) or passusePageSideCompositingForTransitions. Cosmetic — current shape is fine.
Question
- Is there a follow-up to also fix the BeginFrame compositor-handshake so
__hf_page_composite_prepare/resolveworks under BeginFrame? The PR body frames it as "page-side compositing only runs the handshake when captureMode !== beginframe" — that's a permanent constraint vs an addressable bug. Useful to know which it is for the perf budget conversation later (screenshot is the slower path).
What I didn't verify
- The Windows
regression+Windows render verificationshards are still in progress at review time; CI verdict to confirm on completion. - Visual fidelity of the actual shader warp — taking the PR's MP4 + frame-extraction evidence on trust.
Review by Rames D Jusso
vanceingalls
left a comment
There was a problem hiding this comment.
Verdict: LGTM
Head SHA 5671c30d, base main. Reviewed after Rames' LGTM at the same SHA — I converge on his verdict and on his nit (hardcoded usePageSideCompositing: true literal at renderOrchestrator.ts:1393,1406). The angle I'll add that Rames didn't cover is the #1618 interaction analysis, since that PR landed two days ago in the same code area and the question of whether #1656 duplicates / contradicts / supersedes it matters.
Where this sits relative to #1618
#1618 (merged a85a507c, 2026-06-21) fixed two engine-side defects inside the page-side compositor: forcing opacity:1; visibility:visible on cloned scenes in prepareComposite, and un-hiding the settled scene in the seek wrapper. Those fixes only run when the compositor handshake actually runs — i.e. when packages/engine/src/services/frameCapture.ts:1285 (if (hasPendingComposite && session.captureMode !== "beginframe")) gates open.
The gap #1656 closes is upstream of that gate: on Linux + headless-shell, the producer was selecting usePageSideCompositingForTransitions=true, registering HF_PAGE_SIDE_COMPOSITING_STUB, and then acquireBrowser resolved captureMode="beginframe" per frameCapture.ts:357. The !== "beginframe" short-circuit at frameCapture.ts:1285 meant the prepare → micro-screenshot → resolve protocol never ran, so __hf_page_composite_pending stayed true, the compositor canvas stayed hidden, and BeginFrame captured the fallback DOM. #1618's fixes inside engineModePageComposite.ts were dead code on this path.
So #1656 is purely additive to #1618 — it makes #1618's mechanism actually reach the page on Linux producer renders. Not a duplicate signal, not a contradictory rule, not a superseding mechanism. The two symptoms #1618 enumerated (dropped shader transitions + final-scene content drop) are both addressed at their root cause by #1618; #1656 ensures that root-cause fix runs in producer-streaming Linux renders. The disclosure framing in this PR's body ("BeginFrame captured the fallback DOM state instead of the resolved shader canvas") matches that delta exactly.
Independent verification at HEAD
renderOrchestrator.ts:1382-1413: page-side gate (a) installs the pre-head script, (b) discards the probe session if live, (c) flipscaptureForceScreenshot=true. Order matters and is correct — the file server's pre-head script is registered beforecloseCaptureSession(probeSession), so the fresh session that downstream stages will create picks up the stub on next load.renderOrchestrator.ts:1404is the populate side of the read atframeCapture.ts:357: settingforceScreenshot=trueflipspreModefrom"beginframe"to"screenshot", which is what unlocksframeCapture.ts:1285. Dispatch chain checks out end-to-end across the producer→engine boundary.- Probe-discard pattern (close + null + ferry
browserConsoleBufferintolastBrowserConsole) is mirrored from the existingworkerCount > 1discard atrenderOrchestrator.ts:1327-1331. Rames already noted these two discard reasons compose cleanly; agreed. - Test fixture sandboxing (
renderOrchestrator.test.ts:338-371): hermetic<sandbox>/work/innerworkDir,hf-ext/../../etc/passwdresolves to<sandbox>/etc/passwd. WithcompiledDir = join(workDir, "compiled")atrenderOrchestrator.ts:1084, the two..segments cancelcompiledandhf-ext, landing at<workDir>/etc/passwd— matchesescapeTarget = join(workDir, "etc", "passwd"). Math is right, host/etc/passwdis no longer touched. Solid hermetic fix.
Scope-gap check (#1618 symptom coverage)
#1618 enumerated (a) dropped shader transitions and (b) final-scene content drop. #1656 unblocks the compositor handshake on Linux — once unblocked, both of #1618's fixes apply (prepareComposite force-visible covers (a), settled-scene un-hide covers (b)). The PR verification log demonstrates the shader warp landing in the producer-streamed MP4 ((a)); (b) isn't explicitly screenshotted but is covered by the same handshake reaching the page — no separate fix needed.
Note on Rames' follow-up question
Rames asked Miguel whether the BeginFrame-skips-handshake constraint at frameCapture.ts:1285 is a permanent design choice or an addressable bug. Endorsing the question — if BeginFrame ever learns the handshake, Linux page-side renders could drop the screenshot fallback and reclaim the perf delta. Out of scope here; useful to know which it is for downstream planning.
CI: required Linux suites green at time of review; Windows render verification + regression shards still in progress (non-blocking for the page-side Linux path this PR targets).
Review by Via
jrusso1020
left a comment
There was a problem hiding this comment.
Approving at 5671c30d — Rames D Jusso R1 verified the load-bearing checks (7 white-check / 1 nit / 1 yellow question, no blockers). The fix is narrowly gated to usePageSideCompositingForTransitions, so the default BeginFrame path is untouched and the 99% case sees no perf regression. Probe-session discard is safe (captureStreamingStage rehydrates via ?? createCaptureSession(...)), observability is wired correctly (captureMode flip + recreating-session log + capture_strategy checkpoint), tests cover both helpers' happy + inactive paths, and the sandbox-nesting fixture fix is a clean improvement.
Carry-forward question worth tracking (Rames flagged): the PR body frames "page-side compositing only runs the handshake when captureMode !== beginframe" as a permanent constraint. Worth knowing whether there's a follow-up to fix the BeginFrame compositor-handshake so __hf_page_composite_prepare/resolve works under BeginFrame too — affects the perf-budget conversation since screenshot is the slower path. Non-blocking; author can address whenever convenient.
— Jerrai
5671c30 to
6904640
Compare
Summary
__hf_page_composite_prepare/resolveinstalled.renderOrchestratortest file passes on Linux instead of checking host/etc/passwd.Root Cause
There were two stacked issues:
The producer selected page-side shader compositing, but Linux/headless-shell could still use BeginFrame capture.
frameCapture.tsonly runs the page-side compositor handshake (prepare-> micro screenshot paint ->resolve) whensession.captureMode !== "beginframe", so BeginFrame captured the fallback DOM state instead of the resolved shader canvas.The in-process producer often reuses the browser probe session for final capture. The probe page is loaded before
HF_PAGE_SIDE_COMPOSITING_STUBis added to the file server, so even after switching to screenshot capture the reused page did not have__hf_page_composite_prepare/resolve. A fresh capture session after the pre-head script is added does have the compositor and renders the shader warp correctly.Fix
When page-side compositing is selected, the orchestrator now:
captureForceScreenshot=true; andThe new regression fixture (
page-side-shader-compositor-render-compat) uses scripted audio volume to force the browser probe path, then renders a real shader transition through the producer. It fails against the stale BeginFrame/reused-probe behavior and passes once final capture is recreated in screenshot mode with the page-side compositor pre-head script loaded.Verification
beginframe, left__hf_page_composite_pending=true, and kept the compositor canvas hidden.__hf_page_composite_pending=false, compositor canvas visible, and the warped shader transition rendered.Recreating capture session so page-side compositing pre-head script is loaded.usePageSideCompositing=true,forceScreenshot=true, capture modescreenshot/tmp/hf-shader-debug/fixed-full-render-final.mp4, 1293 frames at 30fps, 43.1s8.25sand later transition pointsTests
bunx oxfmt --check packages/producer/src/services/renderOrchestrator.ts packages/producer/src/services/renderOrchestrator.test.tsbunx vitest run src/services/renderOrchestrator.test.ts --root .frompackages/producerbun run --filter @hyperframes/producer typecheckbun run --cwd packages/producer docker:test:update page-side-shader-compositor-render-compatbun run --cwd packages/producer docker:test page-side-shader-compositor-render-compat