Skip to content

fix(engine): say why WSL renders fall back to software and how to get the GPU - #5560

Merged
miguel-heygen merged 2 commits into
mainfrom
fix/wsl-gpu-probe
Oct 13, 2026
Merged

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

Conversation

@miguel-heygen

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

Copy link
Copy Markdown
Collaborator

Part of #5548.

What a user sees now

Under WSL, when auto GPU mode falls back to software, the log says so once, with what was checked and what to do:

[hyperframes] browserGpuMode probe → software (probe failed (GPU probe page did not answer within 15000ms))
[hyperframes] WSL: the GPU check could not finish (see the probe line above), so this render uses software WebGL (SwiftShader) and runs at CPU speed.
  Your Linux user cannot read and write /dev/dri/renderD128 and is not in its group ("render"), which can stop Mesa from reaching the GPU.
  Fix: run `sudo usermod -aG render $USER`, then `wsl --shutdown` from Windows, reopen WSL and render again.
  Or render from native Windows (project copied to a Windows drive, run from PowerShell), where Chrome uses the GPU through Direct3D.
  Pass --no-browser-gpu to skip the GPU check and render in software.
  • When the probe ran and found no hardware renderer, the first line says "the GPU check found no hardware GPU" instead.
  • The usermod fix appears only when the access check fails and the user is not in the group that owns the node. If the user is in that group and still denied, it says so and points to ls -l. If the node is readable and writable, or missing, it says permissions are not the cause.
  • With --browser-gpu, the mode stays hardware as before. Its warning keeps the "unverified" and Chrome-install (hyperframes doctor) advice for a failed or timed-out probe and adds the WSL checks; for a probe that found no GPU, the WSL checks replace the Docker passthrough advice. Nothing claims the render uses SwiftShader in that mode.

Before, the only output was the probe line, and --browser-gpu gave Docker passthrough advice.

Why

#5548 traced the fallback to Mesa failing to open /dev/dri/renderD128 because a default WSL2 Ubuntu user is in video but not render. That is the one cause we can check without a GPU. The note says "can stop" rather than "stops": on an Intel laptop under WSL2 the same kind of user did reach the GPU through Mesa's D3D12 driver, but Chrome took 35 to 60 s to answer, past the probe's 15 s budget.

