diff --git a/packages/producer/src/services/render/capturePlan.test.ts b/packages/producer/src/services/render/capturePlan.test.ts index a8b8c0ac0..8e8352de4 100644 --- a/packages/producer/src/services/render/capturePlan.test.ts +++ b/packages/producer/src/services/render/capturePlan.test.ts @@ -140,6 +140,28 @@ describe("CapturePlan", () => { }); }); + it("forces screenshot on a disk-plan drawElement verify failure (PRINFRA-352 recovery)", () => { + // Parallel disk workers under the explicit fast-capture opt-in verify their + // own captured samples; a breach must re-render the DISK plan on the + // screenshot baseline (not throw, and not stay on drawElement). + const disk = createCapturePlan({ + workerCount: 2, + forceScreenshot: false, + useStreamingEncode: false, + useLayeredComposite: false, + usePageSideCompositing: false, + hasHdrContent: false, + needsAlpha: false, + }); + const next = replanAfterFailure(disk, { kind: "draw_element_verification" }); + expect(next).toMatchObject({ + kind: "sdr_disk", + forceScreenshot: true, + forceParallelStream: false, + workerCount: 2, + }); + }); + it("rejects a streaming transition from a non-streaming plan", () => { const disk = createCapturePlan({ workerCount: 2, diff --git a/packages/producer/src/services/render/stages/captureStreamingStage.ts b/packages/producer/src/services/render/stages/captureStreamingStage.ts index 09a87aa86..7a4fe6d95 100644 --- a/packages/producer/src/services/render/stages/captureStreamingStage.ts +++ b/packages/producer/src/services/render/stages/captureStreamingStage.ts @@ -63,6 +63,7 @@ import { executeParallelCapture, getCapturePerfSummary, psnrDb, + resolveDeVerifyMinDb, recaptureDrawElementFrameForVerify, completeDeferredDrawElementInit, initializeSession, @@ -257,14 +258,14 @@ function createDrainFrameGuard(args: { // check stops meaning anything); above ~60dB natural DE-vs-screenshot // encoder differences (~45dB+) would force a screenshot fallback on every // verified render. Out-of-range or malformed values fall back to 32. - const verifyMinDbRaw = Number(process.env.HF_DE_VERIFY_MIN_DB ?? "32"); - const verifyMinDb = - Number.isFinite(verifyMinDbRaw) && verifyMinDbRaw >= 10 && verifyMinDbRaw <= 60 - ? verifyMinDbRaw - : 32; - if (process.env.HF_DE_VERIFY_MIN_DB !== undefined && verifyMinDb !== verifyMinDbRaw) { - log.warn("[Render] HF_DE_VERIFY_MIN_DB out of range [10,60]; using 32", { - raw: process.env.HF_DE_VERIFY_MIN_DB, + // Single-sourced clamp (psnr.ts) so the disk and streaming verify paths can + // never apply different PSNR floors to the same composition. The warn stays + // here because only this path has a logger in scope. + const verifyMinDb = resolveDeVerifyMinDb(); + const rawEnv = process.env.HF_DE_VERIFY_MIN_DB; + if (rawEnv !== undefined && Number(rawEnv) !== verifyMinDb) { + log.warn(`[Render] HF_DE_VERIFY_MIN_DB out of range [10,60]; using ${verifyMinDb}`, { + raw: rawEnv, }); } const sizes: number[] = []; diff --git a/packages/producer/src/services/renderOrchestrator.ts b/packages/producer/src/services/renderOrchestrator.ts index d903f36c2..362d18c20 100644 --- a/packages/producer/src/services/renderOrchestrator.ts +++ b/packages/producer/src/services/renderOrchestrator.ts @@ -1018,6 +1018,20 @@ export async function executeDiskCaptureWithAdaptiveRetry(options: { if (failure.kind === "cancelled") { throw error; } + // A drawElement self-verify breach (a parallel disk worker's sampled + // frame diverged from its pre-injection ground truth) is a CORRECTNESS + // failure, not a missing-frame one: the damaged frames are written + // complete to disk, so the presence/size-only findMissingFrameRanges + // below would count them present and wrongly return success — shipping + // the exact compositor damage this verify exists to catch. Rethrow so + // the orchestrator's disk-stage screenshot retry fires (mirrors the + // `cancelled` guard; a worker-halving retry here would only re-run + // drawElement and re-damage). Structural detection walks the aggregated + // CaptureFailure → worker CaptureFailure → DrawElementVerificationError + // cause chain. + if (isDrawElementVerificationError(error)) { + throw error; + } const remaining = findMissingFrameRanges( options.totalFrames, options.framesDir,