feat(engine,cli): drawElement fast-capture config + CLI flag (#1916)

## drawElement fast-capture — config + CLI flag (stack 1/6)

Foundation layer for the drawElement fast-capture feature: the config surface and CLI/Docker plumbing that the rest of the stack builds on.

### What this adds
- **`packages/engine/src/config.ts`** — new config fields for fast capture: `useDrawElement` / `enableDrawElementWorkerEncode` (macOS-GPU `drawElementImage` capture + worker-offloaded JPEG encode), resolved from env in `resolveConfig` (env `HF_DE_WORKER_ENCODE`). Wired alongside main's existing `staticFrameDedup` (unified downstream in 4/6).
- **`packages/cli/src/commands/render.ts`** — `--experimental-fast-capture` flag → sets `experimentalFastCapture`; `--debug` passthrough.
- **`packages/cli/src/utils/dockerRunArgs.ts`** — pass the fast-capture env through to the container.
- **`.github/workflows/fast-video-validation.yml`** — CI job validating fast-capture renders.
- `.oxlintrc.json` / `.fallowrc.jsonc` — ignore-pattern housekeeping for the new paths.

### Notes
- Config-only + entrypoint; no capture behavior yet (that's 2/6–4/6).
- Tests: `config.test.ts`, `dockerRunArgs.test.ts` added.

---
**Stack (drawElement fast-capture, rebased onto current `main`, supersedes #1295 + #1444):**
1. **#1916 config + CLI** ← you are here
2. #1917 drawElementImage capture service
3. #1918 3D projection + compositor-effect risk gate
4. #1919 frame-capture core (routing, worker-encode, static-dedup unification)
5. #1920 producer render stages + remote bg-image localizer
6. #1921 lint rule + player media sync

⚠️ Intermediate PRs (1–5) are split by package boundary for review and **do not each compile independently** (cross-file deps); the complete feature is green at the stack tip (#1921) — tsc-clean on engine + producer, 231 tests pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
Vance Ingalls
2026-07-06 15:57:30 -07:00
committed by GitHub
parent 972bedf062
commit e04f6dda37
8 changed files with 178 additions and 4 deletions
+23
View File
@@ -358,6 +358,18 @@ export default defineCommand({
"memory thrash on constrained machines. Default: auto-detected from " +
"total RAM (<= 8 GB). Env: PRODUCER_LOW_MEMORY_MODE.",
},
"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.",
// 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).
},
},
// `run` is the citty handler for `hyperframes render` — sequential flag
// validation + render dispatch. Inherited CRITICAL on main (CRAP 1290);
@@ -513,6 +525,13 @@ export default defineCommand({
process.env.PRODUCER_LOW_MEMORY_MODE = args["low-memory-mode"] ? "true" : "false";
}
// ── Override: experimental fast capture (drawElementImage) ───────────
if (args["experimental-fast-capture"] != null) {
process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE = args["experimental-fast-capture"]
? "true"
: "false";
}
// ── Validate max-concurrent-renders ─────────────────────────────────
if (args["max-concurrent-renders"] != null) {
const parsed = parseInt(args["max-concurrent-renders"], 10);
@@ -915,6 +934,7 @@ export default defineCommand({
entryFile,
outputResolution,
pageSideCompositing: args["page-side-compositing"] !== false,
experimentalFastCapture: args["experimental-fast-capture"] === true,
pageNavigationTimeoutMs,
protocolTimeout,
playerReadyTimeout,
@@ -984,6 +1004,8 @@ interface RenderOptions {
/** Output resolution preset; see `resolveDeviceScaleFactor` for constraints. */
outputResolution?: CanvasResolution;
pageSideCompositing?: boolean;
/** EXPERIMENTAL. drawElementImage frame capture (--experimental-fast-capture). */
experimentalFastCapture?: boolean;
/**
* Puppeteer `page.goto()` timeout for the entry HTML, in milliseconds.
* When omitted, the engine default (60s) applies. Surfaced as
@@ -1293,6 +1315,7 @@ async function renderDocker(
outputResolution: options.outputResolution,
pageSideCompositing: options.pageSideCompositing,
debug: options.debug,
experimentalFastCapture: options.experimentalFastCapture,
pageNavigationTimeoutMs: options.pageNavigationTimeoutMs,
},
});
@@ -173,6 +173,7 @@ describe("buildDockerRunArgs", () => {
quiet: true,
debug: true,
entryFile: "compositions/intro.html",
experimentalFastCapture: true,
},
});
// Each value must reach the container exactly once. If a future option
@@ -195,6 +196,24 @@ describe("buildDockerRunArgs", () => {
expect(args).toContain("--hdr");
expect(args).toContain("--composition");
expect(args).toContain("compositions/intro.html");
expect(args).toContain("--experimental-fast-capture");
});
it("forwards --experimental-fast-capture only when enabled", () => {
const on = buildDockerRunArgs({
...FIXED_INPUT,
options: { ...BASE, experimentalFastCapture: true },
});
expect(on).toContain("--experimental-fast-capture");
const off = buildDockerRunArgs({
...FIXED_INPUT,
options: { ...BASE, experimentalFastCapture: false },
});
expect(off).not.toContain("--experimental-fast-capture");
const absent = buildDockerRunArgs({ ...FIXED_INPUT, options: BASE });
expect(absent).not.toContain("--experimental-fast-capture");
});
it("forwards --format png-sequence to the container", () => {
+3
View File
@@ -56,6 +56,8 @@ export interface DockerRenderOptions {
/** Output resolution preset (e.g. "landscape-4k"). Forwarded as `--resolution`. */
outputResolution?: string;
pageSideCompositing?: boolean;
/** EXPERIMENTAL. drawElementImage frame capture; forwarded as `--experimental-fast-capture`. */
experimentalFastCapture?: boolean;
/**
* Puppeteer page-navigation timeout, in milliseconds. Forwarded to the
* in-container CLI as `--browser-timeout <seconds>` (the CLI takes
@@ -144,6 +146,7 @@ export function buildDockerRunArgs(input: DockerRunArgsInput): string[] {
...(options.entryFile ? ["--composition", options.entryFile] : []),
...(options.outputResolution ? ["--resolution", options.outputResolution] : []),
...(options.pageSideCompositing === false ? ["--no-page-side-compositing"] : []),
...(options.experimentalFastCapture ? ["--experimental-fast-capture"] : []),
...(options.pageNavigationTimeoutMs != null
? ["--browser-timeout", String(options.pageNavigationTimeoutMs / 1000)]
: []),