Skip to content

fix(engine): renders under WSL use the GPU through full Chrome when the headless shell cannot - #5574

Merged
miguel-heygen merged 2 commits into
mainfrom
fix/wsl-gpu-full-chrome
Oct 14, 2026
Merged

miguel-heygen merged 2 commits into
mainfrom
fix/wsl-gpu-full-chrome

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Part of #5548.

What a user sees

Rendering inside WSL2 with a graphics card, the GPU check used to try only Chrome's headless shell. On Mesa's D3D12 driver (the WSL GPU path) the headless shell can fail to set up its GL surface ("No suitable EGL configs found") even after the user joins the render group, while full Chrome in --headless=new reaches the GPU. The render then fell back to software at CPU speed.

Now, under WSL only, when the headless shell finds no GPU:

  • HyperFrames tries full Chrome for Testing from Puppeteer's cache (~/.cache/puppeteer/chrome, the same place the HDR path already looks). If it reaches the GPU, the render uses it, captured by screenshot (full Chrome has no BeginFrame). The log says browserGpuMode probe → hardware (full Chrome: WebGL renderer ...).
  • If full Chrome is not installed, the WSL note says how to install it (npx @puppeteer/browsers install chrome@stable --path ~/.cache/puppeteer), that HYPERFRAMES_BROWSER_PATH can point at one, and that native Windows uses the GPU through Direct3D.
  • If full Chrome also finds no GPU, the note says so and points to native Windows.
  • The note no longer implies that joining the render group alone is enough.

Outside WSL nothing changes: the extra check never runs.

How it works

  • probeAutoBrowserGpuMode runs the usual headless-shell probe; under WSL, on a software result it probes full Chrome with the same hardware flags and remembers that browser when it gets a hardware renderer.
  • resolveCaptureBrowserPath returns that browser for hardware-mode renders. createCaptureSession launches it and picks screenshot capture for it; the headless shell keeps BeginFrame.
  • The full-Chrome locator moved from hdrCapture.ts into browserManager.ts (resolveFullChromePath) and now sorts versions numerically.
  • Troubleshooting docs: the WSL2 section adds the full Chrome route.

Tests

  • browserManager.test.ts: under WSL, the shell finds no GPU and full Chrome does: auto and explicit hardware both resolve to hardware on full Chrome, no warning; both find no GPU: software, the note names full Chrome's result and native Windows; no full Chrome: the note gives the install command; outside WSL the locator is never called. Existing WSL tests no longer read the real Puppeteer cache.
  • frameCapture-wslFullChrome.test.ts: the capture session launches the browser the probe chose, without BeginFrame flags; the headless shell keeps them.
  • wslHost.test.ts: the note's two routes and the group-change wording.

All new tests fail on main. Breaking the two core lines (dropping the chosen browser from the probe result, or keeping BeginFrame for full Chrome) turns three of them red.

Try this branch under WSL

git clone --depth 1 -b fix/wsl-gpu-full-chrome https://github.com/heygen-com/hyperframes.git hf-wsl-gpu
cd hf-wsl-gpu && bun install && bun run build
npx @puppeteer/browsers install chrome@stable --path ~/.cache/puppeteer
cd /path/to/project && node /path/to/hf-wsl-gpu/packages/cli/bin/hyperframes.mjs render --output out.mp4

Expect browserGpuMode probe → software (WebGL unavailable) for the headless shell, then browserGpuMode probe → hardware (full Chrome: ...), and a Browser launched (..., screenshot, ...) line.

Checked on a Windows 11 PC under WSL2 (Intel graphics, Ubuntu 22.04, Mesa 23.2.1)

We have no NVIDIA card, so the headless shell's failure was simulated: a wrapper adds --disable-gpu to the headless shell's GPU-mode launches only, so its GPU check answers "WebGL unavailable" right away, as the shell does on the reporter's machine. Full Chrome was installed with the command the note prints (it fetched 155.0.8059.79). Same 5 s composition, --workers 1.

  • main (0.8.155): probe → software (WebGL unavailable), the WSL note, SwiftShader render.
  • this branch, default 15 s check: the shell answers "WebGL unavailable", then probe → software (full Chrome: probe failed (GPU probe page did not answer within 15000ms)), the note says full Chrome could not finish its check and names native Windows, and the render completes in software. On this PC Mesa's EGL start-up alone takes 90 to 100 s (eglinfo -p surfaceless, no Chrome involved), so any GPU check there can time out.
  • this branch with the check's budget widened to 180 s in a scratch copy of the built CLI (not part of this PR): probe → hardware (full Chrome: ... ANGLE (Microsoft Corporation, D3D12 (Intel(R) Graphics), OpenGL ES 3.0)), Browser launched (Chrome/155.0.8059.79, screenshot, ...), 150 of 150 frames, matching the software render (PSNR 62 dB). Twice. The first capture session sat at frame 0 for 60 s while Chrome's GPU process started and the existing stall retry recovered it; on the reporter's card full Chrome reached the GPU in about 0.1 s, so this Intel start-up cost should not apply there.

Render times on this PC are dominated by that GPU start-up, so they say nothing about GPU drawing speed; the reporter's machine is the one to measure.

@mintlify

mintlify Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
hyperframes 🟢 Ready View Preview Oct 14, 2026, 1:57 AM

💡 Tip: Enable Automations to automatically generate PRs for you.

@jerrai-bot-heygen jerrai-bot-heygen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please fix the optional full-Chrome cache failure path before approval. — Jerrai

export function resolveFullChromePath(): string | undefined {
const baseDir = join(homedir(), ".cache", "puppeteer", "chrome");
if (!existsSync(baseDir)) return undefined;
const versions = readdirSync(baseDir).sort(compareBrowserVersionsDescending);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new optional cache lookup can throw (for example, ~/.cache/puppeteer/chrome exists but is a file or cannot be read). Under WSL, probeAutoBrowserGpuMode calls it after a software/headless-shell result without a catch, so resolveBrowserGpuMode rejects and createCaptureSession aborts before the previously working software render. The headless-shell cache reader already catches unreadable directories; please make this discovery fail-soft too and cover the failure path.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 600ffd0: the full-Chrome lookup now treats any read error on the cache folder as no full Chrome (same as the headless-shell lookup), so the render keeps its software fallback. New test points the cache at a file instead of a folder; it fails on the previous code with ENOTDIR and passes now. Also in this head: when full Chrome's own GPU check times out, the note says it could not finish rather than that it found no GPU.

@jerrai-bot-heygen jerrai-bot-heygen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The optional full-Chrome discovery now catches cache-directory read failures and returns no candidate, preserving the WSL software fallback; the new ENOTDIR regression fails on the prior code. The successor also distinguishes a failed full-Chrome probe from an actual no-GPU result in the diagnostic note. I rechecked the selected-browser screenshot path from the prior head. New-head CI is still running; the prior Windows-render red job stopped at Bun installation on an upstream GitHub API HTTP 500 before rendering. No real WSL GPU run was independently performed. Approval is for code, not merge or release. — Jerrai

@github-actions

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 2061 (base branch 2061), smooth 1576 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (0)

@miguel-heygen
miguel-heygen added this pull request to the merge queue Oct 14, 2026
Merged via the queue into main with commit 9b99605 Oct 14, 2026
182 of 213 checks passed
@miguel-heygen
miguel-heygen deleted the fix/wsl-gpu-full-chrome branch October 14, 2026 03:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants