fix(engine,producer): apply software-GPU screenshot invariant at concrete-resolved point

Addresses Miguel's R1 blockers:

1. `browserGpuMode: "auto"` that runtime-probes to software slipped past the
   `resolveConfig` clamp — that clamp only sees the pre-resolve string. Add
   `shouldClampToScreenshotForConcreteGpu(resolvedGpuMode, currentForceScreenshot, env)`
   in `packages/engine/src/config.ts` and apply it at BOTH concrete-resolution
   sites:
   - `packages/engine/src/services/frameCapture.ts`: downgrades `preMode`
     from "beginframe" to "screenshot" when resolved GPU is software (respects
     `PRODUCER_FORCE_SCREENSHOT=false` env opt-out), fixing the routing.
   - `packages/producer/src/services/renderOrchestrator.ts`: updates
     `captureObservability.forceScreenshot` (and thus `captureMode`) at the
     same call site, fixing the observability truth on the auto → software
     case.

2. New unit tests in `config.test.ts`:
   - Documents the auto-branch gap (resolveConfig leaves auto as
     forceScreenshot=false — the runtime companion closes it).
   - 5 branch tests on `shouldClampToScreenshotForConcreteGpu` covering
     software / hardware / already-forced / env-opt-out / non-"false" env
     values.
   Full suite: 56/56 pass.

Scope narrowing on Blocker 2: the distributed rendering path at
`packages/producer/src/services/distributed/plan.ts:753-754` and
`renderChunk.ts:462-466` explicitly hardcodes `browserGpuMode:"software",
forceScreenshot:false` post-resolveConfig and stays outside this PR's
invariant boundary. `compileStage` may still flip it to true for alpha
formats, but generic MP4 distributed renders on SwiftShader hosts remain
BeginFrame. That's a separate architectural cleanup (needs its own
behavior-change trace); the PR body now scopes the invariant to the
in-process CLI/orchestrator path.
This commit is contained in:
Vance Ingalls
2026-07-13 23:13:30 +00:00
parent ba5168293f
commit 2e44602f99
5 changed files with 123 additions and 7 deletions
@@ -70,6 +70,7 @@ import {
type SubTimelineWaitOutcome,
resolveBrowserGpuMode,
resolveHeadlessShellPath,
shouldClampToScreenshotForConcreteGpu,
scaleProtocolTimeoutForComposition,
isMemoryExhaustionError,
isTransientBrowserError,
@@ -1908,7 +1909,23 @@ export async function executeRenderJob(
chromePath: resolveHeadlessShellPath(cfg),
browserTimeout: cfg.browserTimeout,
});
updateCaptureObservability({ browserGpuMode: resolvedBrowserGpuMode });
// Mirror the frameCapture.ts routing invariant here so observability
// reports the actual capture mode on `browserGpuMode: "auto"` renders
// that probe to software: `resolveConfig` couldn't see this at config
// time, so `captureObservability.forceScreenshot` was still false,
// misreporting `captureMode: "beginframe"` for a session that will
// actually take the screenshot path. Env-level opt-out preserved via
// the shared helper.
const observabilityForceScreenshot =
captureObservability.forceScreenshot ||
shouldClampToScreenshotForConcreteGpu(
resolvedBrowserGpuMode,
captureObservability.forceScreenshot,
);
updateCaptureObservability({
browserGpuMode: resolvedBrowserGpuMode,
forceScreenshot: observabilityForceScreenshot,
});
const videoCaptureBeyondViewport = resolveVideoCaptureBeyondViewport(composition.videos.length);
const captureOptions: CaptureOptions = {