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

* fix(engine): say why WSL renders fall back to software and how to get 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

* fix(engine): keep WSL GPU notes true to the probe result and render mode

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.
This commit is contained in:
Miguel Ángel
2026-10-13 22:18:59 +00:00
committed by GitHub
parent 5de2712ad5
commit 96e2154d7c
10 changed files with 420 additions and 32 deletions
+2
View File
@@ -159,6 +159,8 @@ npx hyperframes render --output final.mp4
```
It starts quickly and can use the computer’s browser GPU.
On WSL2, Chrome may not reach the GPU; see
[the render is slow under WSL2](/guides/troubleshooting#the-render-is-slow-under-wsl2).
Use Docker when a controlled Chrome, FFmpeg, and font environment matters:
+30
View File
@@ -229,6 +229,36 @@ During iteration:
Run `npx hyperframes benchmark` when tuning worker settings.
### The render is slow under WSL2
WSL2 can expose a graphics card to Linux, but headless Chrome may not be able to use
it. The render then falls back to software rendering, which can be several times
slower on a long or effect-heavy video. The render log says so:
```text
[hyperframes] browserGpuMode probe → software (WebGL unavailable)
[hyperframes] WSL: the GPU check found no hardware GPU, so this render uses software WebGL ...
```
The lines after it say what HyperFrames checked. If they say your user is not in
the `render` group, add yourself, restart WSL, and render again:
```bash
sudo usermod -aG render $USER
# then, in Windows PowerShell:
wsl --shutdown
```
If the GPU is still not used, render from Windows instead. Copy the project to
a Windows drive (for example `C:\Users\you\renders\my-video`) and run the same
command from PowerShell, where Chrome uses the graphics card through Direct3D:
```powershell
npx hyperframes render --output final.mp4
```
Run it from a normal Windows folder, not a `\\wsl.localhost\...` path.
### Expected HDR but received SDR
HDR needs a supported output and correct source color metadata. MP4 is the normal HDR output path; WebM and MOV may fall back to SDR.
+1 -14
View File
@@ -1,18 +1,5 @@
import { platform, release } from "node:os";
import { readFileSync } from "node:fs";
// Shared host-platform detectors used by both system.ts (overall metadata)
// and agent_runtime.ts (sandbox fingerprinting). Lives in its own module
// to avoid an import cycle between those two files.
export function detectWSL(): boolean {
if (platform() !== "linux") return false;
try {
const osRelease = release().toLowerCase();
if (osRelease.includes("microsoft") || osRelease.includes("wsl")) return true;
const procVersion = readFileSync("/proc/version", "utf-8").toLowerCase();
return procVersion.includes("microsoft") || procVersion.includes("wsl");
} catch {
return false;
}
}
export { detectWsl as detectWSL } from "@hyperframes/engine/wsl-host";
+1
View File
@@ -101,6 +101,7 @@ var __dirname = __hf_dirname(__filename);`,
"./chrome-host-ceiling",
"./shader-transitions",
"./system-memory",
"./wsl-host",
]),
};
options.loader = { ...options.loader, ".browser.js": "text" };
+6
View File
@@ -25,6 +25,12 @@
"types": "./dist/services/systemMemory.d.ts",
"environments": ["bun", "node"]
},
"./wsl-host": {
"source": "./src/services/wslHost.ts",
"runtime": "./dist/services/wslHost.js",
"types": "./dist/services/wslHost.d.ts",
"environments": ["bun", "node"]
},
"./shader-transitions": {
"source": "./src/utils/shaderTransitions.ts",
"runtime": "./dist/utils/shaderTransitions.js",
+9
View File
@@ -35,6 +35,11 @@
"import": "./src/services/systemMemory.ts",
"types": "./src/services/systemMemory.ts"
},
"./wsl-host": {
"bun": "./src/services/wslHost.ts",
"import": "./src/services/wslHost.ts",
"types": "./src/services/wslHost.ts"
},
"./shader-transitions": {
"bun": "./src/utils/shaderTransitions.ts",
"import": "./src/utils/shaderTransitions.ts",
@@ -61,6 +66,10 @@
"import": "./dist/services/systemMemory.js",
"types": "./dist/services/systemMemory.d.ts"
},
"./wsl-host": {
"import": "./dist/services/wslHost.js",
"types": "./dist/services/wslHost.d.ts"
},
"./shader-transitions": {
"import": "./dist/utils/shaderTransitions.js",
"types": "./dist/utils/shaderTransitions.d.ts"
@@ -369,6 +369,13 @@ describe("resolveBrowserGpuMode", () => {
return { close, launch };
};
const notWsl = {
platform: "linux" as const,
release: "6.8.0-45-generic",
readProcVersion: () => "Linux version 6.8.0-45-generic",
renderNodeAccess: () => ({ state: "read-write" as const }),
};
beforeEach(() => {
_resetAutoBrowserGpuModeCacheForTests();
});
@@ -414,15 +421,15 @@ describe("resolveBrowserGpuMode", () => {
renderer: "ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device))",
});
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
const mode = await resolveBrowserGpuMode("hardware", { platform: "linux" });
const mode = await resolveBrowserGpuMode("hardware", { platform: "linux", wslHost: notWsl });
expect(mode).toBe("hardware");
const warning = warn.mock.calls.map((call) => String(call[0])).join("\n");
expect(warning).toContain("browserGpuMode=hardware was requested");
expect(warning).toContain("--gpus all");
// Once per process, not once per worker — the render path resolves the
// mode for the probe browser plus every parallel worker.
await resolveBrowserGpuMode("hardware", { platform: "linux" });
await resolveBrowserGpuMode("hardware", { platform: "linux" });
await resolveBrowserGpuMode("hardware", { platform: "linux", wslHost: notWsl });
await resolveBrowserGpuMode("hardware", { platform: "linux", wslHost: notWsl });
expect(warn).toHaveBeenCalledTimes(1);
});
@@ -440,7 +447,9 @@ describe("resolveBrowserGpuMode", () => {
renderer: "ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device))",
});
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
expect(await resolveBrowserGpuMode("hardware", { platform: "darwin" })).toBe("hardware");
expect(await resolveBrowserGpuMode("hardware", { platform: "darwin", wslHost: notWsl })).toBe(
"hardware",
);
const warning = String(warn.mock.calls[0]?.[0]);
expect(warning).toContain("host exposes a GPU");
expect(warning).not.toContain("--gpus all");
@@ -454,13 +463,127 @@ describe("resolveBrowserGpuMode", () => {
launch: vi.fn().mockRejectedValue(new Error("spawn ENOENT /bad/chrome")),
} as unknown as PuppeteerNode);
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
expect(await resolveBrowserGpuMode("hardware", { platform: "linux" })).toBe("hardware");
expect(await resolveBrowserGpuMode("hardware", { platform: "linux", wslHost: notWsl })).toBe(
"hardware",
);
const warning = String(warn.mock.calls[0]?.[0]);
expect(warning).toContain("GPU probe could not run");
expect(warning).toContain("hyperframes doctor");
expect(warning).not.toContain("--gpus all");
});
describe("under WSL (heygen-com/hyperframes#5548)", () => {
const wslHost = {
platform: "linux" as const,
release: "5.15.167.4-microsoft-standard-WSL2",
readProcVersion: () => "",
renderNodeAccess: () => ({ state: "denied" as const, group: "render", userInGroup: false }),
};
const output = (spy: { mock: { calls: unknown[][] } }) =>
spy.mock.calls.map((call) => String(call[0])).join("\n");
const neverAnsweringProbe = () => {
const launch = vi.fn().mockResolvedValue({
newPage: vi.fn().mockResolvedValue({ evaluate: () => new Promise(() => {}) }),
close: () => new Promise(() => {}),
process: () => ({ kill: vi.fn() }),
disconnect: vi.fn().mockResolvedValue(undefined),
});
_setPuppeteerForTests({ launch } as unknown as PuppeteerNode);
};
it("auto mode says it fell back to software, why, and the fix, once", async () => {
setMockWebGlProbe({ hasWebGL: false, vendor: "", renderer: "" });
const error = vi.spyOn(console, "error").mockImplementation(() => {});
expect(await resolveBrowserGpuMode("auto", { platform: "linux", wslHost })).toBe("software");
await resolveBrowserGpuMode("auto", { platform: "linux", wslHost });
const text = output(error);
expect(text).toContain("browserGpuMode probe → software (WebGL unavailable)");
expect(text).toContain(
"WSL: the GPU check found no hardware GPU, so this render uses software",
);
expect(text).toContain('is not in its group ("render")');
expect(text).toContain("sudo usermod -aG render $USER");
expect(text.match(/usermod/g)).toHaveLength(1);
});
it("auto mode says the check did not finish when the probe page times out", async () => {
vi.useFakeTimers();
try {
neverAnsweringProbe();
const error = vi.spyOn(console, "error").mockImplementation(() => {});
const mode = resolveBrowserGpuMode("auto", { platform: "linux", wslHost });
await vi.advanceTimersByTimeAsync(15_000 + 250);
await expect(mode).resolves.toBe("software");
const text = output(error);
expect(text).toContain("did not answer within 15000ms");
expect(text).toContain("WSL: the GPU check could not finish");
expect(text).not.toContain("found no hardware GPU");
} finally {
vi.useRealTimers();
}
});
it("explicit 'hardware' after a timeout stays unverified and never claims software", async () => {
vi.useFakeTimers();
try {
neverAnsweringProbe();
const error = vi.spyOn(console, "error").mockImplementation(() => {});
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
const mode = resolveBrowserGpuMode("hardware", { platform: "linux", wslHost });
await vi.advanceTimersByTimeAsync(15_000 + 250);
await expect(mode).resolves.toBe("hardware");
const warning = output(warn);
expect(warning).toContain("hardware acceleration is UNVERIFIED");
expect(warning).toContain("hyperframes doctor");
expect(warning).toContain("sudo usermod -aG render $USER");
expect(warning).not.toContain("found no hardware GPU");
expect(output(error)).not.toContain("this render uses software");
} finally {
vi.useRealTimers();
}
});
it("explicit 'hardware' with a broken Chrome keeps the Chrome-install advice", async () => {
_setPuppeteerForTests({
launch: vi.fn().mockRejectedValue(new Error("spawn ENOENT /bad/chrome")),
} as unknown as PuppeteerNode);
const error = vi.spyOn(console, "error").mockImplementation(() => {});
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
expect(await resolveBrowserGpuMode("hardware", { platform: "linux", wslHost })).toBe(
"hardware",
);
const warning = output(warn);
expect(warning).toContain("GPU probe could not run");
expect(warning).toContain("hyperframes doctor");
expect(warning).not.toContain("--gpus all");
expect(output(error)).not.toContain("this render uses software");
});
it("explicit 'hardware' with no hardware GPU gives the WSL fix, not Docker passthrough", async () => {
setMockWebGlProbe({ hasWebGL: false, vendor: "", renderer: "" });
const error = vi.spyOn(console, "error").mockImplementation(() => {});
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
expect(await resolveBrowserGpuMode("hardware", { platform: "linux", wslHost })).toBe(
"hardware",
);
const warning = output(warn);
expect(warning).toContain("sudo usermod -aG render $USER");
expect(warning).not.toContain("--gpus all");
expect(output(error)).not.toContain("this render uses software");
});
it("stays quiet when WSL hands Chrome a hardware GPU", async () => {
setMockWebGlProbe({
hasWebGL: true,
vendor: "Google Inc. (Microsoft Corporation)",
renderer: "ANGLE (Microsoft Corporation, D3D12 (Intel(R) Graphics), OpenGL ES 3.0)",
});
const error = vi.spyOn(console, "error").mockImplementation(() => {});
expect(await resolveBrowserGpuMode("auto", { platform: "linux", wslHost })).toBe("hardware");
expect(output(error)).not.toContain("WSL");
});
});
it("falls back to 'software' within the probe budget when the probe page never answers", async () => {
vi.useFakeTimers();
try {
+51 -13
View File
@@ -15,6 +15,7 @@ import { homedir } from "os";
import { chromeMajorCeiling, exceedsChromeCeiling } from "./chromeHostCeiling.js";
import { DEFAULT_CONFIG, type EngineConfig } from "../config.js";
import { getSystemTotalMb, LOW_MEMORY_TOTAL_MB_THRESHOLD } from "./systemMemory.js";
import { describeWslGpuAccess, type WslHostFacts } from "./wslHost.js";
import {
BrowserLeasePool,
type BrowserLaunchFingerprint,
@@ -83,6 +84,7 @@ interface GpuProbeOptions {
browserTimeout?: number;
platform?: NodeJS.Platform;
probeLifecycle?: GpuProbeLifecycle;
wslHost?: WslHostFacts;
}
const GPU_PROBE_TIMEOUT_MS = 15_000;
@@ -495,6 +497,8 @@ export const _closeBrowserAfterFailedProbeForTests = closeBrowserAfterFailedProb
interface GpuProbeOutcome {
mode: "software" | "hardware";
cause?: "no-gpu" | "probe-error";
/** Under WSL, what we checked on its GPU path (see describeWslGpuAccess). */
wslAccess?: string[];
}
/**
@@ -514,6 +518,7 @@ let _autoBrowserGpuModeCache: Promise<GpuProbeOutcome> | undefined;
export function _resetAutoBrowserGpuModeCacheForTests(): void {
_autoBrowserGpuModeCache = undefined;
_unverifiedHardwareGpuWarned = false;
_wslSoftwareNoteLogged = false;
}
async function getPuppeteerOrNull(): Promise<PuppeteerNode | null> {
@@ -570,13 +575,19 @@ async function probeAutoBrowserGpuMode(options: GpuProbeOptions): Promise<GpuPro
});
const resolved = resolveWebGlProbeMode(info);
logResolvedBrowserGpuMode(resolved, describeWebGlProbe(info));
return resolved === "hardware" ? { mode: "hardware" } : { mode: "software", cause: "no-gpu" };
if (resolved === "hardware") return { mode: "hardware" };
return { mode: "software", cause: "no-gpu", ...wslAccess(options) };
} catch (err) {
logResolvedBrowserGpuMode("software", formatProbeFailure(err));
return { mode: "software", cause: "probe-error" };
return { mode: "software", cause: "probe-error", ...wslAccess(options) };
}
}
function wslAccess(options: GpuProbeOptions): Pick<GpuProbeOutcome, "wslAccess"> {
const lines = describeWslGpuAccess(options.wslHost);
return lines ? { wslAccess: lines } : {};
}
/**
* Resolve `browserGpuMode` to a concrete `"software" | "hardware"` answer.
*
@@ -610,7 +621,15 @@ export function resolveBrowserGpuMode(
if (mode === "software") return Promise.resolve(mode);
_autoBrowserGpuModeCache ??= probeAutoBrowserGpuMode(options);
if (mode === "auto") return _autoBrowserGpuModeCache.then((probed) => probed.mode);
if (mode === "auto") {
return _autoBrowserGpuModeCache.then((probed) => {
if (probed.mode === "software" && probed.wslAccess && !_wslSoftwareNoteLogged) {
_wslSoftwareNoteLogged = true;
console.error(buildWslSoftwareNote(probed));
}
return probed.mode;
});
}
return _autoBrowserGpuModeCache.then((probed) => {
// Warn once per cache lifetime, not once per caller: `createCaptureSession`
@@ -618,9 +637,7 @@ export function resolveBrowserGpuMode(
// an un-deduplicated warning prints N+1 times and buries itself.
if (probed.mode === "software" && !_unverifiedHardwareGpuWarned) {
_unverifiedHardwareGpuWarned = true;
console.warn(
buildUnverifiedHardwareGpuWarning(options.platform ?? process.platform, probed.cause),
);
console.warn(buildUnverifiedHardwareGpuWarning(options.platform ?? process.platform, probed));
}
return "hardware";
});
@@ -631,6 +648,21 @@ export function resolveBrowserGpuMode(
* cache lifetime (re-armed by `_resetAutoBrowserGpuModeCacheForTests`).
*/
let _unverifiedHardwareGpuWarned = false;
let _wslSoftwareNoteLogged = false;
/** Auto mode under WSL selected software: say so, with what we checked. */
function buildWslSoftwareNote({ cause, wslAccess = [] }: GpuProbeOutcome): string {
const check =
cause === "probe-error"
? "the GPU check could not finish (see the probe line above)"
: "the GPU check found no hardware GPU";
return [
`[hyperframes] WSL: ${check}, so this render uses software WebGL (SwiftShader) and runs ` +
"at CPU speed.",
...wslAccess,
" Pass --no-browser-gpu to skip the GPU check and render in software.",
].join("\n");
}
/**
* Warning text for "you asked for hardware GPU and we could not confirm it".
@@ -643,7 +675,7 @@ let _unverifiedHardwareGpuWarned = false;
*/
function buildUnverifiedHardwareGpuWarning(
platform: NodeJS.Platform | string,
cause: GpuProbeOutcome["cause"],
{ cause, wslAccess }: GpuProbeOutcome,
): string {
if (cause === "probe-error") {
return (
@@ -654,22 +686,28 @@ function buildUnverifiedHardwareGpuWarning(
"`browserGpuMode probe → software (probe failed ...)` line above for the underlying " +
"error, which usually means Chrome could not launch (bad HYPERFRAMES_BROWSER_PATH, " +
"missing shared libraries, or a denied sandbox) rather than a GPU problem.\n" +
" Run `hyperframes doctor` to check the Chrome install."
" Run `hyperframes doctor` to check the Chrome install." +
(wslAccess
? ["", " Under WSL, a GPU path that is slow to start or unreachable can also cause it:"]
.concat(wslAccess)
.join("\n")
: "")
);
}
const remediation =
platform === "linux"
? "Inside Docker, the container needs GPU passthrough: `--gpus all` with the NVIDIA " +
wslAccess?.join("\n") ??
(platform === "linux"
? " Inside Docker, the container needs GPU passthrough: `--gpus all` with the NVIDIA " +
"Container Toolkit installed, or `--device /dev/dri` for Mesa/AMD/Intel. The image " +
"also needs the matching userspace driver + libEGL. Verify with " +
"`hyperframes render --browser-gpu` and watch for this warning disappearing."
: "Check that the host exposes a GPU to this process and that the graphics drivers are " +
"installed.";
: " Check that the host exposes a GPU to this process and that the graphics drivers " +
"are installed.");
return (
"[hyperframes] browserGpuMode=hardware was requested, but the WebGL probe found no " +
"hardware GPU, so Chrome will silently fall back to software WebGL and the capture will " +
"run at CPU speed. Honouring the explicit request anyway.\n" +
` ${remediation}\n` +
`${remediation}\n` +
" Pass --no-browser-gpu to select deterministic SwiftShader instead of waiting on a " +
"hardware path that is not there."
);
@@ -0,0 +1,90 @@
import { describe, expect, it } from "vitest";
import { describeWslGpuAccess, detectWsl, type WslHostFacts } from "./wslHost.js";
const RENDER_NODE = "/dev/dri/renderD128";
function fakeHost(overrides: Partial<WslHostFacts> = {}): WslHostFacts {
return {
platform: "linux",
release: "6.18.54.1-microsoft-standard-WSL2",
readProcVersion: () => "Linux version 6.18.54.1-microsoft-standard-WSL2",
renderNodeAccess: () => ({ state: "denied", group: "render", userInGroup: false }),
...overrides,
};
}
const note = (host: WslHostFacts) => describeWslGpuAccess(host)?.join("\n") ?? null;
describe("detectWsl", () => {
it("detects WSL from the kernel release", () => {
expect(detectWsl(fakeHost({ readProcVersion: () => "" }))).toBe(true);
});
it("falls back to /proc/version when the release string is generic", () => {
expect(detectWsl(fakeHost({ release: "6.8.0-45-generic" }))).toBe(true);
});
it("is false on plain Linux and on other platforms", () => {
const plainLinux = fakeHost({
release: "6.8.0-45-generic",
readProcVersion: () => "Linux version 6.8.0-45-generic (buildd@ubuntu)",
});
expect(detectWsl(plainLinux)).toBe(false);
expect(detectWsl(fakeHost({ platform: "win32" }))).toBe(false);
});
it("is false when /proc/version cannot be read", () => {
const host = fakeHost({
release: "6.8.0-45-generic",
readProcVersion: () => {
throw new Error("EACCES");
},
});
expect(detectWsl(host)).toBe(false);
});
});
describe("describeWslGpuAccess", () => {
it("gives the usermod fix only when access is denied and the user is not in the node's group", () => {
const text = note(fakeHost());
expect(text).toContain(
`cannot read and write ${RENDER_NODE} and is not in its group ("render")`,
);
expect(text).toContain("sudo usermod -aG render $USER");
expect(text).toContain("wsl --shutdown");
expect(text).toContain("native Windows");
expect(text).not.toContain("Docker");
});
it("uses the numeric group id when the group has no name", () => {
const host = fakeHost({
renderNodeAccess: () => ({ state: "denied", group: "993", userInGroup: false }),
});
expect(note(host)).toContain("sudo usermod -aG 993 $USER");
});
it("does not suggest joining a group the user is already in", () => {
const host = fakeHost({
renderNodeAccess: () => ({ state: "denied", group: "render", userInGroup: true }),
});
const text = note(host);
expect(text).toContain('is in the "render" group but still cannot read and write');
expect(text).toContain(`ls -l ${RENDER_NODE}`);
expect(text).not.toContain("usermod");
});
it("does not blame permissions when the node is readable and writable or missing", () => {
const readWrite = note(fakeHost({ renderNodeAccess: () => ({ state: "read-write" }) }));
expect(readWrite).toContain(`${RENDER_NODE} is readable and writable, so file permissions`);
expect(readWrite).not.toContain("usermod");
expect(readWrite).toContain("native Windows");
const missing = note(fakeHost({ renderNodeAccess: () => ({ state: "missing" }) }));
expect(missing).toContain(`${RENDER_NODE} does not exist`);
expect(missing).not.toContain("usermod");
});
it("returns null outside WSL", () => {
expect(describeWslGpuAccess(fakeHost({ platform: "darwin" }))).toBeNull();
});
});
+102
View File
@@ -0,0 +1,102 @@
/**
* WSL host detection and what we can check about its GPU path when Chrome's
* GPU probe does not confirm a hardware GPU (heygen-com/hyperframes#5548).
*/
import { accessSync, constants, existsSync, readFileSync, statSync } from "node:fs";
import { platform, release } from "node:os";
export type RenderNodeAccess =
| { state: "missing" | "read-write" }
| { state: "denied"; group: string; userInGroup: boolean };
/** The host facts these checks read; tests pass a fake. */
export interface WslHostFacts {
platform: NodeJS.Platform;
release: string;
readProcVersion(): string;
renderNodeAccess(path: string): RenderNodeAccess;
}
function groupName(gid: number): string {
try {
const line = readFileSync("/etc/group", "utf-8")
.split("\n")
.find((entry) => entry.split(":")[2] === String(gid));
return line?.split(":")[0] || String(gid);
} catch {
return String(gid);
}
}
const realHost: WslHostFacts = {
get platform() {
return platform();
},
get release() {
return release();
},
readProcVersion: () => readFileSync("/proc/version", "utf-8"),
renderNodeAccess: (path) => {
if (!existsSync(path)) return { state: "missing" };
try {
accessSync(path, constants.R_OK | constants.W_OK);
return { state: "read-write" };
} catch {
const gid = statSync(path, { throwIfNoEntry: false })?.gid ?? -1;
return {
state: "denied",
group: groupName(gid),
userInGroup: process.getgroups?.().includes(gid) ?? false,
};
}
},
};
export function detectWsl(host: WslHostFacts = realHost): boolean {
if (host.platform !== "linux") return false;
try {
const osRelease = host.release.toLowerCase();
if (osRelease.includes("microsoft") || osRelease.includes("wsl")) return true;
const procVersion = host.readProcVersion().toLowerCase();
return procVersion.includes("microsoft") || procVersion.includes("wsl");
} catch {
return false;
}
}
const RENDER_NODE = "/dev/dri/renderD128";
const NATIVE_WINDOWS =
"render from native Windows (project copied to a Windows drive, run from PowerShell), " +
"where Chrome uses the GPU through Direct3D.";
/**
* Indented lines saying what we checked on the WSL GPU path and what to try,
* or null outside WSL. Says nothing about the render mode: callers own that.
*/
export function describeWslGpuAccess(host: WslHostFacts = realHost): string[] | null {
if (!detectWsl(host)) return null;
const access = host.renderNodeAccess(RENDER_NODE);
if (access.state === "denied" && !access.userInGroup) {
return [
` Your Linux user cannot read and write ${RENDER_NODE} and is not in its group ` +
`("${access.group}"), which can stop Mesa from reaching the GPU.`,
` Fix: run \`sudo usermod -aG ${access.group} $USER\`, then \`wsl --shutdown\` from ` +
"Windows, reopen WSL and render again.",
` Or ${NATIVE_WINDOWS}`,
];
}
if (access.state === "denied") {
return [
` Your Linux user is in the "${access.group}" group but still cannot read and write ` +
`${RENDER_NODE}; check its permissions with \`ls -l ${RENDER_NODE}\`.`,
` Or ${NATIVE_WINDOWS}`,
];
}
const seen = access.state === "missing" ? "does not exist" : "is readable and writable";
return [
` ${RENDER_NODE} ${seen}, so file permissions are not the cause.`,
` To use the GPU, ${NATIVE_WINDOWS}`,
];
}