From 879521e8ac0a6b49bd83dbda363d66aa715a3a1c Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Mon, 9 Nov 2026 16:55:27 -0500 Subject: [PATCH] fix(linux): skip the NVIDIA explicit-sync workaround on driver 620+ with WebKitGTK 2.54+ (#48) On native Wayland we set __NV_DISABLE_EXPLICIT_SYNC=1 for every NVIDIA user, to stop the "Error 71" protocol crash seen on driver 615 with WebKitGTK 2.52. On driver 620.70.06 with WebKitGTK 2.54.2 (same RTX 3070) the app launches and runs smoothly without it, while the default was reported as extremely laggy. Skip the flag only when both versions are known and new enough. An older driver, an older WebKitGTK (e.g. the AppImage's bundled one) or a version we can't read keeps the old behaviour, since a wrong guess means the app doesn't open. Setting the variable by hand still forces it. The app logs which path it took on stderr. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA --- src-tauri/src/gpu_workarounds.rs | 163 ++++++++++++++++++++++++++++++- 1 file changed, 158 insertions(+), 5 deletions(-) diff --git a/src-tauri/src/gpu_workarounds.rs b/src-tauri/src/gpu_workarounds.rs index 361fd6d..7974361 100644 --- a/src-tauri/src/gpu_workarounds.rs +++ b/src-tauri/src/gpu_workarounds.rs @@ -7,7 +7,16 @@ //! 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. +//! renderer, so that is the default on native Wayland, but only on the +//! versions that need it (cinny-desktop #48, below). +//! - #48 (2026-11-08, same RTX 3070, native Wayland, Arch): with driver +//! 620.70.06 and webkit2gtk 2.54.2 the app launches and runs smoothly +//! WITHOUT the flag, while the default (flag set) was reported as +//! "extremely laggy" (implicit sync on NVIDIA can stall frames). So the +//! flag is skipped when the driver is at least 620 AND WebKitGTK is at +//! least 2.54, the combination verified. Anything older, or a version we +//! can't read, keeps the flag: getting this wrong means the app doesn't +//! open at all. //! - 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`, @@ -17,10 +26,47 @@ //! - `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. +//! - `__NV_DISABLE_EXPLICIT_SYNC=1` set by hand: forces the old behaviour on +//! versions where we'd skip it (the user's value always wins). + +/// First NVIDIA driver major version verified to work on native Wayland +/// without `__NV_DISABLE_EXPLICIT_SYNC` (620.70.06, #48). +const EXPLICIT_SYNC_OK_DRIVER: u32 = 620; +/// First WebKitGTK (major, minor) verified with it (2.54.2, #48). +const EXPLICIT_SYNC_OK_WEBKIT: (u32, u32) = (2, 54); + +/// Whether this driver + WebKitGTK combination no longer needs explicit sync +/// disabled. Unknown versions count as "still needs it". Pure, for tests. +pub(crate) fn explicit_sync_fixed(driver_major: Option, webkit: Option<(u32, u32)>) -> bool { + matches!( + (driver_major, webkit), + (Some(d), Some(w)) if d >= EXPLICIT_SYNC_OK_DRIVER && w >= EXPLICIT_SYNC_OK_WEBKIT + ) +} + +/// The driver's major version from `/sys/module/nvidia/version` ("620.70.06") +/// or a `/proc/driver/nvidia/version` line ("NVRM version: NVIDIA UNIX Open +/// Kernel Module for x86_64 620.70.06 Release Build …"): the first +/// `N.N` token whose major part is a plausible driver number. +pub(crate) fn parse_driver_major(text: &str) -> Option { + text.split_whitespace().find_map(|tok| { + let (major, rest) = tok.split_once('.')?; + let minor = rest.split('.').next()?; + if major.is_empty() || minor.is_empty() { + return None; + } + if !major.bytes().all(|b| b.is_ascii_digit()) || !minor.bytes().all(|b| b.is_ascii_digit()) + { + return None; + } + major.parse::().ok().filter(|m| *m >= 100) + }) +} /// Environment variables to set: pure, for tests. pub(crate) fn decide( nvidia: bool, + explicit_sync_fixed: bool, get: impl Fn(&str) -> Option, ) -> Vec<(&'static str, &'static str)> { let set = |k: &str| get(k).is_some_and(|v| !v.is_empty()); @@ -45,7 +91,9 @@ pub(crate) fn decide( } }; if native_wayland { - want("__NV_DISABLE_EXPLICIT_SYNC", "1"); + if !explicit_sync_fixed { + want("__NV_DISABLE_EXPLICIT_SYNC", "1"); + } if set("LOTUS_GPU_SAFE_MODE") { want("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); } @@ -61,11 +109,51 @@ fn nvidia_driver_loaded() -> bool { || std::path::Path::new("/sys/module/nvidia").exists() } +#[cfg(target_os = "linux")] +fn nvidia_driver_major() -> Option { + ["/sys/module/nvidia/version", "/proc/driver/nvidia/version"] + .iter() + .find_map(|p| { + std::fs::read_to_string(p) + .ok() + .and_then(|t| parse_driver_major(&t)) + }) +} + +/// The WebKitGTK library actually loaded (the system one for the pacman +/// package, the bundled one for the AppImage). Only reads constants from the +/// library, so it is safe before GTK is initialised. +#[cfg(target_os = "linux")] +fn webkit_version() -> (u32, u32) { + // SAFETY: plain getters with no arguments and no preconditions. + unsafe { + ( + webkit2gtk::ffi::webkit_get_major_version(), + webkit2gtk::ffi::webkit_get_minor_version(), + ) + } +} + /// 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()); + let nvidia = nvidia_driver_loaded(); + let (driver, webkit) = if nvidia { + (nvidia_driver_major(), Some(webkit_version())) + } else { + (None, None) + }; + let fixed = explicit_sync_fixed(driver, webkit); + let changes = decide(nvidia, fixed, |k| std::env::var(k).ok()); + if nvidia && fixed && changes.is_empty() { + let (wm, wn) = webkit.unwrap_or_default(); + eprintln!( + "gpu-workarounds: NVIDIA driver {} with WebKitGTK {wm}.{wn}: no workaround needed \ + (set __NV_DISABLE_EXPLICIT_SYNC=1 if the window fails with \"Error 71\")", + driver.unwrap_or_default() + ); + } if changes.is_empty() { return; } @@ -83,7 +171,7 @@ pub fn apply() { #[cfg(test)] mod tests { - use super::decide; + use super::{decide, explicit_sync_fixed, parse_driver_major}; use std::collections::HashMap; fn run(nvidia: bool, env: &[(&str, &str)]) -> Vec<(&'static str, &'static str)> { @@ -91,7 +179,15 @@ mod tests { .iter() .map(|(k, v)| (k.to_string(), v.to_string())) .collect(); - decide(nvidia, |k| map.get(k).cloned()) + decide(nvidia, false, |k| map.get(k).cloned()) + } + + fn run_fixed(env: &[(&str, &str)]) -> Vec<(&'static str, &'static str)> { + let map: HashMap = env + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(); + decide(true, true, |k| map.get(k).cloned()) } const WAYLAND: &[(&str, &str)] = &[("WAYLAND_DISPLAY", "wayland-0"), ("DISPLAY", ":0")]; @@ -175,4 +271,61 @@ mod tests { env.push(("LOTUS_NO_GPU_WORKAROUNDS", "1")); assert!(run(true, &env).is_empty()); } + + // ── #48: skip the explicit-sync workaround where it is no longer needed ── + + #[test] + fn explicit_sync_fixed_needs_both_new_driver_and_new_webkit() { + assert!(explicit_sync_fixed(Some(620), Some((2, 54)))); + assert!(explicit_sync_fixed(Some(625), Some((2, 56)))); + assert!(explicit_sync_fixed(Some(620), Some((3, 0)))); + // The September crash: driver 615 + WebKitGTK 2.52. + assert!(!explicit_sync_fixed(Some(615), Some((2, 52)))); + // Only one side new (e.g. the AppImage's older bundled WebKitGTK). + assert!(!explicit_sync_fixed(Some(620), Some((2, 52)))); + assert!(!explicit_sync_fixed(Some(615), Some((2, 54)))); + // Unknown versions keep the workaround. + assert!(!explicit_sync_fixed(None, Some((2, 54)))); + assert!(!explicit_sync_fixed(Some(620), None)); + } + + #[test] + fn fixed_versions_set_nothing_on_wayland() { + assert!(run_fixed(WAYLAND).is_empty()); + } + + #[test] + fn fixed_versions_still_honour_safe_mode_and_x11() { + let mut env = WAYLAND.to_vec(); + env.push(("LOTUS_GPU_SAFE_MODE", "1")); + assert_eq!( + run_fixed(&env), + vec![("WEBKIT_DISABLE_DMABUF_RENDERER", "1")] + ); + assert_eq!( + run_fixed(&[("DISPLAY", ":0"), ("XDG_SESSION_TYPE", "x11")]), + vec![("WEBKIT_DISABLE_DMABUF_RENDERER", "1")] + ); + } + + #[test] + fn parses_driver_versions() { + assert_eq!(parse_driver_major("620.70.06\n"), Some(620)); + assert_eq!( + parse_driver_major( + "NVRM version: NVIDIA UNIX Open Kernel Module for x86_64 620.70.06 Release Build (notroot@) Sat Nov 7 14:23:54 UTC 2026\nGCC version: Selected multilib: .;@m64\n" + ), + Some(620) + ); + assert_eq!( + parse_driver_major( + "NVRM version: NVIDIA UNIX x86_64 Kernel Module 615.71.09 Thu Sep 3 2026" + ), + Some(615) + ); + assert_eq!(parse_driver_major("NVRM version: nothing here"), None); + assert_eq!(parse_driver_major(""), None); + // A GCC version must not be mistaken for the driver. + assert_eq!(parse_driver_major("GCC version: gcc 14.2.1"), None); + } }