Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .fallowrc.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"packages/producer/src/runtime-conformance.ts",
"packages/producer/src/benchmark.ts",
"packages/producer/scripts/generate-font-data.ts",
"packages/producer/we-render.mjs",
"packages/producer/scripts/validate-fast-video.ts",
"packages/cli/scripts/generate-font-data.ts",
"packages/engine/scripts/test-fitTextFontSize-browser.ts",
Expand Down
11 changes: 6 additions & 5 deletions packages/cli/src/commands/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,11 +361,12 @@ export default defineCommand({
"experimental-fast-capture": {
type: "boolean",
description:
"EXPERIMENTAL. Capture frames via Chrome's drawElementImage API " +
"instead of Page.captureScreenshot — reads DOM paint records directly, " +
"~46% faster on GPU. Transparent (PNG) renders on SwiftShader (Docker) " +
"auto-fall back to screenshot capture. Incompatible with page-side " +
"shader compositing. Default: false. Env: PRODUCER_EXPERIMENTAL_FAST_CAPTURE.",
"Capture frames via Chrome's drawElementImage API instead of " +
"Page.captureScreenshot — reads DOM paint records directly, ~2x faster. " +
"Default: on where it can engage (macOS + hardware-GPU browser); " +
"incompatible compositions and self-verification failures fall back to " +
"screenshot capture automatically. Pass =false to disable. " +
"Env: PRODUCER_EXPERIMENTAL_FAST_CAPTURE.",
// No `default` — an omitted flag must stay `undefined` so the `!= null`
// guard below leaves PRODUCER_EXPERIMENTAL_FAST_CAPTURE untouched and the
// env fallback survives (matches the --low-memory-mode idiom).
Expand Down
49 changes: 47 additions & 2 deletions packages/engine/src/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,17 +222,53 @@ describe("resolveConfig", () => {
});

describe("useDrawElement (PRODUCER_EXPERIMENTAL_FAST_CAPTURE)", () => {
it("defaults to false", () => {
it("default is clamped off on software-GPU hosts (page-side compositing preserved)", () => {
setEnv("PRODUCER_BROWSER_GPU_MODE", "software");
unsetEnv("PRODUCER_EXPERIMENTAL_FAST_CAPTURE");
unsetEnv("HF_DE_WORKER_ENCODE");
const config = resolveConfig();
expect(config.useDrawElement).toBe(false);
expect(config.enablePageSideCompositing).toBe(true);
});

it("default engages on macOS with a hardware-GPU browser", () => {
setEnv("PRODUCER_BROWSER_GPU_MODE", "hardware");
unsetEnv("PRODUCER_EXPERIMENTAL_FAST_CAPTURE");
unsetEnv("HF_DE_WORKER_ENCODE");
const config = resolveConfig();
expect(config.useDrawElement).toBe(process.platform === "darwin");
});

it("default engages on macOS with auto GPU mode (the stock CLI path)", () => {
setEnv("PRODUCER_BROWSER_GPU_MODE", "auto");
unsetEnv("PRODUCER_EXPERIMENTAL_FAST_CAPTURE");
unsetEnv("HF_DE_WORKER_ENCODE");
const config = resolveConfig();
expect(config.useDrawElement).toBe(process.platform === "darwin");
});

it("default requires worker-encode (the verified drain)", () => {
setEnv("PRODUCER_BROWSER_GPU_MODE", "hardware");
setEnv("HF_DE_WORKER_ENCODE", "false");
unsetEnv("PRODUCER_EXPERIMENTAL_FAST_CAPTURE");
const config = resolveConfig();
expect(config.useDrawElement).toBe(false);
});

it("enabled when PRODUCER_EXPERIMENTAL_FAST_CAPTURE=true", () => {
it("explicit env opt-in skips the platform clamp", () => {
setEnv("PRODUCER_BROWSER_GPU_MODE", "software");
setEnv("PRODUCER_EXPERIMENTAL_FAST_CAPTURE", "true");
const config = resolveConfig();
expect(config.useDrawElement).toBe(true);
});

it("env kill switch wins over the default", () => {
setEnv("PRODUCER_BROWSER_GPU_MODE", "hardware");
setEnv("PRODUCER_EXPERIMENTAL_FAST_CAPTURE", "false");
const config = resolveConfig();
expect(config.useDrawElement).toBe(false);
});

it("explicit override wins over the env var", () => {
setEnv("PRODUCER_EXPERIMENTAL_FAST_CAPTURE", "true");
const config = resolveConfig({ useDrawElement: false });
Expand All @@ -243,6 +279,15 @@ describe("resolveConfig", () => {
const config = resolveConfig({ useDrawElement: true, enablePageSideCompositing: true });
expect(config.useDrawElement).toBe(true);
expect(config.enablePageSideCompositing).toBe(false);
// The auto-disable is recorded so compile-time gates can restore it.
expect(config.pageSideCompositingAutoDisabled).toBe(true);
});

it("does NOT mark auto-disabled when the caller explicitly opted out of page-side compositing", () => {
const config = resolveConfig({ useDrawElement: true, enablePageSideCompositing: false });
expect(config.enablePageSideCompositing).toBe(false);
// Explicit caller intent — a compile-time drawElement gate must not restore it.
expect(config.pageSideCompositingAutoDisabled).not.toBe(true);
});

it("leaves page-side compositing on when fast capture is off", () => {
Expand Down
69 changes: 56 additions & 13 deletions packages/engine/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,20 +64,30 @@ export interface EngineConfig {
*/
staticFrameDedup: boolean;
/**
* EXPERIMENTAL. Use drawElementImage for frame capture (requires the
* CanvasDrawElement Chrome flag, added globally in buildChromeArgs).
* Surfaced via the CLI `--experimental-fast-capture` flag.
* Env fallback: `PRODUCER_EXPERIMENTAL_FAST_CAPTURE`.
* Use drawElementImage for frame capture (requires the CanvasDrawElement
* Chrome flag, added globally in buildChromeArgs). Default ON, clamped in
* `resolveConfig` to hosts where it can actually engage (macOS + hardware-GPU
* browser); compile/init gates and the runtime self-verification net route
* incompatible or damaged renders back to screenshot capture.
* Kill switch: `PRODUCER_EXPERIMENTAL_FAST_CAPTURE=false` (or the CLI
* `--experimental-fast-capture=false`).
*/
useDrawElement: boolean;
/**
* EXPERIMENTAL. Pipeline JPEG encode into an in-page OffscreenCanvas Worker
* for the drawElement fast-capture path (macOS hardware GPU only). The worker
* encodes frame N while the main thread seeks+paints frame N+1, targeting
* ~1.65–1.96× wall-time speedup. No-op unless `useDrawElement` is also true.
* Default: off. Env: `HF_DE_WORKER_ENCODE=true`.
* Pipeline JPEG encode into an in-page OffscreenCanvas Worker for the
* drawElement fast-capture path (macOS hardware GPU only). The worker
* encodes frame N while the main thread seeks+paints frame N+1
* (~1.65–1.96× wall-time speedup). No-op unless `useDrawElement` is also
* true. Kill switch: `HF_DE_WORKER_ENCODE=false`.
*/
enableDrawElementWorkerEncode: boolean;
/**
* INTERNAL. Set by resolveConfig when it disabled enablePageSideCompositing
* solely because drawElement was on. Lets the producer's compile-time gates
* restore page-side compositing without overriding an explicit caller/env
* opt-out. Not intended to be set by callers.
*/
pageSideCompositingAutoDisabled?: boolean;
/**
* Low-memory render profile. When `true`, the orchestrator collapses the
* pipeline to its cheapest shape on memory-constrained hosts: it skips the
Expand Down Expand Up @@ -250,8 +260,8 @@ export const DEFAULT_CONFIG: EngineConfig = {
protocolTimeout: 300_000,
forceScreenshot: false,
staticFrameDedup: true,
useDrawElement: false,
enableDrawElementWorkerEncode: false,
useDrawElement: true,
enableDrawElementWorkerEncode: true,
// Auto-detected per host in `resolveConfig`; defaults off for the raw
// DEFAULT_CONFIG (used directly by tests and worker-sizing fallbacks).
lowMemoryMode: false,
Expand Down Expand Up @@ -516,15 +526,48 @@ export function resolveConfig(overrides?: Partial<EngineConfig>): EngineConfig {
...overrides,
};

// Default-on drawElement is clamped to hosts where it can actually engage
// (macOS with a non-software-GPU browser; SwiftShader drops transparent
// sub-layers — crbug 521434899). "auto" passes the clamp: the stock CLI
// resolves GPU mode to auto, which probes to hardware on real Macs — and if
// it resolves to software after all, the SwiftShader init-time gate still
// routes the session to the screenshot baseline. Without the clamp, the
// default would needlessly disable page-side shader compositing (below) on
// Linux/Docker hosts where DE never runs. An EXPLICIT opt-in (env or caller override)
// skips the clamp and keeps the old semantics — attempt DE, let the
// init-time gates route away — which debugging relies on.
const explicitDrawElementOptIn =
env("PRODUCER_EXPERIMENTAL_FAST_CAPTURE") === "true" || overrides?.useDrawElement === true;
if (
merged.useDrawElement &&
!explicitDrawElementOptIn &&
!(process.platform === "darwin" && merged.browserGpuMode !== "software")
) {
merged.useDrawElement = false;
}
// The runtime self-verification net lives in the worker-encode drain — the
// serial drawElement path has only the blank guard. Default-on drawElement
// therefore requires worker-encode; disabling HF_DE_WORKER_ENCODE without an
// explicit drawElement opt-in falls back to the screenshot baseline rather
// than shipping unverified drawElement frames.
if (merged.useDrawElement && !explicitDrawElementOptIn && !merged.enableDrawElementWorkerEncode) {
merged.useDrawElement = false;
}

// drawElement capture and page-side shader compositing are mutually
// incompatible capture strategies (drawElement reads paint records directly
// and bypasses the page-side prepare→composite→resolve protocol). When
// experimental fast capture is on, force page-side compositing off so shader
// fast capture is on, force page-side compositing off so shader
// transitions fall back to the Node-side layered blend rather than silently
// dropping. This keeps the flag self-consistent and avoids a per-session
// incompatibility warning on every fast-capture render.
if (merged.useDrawElement) {
if (merged.useDrawElement && merged.enablePageSideCompositing) {
merged.enablePageSideCompositing = false;
// Record that THIS resolution (not the caller) turned page-side
// compositing off, so a later compile-time drawElement gate can restore
// it without clobbering an explicit enablePageSideCompositing:false from
// the programmatic API or HF_PAGE_SIDE_COMPOSITING=false.
merged.pageSideCompositingAutoDisabled = true;
}

return {
Expand Down
3 changes: 3 additions & 0 deletions packages/engine/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ export {
captureFrameToBuffer,
captureFrameToBufferPipelined,
captureFramesBatchPipelined,
DrawElementVerificationError,
isDrawElementVerificationError,
recaptureDrawElementFrameForVerify,
writeCapturedFrame,
discardWarmupCapture,
getCompositionDuration,
Expand Down
Loading
Loading