mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 14:50:02 +00:00
fix(gcp): enforce effective BeginFrame capture
This commit is contained in:
@@ -238,6 +238,8 @@ describe("cross-worker idempotency", () => {
|
||||
expect(b.outputKind).toBe("file");
|
||||
expect(a.framesEncoded).toBeGreaterThan(0);
|
||||
expect(b.framesEncoded).toBe(a.framesEncoded);
|
||||
expect(a.captureMode).toBe("beginframe");
|
||||
expect(b.captureMode).toBe("beginframe");
|
||||
|
||||
expect(a.sha256).toBe(b.sha256);
|
||||
assertBytesEqual(outA, outB, "file", `mp4 chunk ${chunkIndex}`);
|
||||
|
||||
@@ -19,6 +19,7 @@ import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { applyConcreteGpuScreenshotClamp, buildChromeArgs } from "@hyperframes/engine";
|
||||
import { recomputePlanHashFromPlanDir } from "../render/stages/freezePlan.js";
|
||||
import { RenderQualityError } from "../renderOrchestrator.js";
|
||||
import { CURRENT_PLAN_PROTOCOL } from "./planProtocol.js";
|
||||
@@ -29,6 +30,7 @@ import {
|
||||
DEFAULT_MAX_PARALLEL_CHUNKS,
|
||||
MIN_CHUNK_SIZE,
|
||||
plan,
|
||||
resolveDistributedEngineConfig,
|
||||
resolveChunkPlan,
|
||||
} from "./plan.js";
|
||||
import { buildSyntheticRenderJob } from "./shared.js";
|
||||
@@ -155,6 +157,26 @@ describe("distributed synthetic render job", () => {
|
||||
|
||||
expect(job.config.variables).toEqual(variables);
|
||||
});
|
||||
|
||||
it("keeps the production software-GPU launch on BeginFrame control", () => {
|
||||
const cfg = resolveDistributedEngineConfig({
|
||||
fps: 30,
|
||||
width: 320,
|
||||
height: 240,
|
||||
format: "mp4",
|
||||
});
|
||||
const forceScreenshot = applyConcreteGpuScreenshotClamp(
|
||||
cfg.forceScreenshot,
|
||||
"software",
|
||||
cfg,
|
||||
{},
|
||||
);
|
||||
const captureMode = forceScreenshot ? "screenshot" : "beginframe";
|
||||
const args = buildChromeArgs({ width: 320, height: 240, captureMode, platform: "linux" }, cfg);
|
||||
|
||||
expect(forceScreenshot).toBe(false);
|
||||
expect(args).toContain("--enable-begin-frame-control");
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveChunkPlan", () => {
|
||||
|
||||
@@ -778,6 +778,20 @@ function resolveNonMp4EncoderTriple(
|
||||
return { encoder: "png-sequence", pixelFormat: "rgba", preset: "lossless" };
|
||||
}
|
||||
|
||||
/** Test-visible construction of the engine config used by distributed planning. */
|
||||
export function resolveDistributedEngineConfig(config: DistributedRenderConfig): EngineConfig {
|
||||
return {
|
||||
...(config.producerConfig ?? config.engineConfig ?? resolveConfig()),
|
||||
browserGpuMode: "software",
|
||||
forceScreenshot: false,
|
||||
// Distributed rendering deliberately opts into deterministic BeginFrame
|
||||
// on Linux SwiftShader. Preserve the provenance bit consumed by the
|
||||
// engine's software-GPU screenshot clamp; assigning the boolean alone
|
||||
// loses the distinction between a default false and this explicit opt-out.
|
||||
forceScreenshotExplicitlyOptedOut: true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Activity A of the distributed render pipeline. Produces a self-contained
|
||||
* `<planDir>/` from a project + config. See module docstring for the
|
||||
@@ -808,11 +822,7 @@ export async function plan(
|
||||
throw new Error("[plan] render_cancelled");
|
||||
}
|
||||
};
|
||||
const cfg: EngineConfig = {
|
||||
...(config.producerConfig ?? config.engineConfig ?? resolveConfig()),
|
||||
browserGpuMode: "software",
|
||||
forceScreenshot: false,
|
||||
};
|
||||
const cfg = resolveDistributedEngineConfig(config);
|
||||
|
||||
const job = buildSyntheticRenderJob({
|
||||
fps: { num: config.fps, den: 1 },
|
||||
|
||||
@@ -203,8 +203,11 @@ describe("renderChunk()", () => {
|
||||
expect(a.captureStageMs).toBeGreaterThan(0);
|
||||
expect(a.encodeStageMs).toBeGreaterThanOrEqual(0);
|
||||
expect(a.workers).toBeGreaterThanOrEqual(1);
|
||||
expect(["beginframe", "screenshot", "drawelement"]).toContain(a.captureMode);
|
||||
expect(b.captureMode).toBe(a.captureMode);
|
||||
expect(a.captureStageMs + a.encodeStageMs).toBeLessThanOrEqual(a.durationMs);
|
||||
const perf = JSON.parse(readFileSync(a.perfPath, "utf-8"));
|
||||
expect(perf.captureMode).toBe(a.captureMode);
|
||||
for (const key of [
|
||||
"planHashMs",
|
||||
"sessionBootMs",
|
||||
|
||||
@@ -43,6 +43,8 @@ import {
|
||||
BROWSER_GPU_NOT_SOFTWARE,
|
||||
calculateOptimalWorkers,
|
||||
type CaptureOptions,
|
||||
type CaptureMode,
|
||||
type CapturePerfSummary,
|
||||
type CaptureSession,
|
||||
closeCaptureSession,
|
||||
createCaptureSession,
|
||||
@@ -158,6 +160,8 @@ export interface ChunkResult {
|
||||
encodeStageMs: number;
|
||||
/** Capture workers used for this chunk (`calculateOptimalWorkers` result). */
|
||||
workers: number;
|
||||
/** Effective engine mode used by every worker, after any browser fallback. */
|
||||
captureMode: CaptureMode;
|
||||
/**
|
||||
* Path to a sidecar JSON containing per-chunk perf counters. Adapters
|
||||
* upload this alongside the chunk so per-chunk regressions are
|
||||
@@ -330,6 +334,7 @@ export function resolveLockedVp9CpuUsed(
|
||||
* outputs — the caller picks the right shape based on `meta/encoder.json`.
|
||||
* `renderChunk` enforces the same choice via `outputKind` on the result.
|
||||
*/
|
||||
// fallow-ignore-next-line complexity
|
||||
export async function renderChunk(
|
||||
planDir: string,
|
||||
chunkIndex: number,
|
||||
@@ -485,6 +490,10 @@ export async function renderChunk(
|
||||
...resolveConfig(),
|
||||
browserGpuMode: "software",
|
||||
forceScreenshot: encoder.forceScreenshot,
|
||||
// `encoder.forceScreenshot=false` is a locked distributed-render
|
||||
// decision, not the engine default. Carry that explicit opt-out through
|
||||
// the software-GPU clamp so buildChromeArgs includes BeginFrameControl.
|
||||
forceScreenshotExplicitlyOptedOut: !encoder.forceScreenshot,
|
||||
};
|
||||
|
||||
// Build the BeforeCaptureHook that injects pre-extracted video frames
|
||||
@@ -585,6 +594,8 @@ export async function renderChunk(
|
||||
let sessionBootMs = 0;
|
||||
let captureStageMs = 0;
|
||||
let encodeStageMs = 0;
|
||||
let captureMode: CaptureMode | undefined;
|
||||
const capturePerfs: CapturePerfSummary[] = [];
|
||||
try {
|
||||
if (chunkWorkerCount === 1) {
|
||||
// Sequential branch reuses the probe session for the actual capture.
|
||||
@@ -638,9 +649,10 @@ export async function renderChunk(
|
||||
log,
|
||||
probeSession: session,
|
||||
captureAttempts: [],
|
||||
// Distributed chunks run on Linux (beginframe) where dedup never arms;
|
||||
// a throwaway sink satisfies the type without per-chunk dedup reporting.
|
||||
dedupPerfs: [],
|
||||
// This sink also records each worker's effective capture mode. That
|
||||
// makes a BeginFrame → screenshot fallback observable to adapters and
|
||||
// end-to-end smoke tests instead of existing only in stderr.
|
||||
dedupPerfs: capturePerfs,
|
||||
buildCaptureOptions: () => captureOptions,
|
||||
createRenderVideoFrameInjector: () => videoInjector,
|
||||
abortSignal: undefined,
|
||||
@@ -650,6 +662,20 @@ export async function renderChunk(
|
||||
// captureStage closes the session it consumed.
|
||||
captureStageMs = Date.now() - captureStarted;
|
||||
session = null;
|
||||
const observedModes = new Set(capturePerfs.map((perf) => perf.captureMode));
|
||||
const validModes = new Set<CaptureMode>(["beginframe", "screenshot", "drawelement"]);
|
||||
if (
|
||||
observedModes.size !== 1 ||
|
||||
![...observedModes].every((mode): mode is CaptureMode =>
|
||||
validModes.has(mode as CaptureMode),
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
`[renderChunk] capture workers reported invalid or inconsistent modes: ` +
|
||||
`${[...observedModes].join(",") || "<none>"}`,
|
||||
);
|
||||
}
|
||||
captureMode = [...observedModes][0] as CaptureMode;
|
||||
framesEncoded = framesInChunk;
|
||||
|
||||
// ── Encode the chunk ──
|
||||
@@ -737,6 +763,9 @@ export async function renderChunk(
|
||||
}
|
||||
|
||||
// ── Hash the output + write the perf sidecar ──
|
||||
if (!captureMode) {
|
||||
throw new Error("[renderChunk] capture stage completed without reporting a capture mode");
|
||||
}
|
||||
const sha256 = hashChunkOutput(outputChunkPath, outputKind);
|
||||
const durationMs = Date.now() - start;
|
||||
const perfPath = `${outputChunkPath}.perf.json`;
|
||||
@@ -752,6 +781,7 @@ export async function renderChunk(
|
||||
captureStageMs,
|
||||
encodeStageMs,
|
||||
workers: chunkWorkerCount,
|
||||
captureMode,
|
||||
sha256,
|
||||
outputKind,
|
||||
producerVersion: plan.producerVersion,
|
||||
@@ -781,6 +811,7 @@ export async function renderChunk(
|
||||
captureStageMs,
|
||||
encodeStageMs,
|
||||
workers: chunkWorkerCount,
|
||||
captureMode,
|
||||
perfPath,
|
||||
};
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user