mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
refactor(engine,producer): adopt requestPaint contract, retire autoAlpha rewrite (#2021)
* refactor(engine,producer): adopt requestPaint contract, retire autoAlpha rewrite crbug 529829538 was closed "working as intended": the html-in-canvas API's contract is mutate -> canvas.requestPaint() -> await the canvas paint event -> drawElementImage, which refreshes the subtree's paint records including compositor-applied properties. Verified on the pinned 151 floor and 152 canary: root opacity, root filter, nested group opacity, and child transforms (incl. will-change-promoted) all capture exactly; the root element's own TRANSFORM is the one property still never baked. - Paint invalidation: all three paint-wait sites (serial capture, worker produce, batch produce) now call canvas.requestPaint() when available and fall back to the __hf_de_tick sentinel background toggle on builds without it. The 250ms unsynchronized-draw safety net is unchanged. - Root-opacity ratio correction REMOVED (all three draw sites + base-opacity recording at injection). Since 151 the paint wait bakes current root opacity into the snapshot as pixel alpha, so the ratio correction DOUBLE-APPLIED animated root fades: a root-fade A/B tripped the runtime self-verify at 30.1dB (frame 24, ~0.92 expected vs ~0.85 rendered). Post-removal the same comp self-verifies at inf and matches the screenshot render at PSNR=inf. The root TRANSFORM correction stays — verified still required. - autoAlpha rewrite machinery DELETED: the opt-in opacity->autoAlpha tween rewrite (default-off since the retraction fix; measured ~28dB damage on comps whose fades it touched), its flush-time transparent-target hiding, the __HF_FAST_CAPTURE_AUTOALPHA__ flag plumbing, and the deferral-time retract/re-assert dance. The stub keeps tween-target tracking (3D projection + at-risk scans depend on it). Validation: canary suite 7/7 with PSNRs identical to baseline (58.30 / 43.13 / 54.15 dB); root-fade A/B PSNR=inf vs screenshot; engine suite 905 passed (1 pre-existing color-grading failure); tsc/oxlint/oxfmt clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(engine,producer): review fixes — gate opacity correction by paint mechanism Max code-review findings on the requestPaint adoption: - Root-opacity ratio correction RESTORED, gated per frame on how the paint was produced: it applies on BeginFrame (sync=false) captures and on builds without canvas.requestPaint() — the two paths where the snapshot holds the root's load-time opacity — and is skipped only on requestPaint-driven paints, where the snapshot bakes the current opacity and the ratio double-applies (the proven 30.1dB root-fade failure). Base opacity is recorded at injection again. - Invalidation extracted to a page-scope helper (__hfDeInvalidate, installed by injectDrawElementCanvas) shared by all three paint-wait sites: sentinel toggle ALWAYS (a paint is guaranteed even if requestPaint elides one on a clean subtree) + requestPaint() in a try/catch (a throwing implementation degrades to sentinel-only instead of rejecting the capture). Returns whether requestPaint ran, feeding the opacity-correction gate. Also removes the triplicated inline block and its three anonymous `as T` casts. - HF_FAST_CAPTURE_AUTOALPHA now logs a retirement warning instead of being a silent no-op (the deleted rewrite's comment documented it as an operator escape hatch). - Batch producer docstring updated (still described the tick-toggle-only paint wait); stub tween observer reshaped to a void fn (observeTweenCall) so no arg-rewriting seam survives. Validation: canary suite 7/7 (58.30/43.13/54.15dB, d95f20b6 clean); root-fade A/B self-verify 4x inf + whole-video PSNR=inf; engine suite 905 passed (1 pre-existing); tsc/oxlint/oxfmt clean; stub regenerated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: WaterrrForever <miao.yang@heygen.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
WaterrrForever
parent
574da2b215
commit
337d0b51bc
@@ -142,10 +142,11 @@ export async function injectDrawElementCanvas(
|
||||
({ w, h }: { w: number; h: number }) => {
|
||||
const root = document.querySelector("[data-composition-id]") as HTMLElement | null;
|
||||
if (!root || document.getElementById("__hf_de_canvas")) return;
|
||||
// Record the root's base opacity now (timeline at 0, before any entrance/
|
||||
// outro tween) so the per-frame capture can correct drawElementImage's stale
|
||||
// opacity by the ratio current/base. (Transform is corrected unconditionally
|
||||
// and needs no base; see captureDrawElementFrame.)
|
||||
// Record the root's base opacity (timeline at 0, before any entrance/
|
||||
// outro tween) for the LEGACY root-opacity ratio correction. The
|
||||
// correction only runs on paths whose paint does not bake the root's
|
||||
// current opacity into the snapshot — BeginFrame (sync=false) captures
|
||||
// and builds without canvas.requestPaint(); see __hfDeInvalidate below.
|
||||
try {
|
||||
(window as unknown as { __HF_ROOT_BASE_OPACITY__?: number }).__HF_ROOT_BASE_OPACITY__ =
|
||||
parseFloat(getComputedStyle(root).opacity) || 1;
|
||||
@@ -165,14 +166,46 @@ export async function injectDrawElementCanvas(
|
||||
parent.insertBefore(canvas, root);
|
||||
canvas.appendChild(root);
|
||||
// Invalidation sentinel: a canvas child OUTSIDE the captured root.
|
||||
// Toggling its `left` each capture dirties the layoutsubtree so a paint
|
||||
// (and a fresh snapshot) is guaranteed even for static frames — without
|
||||
// ever appearing in drawElementImage(root) output.
|
||||
// Toggling its background each capture is a PAINT-level dirty
|
||||
// (layout/transform toggles do NOT fire the canvas `paint` event), so a
|
||||
// paint — and a fresh snapshot — is guaranteed even for static frames,
|
||||
// without the sentinel ever appearing in drawElementImage(root) output.
|
||||
const tick = document.createElement("div");
|
||||
tick.id = "__hf_de_tick";
|
||||
tick.style.cssText =
|
||||
"position:absolute;left:0px;top:0;width:1px;height:1px;background:#000;opacity:0.01;pointer-events:none";
|
||||
canvas.appendChild(tick);
|
||||
// Per-frame invalidation helper shared by every paint-wait site (serial
|
||||
// capture, worker produce, batch produce). Two mechanisms, both applied:
|
||||
// - the sentinel toggle guarantees a paint even if requestPaint() were
|
||||
// to elide one on a clean subtree, and remains the sole mechanism on
|
||||
// builds without requestPaint;
|
||||
// - canvas.requestPaint() is the html-in-canvas API's intended
|
||||
// invalidation (crbug 529829538 triage): it refreshes the subtree's
|
||||
// paint records — compositor-applied props included — where the
|
||||
// sentinel's dirty alone would not on pre-151 builds. Guarded so a
|
||||
// throwing implementation degrades to sentinel-only instead of
|
||||
// rejecting the capture.
|
||||
// Returns true when requestPaint was called — the snapshot then bakes
|
||||
// the root's CURRENT compositor-applied opacity (measured on 151/152),
|
||||
// so callers must skip the legacy root-opacity ratio correction.
|
||||
interface RequestPaintCanvas extends HTMLCanvasElement {
|
||||
requestPaint?: () => void;
|
||||
}
|
||||
(window as Window & { __hfDeInvalidate?: () => boolean }).__hfDeInvalidate = () => {
|
||||
tick.style.backgroundColor =
|
||||
tick.style.backgroundColor === "rgb(0, 0, 0)" ? "rgb(1, 1, 1)" : "rgb(0, 0, 0)";
|
||||
const cvp: RequestPaintCanvas = canvas;
|
||||
if (typeof cvp.requestPaint === "function") {
|
||||
try {
|
||||
cvp.requestPaint();
|
||||
return true;
|
||||
} catch {
|
||||
// Feature drift — sentinel dirty above still forces the paint.
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
},
|
||||
{ w: width, h: height },
|
||||
);
|
||||
@@ -245,8 +278,15 @@ export async function captureDrawElementFrame(
|
||||
__hf_accel_canvases?: HTMLCanvasElement[];
|
||||
__hf_canvas_2d?: HTMLCanvasElement[];
|
||||
__hf3d?: { update: () => void };
|
||||
__hfDeInvalidate?: () => boolean;
|
||||
__HF_ROOT_PROPS__?: boolean;
|
||||
__HF_ROOT_BASE_OPACITY__?: number;
|
||||
};
|
||||
const aw = window as AccelWindow;
|
||||
// True when this frame's paint was requested via canvas.requestPaint()
|
||||
// — the snapshot then bakes the root's current compositor-applied
|
||||
// opacity, so the legacy ratio correction below must be skipped.
|
||||
let usedRequestPaint = false;
|
||||
// Re-project CSS 3D contexts for THIS frame (threeDProjection.ts) so
|
||||
// their WebGL canvases are fresh before being drawImage-composited
|
||||
// below. Must run before the paint wait for the same reason as the
|
||||
@@ -319,38 +359,36 @@ export async function captureDrawElementFrame(
|
||||
// Zero-sized or not-yet-configured canvas — skip this frame.
|
||||
}
|
||||
}
|
||||
// drawElementImage does not reflect post-paint changes to compositor-
|
||||
// applied properties on the captured element ITSELF — opacity, transform,
|
||||
// and filter on the root are applied by its parent at composite time and
|
||||
// never enter the root's content snapshot (baked at the load-time/base
|
||||
// value). Re-apply them to the 2D context, comparing against the base
|
||||
// recorded at injection so a static root is a no-op (no double-apply / no
|
||||
// regression) and an animated root is corrected.
|
||||
// Two distinct behaviours, both measured:
|
||||
// • opacity — the content snapshot DOES bake the root's load-time
|
||||
// opacity, so correct by the ratio current/base (static ⇒ ratio 1 ⇒
|
||||
// no-op; animated ⇒ corrected).
|
||||
// • transform — the snapshot NEVER bakes the root's own transform (even
|
||||
// a static transform renders unscaled), so apply the current matrix
|
||||
// unconditionally about the transform-origin (no transform ⇒ no-op).
|
||||
// filter is intentionally NOT corrected: the per-frame sentinel repaint
|
||||
// already bakes it into the snapshot (correcting it double-applies).
|
||||
const __rw = window as unknown as {
|
||||
__HF_ROOT_PROPS__?: boolean;
|
||||
__HF_ROOT_BASE_OPACITY__?: number;
|
||||
};
|
||||
let __appliedAlpha = false;
|
||||
// Root compositor-prop corrections. Two distinct behaviours:
|
||||
// • transform — the snapshot NEVER bakes the captured element's
|
||||
// own transform (even a static one renders unscaled — the
|
||||
// parent applies it at composite time; verified still true
|
||||
// under the requestPaint contract, crbug 529829538 triage,
|
||||
// probed on 151 + 152 canary 2026-07-07). Apply the current
|
||||
// matrix unconditionally about the transform-origin.
|
||||
// • opacity — DEPENDS on how this frame's paint was produced.
|
||||
// A requestPaint()-driven paint bakes the root's CURRENT
|
||||
// opacity into the snapshot (pixel alpha) — the legacy ratio
|
||||
// correction DOUBLE-APPLIES there (~0.92 → 0.85 effective,
|
||||
// 30.1 dB self-verify failure on a root-fade A/B) and must be
|
||||
// skipped. BeginFrame captures (sync=false) and builds without
|
||||
// requestPaint keep the pre-existing ratio correction: on
|
||||
// those paths the snapshot holds the load-time opacity.
|
||||
// filter is never corrected (paint wait bakes it).
|
||||
let __appliedTransform = false;
|
||||
if (__rw.__HF_ROOT_PROPS__) {
|
||||
let __appliedAlpha = false;
|
||||
if (aw.__HF_ROOT_PROPS__) {
|
||||
try {
|
||||
const rcs = getComputedStyle(root);
|
||||
const baseOp = __rw.__HF_ROOT_BASE_OPACITY__ ?? 1;
|
||||
const curOp = parseFloat(rcs.opacity);
|
||||
if (baseOp > 0.001 && Number.isFinite(curOp)) {
|
||||
const ratio = curOp / baseOp;
|
||||
if (Math.abs(ratio - 1) > 0.002) {
|
||||
ctx.globalAlpha = Math.max(0, Math.min(1, ratio));
|
||||
__appliedAlpha = true;
|
||||
if (!usedRequestPaint) {
|
||||
const baseOp = aw.__HF_ROOT_BASE_OPACITY__ ?? 1;
|
||||
const curOp = parseFloat(rcs.opacity);
|
||||
if (baseOp > 0.001 && Number.isFinite(curOp)) {
|
||||
const ratio = curOp / baseOp;
|
||||
if (Math.abs(ratio - 1) > 0.002) {
|
||||
ctx.globalAlpha = Math.max(0, Math.min(1, ratio));
|
||||
__appliedAlpha = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
const curTransform = rcs.transform;
|
||||
@@ -418,20 +456,14 @@ export async function captureDrawElementFrame(
|
||||
canvas.addEventListener("paint", onPaint);
|
||||
// Force an invalidation so a paint is guaranteed even when this frame's
|
||||
// seek produced no paint-level change (static scene, or transform-only
|
||||
// GSAP updates that are compositor-side and never repaint). The sentinel
|
||||
// is a 1x1 canvas child OUTSIDE the captured root (see
|
||||
// injectDrawElementCanvas): toggling its background is a PAINT-level
|
||||
// change (layout/transform toggles do NOT fire the paint event), so a
|
||||
// paint + fresh snapshot follow promptly — without the sentinel ever
|
||||
// appearing in drawElementImage(root) output.
|
||||
const tick = document.getElementById("__hf_de_tick");
|
||||
if (tick) {
|
||||
tick.style.backgroundColor =
|
||||
tick.style.backgroundColor === "rgb(0, 0, 0)" ? "rgb(1, 1, 1)" : "rgb(0, 0, 0)";
|
||||
}
|
||||
// GSAP updates that are compositor-side and never repaint). Sentinel
|
||||
// dirty + requestPaint(), installed by injectDrawElementCanvas — see
|
||||
// __hfDeInvalidate there for the full mechanism/rationale.
|
||||
usedRequestPaint = aw.__hfDeInvalidate?.() === true;
|
||||
// Safety net: if the paint event doesn't arrive (feature drift /
|
||||
// throttled page), fall back to an unsynchronized draw after 250 ms —
|
||||
// worst case one-frame-stale content rather than a hung render.
|
||||
// worst case one-frame-stale content (the root's alpha may lag its
|
||||
// transform by that frame) rather than a hung render.
|
||||
setTimeout(() => {
|
||||
canvas.removeEventListener("paint", onPaint);
|
||||
drawAndEncode();
|
||||
@@ -682,6 +714,9 @@ export async function produceDrawElementFrame(
|
||||
__hf_accel_canvases?: HTMLCanvasElement[];
|
||||
__hf_canvas_2d?: HTMLCanvasElement[];
|
||||
__hf3d?: { update: () => void };
|
||||
__hfDeInvalidate?: () => boolean;
|
||||
__HF_ROOT_PROPS__?: boolean;
|
||||
__HF_ROOT_BASE_OPACITY__?: number;
|
||||
};
|
||||
const aw = window as AccelWindow;
|
||||
aw.__hf3d?.update();
|
||||
@@ -700,6 +735,7 @@ export async function produceDrawElementFrame(
|
||||
|
||||
return new Promise<void>((resolveCapture, rejectCapture) => {
|
||||
let settled = false;
|
||||
let usedRequestPaint = false;
|
||||
const drawAndKick = () => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
@@ -729,27 +765,23 @@ export async function produceDrawElementFrame(
|
||||
// skip
|
||||
}
|
||||
}
|
||||
// Re-apply the captured root's compositor-applied opacity/transform —
|
||||
// identical to the serial path (see drawAndEncode). drawElementImage does
|
||||
// not reflect post-paint changes to these on the root itself; without this
|
||||
// the worker path damages comps with an animated root opacity/transform
|
||||
// (the serial path corrects it, so worker-on diverged from serial DE).
|
||||
const __rw = window as unknown as {
|
||||
__HF_ROOT_PROPS__?: boolean;
|
||||
__HF_ROOT_BASE_OPACITY__?: number;
|
||||
};
|
||||
let __appliedAlpha = false;
|
||||
// Root compositor-prop corrections — identical to the serial path
|
||||
// (see drawAndEncode's comment): transform always; opacity ratio
|
||||
// only when this frame's paint was NOT requestPaint-driven.
|
||||
let __appliedTransform = false;
|
||||
if (__rw.__HF_ROOT_PROPS__) {
|
||||
let __appliedAlpha = false;
|
||||
if (aw.__HF_ROOT_PROPS__) {
|
||||
try {
|
||||
const rcs = getComputedStyle(root);
|
||||
const baseOp = __rw.__HF_ROOT_BASE_OPACITY__ ?? 1;
|
||||
const curOp = parseFloat(rcs.opacity);
|
||||
if (baseOp > 0.001 && Number.isFinite(curOp)) {
|
||||
const ratio = curOp / baseOp;
|
||||
if (Math.abs(ratio - 1) > 0.002) {
|
||||
ctx.globalAlpha = Math.max(0, Math.min(1, ratio));
|
||||
__appliedAlpha = true;
|
||||
if (!usedRequestPaint) {
|
||||
const baseOp = aw.__HF_ROOT_BASE_OPACITY__ ?? 1;
|
||||
const curOp = parseFloat(rcs.opacity);
|
||||
if (baseOp > 0.001 && Number.isFinite(curOp)) {
|
||||
const ratio = curOp / baseOp;
|
||||
if (Math.abs(ratio - 1) > 0.002) {
|
||||
ctx.globalAlpha = Math.max(0, Math.min(1, ratio));
|
||||
__appliedAlpha = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
const curTransform = rcs.transform;
|
||||
@@ -816,11 +848,9 @@ export async function produceDrawElementFrame(
|
||||
drawAndKick();
|
||||
};
|
||||
canvas.addEventListener("paint", onPaint);
|
||||
const tick = document.getElementById("__hf_de_tick");
|
||||
if (tick) {
|
||||
tick.style.backgroundColor =
|
||||
tick.style.backgroundColor === "rgb(0, 0, 0)" ? "rgb(1, 1, 1)" : "rgb(0, 0, 0)";
|
||||
}
|
||||
// Sentinel dirty + requestPaint() — see __hfDeInvalidate in
|
||||
// injectDrawElementCanvas.
|
||||
usedRequestPaint = aw.__hfDeInvalidate?.() === true;
|
||||
setTimeout(() => {
|
||||
canvas.removeEventListener("paint", onPaint);
|
||||
drawAndKick();
|
||||
@@ -835,8 +865,9 @@ export async function produceDrawElementFrame(
|
||||
|
||||
/**
|
||||
* P6 prototype (HF_DE_BATCH): batch-produce N consecutive frames in ONE CDP
|
||||
* round-trip. In-page loop per frame: `__hf.seek(t)` → paint-wait (tick toggle +
|
||||
* canvas `paint` event) → drawElementImage composite → createImageBitmap →
|
||||
* round-trip. In-page loop per frame: `__hf.seek(t)` → paint-wait
|
||||
* (__hfDeInvalidate: sentinel dirty + requestPaint, then the canvas `paint`
|
||||
* event) → drawElementImage composite → createImageBitmap →
|
||||
* postMessage to the encode worker. Bitmaps are posted per-frame (encode starts
|
||||
* immediately); only the CDP protocol round-trips are amortized N-fold.
|
||||
* Micro-pipeline inside the batch: frame i+1's seek/paint-wait overlaps frame
|
||||
@@ -910,11 +941,13 @@ export async function produceDrawElementFrameBatch(
|
||||
__hf_accel_canvases?: HTMLCanvasElement[];
|
||||
__hf3d?: { update: () => void };
|
||||
__hf?: { seek?: (t: number) => void };
|
||||
__hfDeInvalidate?: () => boolean;
|
||||
__HF_ROOT_PROPS__?: boolean;
|
||||
__HF_ROOT_BASE_OPACITY__?: number;
|
||||
__hfEncWorker?: Worker;
|
||||
};
|
||||
const aw = window as AccelWindow;
|
||||
let usedRequestPaint = false;
|
||||
|
||||
const waitPaint = (): Promise<void> =>
|
||||
new Promise((res) => {
|
||||
@@ -926,11 +959,9 @@ export async function produceDrawElementFrameBatch(
|
||||
res();
|
||||
};
|
||||
canvas.addEventListener("paint", settle);
|
||||
const tick = document.getElementById("__hf_de_tick");
|
||||
if (tick) {
|
||||
tick.style.backgroundColor =
|
||||
tick.style.backgroundColor === "rgb(0, 0, 0)" ? "rgb(1, 1, 1)" : "rgb(0, 0, 0)";
|
||||
}
|
||||
// Sentinel dirty + requestPaint() — see __hfDeInvalidate in
|
||||
// injectDrawElementCanvas.
|
||||
usedRequestPaint = aw.__hfDeInvalidate?.() === true;
|
||||
setTimeout(settle, 250);
|
||||
});
|
||||
|
||||
@@ -978,20 +1009,23 @@ export async function produceDrawElementFrameBatch(
|
||||
// skip
|
||||
}
|
||||
}
|
||||
// Root compositor-applied opacity/transform correction — mirrors
|
||||
// produceDrawElementFrame (see its comment).
|
||||
let appliedAlpha = false;
|
||||
// Root compositor-prop corrections — mirrors produceDrawElementFrame
|
||||
// (see drawAndEncode's comment): transform always; opacity ratio only
|
||||
// when this frame's paint was NOT requestPaint-driven.
|
||||
let appliedTransform = false;
|
||||
let appliedAlpha = false;
|
||||
if (aw.__HF_ROOT_PROPS__) {
|
||||
try {
|
||||
const rcs = getComputedStyle(root);
|
||||
const baseOp = aw.__HF_ROOT_BASE_OPACITY__ ?? 1;
|
||||
const curOp = parseFloat(rcs.opacity);
|
||||
if (baseOp > 0.001 && Number.isFinite(curOp)) {
|
||||
const ratio = curOp / baseOp;
|
||||
if (Math.abs(ratio - 1) > 0.002) {
|
||||
ctx.globalAlpha = Math.max(0, Math.min(1, ratio));
|
||||
appliedAlpha = true;
|
||||
if (!usedRequestPaint) {
|
||||
const baseOp = aw.__HF_ROOT_BASE_OPACITY__ ?? 1;
|
||||
const curOp = parseFloat(rcs.opacity);
|
||||
if (baseOp > 0.001 && Number.isFinite(curOp)) {
|
||||
const ratio = curOp / baseOp;
|
||||
if (Math.abs(ratio - 1) > 0.002) {
|
||||
ctx.globalAlpha = Math.max(0, Math.min(1, ratio));
|
||||
appliedAlpha = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
const curTransform = rcs.transform;
|
||||
|
||||
@@ -501,18 +501,6 @@ async function initDrawElementOrTransparentBackground(
|
||||
"hint forced screenshot capture (e.g. raw requestAnimationFrame composition).",
|
||||
);
|
||||
}
|
||||
// Retract the per-page autoAlpha rewrite flag when a runtime gate routes the
|
||||
// session to screenshot mode. evaluateOnNewDocument already fired; a follow-up
|
||||
// evaluate overrides it in the live page context so hideTransparentAutoAlpha-
|
||||
// Targets does not hide elements on the fallback screenshot render
|
||||
// (up to 21 dB damage if not retracted, A/B proven 2026-06-12).
|
||||
async function retractAutoAlphaFlag(): Promise<void> {
|
||||
await page.evaluate(() => {
|
||||
(
|
||||
window as Window & { __HF_FAST_CAPTURE_AUTOALPHA__?: boolean }
|
||||
).__HF_FAST_CAPTURE_AUTOALPHA__ = false;
|
||||
});
|
||||
}
|
||||
if (useDrawElement) {
|
||||
session.isSwiftShader = await detectSwiftShader(page);
|
||||
const transparent = session.options.format === "png";
|
||||
@@ -521,7 +509,6 @@ async function initDrawElementOrTransparentBackground(
|
||||
if (transparent) {
|
||||
await initTransparentBackground(session.page);
|
||||
}
|
||||
await retractAutoAlphaFlag();
|
||||
// Static-frame dedup is capture-mode-independent (the serial path reuses
|
||||
// lastFrameBuffer regardless of how the frame was captured) and lossless
|
||||
// (anchor-verified). A comp only reaches THIS fallback with useDrawElement=true
|
||||
@@ -646,14 +633,10 @@ async function initDrawElementOrTransparentBackground(
|
||||
// screenshots would capture black <video> boxes, and once the canvas is
|
||||
// injected they can never be retaken. The capture stage completes the
|
||||
// init after prepareCaptureSessionForReuse attaches the injector.
|
||||
// Retract the autoAlpha rewrite flag while deferred — if no path ever
|
||||
// completes the init (e.g. the disk path takes over), the session
|
||||
// captures via screenshot, where the armed flag causes measured damage.
|
||||
if (!session.onBeforeCapture && !forceDE) {
|
||||
const hasVideos = await page.evaluate(() => document.querySelector("video") !== null);
|
||||
if (hasVideos) {
|
||||
session.deInitDeferred = true;
|
||||
await retractAutoAlphaFlag();
|
||||
logInitPhase("drawElement init deferred: video comp awaiting frame injector");
|
||||
return;
|
||||
}
|
||||
@@ -721,15 +704,10 @@ async function finalizeDrawElementInit(
|
||||
* Complete a deferred drawElement init (see CaptureSession.deInitDeferred).
|
||||
* Call after prepareCaptureSessionForReuse has attached the video-frame
|
||||
* injector; no-op when the session is not deferred or still has no injector.
|
||||
* Re-asserts the autoAlpha rewrite flag retracted at deferral time.
|
||||
*/
|
||||
export async function completeDeferredDrawElementInit(session: CaptureSession): Promise<void> {
|
||||
if (!session.deInitDeferred || !session.onBeforeCapture) return;
|
||||
const page = session.page;
|
||||
await page.evaluate(() => {
|
||||
(window as Window & { __HF_FAST_CAPTURE_AUTOALPHA__?: boolean }).__HF_FAST_CAPTURE_AUTOALPHA__ =
|
||||
true;
|
||||
});
|
||||
const logInitPhase = (phase: string) =>
|
||||
console.log(`[initSession:${session.captureMode}] ${phase} (deferred drawElement init)`);
|
||||
await finalizeDrawElementInit(session, page, logInitPhase, {
|
||||
@@ -844,31 +822,24 @@ export async function createCaptureSession(
|
||||
if (useDrawElement) {
|
||||
await page.evaluateOnNewDocument(instrumentAcceleratedCanvases);
|
||||
}
|
||||
// Signal the producer's GSAP stub to rewrite `opacity` → `autoAlpha` in tween
|
||||
// vars (stacked opacity-0 caption layers break drawElementImage capture).
|
||||
//
|
||||
// DEFAULT OFF (opt in with HF_FAST_CAPTURE_AUTOALPHA=true). The rewrite is baked
|
||||
// at tween-creation (page load) and `retractAutoAlphaFlag` (flag-only) can't
|
||||
// un-bake it: GSAP autoAlpha sets visibility:hidden under seek capture and renders
|
||||
// ~28 dB below a clean opacity baseline (corpus eval 2026-06-16, e.g.
|
||||
// 05f22830/06167790: autoAlpha-off = ∞, autoAlpha-on = 28 dB). The rewrite was a
|
||||
// workaround for the stacked-fade opacity-layer drop (crbug 521861819), now fixed
|
||||
// in Chrome 151 — so it damages more than it fixes. Re-enable per render only if a
|
||||
// drawElement comp shows transparent-layer drop on the pinned 151 floor.
|
||||
if (useDrawElement && process.env.HF_FAST_CAPTURE_AUTOALPHA === "true") {
|
||||
await page.evaluateOnNewDocument(() => {
|
||||
(
|
||||
window as Window & { __HF_FAST_CAPTURE_AUTOALPHA__?: boolean }
|
||||
).__HF_FAST_CAPTURE_AUTOALPHA__ = true;
|
||||
});
|
||||
// The opacity → autoAlpha tween rewrite was RETIRED with the requestPaint
|
||||
// contract adoption (crbug 529829538 closed WAI): a requestPaint-driven
|
||||
// paint refreshes nested opacity layers natively, and the rewrite itself
|
||||
// measured ~28 dB of damage on comps whose fades it touched. Warn instead
|
||||
// of silently ignoring the old escape hatch.
|
||||
if (process.env.HF_FAST_CAPTURE_AUTOALPHA !== undefined) {
|
||||
console.warn(
|
||||
"[engine] HF_FAST_CAPTURE_AUTOALPHA is retired and ignored — the requestPaint " +
|
||||
"paint contract captures animated opacity natively (see drawElementService.ts).",
|
||||
);
|
||||
}
|
||||
// Re-apply the captured root's own computed opacity to the 2D context:
|
||||
// drawElementImage does not reflect post-paint changes to compositor-applied
|
||||
// properties on the captured element itself (the root's opacity is applied by
|
||||
// its parent at composite time, never baked into its content snapshot), so an
|
||||
// animated root fade renders at full opacity. captureDrawElementFrame corrects
|
||||
// this by the ratio current/base opacity (no-op for a static root). On by
|
||||
// default; disable with HF_FAST_CAPTURE_ROOT_PROPS=false.
|
||||
// Re-apply the captured root's own compositor-applied props to the 2D
|
||||
// context where the snapshot does not carry them (see the correction
|
||||
// comment in drawElementService.ts drawAndEncode: transform always —
|
||||
// never baked, verified under the requestPaint contract 2026-07-07;
|
||||
// opacity ratio only on non-requestPaint paints, where the snapshot holds
|
||||
// the load-time value). On by default; disable with
|
||||
// HF_FAST_CAPTURE_ROOT_PROPS=false.
|
||||
if (useDrawElement && process.env.HF_FAST_CAPTURE_ROOT_PROPS !== "false") {
|
||||
await page.evaluateOnNewDocument(() => {
|
||||
(window as unknown as { __HF_ROOT_PROPS__?: boolean }).__HF_ROOT_PROPS__ = true;
|
||||
|
||||
Reference in New Issue
Block a user