diff --git a/electron/gpuSwitches.test.ts b/electron/gpuSwitches.test.ts new file mode 100644 index 000000000..4cbb1cdf4 --- /dev/null +++ b/electron/gpuSwitches.test.ts @@ -0,0 +1,67 @@ +import { describe, expect, it } from "vitest"; + +import { getGpuSwitches, shouldForceLinuxEgl } from "./gpuSwitches"; + +describe("shouldForceLinuxEgl", () => { + it("does not force EGL in a Wayland session", () => { + expect( + shouldForceLinuxEgl({ + XDG_SESSION_TYPE: "wayland", + WAYLAND_DISPLAY: "wayland-0", + }), + ).toBe(false); + }); + + it("does not force EGL when Wayland is explicitly requested via Ozone", () => { + expect( + shouldForceLinuxEgl({ + OZONE_PLATFORM: "wayland", + XDG_SESSION_TYPE: "x11", + }), + ).toBe(false); + }); + + it("falls back to Electron's ozone hint when OZONE_PLATFORM is invalid", () => { + expect( + shouldForceLinuxEgl({ + OZONE_PLATFORM: "auto", + ELECTRON_OZONE_PLATFORM_HINT: "wayland", + XDG_SESSION_TYPE: "x11", + }), + ).toBe(false); + }); + + it("forces EGL in an X11 session", () => { + expect(shouldForceLinuxEgl({ XDG_SESSION_TYPE: "x11" })).toBe(true); + }); + + it("forces EGL when x11 is explicitly requested via Electron's ozone hint", () => { + expect( + shouldForceLinuxEgl({ + ELECTRON_OZONE_PLATFORM_HINT: "x11", + WAYLAND_DISPLAY: "wayland-0", + }), + ).toBe(true); + }); +}); + +describe("getGpuSwitches", () => { + it("returns the Linux VAAPI workaround without forcing EGL on Wayland", () => { + expect( + getGpuSwitches("linux", { + XDG_SESSION_TYPE: "wayland", + WAYLAND_DISPLAY: "wayland-0", + }), + ).toEqual({ + useGl: undefined, + disableFeatures: ["VaapiVideoDecoder", "VaapiVideoEncoder"], + }); + }); + + it("returns the X11 EGL workaround on Linux X11", () => { + expect(getGpuSwitches("linux", { XDG_SESSION_TYPE: "x11" })).toEqual({ + useGl: "egl", + disableFeatures: ["VaapiVideoDecoder", "VaapiVideoEncoder"], + }); + }); +}); diff --git a/electron/gpuSwitches.ts b/electron/gpuSwitches.ts new file mode 100644 index 000000000..7b7c81ee8 --- /dev/null +++ b/electron/gpuSwitches.ts @@ -0,0 +1,66 @@ +export interface GpuSwitches { + useAngle?: string; + useGl?: string; + disableFeatures?: string[]; +} + +function normalizeLinuxWindowSystem(value: string | undefined): "wayland" | "x11" | null { + const normalized = value?.trim().toLowerCase(); + if (normalized === "wayland" || normalized === "x11") { + return normalized; + } + + return null; +} + +function getForcedLinuxWindowSystem(env: NodeJS.ProcessEnv): "wayland" | "x11" | null { + return ( + normalizeLinuxWindowSystem(env.OZONE_PLATFORM) ?? + normalizeLinuxWindowSystem(env.ELECTRON_OZONE_PLATFORM_HINT) + ); +} + +export function shouldForceLinuxEgl(env: NodeJS.ProcessEnv): boolean { + const forcedWindowSystem = getForcedLinuxWindowSystem(env); + if (forcedWindowSystem === "wayland") { + return false; + } + if (forcedWindowSystem === "x11") { + return true; + } + + const sessionType = env.XDG_SESSION_TYPE?.toLowerCase(); + if (sessionType === "wayland") { + return false; + } + if (sessionType === "x11") { + return true; + } + + return !env.WAYLAND_DISPLAY; +} + +export function getGpuSwitches( + platform: NodeJS.Platform, + env: NodeJS.ProcessEnv = process.env, +): GpuSwitches { + if (platform === "darwin") { + return { + useAngle: "metal", + disableFeatures: ["MacCatapLoopbackAudioForScreenShare"], + }; + } + + if (platform === "win32") { + return { useAngle: "d3d11" }; + } + + if (platform === "linux") { + return { + useGl: shouldForceLinuxEgl(env) ? "egl" : undefined, + disableFeatures: ["VaapiVideoDecoder", "VaapiVideoEncoder"], + }; + } + + return {}; +} diff --git a/electron/main.ts b/electron/main.ts index 636c29ff5..c8041e5ec 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -17,6 +17,7 @@ import { import { RECORDINGS_DIR } from "./appPaths"; import { showCursor } from "./cursorHider"; import { registerExtensionIpcHandlers } from "./extensions/extensionIpc"; +import { getGpuSwitches } from "./gpuSwitches"; import { cleanupNativeVideoExportSessions, getSelectedSourceId, @@ -58,22 +59,16 @@ app.commandLine.appendSwitch("enable-unsafe-webgpu"); app.commandLine.appendSwitch("enable-gpu-rasterization"); function configureGpuAccelerationSwitches() { - if (process.platform === "darwin") { - app.commandLine.appendSwitch("use-angle", "metal"); - app.commandLine.appendSwitch("disable-features", "MacCatapLoopbackAudioForScreenShare"); - return; + const { useAngle, useGl, disableFeatures } = getGpuSwitches(process.platform, process.env); + if (useAngle) { + app.commandLine.appendSwitch("use-angle", useAngle); } - - if (process.platform === "win32") { - app.commandLine.appendSwitch("use-angle", "d3d11"); - return; + if (useGl) { + app.commandLine.appendSwitch("use-gl", useGl); + } + if (disableFeatures && disableFeatures.length > 0) { + app.commandLine.appendSwitch("disable-features", disableFeatures.join(",")); } - - // Linux (and other Unix): prefer EGL over GLX for better Wayland compatibility. - // Disable VAAPI — many distros ship broken drivers that cause - // "vaInitialize failed" and prevent the renderer from loading. - app.commandLine.appendSwitch("use-gl", "egl"); - app.commandLine.appendSwitch("disable-features", "VaapiVideoDecoder,VaapiVideoEncoder"); } async function logSmokeExportGpuDiagnostics() { @@ -905,8 +900,7 @@ app.whenReady().then(async () => { // source picker entirely). This avoids calling getSources() which // would itself trigger an extra portal dialog. const isLinuxPortalSentinel = - process.platform === "linux" && - (sourceId === "screen:linux-portal" || !sourceId); + process.platform === "linux" && (sourceId === "screen:linux-portal" || !sourceId); if (isLinuxPortalSentinel) { callback({ video: { id: "screen:0:0", name: "Entire screen" } }); return;