From 3ceb718b0705398ab8334607ae1e7501bdfc279f Mon Sep 17 00:00:00 2001 From: Filip Michalski Date: Thu, 22 Oct 2026 14:34:52 +0200 Subject: [PATCH 1/3] fix(linux): reduce idle WebKit GPU memory usage --- README.md | 11 +++++++ src-tauri/src/main.rs | 29 ++++++++++++++--- tests/linuxGtkStartup.test.ts | 61 ++++++++++++++++++++++++++++++++--- 3 files changed, 92 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 008f6b06..a69dd994 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,17 @@ pnpm tauri build The built application will be in `src-tauri/target/release/bundle/`. +### Linux Rendering + +On Linux, the dashboard defaults to software compositing and shared-memory +rendering to reduce idle GPU memory usage. This can increase CPU work during +scrolling. Existing WebKit rendering overrides are preserved. To use hardware +compositing instead: + +```bash +WEBKIT_DISABLE_COMPOSITING_MODE=0 ./Codex-Switcher_*.AppImage +``` + ### Run the Dashboard in a Browser You can also serve the built dashboard over HTTP instead of opening the Tauri shell. diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 1c959637..bf7a9da7 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -6,12 +6,31 @@ mod linux_gtk; fn main() { #[cfg(target_os = "linux")] - for variable in ["GTK_MODULES", "GTK3_MODULES"] { - if let Some(filtered) = std::env::var_os(variable) - .as_deref() - .and_then(linux_gtk::without_appmenu_module) + { + for variable in ["GTK_MODULES", "GTK3_MODULES"] { + if let Some(filtered) = std::env::var_os(variable) + .as_deref() + .and_then(linux_gtk::without_appmenu_module) + { + unsafe { std::env::set_var(variable, filtered) }; + } + } + + // The dashboard does not need accelerated compositing, which retains + // substantial VRAM even while idle. Set the default before WebKit starts. + if std::env::var_os("WEBKIT_DISABLE_COMPOSITING_MODE").is_none() + && std::env::var_os("WEBKIT_FORCE_COMPOSITING_MODE").is_none() + { + unsafe { std::env::set_var("WEBKIT_DISABLE_COMPOSITING_MODE", "1") }; + } + + // Newer WebKit can still allocate GPU painting resources unless the + // DMA-BUF renderer is disabled before the webview is created. + if std::env::var_os("WEBKIT_DISABLE_COMPOSITING_MODE").as_deref() + == Some(std::ffi::OsStr::new("1")) + && std::env::var_os("WEBKIT_DISABLE_DMABUF_RENDERER").is_none() { - unsafe { std::env::set_var(variable, filtered) }; + unsafe { std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1") }; } } diff --git a/tests/linuxGtkStartup.test.ts b/tests/linuxGtkStartup.test.ts index d3d370a4..bb0ec65d 100644 --- a/tests/linuxGtkStartup.test.ts +++ b/tests/linuxGtkStartup.test.ts @@ -8,7 +8,14 @@ import test from "node:test"; const probeSource = ` pub fn run() { - for name in ["GTK_MODULES", "GTK3_MODULES", "GDK_BACKEND"] { + for name in [ + "GTK_MODULES", + "GTK3_MODULES", + "GDK_BACKEND", + "WEBKIT_DISABLE_COMPOSITING_MODE", + "WEBKIT_FORCE_COMPOSITING_MODE", + "WEBKIT_DISABLE_DMABUF_RENDERER", + ] { match std::env::var_os(name) { Some(value) => println!("{}={}", name, value.to_string_lossy()), None => println!("{}=", name), @@ -17,7 +24,7 @@ pub fn run() { } `; -test("Linux entry point filters modules before starting Tauri", { +test("Linux entry point applies GTK and rendering defaults before starting Tauri", { skip: process.platform !== "linux", }, async (t) => { const directory = mkdtempSync(join(tmpdir(), "codex-switcher-startup-")); @@ -43,8 +50,41 @@ test("Linux entry point filters modules before starting Tauri", { gtk3?: string; expectedModules?: string; expectedGtk3?: string; + disableCompositing?: string; + forceCompositing?: string; + expectedDisableCompositing?: string; + disableDmabuf?: string; + expectedDisableDmabuf?: string; }[] = [ - { name: "leaves unset variables unset" }, + { name: "leaves unset GTK modules unset" }, + { + name: "preserves an explicit hardware compositing override", + disableCompositing: "0", + expectedDisableCompositing: "0", + expectedDisableDmabuf: "", + }, + { + name: "preserves an explicitly empty compositing override", + disableCompositing: "", + expectedDisableCompositing: "", + expectedDisableDmabuf: "", + }, + { + name: "does not override forced hardware compositing", + forceCompositing: "1", + expectedDisableCompositing: "", + expectedDisableDmabuf: "", + }, + { + name: "preserves an explicit DMA-BUF renderer override", + disableDmabuf: "0", + expectedDisableDmabuf: "0", + }, + { + name: "preserves an explicitly empty DMA-BUF override", + disableDmabuf: "", + expectedDisableDmabuf: "", + }, { name: "preserves explicitly empty values", modules: "", gtk3: "", expectedModules: "", expectedGtk3: "" }, { name: "handles the reported Ubuntu environment", @@ -82,13 +122,23 @@ test("Linux entry point filters modules before starting Tauri", { }, ]; - for (const { name, modules, gtk3, expectedModules, expectedGtk3 } of cases) { + for (const { + name, modules, gtk3, expectedModules, expectedGtk3, + disableCompositing, forceCompositing, expectedDisableCompositing = "1", + disableDmabuf, expectedDisableDmabuf = "1", + } of cases) { await t.test(name, () => { const env: Record = { ...process.env, GDK_BACKEND: "wayland" }; delete env.GTK_MODULES; delete env.GTK3_MODULES; + delete env.WEBKIT_DISABLE_COMPOSITING_MODE; + delete env.WEBKIT_FORCE_COMPOSITING_MODE; + delete env.WEBKIT_DISABLE_DMABUF_RENDERER; if (modules !== undefined) env.GTK_MODULES = modules; if (gtk3 !== undefined) env.GTK3_MODULES = gtk3; + if (disableCompositing !== undefined) env.WEBKIT_DISABLE_COMPOSITING_MODE = disableCompositing; + if (forceCompositing !== undefined) env.WEBKIT_FORCE_COMPOSITING_MODE = forceCompositing; + if (disableDmabuf !== undefined) env.WEBKIT_DISABLE_DMABUF_RENDERER = disableDmabuf; const result = spawnSync(executable, [], { env, encoding: "utf8" }); assert.ifError(result.error); assert.equal(result.status, 0, result.stderr); @@ -96,6 +146,9 @@ test("Linux entry point filters modules before starting Tauri", { `GTK_MODULES=${expectedModules ?? ""}`, `GTK3_MODULES=${expectedGtk3 ?? ""}`, "GDK_BACKEND=wayland", + `WEBKIT_DISABLE_COMPOSITING_MODE=${expectedDisableCompositing}`, + `WEBKIT_FORCE_COMPOSITING_MODE=${forceCompositing ?? ""}`, + `WEBKIT_DISABLE_DMABUF_RENDERER=${expectedDisableDmabuf}`, "", ].join("\n")); }); From ebe89335eea16ad0efba83421278954689f4e67a Mon Sep 17 00:00:00 2001 From: Filip Michalski Date: Thu, 22 Oct 2026 19:23:46 +0200 Subject: [PATCH 2/3] fix(linux): retain GPU compositing for smooth scrolling --- README.md | 10 +++---- src-tauri/src/main.rs | 19 +++--------- tests/linuxGtkStartup.test.ts | 54 ++++++++++------------------------- 3 files changed, 24 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index a69dd994..81929e5a 100644 --- a/README.md +++ b/README.md @@ -91,13 +91,13 @@ The built application will be in `src-tauri/target/release/bundle/`. ### Linux Rendering -On Linux, the dashboard defaults to software compositing and shared-memory -rendering to reduce idle GPU memory usage. This can increase CPU work during -scrolling. Existing WebKit rendering overrides are preserved. To use hardware -compositing instead: +On Linux, the dashboard defaults to CPU tile painting to reduce Skia's GPU +memory usage while keeping GPU compositing and asynchronous scrolling enabled. +Savings depend on the WebKit version. Explicit WebKit rendering overrides are +preserved. To use GPU tile painting instead: ```bash -WEBKIT_DISABLE_COMPOSITING_MODE=0 ./Codex-Switcher_*.AppImage +WEBKIT_SKIA_ENABLE_CPU_RENDERING=0 ./Codex-Switcher_*.AppImage ``` ### Run the Dashboard in a Browser diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index bf7a9da7..74cbd065 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -16,21 +16,10 @@ fn main() { } } - // The dashboard does not need accelerated compositing, which retains - // substantial VRAM even while idle. Set the default before WebKit starts. - if std::env::var_os("WEBKIT_DISABLE_COMPOSITING_MODE").is_none() - && std::env::var_os("WEBKIT_FORCE_COMPOSITING_MODE").is_none() - { - unsafe { std::env::set_var("WEBKIT_DISABLE_COMPOSITING_MODE", "1") }; - } - - // Newer WebKit can still allocate GPU painting resources unless the - // DMA-BUF renderer is disabled before the webview is created. - if std::env::var_os("WEBKIT_DISABLE_COMPOSITING_MODE").as_deref() - == Some(std::ffi::OsStr::new("1")) - && std::env::var_os("WEBKIT_DISABLE_DMABUF_RENDERER").is_none() - { - unsafe { std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1") }; + // CPU tile painting reduces Skia's GPU allocations while keeping GPU + // compositing and asynchronous scrolling. Preserve explicit overrides. + if std::env::var_os("WEBKIT_SKIA_ENABLE_CPU_RENDERING").is_none() { + unsafe { std::env::set_var("WEBKIT_SKIA_ENABLE_CPU_RENDERING", "1") }; } } diff --git a/tests/linuxGtkStartup.test.ts b/tests/linuxGtkStartup.test.ts index bb0ec65d..0379438c 100644 --- a/tests/linuxGtkStartup.test.ts +++ b/tests/linuxGtkStartup.test.ts @@ -13,7 +13,7 @@ pub fn run() { "GTK3_MODULES", "GDK_BACKEND", "WEBKIT_DISABLE_COMPOSITING_MODE", - "WEBKIT_FORCE_COMPOSITING_MODE", + "WEBKIT_SKIA_ENABLE_CPU_RENDERING", "WEBKIT_DISABLE_DMABUF_RENDERER", ] { match std::env::var_os(name) { @@ -50,40 +50,19 @@ test("Linux entry point applies GTK and rendering defaults before starting Tauri gtk3?: string; expectedModules?: string; expectedGtk3?: string; - disableCompositing?: string; - forceCompositing?: string; - expectedDisableCompositing?: string; - disableDmabuf?: string; - expectedDisableDmabuf?: string; + cpuRendering?: string; + expectedCpuRendering?: string; }[] = [ { name: "leaves unset GTK modules unset" }, { - name: "preserves an explicit hardware compositing override", - disableCompositing: "0", - expectedDisableCompositing: "0", - expectedDisableDmabuf: "", + name: "preserves an explicit GPU painting override", + cpuRendering: "0", + expectedCpuRendering: "0", }, { - name: "preserves an explicitly empty compositing override", - disableCompositing: "", - expectedDisableCompositing: "", - expectedDisableDmabuf: "", - }, - { - name: "does not override forced hardware compositing", - forceCompositing: "1", - expectedDisableCompositing: "", - expectedDisableDmabuf: "", - }, - { - name: "preserves an explicit DMA-BUF renderer override", - disableDmabuf: "0", - expectedDisableDmabuf: "0", - }, - { - name: "preserves an explicitly empty DMA-BUF override", - disableDmabuf: "", - expectedDisableDmabuf: "", + name: "preserves an explicitly empty painting override", + cpuRendering: "", + expectedCpuRendering: "", }, { name: "preserves explicitly empty values", modules: "", gtk3: "", expectedModules: "", expectedGtk3: "" }, { @@ -124,21 +103,18 @@ test("Linux entry point applies GTK and rendering defaults before starting Tauri for (const { name, modules, gtk3, expectedModules, expectedGtk3, - disableCompositing, forceCompositing, expectedDisableCompositing = "1", - disableDmabuf, expectedDisableDmabuf = "1", + cpuRendering, expectedCpuRendering = "1", } of cases) { await t.test(name, () => { const env: Record = { ...process.env, GDK_BACKEND: "wayland" }; delete env.GTK_MODULES; delete env.GTK3_MODULES; delete env.WEBKIT_DISABLE_COMPOSITING_MODE; - delete env.WEBKIT_FORCE_COMPOSITING_MODE; + delete env.WEBKIT_SKIA_ENABLE_CPU_RENDERING; delete env.WEBKIT_DISABLE_DMABUF_RENDERER; if (modules !== undefined) env.GTK_MODULES = modules; if (gtk3 !== undefined) env.GTK3_MODULES = gtk3; - if (disableCompositing !== undefined) env.WEBKIT_DISABLE_COMPOSITING_MODE = disableCompositing; - if (forceCompositing !== undefined) env.WEBKIT_FORCE_COMPOSITING_MODE = forceCompositing; - if (disableDmabuf !== undefined) env.WEBKIT_DISABLE_DMABUF_RENDERER = disableDmabuf; + if (cpuRendering !== undefined) env.WEBKIT_SKIA_ENABLE_CPU_RENDERING = cpuRendering; const result = spawnSync(executable, [], { env, encoding: "utf8" }); assert.ifError(result.error); assert.equal(result.status, 0, result.stderr); @@ -146,9 +122,9 @@ test("Linux entry point applies GTK and rendering defaults before starting Tauri `GTK_MODULES=${expectedModules ?? ""}`, `GTK3_MODULES=${expectedGtk3 ?? ""}`, "GDK_BACKEND=wayland", - `WEBKIT_DISABLE_COMPOSITING_MODE=${expectedDisableCompositing}`, - `WEBKIT_FORCE_COMPOSITING_MODE=${forceCompositing ?? ""}`, - `WEBKIT_DISABLE_DMABUF_RENDERER=${expectedDisableDmabuf}`, + "WEBKIT_DISABLE_COMPOSITING_MODE=", + `WEBKIT_SKIA_ENABLE_CPU_RENDERING=${expectedCpuRendering}`, + "WEBKIT_DISABLE_DMABUF_RENDERER=", "", ].join("\n")); }); From bedf983a52b48023f812e8a19a0f32eb6709bb67 Mon Sep 17 00:00:00 2001 From: Filip Michalski Date: Thu, 22 Oct 2026 20:52:35 +0200 Subject: [PATCH 3/3] docs(linux): describe optional software OpenGL rendering --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 81929e5a..22299316 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,17 @@ preserved. To use GPU tile painting instead: WEBKIT_SKIA_ENABLE_CPU_RENDERING=0 ./Codex-Switcher_*.AppImage ``` +On systems using Mesa, software OpenGL can reduce GPU allocations further while +keeping WebKit's threaded compositor and scrolling. This uses more CPU during +scrolling and more system RAM. To try it for one launch: + +```bash +LIBGL_ALWAYS_SOFTWARE=1 WEBKIT_SKIA_ENABLE_CPU_RENDERING=1 ./Codex-Switcher_*.AppImage +``` + +This setting also applies to graphical child processes started by the app. +The desktop compositor may still allocate GPU memory to display the window. + ### Run the Dashboard in a Browser You can also serve the built dashboard over HTTP instead of opening the Tauri shell.