Skip to main content

webkit2gtk_nvidia_quirk/
lib.rs

1// Copyright 2026 hrzlgnm
2// SPDX-License-Identifier: MIT-0
3
4//! # webkit2gtk-nvidia-quirk
5//!
6//! A crate that provides session-aware workarounds for WebKitGTK rendering issues
7//! on Linux systems with the proprietary NVIDIA driver.
8//!
9//! ## Problem
10//!
11//! When running WebKitGTK-based applications (such as Tauri apps) on Linux
12//! with the proprietary NVIDIA driver, rendering issues occur that vary by session type:
13//!
14//! - **X11**: The DMABUF renderer causes blank windows
15//! - **Wayland**: The application does not start
16//!
17//! Related upstream issues:
18//! - [tauri-apps/tauri#10702](https://github.com/tauri-apps/tauri/issues/10702)
19//! - [tauri-apps/tauri#9304](https://github.com/tauri-apps/tauri/issues/9304)
20//! - [WebKitGTK Bug #280210](https://bugs.webkit.org/show_bug.cgi?id=280210)
21//!
22//! ## Solution
23//!
24//! This crate detects the proprietary NVIDIA driver and the session type (X11/Wayland),
25//! then applies the appropriate workaround:
26//!
27//! | Session Type | Workaround | Environment Variable |
28//! |-------------|------------|---------------------|
29//! | X11 | Disable DMABUF renderer | `WEBKIT_DISABLE_DMABUF_RENDERER=1` |
30//! | Wayland | Disable NVIDIA explicit sync | `__NV_DISABLE_EXPLICIT_SYNC=1` |
31//!
32//! ## Detection Method
33//!
34//! The crate detects the NVIDIA driver by:
35//! 1. If the primary/boot GPU (via `boot_display` or `boot_vga` attributes) has vendor ID 0x10de
36//! 2. If the proprietary `nvidia` kernel module is loaded (`/sys/module/nvidia` exists)
37//!
38//! GPU detection uses sysfs exclusively (`/sys/class/drm/`). This provides a simpler and
39//! more reliable detection mechanism with no external runtime dependencies.
40//!
41//! This specifically targets the proprietary NVIDIA driver, not the open-source nouveau driver.
42//!
43//! ## Usage
44//!
45//! ```rust,no_run
46//! use webkit2gtk_nvidia_quirk::{ApplyWorkaroundOptions, apply_workaround_with_options};
47//!
48//! let disable_dmabuf = std::env::args().any(|arg| arg == "--disable-dmabuf-renderer");
49//! let disable_nv_sync = std::env::args().any(|arg| arg == "--disable-nv-explicit-sync");
50//!
51//! let options = ApplyWorkaroundOptions::default()
52//!     .force_disable_dmabuf(disable_dmabuf)
53//!     .force_disable_nv_explicit_sync(disable_nv_sync);
54//!
55//! apply_workaround_with_options(options);
56//! ```
57//!
58//! ## API
59//!
60//! ### `is_primary_gpu_nvidia() -> bool`
61//!
62//! Checks whether the primary GPU is an NVIDIA GPU.
63//!
64//! Returns `true` if the primary GPU (boot_display or boot_vga attribute) has vendor ID 0x10de (NVIDIA),
65//! Returns `false` otherwise. This function does not check kernel module loading.
66//!
67//! ### `needs_workaround() -> WorkaroundKind`
68//!
69//! Determines which workaround should be applied based on NVIDIA detection and session type.
70//!
71//! Returns `WorkaroundKind::None` if no workaround is needed, `WorkaroundKind::DisableWebkitDmabufRenderer`
72//! for X11 sessions, or `WorkaroundKind::DisableNvExplicitSync` for Wayland sessions.
73//!
74//! ### `set_webkit_disable_dmabuf_renderer()`
75//!
76//! Sets the `WEBKIT_DISABLE_DMABUF_RENDERER` environment variable. Use this for X11 sessions.
77//!
78//! ### `nv_disable_explicit_sync()`
79//!
80//! Sets the `__NV_DISABLE_EXPLICIT_SYNC` environment variable. Use this for Wayland sessions.
81//!
82//! ### `apply_workaround_with_options(options: ApplyWorkaroundOptions)`
83//!
84//! Convenience function that applies workarounds based on the provided options.
85//! If any force options are set, it applies those directly. Otherwise, it calls
86//! [`needs_workaround`] to detect which workaround is needed.
87//!
88//! This is the recommended way to apply workarounds from CLI arguments.
89//!
90//! ### `WorkaroundKind`
91//!
92//! Enum representing the type of workaround to apply:
93//! - `None`: No workaround needed
94//! - `DisableWebkitDmabufRenderer`: Disable the DMABUF renderer (for X11)
95//! - `DisableNvExplicitSync`: Disable NVIDIA explicit sync (for Wayland)
96//!
97//! ### `ApplyWorkaroundOptions`
98//!
99//! Builder struct for configuring which workarounds to force-apply.
100//! Use the builder pattern to set options:
101//! ```rust,no_run
102//! use webkit2gtk_nvidia_quirk::{ApplyWorkaroundOptions, apply_workaround_with_options};
103//!
104//! let options = ApplyWorkaroundOptions::default()
105//!     .force_disable_dmabuf(true);
106//!
107//! apply_workaround_with_options(options);
108//! ```
109//!
110//! ## Platform Support
111//!
112//! This crate is Linux-only and provides no functionality on other platforms.
113
114#![cfg(target_os = "linux")]
115
116use std::path::{Path, PathBuf};
117
118#[derive(Debug)]
119struct GpuDevice {
120    is_primary: bool,
121    is_nvidia: bool,
122}
123
124fn read_sysfs_file(path: &Path) -> Option<String> {
125    std::fs::read_to_string(path)
126        .ok()
127        .map(|s| s.trim().to_string())
128}
129
130fn parse_vendor_id(card_path: &Path) -> u16 {
131    let vendor_path = card_path.join("device/vendor");
132    if let Some(content) = read_sysfs_file(&vendor_path) {
133        return u16::from_str_radix(content.strip_prefix("0x").unwrap_or(&content), 16)
134            .unwrap_or(0);
135    }
136    0
137}
138
139fn is_sysfs_attr_one(card_path: &Path, attr: &str) -> bool {
140    read_sysfs_file(&card_path.join(attr)).as_deref() == Some("1")
141}
142
143fn enumerate_gpus() -> Vec<GpuDevice> {
144    let mut devices = Vec::new();
145    let drm_path = PathBuf::from("/sys/class/drm");
146
147    let entries = match std::fs::read_dir(&drm_path) {
148        Ok(e) => e,
149        Err(_) => return devices,
150    };
151
152    for entry in entries.flatten() {
153        let file_name = entry.file_name();
154        let sysname = match file_name.to_str() {
155            Some(s) => s,
156            None => continue,
157        };
158
159        if !sysname.starts_with("card") || sysname.contains('-') {
160            continue;
161        }
162
163        let card_path = entry.path();
164        let vendor_id = parse_vendor_id(&card_path);
165
166        let is_primary = is_sysfs_attr_one(&card_path, "boot_display")
167            || is_sysfs_attr_one(&card_path.join("device"), "boot_display")
168            || is_sysfs_attr_one(&card_path.join("device"), "boot_vga");
169
170        let is_nvidia = vendor_id == 0x10de;
171
172        devices.push(GpuDevice {
173            is_primary,
174            is_nvidia,
175        });
176    }
177
178    devices
179}
180
181fn nvidia_driver_loaded() -> bool {
182    std::path::Path::new("/sys/module/nvidia").exists()
183}
184
185enum SessionType {
186    Wayland,
187    X11,
188    Unknown,
189}
190
191/// Detects the used session type based upon the XDG_SESSION_TYPE environment variable
192fn get_session_type() -> SessionType {
193    match std::env::var("XDG_SESSION_TYPE") {
194        Ok(session) => match session.as_str() {
195            "x11" => SessionType::X11,
196            "wayland" => SessionType::Wayland,
197            _ => SessionType::Unknown,
198        },
199        _ => SessionType::Unknown,
200    }
201}
202
203/// Represents the type of workaround to apply for NVIDIA WebKitGTK issues.
204///
205/// Use this enum to determine which workaround is needed based on the session type
206/// and whether NVIDIA is detected.
207#[derive(Debug, Clone, Copy, PartialEq)]
208pub enum WorkaroundKind {
209    /// No workaround needed.
210    None,
211    /// Disable the WebKit DMABUF renderer.
212    ///
213    /// This workaround is needed for X11 sessions with NVIDIA drivers.
214    DisableWebkitDmabufRenderer,
215    /// Disable NVIDIA explicit sync.
216    ///
217    /// This workaround is needed for Wayland sessions with NVIDIA drivers.
218    DisableNvExplicitSync,
219}
220
221/// Checks if a workaround should be applied.
222///
223/// This function checks if the proprietary NVIDIA driver is loaded and the primary GPU is NVIDIA.
224/// If so, it detects the session type (X11 or Wayland) and returns which workaround should be applied.
225///
226/// # Returns
227///
228///  `None` if no workaround is needed
229///  `DisableWebkitDmabufRenderer` if disabling the dmabuf renderer should be applied
230///  `DisableNvExplicitSync` if disabling nvidia explicit sync should be applied
231///
232/// # Note
233///
234/// This function only performs detection. Use [`set_webkit_disable_dmabuf_renderer`] or
235/// [`nv_disable_explicit_sync`] to apply the respective workaround.
236/// Call this first, then call the workaround if needed - ideally before spawning any threads.
237pub fn needs_workaround() -> WorkaroundKind {
238    let session = get_session_type();
239
240    if !is_primary_gpu_nvidia() || !nvidia_driver_loaded() {
241        return WorkaroundKind::None;
242    }
243    match session {
244        SessionType::Wayland => WorkaroundKind::DisableNvExplicitSync,
245        SessionType::X11 => WorkaroundKind::DisableWebkitDmabufRenderer,
246        SessionType::Unknown => WorkaroundKind::None,
247    }
248}
249
250/// Checks if the primary GPU is an NVIDIA GPU.
251///
252/// Returns `true` if the primary GPU (boot_display or boot_vga) is NVIDIA
253/// Returns `false` otherwise.
254pub fn is_primary_gpu_nvidia() -> bool {
255    let devices = enumerate_gpus();
256
257    devices.iter().any(|d| d.is_primary && d.is_nvidia)
258}
259
260/// Sets the `WEBKIT_DISABLE_DMABUF_RENDERER` environment variable.
261///
262/// This function should be called explicitly from single-threaded startup
263/// (main) before spawning threads or when launching subprocesses.
264///
265/// # Note
266///
267/// This function modifies the process environment. Call it early in your
268/// application's startup, before any threading has begun.
269pub fn set_webkit_disable_dmabuf_renderer() {
270    eprintln!("Note: disabling dmabuf renderer, expect degraded renderer performance.");
271    eprintln!("See https://github.com/tauri-apps/tauri/issues/9304 for more details.");
272    std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
273}
274
275/// Sets the `__NV_DISABLE_EXPLICIT_SYNC` environment variable.
276///
277/// This function should be called explicitly from single-threaded startup
278/// (main) before spawning threads or when launching subprocesses.
279///
280/// # Note
281///
282/// This function modifies the process environment. Call it early in your
283/// application's startup, before any threading has begun.
284pub fn nv_disable_explicit_sync() {
285    eprintln!("Note: disabling nvidia explicit sync.");
286    eprintln!("See https://bugs.webkit.org/show_bug.cgi?id=280210 for more details");
287    std::env::set_var("__NV_DISABLE_EXPLICIT_SYNC", "1");
288}
289
290/// Builder struct for configuring which workarounds to force-apply.
291///
292/// Use the builder pattern to set options before passing to [`apply_workaround_with_options`].
293///
294/// # Example
295///
296/// ```rust,no_run
297/// use webkit2gtk_nvidia_quirk::{ApplyWorkaroundOptions, apply_workaround_with_options};
298///
299/// let options = ApplyWorkaroundOptions::default()
300///     .force_disable_dmabuf(true)
301///     .force_disable_nv_explicit_sync(true);
302///
303/// apply_workaround_with_options(options);
304/// ```
305#[derive(Default)]
306pub struct ApplyWorkaroundOptions {
307    /// Force disable the DMABUF renderer.
308    pub force_disable_dmabuf: bool,
309    /// Force disable NVIDIA explicit sync.
310    pub force_disable_nv_explicit_sync: bool,
311}
312
313impl ApplyWorkaroundOptions {
314    /// Sets the `force_disable_dmabuf` option.
315    ///
316    /// When `true`, the DMABUF renderer will be disabled regardless of
317    /// whether NVIDIA is detected.
318    pub fn force_disable_dmabuf(mut self, value: bool) -> Self {
319        self.force_disable_dmabuf = value;
320        self
321    }
322
323    /// Sets the `force_disable_nv_explicit_sync` option.
324    ///
325    /// When `true`, NVIDIA explicit sync will be disabled regardless of
326    /// whether NVIDIA is detected.
327    pub fn force_disable_nv_explicit_sync(mut self, value: bool) -> Self {
328        self.force_disable_nv_explicit_sync = value;
329        self
330    }
331}
332
333/// Applies workarounds based on the provided options.
334///
335/// If any force options are set in `options`, those workarounds are applied directly.
336/// Otherwise, it calls [`needs_workaround`] to detect which workaround is needed.
337///
338/// # Arguments
339///
340/// * `options` - The workaround options to apply
341///
342/// # Note
343///
344/// This function modifies the process environment. Call it early in your
345/// application's startup, before any threading has begun.
346pub fn apply_workaround_with_options(options: ApplyWorkaroundOptions) {
347    if options.force_disable_dmabuf {
348        set_webkit_disable_dmabuf_renderer();
349    }
350    if options.force_disable_nv_explicit_sync {
351        nv_disable_explicit_sync();
352    }
353    if !options.force_disable_dmabuf && !options.force_disable_nv_explicit_sync {
354        match needs_workaround() {
355            WorkaroundKind::None => {}
356            WorkaroundKind::DisableWebkitDmabufRenderer => set_webkit_disable_dmabuf_renderer(),
357            WorkaroundKind::DisableNvExplicitSync => nv_disable_explicit_sync(),
358        }
359    }
360}