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}