From 5ebe3f8987c38cec551787de4ba1f97010225d55 Mon Sep 17 00:00:00 2001 From: James Pan Date: Thu, 25 Jun 2026 12:14:00 +0800 Subject: [PATCH 1/3] fix(linux): stop forcing --use-gl=egl which crashes the GPU process getGpuSwitches() appended `--use-gl=egl` on Linux X11. Native EGL (gl=egl-gles2) is not among the allowed GL implementations in Electron's Linux build (only egl-angle is), so the GPU process failed to initialize and crash-looped on every launch: ERROR:ui/gl/init/gl_factory.cc:102] Requested GL implementation (gl=egl-gles2,angle=none) not found in allowed implementations: [(gl=egl-angle,angle=default)] ERROR:components/viz/service/main/viz_main_impl.cc:189] Exiting GPU process due to errors during initialization This forced the app into software compositing (the viz compositor thread pinned near a full core even when idle) and broke features that require the GPU process. Let Chromium use its default ANGLE backend instead of forcing native EGL. Co-Authored-By: Claude --- electron/gpuSwitches.test.ts | 3 +-- electron/gpuSwitches.ts | 9 +++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/electron/gpuSwitches.test.ts b/electron/gpuSwitches.test.ts index 4cbb1cdf4..718cc04c6 100644 --- a/electron/gpuSwitches.test.ts +++ b/electron/gpuSwitches.test.ts @@ -58,9 +58,8 @@ describe("getGpuSwitches", () => { }); }); - it("returns the X11 EGL workaround on Linux X11", () => { + it("does not force EGL on Linux X11 (default ANGLE backend)", () => { expect(getGpuSwitches("linux", { XDG_SESSION_TYPE: "x11" })).toEqual({ - useGl: "egl", disableFeatures: ["VaapiVideoDecoder", "VaapiVideoEncoder"], }); }); diff --git a/electron/gpuSwitches.ts b/electron/gpuSwitches.ts index 7b7c81ee8..a9b53f5f5 100644 --- a/electron/gpuSwitches.ts +++ b/electron/gpuSwitches.ts @@ -42,7 +42,7 @@ export function shouldForceLinuxEgl(env: NodeJS.ProcessEnv): boolean { export function getGpuSwitches( platform: NodeJS.Platform, - env: NodeJS.ProcessEnv = process.env, + _env: NodeJS.ProcessEnv = process.env, ): GpuSwitches { if (platform === "darwin") { return { @@ -56,8 +56,13 @@ export function getGpuSwitches( } if (platform === "linux") { + // Do NOT force --use-gl=egl: native EGL (gl=egl-gles2,angle=none) is not + // an allowed GL implementation on Electron's Linux builds, which only + // ship ANGLE (gl=egl-angle). Requesting it makes the GPU process + // crash-loop on init, which in turn breaks screen capture and forces the + // renderer into software compositing. Let Chromium use its default ANGLE + // backend instead. return { - useGl: shouldForceLinuxEgl(env) ? "egl" : undefined, disableFeatures: ["VaapiVideoDecoder", "VaapiVideoEncoder"], }; } From 807d869e71901033c237e451885b1716431ccfb2 Mon Sep 17 00:00:00 2001 From: James Pan Date: Thu, 25 Jun 2026 12:14:08 +0800 Subject: [PATCH 2/3] fix(linux): return a real desktopCapturer source on X11 for getDisplayMedia setDisplayMediaRequestHandler returned a synthetic source id "screen:0:0" on all Linux. That is a Wayland trick to make Chromium open the xdg-desktop-portal picker exactly once. On X11 there is no portal path for getDisplayMedia: Chromium must receive a *real* desktopCapturer source id, otherwise it cannot resolve the capture and getDisplayMedia rejects with "Could not start video source" (no picker is ever shown). Gate the synthetic sentinel to Wayland only (detected via shouldForceLinuxEgl, which is false on Wayland). On X11, enumerate desktopCapturer sources and return a real `screen:` source. Verified on X11/GNOME with a minimal Electron harness: both getDisplayMedia and the legacy getUserMedia({chromeMediaSource: 'desktop'}) succeed once a real desktopCapturer source id is returned, while the synthetic id reliably fails. Co-Authored-By: Claude --- electron/main.ts | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/electron/main.ts b/electron/main.ts index ed05ffeb6..b16fb1c84 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -17,7 +17,7 @@ import { import { RECORDINGS_DIR } from "./appPaths"; import { showCursor } from "./cursorHider"; import { registerExtensionIpcHandlers } from "./extensions/extensionIpc"; -import { getGpuSwitches } from "./gpuSwitches"; +import { getGpuSwitches, shouldForceLinuxEgl } from "./gpuSwitches"; import { cleanupAllExportStreams, cleanupNativeVideoExportSessions, @@ -1006,28 +1006,26 @@ app.whenReady().then(async () => { session.defaultSession.setDisplayMediaRequestHandler(async (_request, callback) => { try { const sourceId = getSelectedSourceId(); - // On Linux/Wayland, calling desktopCapturer.getSources() itself - // invokes the xdg-desktop-portal picker. If we then return one of - // those sources, Chromium triggers a SECOND portal because the - // pre-enumerated source IDs are stale on Wayland. To collapse this - // into a single portal invocation, when the Linux portal sentinel - // is set we skip getSources entirely and hand back a synthetic - // source id; Chromium then opens the portal once to actually - // resolve the capture. - // Default to the sentinel on Linux when no source has been - // pre-selected (e.g. fresh session where the renderer skipped the - // source picker entirely). This avoids calling getSources() which - // would itself trigger an extra portal dialog. + // The synthetic sentinel source id is ONLY correct on Wayland, where + // Chromium resolves getDisplayMedia through xdg-desktop-portal and + // hand-back of a real desktopCapturer id would trigger a duplicate + // portal dialog. On X11 there is no portal path for getDisplayMedia: + // Chromium must receive a REAL desktopCapturer source id, otherwise it + // cannot resolve the capture and throws "Could not start video source". + const isWaylandSession = + process.platform === "linux" && !shouldForceLinuxEgl(process.env); const isLinuxPortalSentinel = - process.platform === "linux" && (sourceId === "screen:linux-portal" || !sourceId); + isWaylandSession && (sourceId === "screen:linux-portal" || !sourceId); if (isLinuxPortalSentinel) { callback({ video: { id: "screen:0:0", name: "Entire screen" } }); return; } const sources = await desktopCapturer.getSources({ types: ["screen", "window"] }); const source = sourceId - ? (sources.find((s) => s.id === sourceId) ?? sources[0]) - : sources[0]; + ? (sources.find((s) => s.id === sourceId) ?? + sources.find((s) => s.id.startsWith("screen:")) ?? + sources[0]) + : (sources.find((s) => s.id.startsWith("screen:")) ?? sources[0]); if (source) { callback({ video: { id: source.id, name: source.name }, From afdd9f6831cd4c711c03b31487161e190bb45331 Mon Sep 17 00:00:00 2001 From: James Pan Date: Thu, 25 Jun 2026 12:35:51 +0800 Subject: [PATCH 3/3] fix(linux): expand HUD overlay fallback so popovers aren't clipped MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Linux, isHudOverlayMousePassthroughSupported() returns false, so the HUD overlay uses the compact 860x160 fallback window. That fallback is meant to expand to 860x540 while the HUD is interactive (so menus and popovers have room to render), but the expansion was explicitly skipped on Linux: if (!isHudOverlayMousePassthroughSupported()) { if (process.platform !== "linux") { // expansion skipped on Linux setHudOverlayFallbackExpanded(!ignore); } ... } With the window stuck at 860x160, upward popovers (e.g. the countdown delay menu) are clipped by the window edge — only the last couple of items are visible — and the toolbar has almost no room to drag within the window. Call setHudOverlayFallbackExpanded(!ignore) regardless of platform. The window still shrinks back to the compact size when the HUD is not interactive, so the on-screen footprint stays small when not in use. (The Win32-only transparent-overlay corruption referenced elsewhere in main.ts is on a different code path and unrelated to this change.) Co-Authored-By: Claude --- electron/windows.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/electron/windows.ts b/electron/windows.ts index 478584b55..d22ba74d4 100644 --- a/electron/windows.ts +++ b/electron/windows.ts @@ -301,9 +301,13 @@ function setHudOverlayMousePassthrough(ignore: boolean) { } if (!isHudOverlayMousePassthroughSupported()) { - if (process.platform !== "linux") { - setHudOverlayFallbackExpanded(!ignore); - } + // Expand the fallback overlay while the HUD is interactive so that + // popovers/menus have room to render. Without this the window stays at + // the compact 860x160 fallback and upward popovers are clipped by the + // window edge (e.g. only the last items visible). This previously + // skipped Linux, where passthrough is unsupported, so the bug showed up + // exactly there. + setHudOverlayFallbackExpanded(!ignore); hudOverlayWindow.setIgnoreMouseEvents(false); return; }