//! WebKitGTK workarounds for NVIDIA's proprietary driver on Linux. //! //! Diagnosed on CachyOS/KDE Wayland, RTX 3070, driver 615.71.09, webkit2gtk //! 2.52.6 (reported 2026-09-27, WAYLAND_DEBUG trace in the PR): //! - Native Wayland: the compositor kills the connection with //! `wp_linux_drm_syncobj_surface_v1 … "explicit sync is used, but no acquire //! point is set"` (GDK: "Error 71 (Protocol error)"), i.e. an explicit-sync //! bug between WebKit and the driver, not a buffer-format problem. //! `__NV_DISABLE_EXPLICIT_SYNC=1` fixes it and keeps WebKit's GPU (DMA-BUF) //! renderer, so that is the default on native Wayland. //! - X11 / XWayland: the DMA-BUF renderer can't allocate or import buffers //! ("Failed to create GBM buffer … Invalid argument", "Failed to import //! DMABuf"), so there it is disabled (`WEBKIT_DISABLE_DMABUF_RENDERER=1`, //! shared-memory frames). We never force X11 any more. //! //! Opt-ins / opt-outs (anything the user already set always wins): //! - `LOTUS_GPU_SAFE_MODE=1`: last resort, shared-memory rendering on Wayland //! too (slower, especially at high resolutions). //! - `LOTUS_NO_GPU_WORKAROUNDS=1`: change nothing. /// Environment variables to set: pure, for tests. pub(crate) fn decide( nvidia: bool, get: impl Fn(&str) -> Option, ) -> Vec<(&'static str, &'static str)> { let set = |k: &str| get(k).is_some_and(|v| !v.is_empty()); if !nvidia || set("LOTUS_NO_GPU_WORKAROUNDS") { return Vec::new(); } let wayland_session = set("WAYLAND_DISPLAY") || get("XDG_SESSION_TYPE").is_some_and(|v| v.eq_ignore_ascii_case("wayland")); // GTK uses Wayland unless GDK_BACKEND says otherwise (it may list // several, e.g. "wayland,x11": the first one wins). let native_wayland = wayland_session && get("GDK_BACKEND").map_or(true, |v| { v.is_empty() || v.trim_start().to_ascii_lowercase().starts_with("wayland") || v.trim() == "*" }); let mut out = Vec::new(); let mut want = |k: &'static str, v: &'static str| { if !set(k) { out.push((k, v)); } }; if native_wayland { want("__NV_DISABLE_EXPLICIT_SYNC", "1"); if set("LOTUS_GPU_SAFE_MODE") { want("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); } } else { want("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); } out } #[cfg(target_os = "linux")] fn nvidia_driver_loaded() -> bool { std::path::Path::new("/proc/driver/nvidia/version").exists() || std::path::Path::new("/sys/module/nvidia").exists() } /// Call first thing in `main`, before anything starts GTK or spawns threads. pub fn apply() { #[cfg(target_os = "linux")] { let changes = decide(nvidia_driver_loaded(), |k| std::env::var(k).ok()); if changes.is_empty() { return; } let list: Vec = changes.iter().map(|(k, v)| format!("{k}={v}")).collect(); eprintln!( "gpu-workarounds: NVIDIA driver detected, setting {} \ (LOTUS_GPU_SAFE_MODE=1 for shared-memory rendering, LOTUS_NO_GPU_WORKAROUNDS=1 to disable)", list.join(" ") ); for (key, value) in changes { std::env::set_var(key, value); } } } #[cfg(test)] mod tests { use super::decide; use std::collections::HashMap; fn run(nvidia: bool, env: &[(&str, &str)]) -> Vec<(&'static str, &'static str)> { let map: HashMap = env .iter() .map(|(k, v)| (k.to_string(), v.to_string())) .collect(); decide(nvidia, |k| map.get(k).cloned()) } const WAYLAND: &[(&str, &str)] = &[("WAYLAND_DISPLAY", "wayland-0"), ("DISPLAY", ":0")]; #[test] fn nothing_without_nvidia() { assert!(run(false, WAYLAND).is_empty()); } #[test] fn nvidia_wayland_keeps_gpu_renderer_and_disables_explicit_sync() { assert_eq!( run(true, WAYLAND), vec![("__NV_DISABLE_EXPLICIT_SYNC", "1")] ); } #[test] fn never_forces_x11() { for env in [ WAYLAND, &[("DISPLAY", ":0")][..], &[("WAYLAND_DISPLAY", "w")][..], ] { assert!(run(true, env).iter().all(|(k, _)| *k != "GDK_BACKEND")); } } #[test] fn xdg_session_type_counts_as_wayland() { assert_eq!( run(true, &[("XDG_SESSION_TYPE", "wayland")]), vec![("__NV_DISABLE_EXPLICIT_SYNC", "1")] ); } #[test] fn nvidia_x11_session_disables_dmabuf_renderer() { assert_eq!( run(true, &[("DISPLAY", ":0"), ("XDG_SESSION_TYPE", "x11")]), vec![("WEBKIT_DISABLE_DMABUF_RENDERER", "1")] ); } #[test] fn user_forced_x11_on_wayland_counts_as_x11() { let mut env = WAYLAND.to_vec(); env.push(("GDK_BACKEND", "x11")); assert_eq!( run(true, &env), vec![("WEBKIT_DISABLE_DMABUF_RENDERER", "1")] ); let mut env = WAYLAND.to_vec(); env.push(("GDK_BACKEND", "wayland,x11")); assert_eq!(run(true, &env), vec![("__NV_DISABLE_EXPLICIT_SYNC", "1")]); } #[test] fn safe_mode_adds_shared_memory_rendering_on_wayland() { let mut env = WAYLAND.to_vec(); env.push(("LOTUS_GPU_SAFE_MODE", "1")); assert_eq!( run(true, &env), vec![ ("__NV_DISABLE_EXPLICIT_SYNC", "1"), ("WEBKIT_DISABLE_DMABUF_RENDERER", "1") ] ); } #[test] fn user_settings_win() { let mut env = WAYLAND.to_vec(); env.push(("__NV_DISABLE_EXPLICIT_SYNC", "0")); assert!(run(true, &env).is_empty()); } #[test] fn opt_out_disables_everything() { let mut env = WAYLAND.to_vec(); env.push(("LOTUS_NO_GPU_WORKAROUNDS", "1")); assert!(run(true, &env).is_empty()); } }