feat(engine): open drawElement fast capture to Windows hardware GPU - #2841
Conversation
Widen the default-on drawElement clamp from darwin-only to darwin|win32 (still requiring a non-software-GPU browser). The darwin restriction was a validation envelope, not an architectural limit — the CanvasDrawElement Chrome flag ships on every platform, and every safety layer that made the macOS default-on release (v0.7.38) survivable is platform-neutral: compile-time gates, the SwiftShader init gate, per-render worker-encode self-verification with screenshot fallback, and the blank guard. Worst case on an unvalidated D3D11 backend is the same as on Metal: verify catches a bad frame and the render re-runs on the screenshot baseline. Why now: 30-day telemetry shows ~206k non-CI hardware-GPU Windows renders (~78% of the win32 fleet, 18k installs) held on the slow screenshot path by the clamp — the second-largest perf population after macOS, carrying ~1,550 capture-hours/month in the DE-eligible >=700-frame band alone at a measured ~2x speedup opportunity. Instrumentation for the new cohort: drawElement session init now records the raw WebGL UNMASKED_RENDERER_WEBGL string (detectSwiftShader generalized to detectGpuBackend — same single evaluate, the string was previously read and discarded) and threads it session -> CapturePerfSummary -> RenderPerfSummary -> render_complete as `gpu_renderer`. drawElement damage proved compositor-backend-specific throughout the macOS rollout, so D3D11-cohort failures must cluster by ANGLE backend + GPU vendor (NVIDIA/AMD/Intel), not just `os`. The two DE clamp branches are extracted into a pure, unit-tested `resolveDefaultDrawElement` (platform + GPU mode + worker-encode + explicit opt-in), which also drops resolveConfig's cyclomatic complexity. The win32 streaming-encode compound tests collapse onto one shared helper. Linux stays excluded: that fleet is headless/Docker SwiftShader, where DE has no speedup and known rendering defects. Kill switches unchanged: PRODUCER_EXPERIMENTAL_FAST_CAPTURE=false, --experimental-fast-capture=false. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three review findings on the win32 drawElement PR: 1. gpu_renderer shipped the raw UNMASKED_RENDERER_WEBGL string — unbounded, driver-authored, GPU-model-specific, and |-joined across parallel sessions, i.e. high cardinality by construction, against this file's own convention of sanitizing engine-sourced strings (deGateReason is a bucket; error messages go through redactTelemetryString). Now bucketed at the source by classifyGpuRenderer to <backend>/<vendor> (metal/apple, d3d11/nvidia, swiftshader/other, ...), which is the whole analytic signal the win32 rollout needs and nothing else. The raw string never leaves the engine. 2. gpu_renderer reached render_complete only, so a crashed render — the cohort the field exists to attribute — carried no backend. It now rides RenderCaptureObservability (deGpuRenderer, sourced from the live probe session like the de_* counters), so both render_complete and render_error carry it and a hard failure still reports its GPU backend. On render_complete the perfSummary value still wins by spread order. 3. Restore the fallow-ignore-next-line suppression above __resetDeParallelRouterTrialStateForTests: CLI test files are not fallow entry points, so removing it fails the CI dead-code audit (local pre-commit passed only because of its changed-file scope). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
miga-heygen
left a comment
There was a problem hiding this comment.
SSOT -- resolveDefaultDrawElement is a clean canonical decision point
The refactor extracts the inline platform + gpu + workerEncode clamp into a pure, tested function. I traced every combination against the old inline logic:
| platform | gpuMode | workerEncode | explicitOptIn | old result | new result |
|---|---|---|---|---|---|
| darwin | hardware | true | false | true | true |
| darwin | auto | true | false | true | true |
| darwin | software | true | false | false | false |
| win32 | hardware | true | false | false | true |
| win32 | auto | true | false | false | true |
| win32 | software | true | false | false | false |
| linux | hardware | true | false | false | false |
| any | any | false | false | false | false |
| linux | software | false | true | true | true |
The intentional change (darwin-only to darwin|win32) is exactly where it should be, and explicitOptIn bypasses both the platform gate AND the workerEncode gate, preserving the old semantics. The isDrawElementPlatform helper is private, resolveDefaultDrawElement is the exported tested surface. The function is genuinely pure -- no process.platform read, everything injected. Good design.
The "auto" GPU mode correctly passes the clamp (it is not "software"), matching the old !== "software" check. Both tests and comments confirm this.
No issues here.
detectGpuBackend backward compatibility
detectSwiftShader is preserved as a thin wrapper over the new detectGpuBackend. The old page.evaluate callback returned a boolean; the new one returns { isSwiftShader, renderer }. The wrapper extracts .isSwiftShader. The only call site (frameCapture.ts line 522) is migrated to the new detectGpuBackend directly. Clean backward-compatible split.
The pre-existing as string cast on gl.getParameter(ext.UNMASKED_RENDERER_WEBGL) is carried forward (WebGL's getParameter returns any). Not new to this PR.
No issues here.
classifyGpuRenderer design
Low-cardinality bucket (<backend>/<vendor>) is the right abstraction for telemetry. The priority-ordered matching (swiftshader before metal before d3d11 before vulkan before opengl/angle) handles overlapping substrings correctly -- e.g. "ANGLE (Apple, ANGLE Metal Renderer..." hits metal before the catch-all angle check. The cardinality test (two different NVIDIA cards collapse to one bucket) is a nice explicit proof that the design goal holds.
The "other" fallback for both backend and vendor means unknown combinations still emit a bucket rather than crashing the telemetry pipeline. Undefined input returns undefined (not a bogus bucket). Clean.
No issues here.
gpu_renderer telemetry threading
I traced the full chain:
detectGpuBackend(page) -- raw renderer string
-> classifyGpuRenderer(renderer) -- "<backend>/<vendor>" bucket
-> session.gpuRenderer -- CaptureSession field
-> getCapturePerfSummary().gpuRenderer -- CapturePerfSummary
-> aggregateDrawElement().gpuRenderer -- RenderPerfSummary (|-joined, Set-deduped)
-> trackRenderMetrics: gpuRenderer -- props to trackRenderComplete
-> trackRenderComplete: gpu_renderer -- telemetry event key
Parallel path (error resilience):
-> probeSession?.gpuRenderer -- live observability at pipeline start
-> RenderCaptureObservability.deGpuRenderer
-> captureDeGpuRenderer -- renderObservabilityTelemetryPayload
-> renderObservabilityEventProperties -- gpu_renderer in spread
-> trackRenderError: gpu_renderer -- via spread (only source on error path)
No gaps in the chain. The error path comment in trackRenderError explicitly documents that gpu_renderer arrives via the spread -- good.
Issue: gpu_renderer shadowed in trackRenderComplete
In trackRenderComplete, gpu_renderer is set twice:
- Explicitly:
gpu_renderer: props.gpuRenderer(from perf summary, the aggregated success-path value) - Via spread:
...renderObservabilityEventProperties(props)which includesgpu_renderer: props.captureDeGpuRenderer(from live observability, set at pipeline start)
The spread comes after the explicit property in the object literal, so it overwrites. The explicit gpu_renderer: props.gpuRenderer is dead code -- the observability value always wins.
On the success path both values should be identical (same host, same GPU), so this does not corrupt data. But it is misleading: a reader would expect the aggregated perf-summary value to be emitted, while in reality the pipeline-start observability value is what ships. If they ever diverge (e.g. multi-worker heterogeneous GPU in some future scenario), the wrong value would win silently.
Suggestion: Remove the explicit gpu_renderer: props.gpuRenderer from the trackRenderComplete event body (the spread already covers it), or add a comment noting the intentional override order. The trackRenderError path already has such a comment and handles this correctly by not adding a duplicate explicit property.
Severity: nit (no data impact today, but the dead code is confusing).
Standards
- No bare
as Tcasts introduced by this PR (theas stringongl.getParameterandas WebGLRenderingContext | nullongetContextare pre-existing patterns carried forward from the olddetectSwiftShader). - No
!non-null assertions. - TypeScript types are clean:
GpuBackendInfointerface, properstring | null | undefinedhandling inclassifyGpuRenderer. - Conventional commit format:
feat(engine):-- correct.
Code quality
isDrawElementPlatformis private,resolveDefaultDrawElementis exported. Good layering.- The win32 compound test helper (
expectCompoundOutcome) eliminates real duplication without losing coverage. Each test case is now a clean declarative object. - The
resolveDefaultDrawElementtest suite covers all branches. Minor gap: no explicit test foruseDrawElement: falseas input, but this is a trivial early-return (if (!args.useDrawElement) return false) that the integration tests implicitly cover. - Comments are thorough and explain the why (telemetry cardinality, safety contract parity, kill switches).
CI
All 47 check runs completed: 40 success, 7 skipped (expected -- skills, codex plugin, perf shards, GCP BeginFrame). No failures. Windows-specific checks (Render on windows-latest, Tests on windows-latest) both pass -- critical for this PR.
Verdict
Approve. The platform widening is mechanically sound, the safety contract (compile/init gates + worker-encode self-verify + screenshot fallback) is unchanged, and gpu_renderer telemetry gives the D3D11 cohort the attribution the rollout needs. The one dead-code nit (shadowed gpu_renderer in trackRenderComplete) is worth cleaning up but not blocking.
-- Miga
miguel-heygen
left a comment
There was a problem hiding this comment.
Exact-head review at 4520cd240b9a:
Strengths
packages/engine/src/config.ts:731-742makes the platform/GPU/worker-encode decision a pure boundary, andconfig.test.tspins the intentional win32 widening plus every safety clamp.packages/engine/src/services/frameCapture.ts:717-720reuses the existing single WebGL probe, retains only a bounded<backend>/<vendor>bucket, and preserves the SwiftShader fallback.- The telemetry chain is complete on both outcomes: session → perf aggregation →
render_complete, and live capture observability →render_error.packages/cli/src/telemetry/events.ts:253-287spreads the observability fallback first, then writes the authoritative success-pathgpuRenderer, so Miga's shadowing nit does not apply to this head.
The default still requires worker encode/self-verification, excludes Linux and software GPU, and preserves explicit opt-in/kill-switch behavior. All current checks are complete and green, including Windows render/tests, typecheck, producer tests, CodeQL, and all regression shards.
Verdict: APPROVE
Reasoning: The Windows expansion is isolated to the validated hardware-GPU envelope, the safety/fallback contract is unchanged, and backend telemetry reaches both success and failure events without raw high-cardinality renderer data.
— Magi
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
Reviewed at 4520cd24.
Clamp widens at packages/engine/src/config.ts:718-720 — platform === "darwin" || platform === "win32". Hardware-vs-software discriminator is args.browserGpuMode === "software" in resolveDefaultDrawElement at config.ts:731-742. Software-renderer allowlist (SwiftShader, LLVMpipe, Lavapipe, Softpipe, Mesa Offscreen, microsoft basic render driver, software rasterizer) at browserManager.ts:38-49 catches WARP, called from resolveBrowserGpuMode("auto") at browserManager.ts:504-517.
gpu_renderer bucketing at drawElementService.ts:135-163 (classifyGpuRenderer) — closed enum: 7 backends × 6 vendors = 42 buckets. No PII vectors, driver strings discarded post-classification. Type contract explicitly warns callers at drawElementService.ts:115-117 ("LOCAL USE ONLY … must not be shipped to telemetry verbatim") — verified .gpuRenderer never propagates further than the bucket.
Emitted on both events per [[feedback_ab_experiment_instrumentation_parity_precision]]: render_complete via events.ts:287 from perfSummary.drawElement.gpuRenderer, render_error via events.ts:112,386 from RenderCaptureObservability.deGpuRenderer (set at renderOrchestrator.ts:2651). No cross-repo consumers found in hyperframes-internal/EF/heygen-cli/pacific — this is a HF-only telemetry field.
Blockers
None.
Concerns
1. WARP escape hatch when PRODUCER_BROWSER_GPU_MODE=hardware is explicit. browserManager.ts:504-517 catches WARP (microsoft basic render driver), BUT only inside resolveBrowserGpuMode("auto"). When a user sets PRODUCER_BROWSER_GPU_MODE=hardware (explicit opt-in), the probe is skipped at browserManager.ts:512. The DE session's own gate at drawElementService.ts:173-185 (detectGpuBackend) only checks the substring "swiftshader" — WARP passes. classifyGpuRenderer then buckets it as d3d11/microsoft (not as software), and resolveDrawElementCaptureMode(false, transparent) returns "drawelement". DE engages on a CPU renderer.
Before this PR this was moot (Windows was clamped out entirely). Post-PR: a Windows user with a broken/missing GPU driver who has explicitly opted into hardware gets DE running on WARP with no perf gain — the PR body's GPU→CPU readback IPC savings claim doesn't hold on a CPU renderer, and there's no fallback trigger. Two options: (a) add WARP as a software-renderer substring to the detectGpuBackend/classifyGpuRenderer check so DE clamps down on WARP too, or (b) document that PRODUCER_BROWSER_GPU_MODE=hardware on Windows requires a hardware GPU and skip DE if the probe surfaces microsoft basic render driver. Not a blocker because the escape hatch exists on darwin too (software SwiftShader on darwin bypasses the auto-detect gate in the same way), but darwin doesn't have a WARP analogue with the same probability of being hit in the wild.
Nits
- N1
gpu_renderershape asymmetry between events:render_complete(events.ts:287) emits a|-joined enum aggregating parallel workers (perperfSummary.ts:114-127),render_error(events.ts:112,386) emits a single bucket from the probe session. Analytics consumers must handle"d3d11/nvidia|d3d11/intel"on the success side but not on the error side. Documented but a foot-gun for GROUP BY queries. - N2 Coverage gap on
render_errorgpu_renderer atrenderOrchestrator.ts:2651:deGpuRenderer: probeSession?.gpuRendererpopulates only after probe init reachesdetectGpuBackend. A Chrome-launch-time failure (missing binary, permission denial, port collision) gives no gpu_renderer attribution — contradicting the comment atevents.ts:382-385that promises "MOST here". Not a regression from any prior state, but the failure surface the PR body attributes togpu_rendererdoesn't cover the pre-probe cohort. - N3 ~206k renders/month claim in
config.ts:904-906is not traceable to any linked dashboard/query/artifact in the repo. PR-body-only. Not required by anything downstream, but consider linking the source PostHog query so future revisit can decompose the number. - N4
config.test.ts:236-268— no explicitdarwin+software=offassertion; onlywin32+software=offis asserted directly. Coverage is by-implication via shared code path, but a direct assertion would prevent silent divergence. - N5
drawElementService.test.ts:52-86— no test case forMicrosoft Basic Render Driverstring inclassifyGpuRenderer. Ties to Concern-1 above. If you add WARP as a software fingerprint, add a case with the raw D3D-adapter string that Chrome surfaces on a WARP fallback.
Questions
- Is there a Chrome-side flag that could FORCE the hardware GPU on Windows and fail loudly if no hardware GPU is available (instead of silently falling back to WARP)? That would be a cleaner discriminator than probing after the fact.
- On darwin, the exact same escape hatch exists (explicit
hardwaremode skips the probe; if Chrome fell back to SwiftShader for some reason, DE engages on CPU). Has this ever fired in prod on darwin, and if not, is the win32 concern hypothetical?
What I didn't verify
- Windows Chrome behavior when the hardware GPU is present but the D3D11 adapter probe fails — whether Chrome shortlists WARP as a first-class renderer or exits.
- The raw D3D adapter string Chrome writes on Windows hardware GPU renders — the bucket names (
d3d11/microsoft,d3d11/nvidia, etc.) look right against the ANGLE conventions but no committed sample confirms.
What
darwin→darwin || win32(still requires a non-software-GPU browser). Linux stays excluded — that fleet is headless/Docker SwiftShader, where DE has known defects and no speedup.gpu_renderertelemetry — DE session init now keeps the WebGLUNMASKED_RENDERER_WEBGLstring it was already reading and discarding (detectSwiftShadergeneralized todetectGpuBackend, same single page evaluate), threaded session →CapturePerfSummary→RenderPerfSummary→render_complete.Why
30-day telemetry: ~206k non-CI hardware-GPU Windows renders (~78% of the win32 fleet, 18k installs) are pinned to the slow screenshot path purely by the darwin-only clamp — the second-largest perf population after macOS, carrying ~1,550 capture-hours/month in the DE-eligible ≥700f band at a measured ~2× speedup opportunity.
The darwin restriction was a validation envelope, not an architectural limit: the CanvasDrawElement flag ships on every platform. Every layer that made the macOS default-on release (v0.7.38) survivable is platform-neutral and untouched here:
Why gpu_renderer specifically
drawElement damage proved compositor-backend-specific through the entire macOS rollout (Metal vs SwiftShader behaved differently at the paint-record level). D3D11 is a third backend, so its failures must cluster by ANGLE backend + GPU vendor (NVIDIA/AMD/Intel), not just
os=win32. Post-merge watch on dashboard 1783183:de_self_verify_fallback,de_fallback_reason,de_verify_min_dbsegmented byos×gpu_renderer.Refactor included
The two DE clamp branches became a pure, unit-tested
resolveDefaultDrawElement(platform, gpuMode, workerEncode, explicitOptIn)— also dropsresolveConfig's cyclomatic complexity below the fallow threshold. The win32 streaming-encode compound tests collapse onto one shared helper (fallow clone groups cleared).Verification
resolveDefaultDrawElement: darwin/win32 engage, linux doesn't, software clamps, opt-in overrides, no-worker-encode clamps — asserted directly per platform rather than conditioned on the hostdetectGpuBackend: renderer carried, null-WebGL, SwiftShader wrapper back-compatKill switches (unchanged)
PRODUCER_EXPERIMENTAL_FAST_CAPTURE=false,--experimental-fast-capture=false, per-session init gates, per-frame verify.Composes with #2838 / #2840: once all three land, win32 DE renders become parallel-router eligible under the same 700-frame floor.
🤖 Generated with Claude Code