From 60ae74b656a8e612d726575521c75cba4433e688 Mon Sep 17 00:00:00 2001 From: Taksh Kothari Date: Fri, 7 Aug 2026 21:54:27 +0530 Subject: [PATCH] fix(desktop): use WEBKIT_DMABUF_RENDERER_FORCE_SHM for NVIDIA/AppImage (#3654) (#4505) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Heuristic and `--safe-rendering` now set `WEBKIT_DMABUF_RENDERER_FORCE_SHM=1` instead of `WEBKIT_DISABLE_DMABUF_RENDERER=1` - Legacy `DISABLE_DMABUF` stays owned so operators can still set `=0`/`=1` and take over the decision - Linux troubleshooting docs updated to match (#3654) ## Test plan - [ ] unit tests in `webkit_rendering::tests` - [ ] On NVIDIA + WebKitGTK 2.52: workspace switch no longer SIGSEGVs where the distro NVIDIA guard does not fire (Debian/Ubuntu proprietary-NVIDIA may still crash — #3654 stays open for that path) - [ ] `WEBKIT_DISABLE_DMABUF_RENDERER=0` still stands the heuristic down --------- Signed-off-by: Taksh Signed-off-by: Will Pfleger Co-authored-by: Alia Co-authored-by: Will Pfleger --- desktop/src-tauri/src/webkit_rendering.rs | 64 +++++++++++++------ .../src-tauri/src/webkit_rendering/tests.rs | 44 ++++++++++--- docs/linux-rendering-troubleshooting.md | 18 +++--- 3 files changed, 92 insertions(+), 34 deletions(-) diff --git a/desktop/src-tauri/src/webkit_rendering.rs b/desktop/src-tauri/src/webkit_rendering.rs index 905da5eee..c67816a05 100644 --- a/desktop/src-tauri/src/webkit_rendering.rs +++ b/desktop/src-tauri/src/webkit_rendering.rs @@ -2,9 +2,14 @@ //! //! WebKitGTK's dmabuf renderer aborts the web process during startup on some //! GPU/driver/compositor combinations, so Buzz comes up with no window at all -//! and the user has no way to fix it (#2338, upstream tauri#9394). Setting -//! `WEBKIT_DISABLE_DMABUF_RENDERER=1` avoids the abort by falling back to the -//! shared-memory buffer path. +//! and the user has no way to fix it (#2338, upstream tauri#9394). +//! +//! Historically Buzz set `WEBKIT_DISABLE_DMABUF_RENDERER=1`, which used to fall +//! back to shared-memory buffers. On current WebKitGTK that variable leaves the +//! transport mode empty, so `AcceleratedBackingStore::create()` returns null +//! and the UI SIGSEGVs the first time compositing is needed (#3654). +//! `WEBKIT_DMABUF_RENDERER_FORCE_SHM=1` is the documented replacement: it keeps +//! SharedMemory in the transport set and still avoids the hardware dmabuf path. //! //! WebKit reads each of these variables exactly once per process, so the choice //! has to be made before anything initializes — there is no runtime toggle and @@ -20,7 +25,7 @@ //! //! This is the shape the Tauri ecosystem converged on: clash-verge-rev's //! `utils/linux/workarounds.rs` and screenpipe's `linux_webkit_env.rs` both set -//! the same variable from the same signals at the same point in startup. +//! WebKit dmabuf env vars from the same signals at the same point in startup. use std::ffi::{OsStr, OsString}; use std::path::Path; @@ -34,21 +39,30 @@ const NVIDIA_PCI_VENDOR: &str = "0x10de"; /// Where DRM devices advertise their PCI vendor. const DRM_ROOT: &str = "/sys/class/drm"; -/// Drops the zero-copy dmabuf buffer path. The workaround for #2338. +/// Prefer shared-memory dmabuf transport. The #3654 replacement for +/// `WEBKIT_DISABLE_DMABUF_RENDERER` on current WebKitGTK. +const FORCE_SHM: &str = "WEBKIT_DMABUF_RENDERER_FORCE_SHM"; +/// Legacy kill-switch. Still owned so operators can set `=0` / `=1` and take +/// the decision away from this module, but never written by the heuristic +/// (it crashes modern WebKitGTK — see #3654). const DISABLE_DMABUF: &str = "WEBKIT_DISABLE_DMABUF_RENDERER"; /// Drops accelerated compositing as well. `--safe-rendering` only. const DISABLE_COMPOSITING: &str = "WEBKIT_DISABLE_COMPOSITING_MODE"; -/// What the heuristic applies: the #2338 workaround alone, matching the -/// ecosystem precedents. `DISABLE_COMPOSITING` is deliberately not here — no -/// report has isolated it as necessary, and it costs more rendering than this. -const HEURISTIC: [&str; 1] = [DISABLE_DMABUF]; +/// What the heuristic applies: force shared-memory transport without emptying +/// the buffer mode set (#3654). +const HEURISTIC: [&str; 1] = [FORCE_SHM]; -/// What `--safe-rendering` applies, which is also every variable this module may -/// set and therefore every variable a user assignment takes away from it. Being -/// the same list is the invariant: nothing outside it is ever written, so a user -/// value for any other WebKit variable is not a conflict. -const OWNED: [&str; 2] = [DISABLE_DMABUF, DISABLE_COMPOSITING]; +/// What `--safe-rendering` applies: FORCE_SHM plus compositing off. Deliberately +/// omits DISABLE_DMABUF — that variable is the #3654 crash on current WebKit. +const SAFE_VARS: [&str; 2] = [FORCE_SHM, DISABLE_COMPOSITING]; + +/// Every variable this module may set, and therefore every variable a user +/// assignment takes away from it. Being the same list is the invariant: nothing +/// outside it is ever written, so a user value for any other WebKit variable is +/// not a conflict. DISABLE_DMABUF stays owned so `=0` still stands the heuristic +/// down for operators who need the old path or an explicit override. +const OWNED: [&str; 3] = [FORCE_SHM, DISABLE_DMABUF, DISABLE_COMPOSITING]; /// Reads one environment variable. Injected so the decision is testable without /// mutating the process environment. `OsString` rather than `String` because @@ -124,15 +138,29 @@ fn plan( true => Plan::Fatal { diagnostic: conflict(&user_set), }, - false => Plan::Leave { - why: format!("{} set in the environment", describe(&user_set)), - }, + false => { + let mut why = format!("{} set in the environment", describe(&user_set)); + // Older docs told people to export DISABLE_DMABUF=1; on current + // WebKitGTK that empties the transport and SIGSEGVs (#3654). + // Leave the takeover alone, but point survivors at FORCE_SHM. + if user_set + .iter() + .any(|(key, value)| *key == DISABLE_DMABUF && value.as_os_str() != "0") + { + why.push_str(&format!( + "; warning: {DISABLE_DMABUF} (other than =0) empties the \ + transport on current WebKitGTK and SIGSEGVs — prefer \ + {FORCE_SHM}=1 (see #3654)" + )); + } + Plan::Leave { why } + } }; } if safe_rendering { return Plan::Apply { - vars: &OWNED, + vars: &SAFE_VARS, why: format!("{SAFE_RENDERING} requested, this launch only"), }; } diff --git a/desktop/src-tauri/src/webkit_rendering/tests.rs b/desktop/src-tauri/src/webkit_rendering/tests.rs index 5be1612b2..f9df47a90 100644 --- a/desktop/src-tauri/src/webkit_rendering/tests.rs +++ b/desktop/src-tauri/src/webkit_rendering/tests.rs @@ -44,13 +44,13 @@ fn applied(plan: &Plan) -> Option<&[&str]> { // ── Detection ─────────────────────────────────────────────────────────────── #[test] -fn test_nvidia_gpu_disables_the_dmabuf_renderer() { +fn test_nvidia_gpu_forces_shared_memory_dmabuf_transport() { let drm = drm(&["0x10de"]); let plan = plan(NO_ARGS, &env_from(&[]), drm.path()); assert_eq!( applied(&plan), - Some(&["WEBKIT_DISABLE_DMABUF_RENDERER"][..]) + Some(&["WEBKIT_DMABUF_RENDERER_FORCE_SHM"][..]) ); let Plan::Apply { why, .. } = &plan else { unreachable!() @@ -66,7 +66,7 @@ fn test_an_nvidia_gpu_alongside_another_vendor_still_counts() { assert_eq!( applied(&plan(NO_ARGS, &env_from(&[]), drm.path())), - Some(&["WEBKIT_DISABLE_DMABUF_RENDERER"][..]) + Some(&["WEBKIT_DMABUF_RENDERER_FORCE_SHM"][..]) ); } @@ -76,12 +76,12 @@ fn test_the_vendor_id_match_ignores_case() { assert_eq!( applied(&plan(NO_ARGS, &env_from(&[]), drm.path())), - Some(&["WEBKIT_DISABLE_DMABUF_RENDERER"][..]) + Some(&["WEBKIT_DMABUF_RENDERER_FORCE_SHM"][..]) ); } #[test] -fn test_an_appimage_launch_disables_the_dmabuf_renderer() { +fn test_an_appimage_launch_forces_shared_memory_dmabuf_transport() { // No NVIDIA GPU: the AppImage signal has to carry this on its own, which is // #2338's reporter (Intel Mesa under the AppRun's pinned XWayland backend). let drm = drm(&["0x8086"]); @@ -90,7 +90,7 @@ fn test_an_appimage_launch_disables_the_dmabuf_renderer() { assert_eq!( applied(&plan), - Some(&["WEBKIT_DISABLE_DMABUF_RENDERER"][..]) + Some(&["WEBKIT_DMABUF_RENDERER_FORCE_SHM"][..]) ); let Plan::Apply { why, .. } = &plan else { unreachable!() @@ -133,7 +133,7 @@ fn test_a_device_without_a_vendor_file_is_skipped_not_fatal() { assert_eq!( applied(&plan(NO_ARGS, &env_from(&[]), root.path())), - Some(&["WEBKIT_DISABLE_DMABUF_RENDERER"][..]) + Some(&["WEBKIT_DMABUF_RENDERER_FORCE_SHM"][..]) ); } @@ -153,6 +153,34 @@ fn test_a_user_set_variable_disables_the_heuristic_wholesale() { assert!(why.contains("WEBKIT_DISABLE_DMABUF_RENDERER=0"), "{why}"); } +#[test] +fn test_a_user_set_force_shm_also_stands_the_heuristic_down() { + // FORCE_SHM joined OWNED in the #3654 swap; a user export must take the + // whole decision away, same as the older DISABLE_DMABUF takeover. + let drm = drm(&["0x10de"]); + let env = env_from(&[(FORCE_SHM, "1")]); + let plan = plan(NO_ARGS, &env, drm.path()); + + let Plan::Leave { why } = &plan else { + panic!("a user FORCE_SHM assignment must not be overwritten: {plan:?}"); + }; + assert!(why.contains("WEBKIT_DMABUF_RENDERER_FORCE_SHM=1"), "{why}"); +} + +#[test] +fn test_user_set_disable_dmabuf_one_warns_about_the_crashy_var() { + let drm = drm(&["0x10de"]); + let env = env_from(&[(DISABLE_DMABUF, "1")]); + let plan = plan(NO_ARGS, &env, drm.path()); + + let Plan::Leave { why } = &plan else { + panic!("expected Leave: {plan:?}"); + }; + assert!(why.contains("WEBKIT_DISABLE_DMABUF_RENDERER=1"), "{why}"); + assert!(why.contains("WEBKIT_DMABUF_RENDERER_FORCE_SHM"), "{why}"); + assert!(why.contains("#3654"), "{why}"); +} + #[test] fn test_an_empty_assignment_is_still_a_user_assignment() { let drm = drm(&["0x10de"]); @@ -192,7 +220,7 @@ fn test_safe_rendering_applies_the_safest_set_without_any_hardware_signal() { applied(&plan), Some( &[ - "WEBKIT_DISABLE_DMABUF_RENDERER", + "WEBKIT_DMABUF_RENDERER_FORCE_SHM", "WEBKIT_DISABLE_COMPOSITING_MODE" ][..] ) diff --git a/docs/linux-rendering-troubleshooting.md b/docs/linux-rendering-troubleshooting.md index 1e09ef1ac..4bc8e80eb 100644 --- a/docs/linux-rendering-troubleshooting.md +++ b/docs/linux-rendering-troubleshooting.md @@ -7,7 +7,7 @@ This guide covers the most common rendering failures on Linux and how to resolve | Symptom | Likely cause | Fix | |---------|-------------|-----| | Blank or transparent window, then `SIGABRT` with `colrv1_configure_skpaint` in the output | COLRv1 color emoji font (AppImage only) | Upgrade to the latest AppImage (v0.5.2+) | -| Blank window on startup, no crash output | dmabuf renderer incompatibility (NVIDIA or AppImage) | `WEBKIT_DISABLE_DMABUF_RENDERER=1 ./Buzz.AppImage` or `--safe-rendering` | +| Blank window on startup / SIGSEGV when switching workspaces | dmabuf renderer incompatibility (NVIDIA or AppImage) | Prefer `WEBKIT_DMABUF_RENDERER_FORCE_SHM=1` (shipped automatically) or `--safe-rendering`. Do **not** set `WEBKIT_DISABLE_DMABUF_RENDERER=1` on current WebKitGTK — see [#3654](https://github.com/block/buzz/issues/3654). On Debian/Ubuntu with the proprietary NVIDIA driver the crash can persist (distro WebKit patch) — [#3654](https://github.com/block/buzz/issues/3654) stays open for that path. | | Blank window on any hardware, no crash output | Unknown GPU/driver combination | `--safe-rendering` flag (see below) | --- @@ -69,9 +69,11 @@ FONTCONFIG_FILE=~/.config/buzz-fontconfig/fonts.conf ./Buzz_*.AppImage **Root cause:** WebKitGTK's dmabuf zero-copy buffer path is incompatible with some GPU/driver/compositor combinations. The WebKit child process silently fails to paint. -**Fix (shipped automatically starting with the first release containing [#3271](https://github.com/block/buzz/pull/3271) (v0.5.1)):** Buzz sets `WEBKIT_DISABLE_DMABUF_RENDERER=1` automatically before WebKit initializes when it detects an NVIDIA GPU (`/sys/class/drm` vendor ID `0x10de`) or when running as an AppImage. This restores a slightly slower shared-memory rendering path that works universally. +**Fix (shipped automatically starting with the first release containing [#3271](https://github.com/block/buzz/pull/3271) (v0.5.1), updated for [#3654](https://github.com/block/buzz/issues/3654)):** Buzz sets `WEBKIT_DMABUF_RENDERER_FORCE_SHM=1` automatically before WebKit initializes when it detects an NVIDIA GPU (`/sys/class/drm` vendor ID `0x10de`) or when running as an AppImage. That keeps SharedMemory in WebKit's transport set while skipping the hardware dmabuf path (upstream WebKitGTK; Debian/Ubuntu's NVIDIA dmabuf patch can bypass this, so the crash can persist there — [#3654](https://github.com/block/buzz/issues/3654) stays open for that path). `WEBKIT_DMABUF_RENDERER_FORCE_SHM` exists in WebKitGTK ≥ 2.44 (absent at 2.42); on older system WebKit the export is a silent no-op. -**If automatic detection doesn't help (`--safe-rendering`):** Pass `--safe-rendering` to force both `WEBKIT_DISABLE_DMABUF_RENDERER=1` and `WEBKIT_DISABLE_COMPOSITING_MODE=1` for that launch: +**Do not use `WEBKIT_DISABLE_DMABUF_RENDERER=1` on current WebKitGTK** (2.52+): that variable no longer falls back to shared memory. It empties the transport mode, `AcceleratedBackingStore::create()` returns null, and the UI SIGSEGVs the first time compositing is needed (often on workspace switch). See [#3654](https://github.com/block/buzz/issues/3654). + +**If automatic detection doesn't help (`--safe-rendering`):** Pass `--safe-rendering` to force both `WEBKIT_DMABUF_RENDERER_FORCE_SHM=1` and `WEBKIT_DISABLE_COMPOSITING_MODE=1` for that launch: ```bash ./Buzz_*.AppImage --safe-rendering @@ -83,10 +85,10 @@ buzz-desktop --safe-rendering ```bash # ~/.bashrc or ~/.profile -export WEBKIT_DISABLE_DMABUF_RENDERER=1 +export WEBKIT_DMABUF_RENDERER_FORCE_SHM=1 ``` -**Conflict detection:** If you set a WebKit variable in your environment and also pass `--safe-rendering`, Buzz will refuse to start and print exactly which variable conflicts. Unset the conflicting variable or drop the flag. +**Conflict detection:** If you set a WebKit variable in your environment and also pass `--safe-rendering`, Buzz will refuse to start and print exactly which variable conflicts. Unset the conflicting variable or drop the flag. Operators who previously exported `WEBKIT_DISABLE_DMABUF_RENDERER=0` to override the old heuristic can keep that — the module still treats that assignment as a user takeover. --- @@ -96,11 +98,11 @@ export WEBKIT_DISABLE_DMABUF_RENDERER=1 **Symptom:** The Buzz window is transparent or renders with graphical corruption on AMD RDNA4 hardware. -**Workaround (verified by reporter):** Set these three variables before launching Buzz: +**Workaround (recommended; FORCE_SHM swap not re-verified on RDNA4):** Set these three variables before launching Buzz. The reporter originally verified a three-var set that used `WEBKIT_DISABLE_DMABUF_RENDERER=1`; that var is the [#3654](https://github.com/block/buzz/issues/3654) crash on current WebKitGTK, so this recipe swaps in `WEBKIT_DMABUF_RENDERER_FORCE_SHM=1` instead. Please re-confirm on RDNA4 if you can. ```bash export GDK_BACKEND=x11 -export WEBKIT_DISABLE_DMABUF_RENDERER=1 +export WEBKIT_DMABUF_RENDERER_FORCE_SHM=1 export WEBKIT_SKIA_ENABLE_CPU_RENDERING=1 ./Buzz_*.AppImage # or for native: @@ -109,7 +111,7 @@ buzz-desktop - `WEBKIT_SKIA_ENABLE_CPU_RENDERING=1` forces Skia to use CPU rendering, bypassing the RDNA4 Skia/radv paint failure. - `GDK_BACKEND=x11` avoids the blank window that appears when running under a Plasma-Wayland compositor. -- `WEBKIT_DISABLE_DMABUF_RENDERER=1` prevents post-first-paint transparency from the dmabuf renderer. +- `WEBKIT_DMABUF_RENDERER_FORCE_SHM=1` keeps shared-memory transport without the #3654 empty-mode crash from `WEBKIT_DISABLE_DMABUF_RENDERER` (needs WebKitGTK ≥ 2.44). A dedicated fix for RDNA4 detection is being tracked in [#2643](https://github.com/block/buzz/issues/2643).