Skip to content
64 changes: 46 additions & 18 deletions desktop/src-tauri/src/webkit_rendering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand All @@ -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).
Comment thread
wpfleger96 marked this conversation as resolved.
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: 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];

/// 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];
/// 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
Expand Down Expand Up @@ -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"),
};
}
Expand Down
44 changes: 36 additions & 8 deletions desktop/src-tauri/src/webkit_rendering/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!()
Expand All @@ -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"][..])
);
}

Expand All @@ -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"]);
Expand All @@ -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!()
Expand Down Expand Up @@ -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"][..])
);
}

Expand All @@ -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"]);
Expand Down Expand Up @@ -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",
Comment thread
wpfleger96 marked this conversation as resolved.
"WEBKIT_DISABLE_COMPOSITING_MODE"
][..]
)
Expand Down
18 changes: 10 additions & 8 deletions docs/linux-rendering-troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |

---
Expand Down Expand Up @@ -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
Expand All @@ -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.

---

Expand All @@ -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
Comment thread
wpfleger96 marked this conversation as resolved.
export WEBKIT_SKIA_ENABLE_CPU_RENDERING=1
./Buzz_*.AppImage
# or for native:
Expand All @@ -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).

Expand Down
Loading