fix(gcp): enforce effective BeginFrame capture

This commit is contained in:
James
2026-07-27 00:02:37 +00:00
parent 2dddb4c463
commit 2a284a8e3a
21 changed files with 843 additions and 103 deletions
@@ -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 {