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
+30
View File
@@ -221,6 +221,36 @@ describe("resolveConfig", () => {
});
});
describe("useDrawElement (PRODUCER_EXPERIMENTAL_FAST_CAPTURE)", () => {
it("defaults to false", () => {
const config = resolveConfig();
expect(config.useDrawElement).toBe(false);
});
it("enabled when PRODUCER_EXPERIMENTAL_FAST_CAPTURE=true", () => {
setEnv("PRODUCER_EXPERIMENTAL_FAST_CAPTURE", "true");
const config = resolveConfig();
expect(config.useDrawElement).toBe(true);
});
it("explicit override wins over the env var", () => {
setEnv("PRODUCER_EXPERIMENTAL_FAST_CAPTURE", "true");
const config = resolveConfig({ useDrawElement: false });
expect(config.useDrawElement).toBe(false);
});
it("forces page-side compositing off when enabled (incompatible strategies)", () => {
const config = resolveConfig({ useDrawElement: true, enablePageSideCompositing: true });
expect(config.useDrawElement).toBe(true);
expect(config.enablePageSideCompositing).toBe(false);
});
it("leaves page-side compositing on when fast capture is off", () => {
const config = resolveConfig({ useDrawElement: false });
expect(config.enablePageSideCompositing).toBe(true);
});
});
describe("lowMemoryMode", () => {
it("forces on for truthy PRODUCER_LOW_MEMORY_MODE values", () => {
setEnv("PRODUCER_LOW_MEMORY_MODE", "true");
+34
View File
@@ -63,6 +63,21 @@ export interface EngineConfig {
* `HF_STATIC_DEDUP` in {false,0,off}. Only arms in screenshot capture mode.
*/
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`.
*/
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.651.96× wall-time speedup. No-op unless `useDrawElement` is also true.
* Default: off. Env: `HF_DE_WORKER_ENCODE=true`.
*/
enableDrawElementWorkerEncode: boolean;
/**
* Low-memory render profile. When `true`, the orchestrator collapses the
* pipeline to its cheapest shape on memory-constrained hosts: it skips the
@@ -235,6 +250,8 @@ export const DEFAULT_CONFIG: EngineConfig = {
protocolTimeout: 300_000,
forceScreenshot: false,
staticFrameDedup: true,
useDrawElement: false,
enableDrawElementWorkerEncode: false,
// Auto-detected per host in `resolveConfig`; defaults off for the raw
// DEFAULT_CONFIG (used directly by tests and worker-sizing fallbacks).
lowMemoryMode: false,
@@ -412,6 +429,11 @@ export function resolveConfig(overrides?: Partial<EngineConfig>): EngineConfig {
forceScreenshot: envBool("PRODUCER_FORCE_SCREENSHOT", DEFAULT_CONFIG.forceScreenshot),
staticFrameDedup: resolveStaticFrameDedup(),
useDrawElement: envBool("PRODUCER_EXPERIMENTAL_FAST_CAPTURE", DEFAULT_CONFIG.useDrawElement),
enableDrawElementWorkerEncode: envBool(
"HF_DE_WORKER_ENCODE",
DEFAULT_CONFIG.enableDrawElementWorkerEncode,
),
lowMemoryMode: resolveLowMemoryMode(),
enablePageSideCompositing: envBool(
"HF_PAGE_SIDE_COMPOSITING",
@@ -493,6 +515,18 @@ export function resolveConfig(overrides?: Partial<EngineConfig>): EngineConfig {
...cleanEnv,
...overrides,
};
// 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
// 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) {
merged.enablePageSideCompositing = false;
}
return {
...merged,
vp9CpuUsed: normalizeVp9CpuUsed(merged.vp9CpuUsed),