How

  • packages/engine/src/services/wslHost.ts: WSL detection (moved from the CLI's telemetry so both use one detector, exported as @hyperframes/engine/wsl-host) and the render-node check: an access check for read and write (not an open), the owning group's name, and whether the user is in it.
  • browserManager.ts: the probe records the WSL checks with its outcome; auto mode prints the note when it selects software; the explicit-hardware warning includes the checks. Both print once per process.
  • Docs: a WSL2 section in troubleshooting, linked from the rendering guide.

Checked on a real WSL2 machine

Windows 11, Intel graphics, WSL 3.0.3, Ubuntu 22.04, Mesa 23.2.1, a fresh non-root user in video but not render, 10 s example composition:

Run Probe Output Render
main (0.8.153), auto, first run hardware (answered in time) none 2m 12s
main (0.8.153), auto, second run software, probe timed out none capture 20 s
this branch, auto, user not in render software, probe timed out note with the usermod fix 39 s
this branch, auto, after usermod -aG render + wsl --shutdown software, probe timed out "readable and writable, permissions are not the cause" 37 s
this branch, --browser-gpu, user not in render software, probe timed out unverified warning + doctor + WSL checks; renders on hardware GL 1m 26s

The usermod + wsl --shutdown fix takes effect (the user gets render and the node becomes readable and writable). On this Intel machine it does not make Chrome use the GPU, because the D3D12 path is too slow to start, and the runs that did use hardware GL were slower than SwiftShader, so falling back is the right call here. Whether the fix gets an NVIDIA card working, as in the report, is not tested here: no NVIDIA card was available.

The same composition rendered from native Windows on that machine picked the GPU in auto mode (ANGLE (Intel ... Direct3D11)), which is what the note and docs point to.

Not in this PR: the firewall prompt (#5548 suggestion 4)

The render's local file server already listens on 127.0.0.1 only. On native Windows, sampling every listening TCP socket and UDP endpoint of node.exe and Chrome during a full hyperframes render showed only 127.0.0.1 (the file server, and Chrome's debugging port), so there is nothing in the render path to rebind. The CLI commands that do bind every interface are preview's port check, play and present, which are a different change.

Tests

  • wslHost.test.ts: detection from the kernel release and /proc/version; the four render-node states (denied and not in the group, denied but in the group, readable and writable, missing); a numeric group id when the group has no name; nothing outside WSL.
  • browserManager.test.ts, with a faked WSL host: auto mode prints the note once with the right first line for no GPU and for a real 15 s page timeout; explicit hardware after a timeout or a Chrome launch failure keeps the unverified/doctor advice and never claims software; explicit hardware with no GPU gives the WSL fix instead of Docker; a WSL host with a hardware renderer stays quiet. The existing Linux and macOS warning tests now pass a non-WSL host, so they no longer depend on the machine running them.
  • Broken on purpose: printing the note in hardware mode turns three tests red; dropping the group-membership check turns one red.

… the GPU

Under WSL the GPU check often falls back to SwiftShader and the only output
was the probe line, with Docker passthrough advice for --browser-gpu. Print
one note that says what was checked (can this user open the render node),
the exact fix, and the native Windows alternative. The CLI's WSL detector
moves to the engine so both use one.

Part of #5548
@mintlify

mintlify Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

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

Project Status Preview Updated
hyperframes 🟢 Ready View Preview Oct 13, 2026, 9:13 PM

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

@terencecho terencecho 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.

REQUEST CHANGES — the WSL diagnostic conflates probe failure with the selected render mode.

Reviewed exact HEAD aaf712787732804b50193cd493682c1ecef07e79. API base: ab8a4da53f31653350632980fa63cb07e957f0f6; effective Git/GitHub merge-base: 6f76b306751fdab465d9c6702228b119edd8ce30. Complete effective scope: one commit, ten files, +278/−20.

1. [P2] Preserve probe-failure uncertainty and the actual requested render mode

browserManager.ts:578–587,657–673 now prints the same WSL fallback note for both a completed software result and every caught probe error. The WSL warning branch then runs before cause === "probe-error" and says the probe “found no usable GPU,” replacing the existing unverified/Chrome-install/hyperframes doctor guidance. For example, a WSL --browser-gpu invocation with a bad Chrome path produces a launch failure, not evidence that its GPU is absent. The raw error line remains visible; the regression is the contradictory diagnosis and wrong remediation that follow it.

The same note unconditionally says “this render uses software WebGL (SwiftShader)” in wslHost.ts:54–56, but the explicit-hardware caller still returns "hardware" (browserManager.ts:623–635). The real capture path passes that result into buildChromeArgs (frameCapture.ts:1290–1294,1327–1331); Linux hardware arguments remain gl-egl plus --disable-software-rasterizer, not the SwiftShader arguments. A probe timeout therefore does not establish either a missing GPU or a software render; this matters especially for the slow-but-capable WSL hardware described in the PR body.

Keep the useful WSL permission observation, but distinguish probe result, probe failure/timeout, and selected render mode. Preserve the Chrome/probe-error remediation, describe explicit hardware as unverified when appropriate, and reserve the definite SwiftShader-fallback sentence for a caller that actually selects software. Add a WSL launch-error and explicit-hardware timeout regression that checks the absence of these false claims; do not change the explicit-hardware selection merely to make the wording true.

Additional [P3] diagnostic cleanup — not a separate merge hold

Do not infer missing render-group membership from an access failure.

wslHost.ts:27–33,61–66 checks only accessSync(R_OK | W_OK), catches every error, and reports “not in the "render" group” with usermod as the fix. No group membership, device ownership, mode or error reason is checked.

A non-root user already in render with a root:render node whose mode is 0600 still fails this predicate. Restrictive ACLs can do the same. The new diagnostic falsely identifies missing membership and recommends adding a group the user already has, which cannot repair that denial. That is an introduced wrong diagnosis for configured/nonstandard device permissions, not evidence of a default Ubuntu regression. I am not making this edge case a separate merge hold or asking for a general driver/permission repair system.

Report the observed read/write access denial and make render-group membership a possible cause/conditional suggestion, or verify membership before asserting it. Align the troubleshooting wording and tests with that narrower evidence. Also describe this as an access check rather than an actual device-open test: the production implementation does not call open.

Coverage and evidence bounds

The detector migration, retained CLI detectWSL alias, engine source/published subpath declarations and CLI bundle alias are coherent. The existing cached probe Promise and warning latch retain once-per-process behavior; explicit software still bypasses probing and a successful hardware probe stays quiet. No source blocker was identified in those paths.

The new assertions cover the three injected render-node states and WSL note routing, but encode the unverified group assertion. The test named as a timed-out probe rejects launch immediately; it does not exercise the real 15-second page deadline. The new explicit-hardware assertion checks only the warning, not the false SwiftShader sentence on stderr. Existing generic/Docker/probe-error warning tests supply a platform override but no non-WSL host facts, so their expectations also become dependent on the machine's actual WSL detector; make those fixtures deterministic when extending this coverage.

Audited: the complete ten-file effective diff; immutable changed-file blob reconciliation; current PR/commit prose and exhausted review/comment histories; independent browser/control-flow and WSL-contract/packaging source lenses. Parent independently read the new helper/tests, changed package/CLI declarations, browser probe/warning/cache and relevant test sources, documentation hunks, the pre-change failure branch, and real capture/launch-mode witnesses. Unchanged dependency files were reviewed selectively, not exhaustively.

Trusting / not exercised: author-reported engine/CLI checks, repeated local runs and real WSL/Windows/Intel measurements. I did not run repository tests, Chrome/GPU/native fixtures, usermod, wsl --shutdown, installs, builds or CI reruns; no source/configuration fixes, deployment/release triggers or merge were performed. These findings are source-backed diagnostic regressions, not a hold for missing native evidence or pending CI. Home retains merge ownership.

— tai

The WSL note now prints only when auto mode really selects software, and
says whether the GPU check found no GPU or could not finish. Explicit
--browser-gpu keeps its unverified and Chrome-install advice and adds the
WSL checks. The render-node check verifies group membership before
suggesting usermod.
@miguel-heygen

Copy link
Copy Markdown
Collaborator Author

Thanks, both findings were right. Fixed at 20f45bd:

P2, probe result vs render mode. The probe now only records the WSL checks with its outcome; it prints nothing itself. The "this render uses software WebGL (SwiftShader)" note prints only from the auto-mode caller, which is the only one that selects software, and its first line says either "found no hardware GPU" (probe ran) or "could not finish" (probe failed or timed out). Explicit --browser-gpu still returns hardware; its probe-error branch keeps the unverified + hyperframes doctor text and appends the WSL checks as a possible extra cause, and its no-GPU branch swaps the Docker advice for the WSL checks. New regressions: auto with a real 15 s page timeout (fake timers, same as the existing budget test), explicit hardware after that timeout, and explicit hardware with a Chrome launch failure; the last two assert no "this render uses software" on stderr. Printing the note in hardware mode turns three of them red.

P3, group membership. The check now reads the node's owning gid and the process groups. usermod -aG <group> is suggested only when the read/write access check fails and the user is not in that group; in-group-but-denied gets an ls -l pointer instead. The wording says "cannot read and write" (it is an access check, not an open). Dropping the membership check turns the new in-group test red.

Determinism. The existing Linux/macOS warning tests now pass a non-WSL host.

Re-checked on the WSL2 machine at this head: auto prints the "could not finish" note with the fix; --browser-gpu prints the unverified warning with the WSL checks and renders on hardware GL. Table in the PR body.

@terencecho terencecho 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.

APPROVE — the prior diagnostic findings are repaired at this head; no introduced blocker found.

Reviewed exact HEAD 20f45bd04f28e4046550e24606c8b6b5e7de89c3. API base: ab8a4da53f31653350632980fa63cb07e957f0f6; independently verified Git/GitHub merge-base: 6f76b306751fdab465d9c6702228b119edd8ce30. Complete effective scope: two commits, ten files, +420/−32.

The P2 in my previous review is fixed:

  • browserManager.ts:624–642,654–695 prints the definite SwiftShader sentence only when auto actually selects software. A completed no-GPU result and a failed/timed-out probe now have different wording. Explicit hardware remains hardware; probe errors retain UNVERIFIED, the underlying Chrome/probe explanation and hyperframes doctor, with WSL observations appended as another possible cause rather than proof of a missing GPU.
  • The real capture path still forwards that returned selection to Chrome arguments (frameCapture.ts:1290–1294,1327–1333; browserManager.ts:1138–1175). The fix does not change requested hardware to make its prose true.
  • The additional P3 membership issue is also repaired: wslHost.ts:40–51,81–94 reads the node's owning group and process groups, and suggests usermod only for access denial and absent membership. Already-in-group denial instead reports the observed denial and points to permissions. This is a bounded diagnostic, not a universal mode/ACL/driver repair promise.

Source-inspected regressions now reach the real page-probe deadline with fake timers, separately cover Chrome launch rejection and explicit hardware, and check that stderr does not claim software in hardware mode. The in-group-denied test rejects an unnecessary usermod suggestion; generic Linux/macOS fixtures explicitly supply non-WSL host facts. The shared cached probe and separate per-mode logging latches remain coherent for concurrent auto/hardware callers. Detector migration, the CLI detectWSL alias, source/published engine subpaths, bundle alias and documentation align.

Audited: all ten effective-diff files; current PR and full commit/review/comment prose; independently reconciled all changed-file blobs and actual merge-base; a finite independent source audit plus parent verification of diagnostics, regressions and actual capture/launch consumers. Unchanged dependencies were inspected selectively.

Trusting / not exercised: author-reported tests, mutation checks and real WSL/Windows measurements. I did not run repository suites, Chrome/GPU/WSL/native fixtures, permission changes, installs, builds or CI reruns. No source/configuration edits, direct merge or release action. This is a code-merit approval, not a new native-performance receipt or a waiver of independent release gates; Home retains merge ownership.

— tai

@jrusso1020

Copy link
Copy Markdown
Collaborator

@miguel-heygen did you test this on the windows computer?

@github-actions

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 2061 (base branch 2061), smooth 1510 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)

Unstable (1)

  • crop-none-px-r0-nested-z100: tracking 0.04, pressJump 0, drop 40.07, reload 40.07, render 40.03, renderKey -, undo true, teleport true / tracking 0.04, pressJump 0, drop 0.08, reload 0.08, render 0.03, renderKey -, undo true, teleport true / tracking 0.04, pressJump 0, drop 0.08, reload 0.08, render 0.03, renderKey -, undo true, teleport true

This branch was successfully deployed

1 active deployment
staging - docs — 20f45bd0 Deployed Oct 13, 2026 by mintlify[bot]
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.

3 participants