diff --git a/packages/producer/src/services/distributed/assemble.ts b/packages/producer/src/services/distributed/assemble.ts
index 214cfc451c..ec6eb98f1d 100644
--- a/packages/producer/src/services/distributed/assemble.ts
+++ b/packages/producer/src/services/distributed/assemble.ts
@@ -37,6 +37,7 @@ import { dirname, join } from "node:path";
import { applyFaststart, muxVideoWithAudio, runFfmpeg } from "@hyperframes/engine";
import { fpsToFfmpegArg } from "@hyperframes/core";
import { defaultLogger, type ProducerLogger } from "../../logger.js";
+import { formatExportFrameName } from "../../utils/paths.js";
import { padOrTrimAudioToVideoFrameCount } from "../render/audioPadTrim.js";
import type { ChunkSliceJson } from "../render/stages/freezePlan.js";
import type { DistributedFormat } from "./shared.js";
@@ -397,7 +398,7 @@ function mergePngFrameDirs(
throw new Error(`[assemble] png-sequence chunk has no frames: ${chunkDir}`);
}
for (const frame of frames) {
- const dst = join(outputPath, `frame_${String(globalIdx + 1).padStart(6, "0")}.png`);
+ const dst = join(outputPath, formatExportFrameName(globalIdx, "png"));
cpSync(join(chunkDir, frame), dst);
globalIdx += 1;
}
diff --git a/packages/producer/src/services/hdrCompositor.ts b/packages/producer/src/services/hdrCompositor.ts
index 35c2bc9320..4659dde380 100644
--- a/packages/producer/src/services/hdrCompositor.ts
+++ b/packages/producer/src/services/hdrCompositor.ts
@@ -34,7 +34,7 @@ import {
import type { ProducerLogger } from "../logger.js";
import { type HdrImageTransferCache } from "./hdrImageTransferCache.js";
import { writeFileExclusiveSync } from "./render/shared.js";
-import { type HdrPerfCollector, addHdrTiming } from "./render/hdrPerf.js";
+import { type HdrPerfCollector, timeHdrPhase, timeHdrPhaseAsync } from "./render/hdrPerf.js";
// ─── Diagnostic helpers ────────────────────────────────────────────────────
@@ -169,25 +169,19 @@ export function blitHdrVideoLayer(
try {
if (hdrPerf) hdrPerf.hdrVideoLayerBlits += 1;
- let timingStart = Date.now();
- const bytesRead = readSync(
- frameSource.fd,
- frameSource.scratch,
- 0,
- frameSource.frameSize,
- frameOffset,
+ const bytesRead = timeHdrPhase(hdrPerf, "hdrVideoReadDecodeMs", () =>
+ readSync(frameSource.fd, frameSource.scratch, 0, frameSource.frameSize, frameOffset),
);
if (bytesRead !== frameSource.frameSize) return;
const hdrRgb = frameSource.scratch;
const srcW = frameSource.width;
const srcH = frameSource.height;
- addHdrTiming(hdrPerf, "hdrVideoReadDecodeMs", timingStart);
// Convert between HDR transfer functions if source doesn't match output
if (sourceTransfer && targetTransfer && sourceTransfer !== targetTransfer) {
- timingStart = Date.now();
- convertTransfer(hdrRgb, sourceTransfer, targetTransfer);
- addHdrTiming(hdrPerf, "hdrVideoTransferMs", timingStart);
+ timeHdrPhase(hdrPerf, "hdrVideoTransferMs", () =>
+ convertTransfer(hdrRgb, sourceTransfer, targetTransfer),
+ );
}
const viewportMatrix = parseTransformMatrix(el.transform);
@@ -241,54 +235,54 @@ export function blitHdrVideoLayer(
Math.abs(viewportMatrix[3]! - 1) < 0.001
);
- timingStart = Date.now();
- if (viewportMatrix && !isTranslationOnly) {
- if (clipped && log) {
- log.debug(
- `HDR clip rect on affine-transformed element ${el.id} — clip not applied (affine scissor not yet supported)`,
+ timeHdrPhase(hdrPerf, "hdrVideoBlitMs", () => {
+ if (viewportMatrix && !isTranslationOnly) {
+ if (clipped && log) {
+ log.debug(
+ `HDR clip rect on affine-transformed element ${el.id} — clip not applied (affine scissor not yet supported)`,
+ );
+ }
+ blitRgb48leAffine(
+ canvas,
+ hdrRgb,
+ viewportMatrix,
+ srcW,
+ srcH,
+ width,
+ height,
+ el.opacity < 0.999 ? el.opacity : undefined,
+ borderRadiusParam,
+ );
+ } else if (clipped) {
+ // Crop the source buffer to the clipped region before blitting
+ const croppedBuf = cropRgb48le(hdrRgb, srcW, srcH, blitSrcX, blitSrcY, blitW, blitH);
+ blitRgb48leRegion(
+ canvas,
+ croppedBuf,
+ blitX,
+ blitY,
+ blitW,
+ blitH,
+ width,
+ height,
+ el.opacity < 0.999 ? el.opacity : undefined,
+ borderRadiusParam,
+ );
+ } else {
+ blitRgb48leRegion(
+ canvas,
+ hdrRgb,
+ el.x,
+ el.y,
+ srcW,
+ srcH,
+ width,
+ height,
+ el.opacity < 0.999 ? el.opacity : undefined,
+ borderRadiusParam,
);
}
- blitRgb48leAffine(
- canvas,
- hdrRgb,
- viewportMatrix,
- srcW,
- srcH,
- width,
- height,
- el.opacity < 0.999 ? el.opacity : undefined,
- borderRadiusParam,
- );
- } else if (clipped) {
- // Crop the source buffer to the clipped region before blitting
- const croppedBuf = cropRgb48le(hdrRgb, srcW, srcH, blitSrcX, blitSrcY, blitW, blitH);
- blitRgb48leRegion(
- canvas,
- croppedBuf,
- blitX,
- blitY,
- blitW,
- blitH,
- width,
- height,
- el.opacity < 0.999 ? el.opacity : undefined,
- borderRadiusParam,
- );
- } else {
- blitRgb48leRegion(
- canvas,
- hdrRgb,
- el.x,
- el.y,
- srcW,
- srcH,
- width,
- height,
- el.opacity < 0.999 ? el.opacity : undefined,
- borderRadiusParam,
- );
- }
- addHdrTiming(hdrPerf, "hdrVideoBlitMs", timingStart);
+ });
} catch (err) {
if (log) {
log.debug(`HDR blit failed for ${el.id}`, {
@@ -343,12 +337,11 @@ export function blitHdrImageLayer(
// The cache returns `buf.data` unchanged when no conversion is needed,
// and otherwise returns a per-(imageId, targetTransfer) buffer that was
// converted exactly once and reused across every subsequent frame.
- let timingStart = Date.now();
- const hdrRgb =
+ const hdrRgb = timeHdrPhase(hdrPerf, "hdrImageTransferMs", () =>
sourceTransfer && targetTransfer
? hdrImageTransferCache.getConverted(el.id, sourceTransfer, targetTransfer, buf.data)
- : buf.data;
- addHdrTiming(hdrPerf, "hdrImageTransferMs", timingStart);
+ : buf.data,
+ );
const viewportMatrix = parseTransformMatrix(el.transform);
@@ -356,34 +349,34 @@ export function blitHdrImageLayer(
const hasBorderRadius = br[0] > 0 || br[1] > 0 || br[2] > 0 || br[3] > 0;
const borderRadiusParam = hasBorderRadius ? br : undefined;
- timingStart = Date.now();
- if (viewportMatrix) {
- blitRgb48leAffine(
- canvas,
- hdrRgb,
- viewportMatrix,
- buf.width,
- buf.height,
- width,
- height,
- el.opacity < 0.999 ? el.opacity : undefined,
- borderRadiusParam,
- );
- } else {
- blitRgb48leRegion(
- canvas,
- hdrRgb,
- el.x,
- el.y,
- buf.width,
- buf.height,
- width,
- height,
- el.opacity < 0.999 ? el.opacity : undefined,
- borderRadiusParam,
- );
- }
- addHdrTiming(hdrPerf, "hdrImageBlitMs", timingStart);
+ timeHdrPhase(hdrPerf, "hdrImageBlitMs", () => {
+ if (viewportMatrix) {
+ blitRgb48leAffine(
+ canvas,
+ hdrRgb,
+ viewportMatrix,
+ buf.width,
+ buf.height,
+ width,
+ height,
+ el.opacity < 0.999 ? el.opacity : undefined,
+ borderRadiusParam,
+ );
+ } else {
+ blitRgb48leRegion(
+ canvas,
+ hdrRgb,
+ el.x,
+ el.y,
+ buf.width,
+ buf.height,
+ width,
+ height,
+ el.opacity < 0.999 ? el.opacity : undefined,
+ borderRadiusParam,
+ );
+ }
+ });
} catch (err) {
if (log) {
log.debug(`HDR image blit failed for ${el.id}`, {
@@ -505,6 +498,7 @@ export async function compositeHdrFrame(
// generation (their
replacements must be hidden from sibling
// screenshots). The actual blit is skipped in the compositing loop below.
const layers = groupIntoLayers(filteredStacking);
+ const allElementIds = fullStacking.map((e) => e.id);
const shouldLog = debugDumpEnabled && debugFrameIndex >= 0;
if (shouldLog) {
@@ -622,49 +616,46 @@ export async function compositeHdrFrame(
// (root background, sibling scenes' static content, the painted
// border/box-shadow of cards, etc.) and the resulting opaque
// pixels overwrite previously composited HDR content beneath.
- const allElementIds = fullStacking.map((e) => e.id);
const layerIds = new Set(layer.elementIds);
const hideIds = allElementIds.filter((id) => !layerIds.has(id));
if (hdrPerf) hdrPerf.domLayerCaptures += 1;
// 1. Seek GSAP to restore all animated properties from clean state
- let timingStart = Date.now();
- await domSession.page.evaluate((t: number) => {
- if (window.__hf && typeof window.__hf.seek === "function") window.__hf.seek(t);
- }, time);
- addHdrTiming(hdrPerf, "domLayerSeekMs", timingStart);
+ await timeHdrPhaseAsync(hdrPerf, "domLayerSeekMs", () =>
+ domSession.page.evaluate((t: number) => {
+ if (window.__hf && typeof window.__hf.seek === "function") window.__hf.seek(t);
+ }, time),
+ );
// 2. Run frame injector to set correct SDR video visibility
if (beforeCaptureHook) {
- timingStart = Date.now();
- await beforeCaptureHook(domSession.page, time);
- addHdrTiming(hdrPerf, "domLayerInjectMs", timingStart);
+ await timeHdrPhaseAsync(hdrPerf, "domLayerInjectMs", () =>
+ beforeCaptureHook(domSession.page, time),
+ );
}
// 3. Install the mask (mass-hide stylesheet + inline-hide non-layer ids)
- timingStart = Date.now();
- await applyDomLayerMask(domSession.page, layer.elementIds, hideIds);
- addHdrTiming(hdrPerf, "domMaskApplyMs", timingStart);
+ await timeHdrPhaseAsync(hdrPerf, "domMaskApplyMs", () =>
+ applyDomLayerMask(domSession.page, layer.elementIds, hideIds),
+ );
// 4. Screenshot
- timingStart = Date.now();
- const domPng = await captureAlphaPng(domSession.page, width, height);
- addHdrTiming(hdrPerf, "domScreenshotMs", timingStart);
+ const domPng = await timeHdrPhaseAsync(hdrPerf, "domScreenshotMs", () =>
+ captureAlphaPng(domSession.page, width, height),
+ );
// 5. Tear down the mask
- timingStart = Date.now();
- await removeDomLayerMask(domSession.page, hideIds);
- addHdrTiming(hdrPerf, "domMaskRemoveMs", timingStart);
+ await timeHdrPhaseAsync(hdrPerf, "domMaskRemoveMs", () =>
+ removeDomLayerMask(domSession.page, hideIds),
+ );
try {
- timingStart = Date.now();
- const { data: domRgba } = decodePng(domPng);
- addHdrTiming(hdrPerf, "domPngDecodeMs", timingStart);
+ const { data: domRgba } = timeHdrPhase(hdrPerf, "domPngDecodeMs", () => decodePng(domPng));
const before = shouldLog ? countNonZeroRgb48(canvas) : 0;
const alphaPixels = shouldLog ? countNonZeroAlpha(domRgba) : 0;
- timingStart = Date.now();
- blitRgba8OverRgb48le(domRgba, canvas, width, height, compositeTransfer);
- addHdrTiming(hdrPerf, "domBlitMs", timingStart);
+ timeHdrPhase(hdrPerf, "domBlitMs", () =>
+ blitRgba8OverRgb48le(domRgba, canvas, width, height, compositeTransfer),
+ );
if (shouldLog && debugDumpDir) {
const after = countNonZeroRgb48(canvas);
const dumpName = `frame_${String(debugFrameIndex).padStart(4, "0")}_layer_${String(layerIdx).padStart(2, "0")}_dom.png`;
diff --git a/packages/producer/src/services/render/hdrPerf.ts b/packages/producer/src/services/render/hdrPerf.ts
index c033fad5c3..0f23c9bc81 100644
--- a/packages/producer/src/services/render/hdrPerf.ts
+++ b/packages/producer/src/services/render/hdrPerf.ts
@@ -90,6 +90,30 @@ export function addHdrTiming(
perf.timings[key] += Date.now() - startMs;
}
+export function timeHdrPhase(
+ perf: HdrPerfCollector | undefined,
+ key: HdrPerfTimingKey,
+ fn: () => T,
+): T {
+ if (!perf) return fn();
+ const start = Date.now();
+ const result = fn();
+ addHdrTiming(perf, key, start);
+ return result;
+}
+
+export async function timeHdrPhaseAsync(
+ perf: HdrPerfCollector | undefined,
+ key: HdrPerfTimingKey,
+ fn: () => Promise,
+): Promise {
+ if (!perf) return fn();
+ const start = Date.now();
+ const result = await fn();
+ addHdrTiming(perf, key, start);
+ return result;
+}
+
function averageTiming(totalMs: number, count: number): number {
return count > 0 ? Math.round((totalMs / count) * 100) / 100 : 0;
}
diff --git a/packages/producer/src/services/render/stages/captureHdrFrameShared.ts b/packages/producer/src/services/render/stages/captureHdrFrameShared.ts
index a6fa7dc38a..a6c0458677 100644
--- a/packages/producer/src/services/render/stages/captureHdrFrameShared.ts
+++ b/packages/producer/src/services/render/stages/captureHdrFrameShared.ts
@@ -29,7 +29,7 @@ import {
blitHdrVideoLayer,
closeHdrVideoFrameSource,
} from "../../hdrCompositor.js";
-import { type HdrPerfCollector, addHdrTiming } from "../hdrPerf.js";
+import { type HdrPerfCollector, timeHdrPhase, timeHdrPhaseAsync } from "../hdrPerf.js";
// ─── Hybrid path gating + partitioning ─────────────────────────────────────
@@ -141,15 +141,15 @@ export async function captureSceneIntoBuffer(a: CaptureSceneArgs): Promise
log,
frameIdx,
} = a;
- let timingStart = Date.now();
- await session.page.evaluate((t: number) => {
- if (window.__hf && typeof window.__hf.seek === "function") window.__hf.seek(t);
- }, time);
- addHdrTiming(hdrPerf, "domLayerSeekMs", timingStart);
+ await timeHdrPhaseAsync(hdrPerf, "domLayerSeekMs", () =>
+ session.page.evaluate((t: number) => {
+ if (window.__hf && typeof window.__hf.seek === "function") window.__hf.seek(t);
+ }, time),
+ );
if (beforeCaptureHook) {
- timingStart = Date.now();
- await beforeCaptureHook(session.page, time);
- addHdrTiming(hdrPerf, "domLayerInjectMs", timingStart);
+ await timeHdrPhaseAsync(hdrPerf, "domLayerInjectMs", () =>
+ beforeCaptureHook(session.page, time),
+ );
}
for (const el of stackingInfo) {
if (!el.isHdr || !sceneIds.has(el.id)) continue;
@@ -188,22 +188,20 @@ export async function captureSceneIntoBuffer(a: CaptureSceneArgs): Promise
.map((e) => e.id)
.filter((id) => !sceneIds.has(id) || nativeHdrIds.has(id));
if (hdrPerf) hdrPerf.domLayerCaptures += 1;
- timingStart = Date.now();
- await applyDomLayerMask(session.page, showIds, hideIds);
- addHdrTiming(hdrPerf, "domMaskApplyMs", timingStart);
- timingStart = Date.now();
- const domPng = await captureAlphaPng(session.page, width, height);
- addHdrTiming(hdrPerf, "domScreenshotMs", timingStart);
- timingStart = Date.now();
- await removeDomLayerMask(session.page, hideIds);
- addHdrTiming(hdrPerf, "domMaskRemoveMs", timingStart);
+ await timeHdrPhaseAsync(hdrPerf, "domMaskApplyMs", () =>
+ applyDomLayerMask(session.page, showIds, hideIds),
+ );
+ const domPng = await timeHdrPhaseAsync(hdrPerf, "domScreenshotMs", () =>
+ captureAlphaPng(session.page, width, height),
+ );
+ await timeHdrPhaseAsync(hdrPerf, "domMaskRemoveMs", () =>
+ removeDomLayerMask(session.page, hideIds),
+ );
try {
- timingStart = Date.now();
- const { data: domRgba } = decodePng(domPng);
- addHdrTiming(hdrPerf, "domPngDecodeMs", timingStart);
- timingStart = Date.now();
- blitRgba8OverRgb48le(domRgba, sceneBuf, width, height, compositeTransfer);
- addHdrTiming(hdrPerf, "domBlitMs", timingStart);
+ const { data: domRgba } = timeHdrPhase(hdrPerf, "domPngDecodeMs", () => decodePng(domPng));
+ timeHdrPhase(hdrPerf, "domBlitMs", () =>
+ blitRgba8OverRgb48le(domRgba, sceneBuf, width, height, compositeTransfer),
+ );
} catch (err) {
log.warn("DOM layer decode/blit failed; skipping overlay for transition scene", {
frameIndex: frameIdx,
@@ -258,19 +256,17 @@ export async function captureTransitionFrameOnWorker(
hdrPerf.frames += 1;
hdrPerf.transitionFrames += 1;
}
- let timingStart = Date.now();
- await session.page.evaluate((t: number) => {
- if (window.__hf && typeof window.__hf.seek === "function") window.__hf.seek(t);
- }, time);
- addHdrTiming(hdrPerf, "frameSeekMs", timingStart);
+ await timeHdrPhaseAsync(hdrPerf, "frameSeekMs", () =>
+ session.page.evaluate((t: number) => {
+ if (window.__hf && typeof window.__hf.seek === "function") window.__hf.seek(t);
+ }, time),
+ );
if (beforeCaptureHook) {
- timingStart = Date.now();
- await beforeCaptureHook(session.page, time);
- addHdrTiming(hdrPerf, "frameInjectMs", timingStart);
+ await timeHdrPhaseAsync(hdrPerf, "frameInjectMs", () => beforeCaptureHook(session.page, time));
}
- timingStart = Date.now();
- const stackingInfo = await queryElementStacking(session.page, nativeHdrIds);
- addHdrTiming(hdrPerf, "stackingQueryMs", timingStart);
+ const stackingInfo = await timeHdrPhaseAsync(hdrPerf, "stackingQueryMs", () =>
+ queryElementStacking(session.page, nativeHdrIds),
+ );
const sceneAIds = new Set(sceneElements[transition.fromScene] ?? []);
const sceneBIds = new Set(sceneElements[transition.toScene] ?? []);
buffers.bufferA.fill(0);
diff --git a/packages/producer/src/services/render/stages/captureHdrHybridLoop.ts b/packages/producer/src/services/render/stages/captureHdrHybridLoop.ts
index d78a03d367..5479cd2356 100644
--- a/packages/producer/src/services/render/stages/captureHdrHybridLoop.ts
+++ b/packages/producer/src/services/render/stages/captureHdrHybridLoop.ts
@@ -41,7 +41,7 @@ import {
type TransitionRange,
compositeHdrFrame,
} from "../../hdrCompositor.js";
-import { type HdrPerfCollector, addHdrTiming } from "../hdrPerf.js";
+import { type HdrPerfCollector, addHdrTiming, timeHdrPhaseAsync } from "../hdrPerf.js";
import type { ProgressCallback, RenderJob } from "../../renderOrchestrator.js";
import { writeFileExclusiveSync } from "../shared.js";
import {
@@ -306,25 +306,25 @@ export async function runHybridLayeredFrameLoop(input: HybridLoopInput): Promise
});
} else {
const beforeCaptureHook = session.onBeforeCapture;
- let timingStart = Date.now();
- await session.page.evaluate((t: number) => {
- if (window.__hf && typeof window.__hf.seek === "function") window.__hf.seek(t);
- }, time);
- addHdrTiming(hdrPerf, "frameSeekMs", timingStart);
+ await timeHdrPhaseAsync(hdrPerf, "frameSeekMs", () =>
+ session.page.evaluate((t: number) => {
+ if (window.__hf && typeof window.__hf.seek === "function") window.__hf.seek(t);
+ }, time),
+ );
if (beforeCaptureHook) {
- timingStart = Date.now();
- await beforeCaptureHook(session.page, time);
- addHdrTiming(hdrPerf, "frameInjectMs", timingStart);
+ await timeHdrPhaseAsync(hdrPerf, "frameInjectMs", () =>
+ beforeCaptureHook(session.page, time),
+ );
}
- timingStart = Date.now();
- const stackingInfo = await queryElementStacking(session.page, nativeHdrIds);
- addHdrTiming(hdrPerf, "stackingQueryMs", timingStart);
+ const stackingInfo = await timeHdrPhaseAsync(hdrPerf, "stackingQueryMs", () =>
+ queryElementStacking(session.page, nativeHdrIds),
+ );
canvas.fill(0);
// Rebind ctx to this worker's session for per-layer captures
const wctx: HdrCompositeContext = { ...hdrCompositeCtx, domSession: session };
- timingStart = Date.now();
- await compositeHdrFrame(wctx, canvas, time, stackingInfo, undefined, i);
- addHdrTiming(hdrPerf, "normalCompositeMs", timingStart);
+ await timeHdrPhaseAsync(hdrPerf, "normalCompositeMs", () =>
+ compositeHdrFrame(wctx, canvas, time, stackingInfo, undefined, i),
+ );
if (debugDumpEnabled && debugDumpDir && i % 30 === 0) {
writeFileExclusiveSync(
join(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
diff --git a/packages/producer/src/services/render/stages/captureHdrSequentialLoop.ts b/packages/producer/src/services/render/stages/captureHdrSequentialLoop.ts
index 5360bd980d..11141c2051 100644
--- a/packages/producer/src/services/render/stages/captureHdrSequentialLoop.ts
+++ b/packages/producer/src/services/render/stages/captureHdrSequentialLoop.ts
@@ -26,7 +26,12 @@ import {
type TransitionRange,
compositeHdrFrame,
} from "../../hdrCompositor.js";
-import { type HdrPerfCollector, addHdrTiming } from "../hdrPerf.js";
+import {
+ type HdrPerfCollector,
+ addHdrTiming,
+ timeHdrPhase,
+ timeHdrPhaseAsync,
+} from "../hdrPerf.js";
import type { ProgressCallback, RenderJob } from "../../renderOrchestrator.js";
import { writeFileExclusiveSync } from "../shared.js";
import {
@@ -104,20 +109,20 @@ export async function runSequentialLayeredFrameLoop(input: SequentialLoopInput):
const time = (i * job.config.fps.den) / job.config.fps.num;
if (hdrPerf) hdrPerf.frames += 1;
- let timingStart = Date.now();
- await domSession.page.evaluate((t: number) => {
- if (window.__hf && typeof window.__hf.seek === "function") window.__hf.seek(t);
- }, time);
- addHdrTiming(hdrPerf, "frameSeekMs", timingStart);
+ await timeHdrPhaseAsync(hdrPerf, "frameSeekMs", () =>
+ domSession.page.evaluate((t: number) => {
+ if (window.__hf && typeof window.__hf.seek === "function") window.__hf.seek(t);
+ }, time),
+ );
if (beforeCaptureHook) {
- timingStart = Date.now();
- await beforeCaptureHook(domSession.page, time);
- addHdrTiming(hdrPerf, "frameInjectMs", timingStart);
+ await timeHdrPhaseAsync(hdrPerf, "frameInjectMs", () =>
+ beforeCaptureHook(domSession.page, time),
+ );
}
- timingStart = Date.now();
- const stackingInfo = await queryElementStacking(domSession.page, nativeHdrIds);
- addHdrTiming(hdrPerf, "stackingQueryMs", timingStart);
+ const stackingInfo = await timeHdrPhaseAsync(hdrPerf, "stackingQueryMs", () =>
+ queryElementStacking(domSession.page, nativeHdrIds),
+ );
const activeTransition = transitionRanges.find((t) => i >= t.startFrame && i <= t.endFrame);
if (i % 30 === 0 && (log.isLevelEnabled?.("debug") ?? true)) {
@@ -141,10 +146,10 @@ export async function runSequentialLayeredFrameLoop(input: SequentialLoopInput):
(activeTransition.endFrame - activeTransition.startFrame);
const sceneAIds = new Set(sceneElements[activeTransition.fromScene] ?? []);
const sceneBIds = new Set(sceneElements[activeTransition.toScene] ?? []);
- timingStart = Date.now();
- transitionBuffers.bufferA.fill(0);
- transitionBuffers.bufferB.fill(0);
- addHdrTiming(hdrPerf, "canvasClearMs", timingStart);
+ timeHdrPhase(hdrPerf, "canvasClearMs", () => {
+ transitionBuffers.bufferA.fill(0);
+ transitionBuffers.bufferB.fill(0);
+ });
for (const [sceneBuf, sceneIds] of [
[transitionBuffers.bufferA, sceneAIds],
@@ -187,26 +192,24 @@ export async function runSequentialLayeredFrameLoop(input: SequentialLoopInput):
progress,
);
addHdrTiming(hdrPerf, "transitionCompositeMs", transitionTimingStart);
- timingStart = Date.now();
- ensureFrameWritten(await hdrEncoder.writeFrame(transitionBuffers.output), i);
- addHdrTiming(hdrPerf, "encoderWriteMs", timingStart);
+ await timeHdrPhaseAsync(hdrPerf, "encoderWriteMs", async () =>
+ ensureFrameWritten(await hdrEncoder.writeFrame(transitionBuffers.output), i),
+ );
} else {
if (hdrPerf) hdrPerf.normalFrames += 1;
- timingStart = Date.now();
- normalCanvas.fill(0);
- addHdrTiming(hdrPerf, "canvasClearMs", timingStart);
- timingStart = Date.now();
- await compositeHdrFrame(hdrCompositeCtx, normalCanvas, time, stackingInfo, undefined, i);
- addHdrTiming(hdrPerf, "normalCompositeMs", timingStart);
+ timeHdrPhase(hdrPerf, "canvasClearMs", () => normalCanvas.fill(0));
+ await timeHdrPhaseAsync(hdrPerf, "normalCompositeMs", () =>
+ compositeHdrFrame(hdrCompositeCtx, normalCanvas, time, stackingInfo, undefined, i),
+ );
if (debugDumpEnabled && debugDumpDir && i % 30 === 0) {
writeFileExclusiveSync(
join(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
normalCanvas,
);
}
- timingStart = Date.now();
- ensureFrameWritten(await hdrEncoder.writeFrame(normalCanvas), i);
- addHdrTiming(hdrPerf, "encoderWriteMs", timingStart);
+ await timeHdrPhaseAsync(hdrPerf, "encoderWriteMs", async () =>
+ ensureFrameWritten(await hdrEncoder.writeFrame(normalCanvas), i),
+ );
}
cleanupEndedHdrVideos({
diff --git a/packages/producer/src/services/render/stages/encodeStage.ts b/packages/producer/src/services/render/stages/encodeStage.ts
index 0ac575b0db..4c4ae8278f 100644
--- a/packages/producer/src/services/render/stages/encodeStage.ts
+++ b/packages/producer/src/services/render/stages/encodeStage.ts
@@ -42,6 +42,7 @@ import {
} from "@hyperframes/engine";
import type { Fps } from "@hyperframes/core";
import type { ProducerLogger } from "../../../logger.js";
+import { formatExportFrameName } from "../../../utils/paths.js";
import type { ProgressCallback, RenderJob } from "../../renderOrchestrator.js";
import {
buildGifPalettegenArgs,
@@ -236,7 +237,7 @@ export async function runEncodeStage(input: EncodeStageInput): Promise {
- const dst = join(outputPath, `frame_${String(i + 1).padStart(6, "0")}.png`);
+ const dst = join(outputPath, formatExportFrameName(i, "png"));
copyFileSync(join(framesDir, name), dst);
});
if (hasAudio && audioOutputPath && existsSync(audioOutputPath)) {
diff --git a/packages/producer/src/services/renderOrchestrator.ts b/packages/producer/src/services/renderOrchestrator.ts
index 44e8d231e2..e6b99b10f6 100644
--- a/packages/producer/src/services/renderOrchestrator.ts
+++ b/packages/producer/src/services/renderOrchestrator.ts
@@ -84,6 +84,7 @@ import {
} from "./render/shared.js";
import { buildRenderErrorDetails, cleanupRenderResources, safeCleanup } from "./render/cleanup.js";
import { normalizeErrorMessage } from "../utils/errorMessage.js";
+import { formatCaptureFrameName } from "../utils/paths.js";
import { resolveEffectiveHdrMode } from "./render/hdrMode.js";
import { buildRenderPerfSummary } from "./render/perfSummary.js";
import { getCaptureStageBrowserConsole } from "./render/captureStageError.js";
@@ -467,7 +468,7 @@ export function findMissingFrameRanges(
let rangeStart: number | null = null;
for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
- const framePath = join(framesDir, `frame_${String(frameIndex).padStart(6, "0")}.${frameExt}`);
+ const framePath = join(framesDir, formatCaptureFrameName(frameIndex, frameExt));
const missing = !existsSync(framePath);
if (missing && rangeStart === null) {
rangeStart = frameIndex;
@@ -535,7 +536,7 @@ function countCapturedFrames(
): number {
let captured = 0;
for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
- const framePath = join(framesDir, `frame_${String(frameIndex).padStart(6, "0")}.${frameExt}`);
+ const framePath = join(framesDir, formatCaptureFrameName(frameIndex, frameExt));
if (existsSync(framePath)) captured++;
}
return captured;
diff --git a/packages/producer/src/utils/paths.test.ts b/packages/producer/src/utils/paths.test.ts
index 336938c6e0..abbffb0001 100644
--- a/packages/producer/src/utils/paths.test.ts
+++ b/packages/producer/src/utils/paths.test.ts
@@ -14,7 +14,12 @@
import { describe, expect, it } from "vitest";
import { resolve, win32 } from "node:path";
-import { isPathInside, toExternalAssetKey } from "./paths.js";
+import {
+ isPathInside,
+ toExternalAssetKey,
+ formatCaptureFrameName,
+ formatExportFrameName,
+} from "./paths.js";
describe("isPathInside", () => {
it("returns true when child is directly inside parent", () => {
@@ -133,3 +138,27 @@ describe("toExternalAssetKey", () => {
expect(/^[A-Za-z]:/.test(key)).toBe(false);
});
});
+
+describe("formatCaptureFrameName", () => {
+ it("returns zero-padded filename for zero-based index", () => {
+ expect(formatCaptureFrameName(0, "jpg")).toBe("frame_000000.jpg");
+ });
+
+ it("pads to 6 digits for large indices", () => {
+ expect(formatCaptureFrameName(999999, "png")).toBe("frame_999999.png");
+ });
+
+ it("handles mid-range indices", () => {
+ expect(formatCaptureFrameName(42, "jpg")).toBe("frame_000042.jpg");
+ });
+});
+
+describe("formatExportFrameName", () => {
+ it("returns one-based filename from zero-based input", () => {
+ expect(formatExportFrameName(0, "png")).toBe("frame_000001.png");
+ });
+
+ it("increments index by 1", () => {
+ expect(formatExportFrameName(4, "png")).toBe("frame_000005.png");
+ });
+});
diff --git a/packages/producer/src/utils/paths.ts b/packages/producer/src/utils/paths.ts
index be93fd5ad1..ebf8688ca0 100644
--- a/packages/producer/src/utils/paths.ts
+++ b/packages/producer/src/utils/paths.ts
@@ -109,6 +109,14 @@ export function toExternalAssetKey(absPath: string): string {
return "hf-ext/" + normalised;
}
+export function formatCaptureFrameName(index: number, ext: string): string {
+ return `frame_${String(index).padStart(6, "0")}.${ext}`;
+}
+
+export function formatExportFrameName(index: number, ext: string): string {
+ return `frame_${String(index + 1).padStart(6, "0")}.${ext}`;
+}
+
export function resolveRenderPaths(
projectDir: string,
outputPath: string | null | undefined,