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); + } }