From fecae38c63a3e284d5de0096e412000ff96abcf0 Mon Sep 17 00:00:00 2001 From: Sline Date: Sat, 18 Oct 2025 12:13:00 +0800 Subject: [PATCH] refactor: Linux environment detection logic (#5108) * fix: wayland framebuffer * refactor(utils): move linux env heuristics into platform helper * refactor(linux): let DMABUF override helper use resolved decision * fix: clippy * fix: clippy * feat: NVIDIA detection * fix: clippy --- UPDATELOG.md | 1 + src-tauri/src/lib.rs | 86 +----- src-tauri/src/utils/linux.rs | 522 +++++++++++++++++++++++++++++++++++ src-tauri/src/utils/mod.rs | 2 + 4 files changed, 528 insertions(+), 83 deletions(-) create mode 100644 src-tauri/src/utils/linux.rs diff --git a/UPDATELOG.md b/UPDATELOG.md index dd62f8de..8b813ec4 100644 --- a/UPDATELOG.md +++ b/UPDATELOG.md @@ -59,6 +59,7 @@ - 修复删除订阅时未能实际删除相关文件 - 修复 macOS 连接界面显示异常 - 修复规则配置项在不同配置文件间全局共享导致切换被重置的问题 +- 修复 Linux Wayland 下部分 GPU 可能出现的 UI 渲染问题 ## v2.4.2 diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 9f501c74..69df3a31 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -9,6 +9,8 @@ mod feat; mod module; mod process; pub mod utils; +#[cfg(target_os = "linux")] +use crate::utils::linux; #[cfg(target_os = "macos")] use crate::utils::window_manager::WindowManager; use crate::{ @@ -234,89 +236,7 @@ pub fn run() { // Set Linux environment variable #[cfg(target_os = "linux")] - { - let desktop_env = std::env::var("XDG_CURRENT_DESKTOP") - .unwrap_or_default() - .to_uppercase(); - let session_desktop = std::env::var("XDG_SESSION_DESKTOP") - .unwrap_or_default() - .to_uppercase(); - let desktop_session = std::env::var("DESKTOP_SESSION") - .unwrap_or_default() - .to_uppercase(); - let is_kde_desktop = desktop_env.contains("KDE"); - let is_plasma_desktop = desktop_env.contains("PLASMA"); - let is_hyprland_desktop = desktop_env.contains("HYPR") - || session_desktop.contains("HYPR") - || desktop_session.contains("HYPR"); - - let is_wayland_session = std::env::var("XDG_SESSION_TYPE") - .map(|value| value.eq_ignore_ascii_case("wayland")) - .unwrap_or(false) - || std::env::var("WAYLAND_DISPLAY").is_ok(); - let prefer_native_wayland = - is_wayland_session && (is_kde_desktop || is_plasma_desktop || is_hyprland_desktop); - let dmabuf_override = std::env::var("WEBKIT_DISABLE_DMABUF_RENDERER"); - - if prefer_native_wayland { - let compositor_label = if is_hyprland_desktop { - "Hyprland" - } else if is_plasma_desktop { - "KDE Plasma" - } else { - "KDE" - }; - - if matches!(dmabuf_override.as_deref(), Ok("1")) { - unsafe { - std::env::remove_var("WEBKIT_DISABLE_DMABUF_RENDERER"); - } - logging!( - info, - Type::Setup, - "Wayland + {} detected: Re-enabled WebKit DMABUF renderer to avoid Cairo surface failures.", - compositor_label - ); - } else { - logging!( - info, - Type::Setup, - "Wayland + {} detected: Using native Wayland backend for reliable rendering.", - compositor_label - ); - } - } else { - if dmabuf_override.is_err() { - unsafe { - std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); - } - } - - // Force X11 backend for tray icon compatibility on Wayland - if is_wayland_session { - unsafe { - std::env::set_var("GDK_BACKEND", "x11"); - std::env::remove_var("WAYLAND_DISPLAY"); - } - logging!( - info, - Type::Setup, - "Wayland detected: Forcing X11 backend for tray icon compatibility" - ); - } - } - - if is_kde_desktop || is_plasma_desktop { - unsafe { - std::env::set_var("GTK_CSD", "0"); - } - logging!( - info, - Type::Setup, - "KDE detected: Disabled GTK CSD for better titlebar stability." - ); - } - } + linux::configure_environment(); // Create and configure the Tauri builder let builder = app_init::setup_plugins(tauri::Builder::default()) diff --git a/src-tauri/src/utils/linux.rs b/src-tauri/src/utils/linux.rs new file mode 100644 index 00000000..2094811b --- /dev/null +++ b/src-tauri/src/utils/linux.rs @@ -0,0 +1,522 @@ +use crate::logging; +use crate::utils::logging::Type; +use std::collections::HashSet; +use std::env; +use std::fs; +use std::path::PathBuf; + +const DRM_PATH: &str = "/sys/class/drm"; +const INTEL_VENDOR_ID: &str = "0x8086"; +const NVIDIA_VENDOR_ID: &str = "0x10de"; +const NVIDIA_VERSION_PATH: &str = "/proc/driver/nvidia/version"; + +#[derive(Debug, Default, Clone, Copy)] +struct IntelGpuDetection { + has_intel: bool, + intel_is_primary: bool, + inconclusive: bool, +} + +impl IntelGpuDetection { + fn should_disable_dmabuf(&self) -> bool { + self.intel_is_primary || self.inconclusive + } +} + +#[derive(Debug, Default, Clone)] +struct NvidiaGpuDetection { + has_nvidia: bool, + nvidia_is_primary: bool, + missing_boot_vga: bool, + open_kernel_module: bool, + driver_summary: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum NvidiaDmabufDisableReason { + PrimaryOpenKernelModule, + MissingBootVga, + PreferNativeWayland, +} + +impl NvidiaGpuDetection { + fn disable_reason(&self, session: &SessionEnv) -> Option { + if !session.is_wayland { + return None; + } + + if !self.has_nvidia { + return None; + } + + if !self.open_kernel_module { + return None; + } + + if self.nvidia_is_primary { + return Some(NvidiaDmabufDisableReason::PrimaryOpenKernelModule); + } + + if self.missing_boot_vga { + return Some(NvidiaDmabufDisableReason::MissingBootVga); + } + + if session.prefer_native_wayland { + return Some(NvidiaDmabufDisableReason::PreferNativeWayland); + } + + None + } +} + +#[derive(Debug)] +struct SessionEnv { + is_kde_plasma: bool, + is_wayland: bool, + prefer_native_wayland: bool, + compositor_label: String, +} + +impl SessionEnv { + fn gather() -> Self { + let desktop_env = env::var("XDG_CURRENT_DESKTOP") + .unwrap_or_default() + .to_uppercase(); + let session_desktop = env::var("XDG_SESSION_DESKTOP") + .unwrap_or_default() + .to_uppercase(); + let desktop_session = env::var("DESKTOP_SESSION") + .unwrap_or_default() + .to_uppercase(); + + let is_kde_plasma = desktop_env.contains("KDE") + || session_desktop.contains("KDE") + || desktop_session.contains("KDE") + || desktop_env.contains("PLASMA") + || session_desktop.contains("PLASMA") + || desktop_session.contains("PLASMA"); + let is_hyprland = desktop_env.contains("HYPR") + || session_desktop.contains("HYPR") + || desktop_session.contains("HYPR"); + let is_wayland = env::var("XDG_SESSION_TYPE") + .map(|value| value.eq_ignore_ascii_case("wayland")) + .unwrap_or(false) + || env::var("WAYLAND_DISPLAY").is_ok(); + let prefer_native_wayland = is_wayland && (is_kde_plasma || is_hyprland); + let compositor_label = if is_hyprland { + String::from("Hyprland") + } else if is_kde_plasma { + String::from("KDE Plasma") + } else { + String::from("Wayland compositor") + }; + + Self { + is_kde_plasma, + is_wayland, + prefer_native_wayland, + compositor_label, + } + } +} + +#[derive(Debug)] +struct DmabufOverrides { + user_preference: Option, + dmabuf_override: Option, +} + +impl DmabufOverrides { + fn gather() -> Self { + let user_preference = env::var("CLASH_VERGE_DMABUF").ok().and_then(|value| { + match value.trim().to_ascii_lowercase().as_str() { + "1" | "true" | "enable" | "on" => Some(true), + "0" | "false" | "disable" | "off" => Some(false), + _ => None, + } + }); + let dmabuf_override = env::var("WEBKIT_DISABLE_DMABUF_RENDERER").ok(); + + Self { + user_preference, + dmabuf_override, + } + } + + fn has_env_override(&self) -> bool { + self.dmabuf_override.is_some() + } + + fn should_override_env(&self, decision: &DmabufDecision) -> bool { + if self.user_preference.is_some() { + return true; + } + + if decision.enable_dmabuf { + return true; + } + + !self.has_env_override() + } +} + +#[derive(Debug)] +struct DmabufDecision { + enable_dmabuf: bool, + force_x11_backend: bool, + warn: bool, + message: Option, +} + +impl DmabufDecision { + fn resolve( + session: &SessionEnv, + overrides: &DmabufOverrides, + intel_gpu: IntelGpuDetection, + nvidia_gpu: &NvidiaGpuDetection, + ) -> Self { + let mut decision = Self { + enable_dmabuf: true, + force_x11_backend: false, + warn: false, + message: None, + }; + + match overrides.user_preference { + Some(true) => { + decision.enable_dmabuf = true; + decision.message = + Some("CLASH_VERGE_DMABUF=1: 强制启用 WebKit DMABUF 渲染。".into()); + } + Some(false) => { + decision.enable_dmabuf = false; + decision.message = + Some("CLASH_VERGE_DMABUF=0: 强制禁用 WebKit DMABUF 渲染。".into()); + if session.is_wayland && !session.prefer_native_wayland { + decision.force_x11_backend = true; + } + } + None => { + if overrides.has_env_override() { + if overrides.dmabuf_override.as_deref() == Some("1") { + decision.enable_dmabuf = false; + decision.message = Some( + "检测到 WEBKIT_DISABLE_DMABUF_RENDERER=1,沿用用户的软件渲染配置。" + .into(), + ); + if session.is_wayland && !session.prefer_native_wayland { + decision.force_x11_backend = true; + } + } else { + decision.enable_dmabuf = true; + let value = overrides.dmabuf_override.clone().unwrap_or_default(); + decision.message = Some(format!( + "检测到 WEBKIT_DISABLE_DMABUF_RENDERER={},沿用用户配置。", + value + )); + } + } else if let Some(reason) = nvidia_gpu.disable_reason(session) { + decision.enable_dmabuf = false; + decision.warn = true; + if session.is_wayland && !session.prefer_native_wayland { + decision.force_x11_backend = true; + } + let summary = nvidia_gpu + .driver_summary + .as_deref() + .and_then(|line| { + extract_nvidia_driver_version(line) + .map(|version| format!("NVIDIA Open Kernel Module {}", version)) + }) + .unwrap_or_else(|| String::from("NVIDIA Open Kernel Module")); + let message = match reason { + NvidiaDmabufDisableReason::PrimaryOpenKernelModule => format!( + "Wayland 会话检测到 {}:禁用 WebKit DMABUF 渲染以规避协议错误。", + summary + ), + NvidiaDmabufDisableReason::MissingBootVga => format!( + "Wayland 会话检测到 {},但缺少 boot_vga 信息:预防性禁用 WebKit DMABUF。", + summary + ), + NvidiaDmabufDisableReason::PreferNativeWayland => format!( + "Wayland ({}) + {}:检测到 NVIDIA Open Kernel Module 在辅 GPU 上运行,预防性禁用 WebKit DMABUF。", + session.compositor_label, summary + ), + }; + decision.message = Some(message); + } else if session.prefer_native_wayland && !intel_gpu.should_disable_dmabuf() { + decision.enable_dmabuf = true; + decision.message = Some(format!( + "Wayland + {} detected: 使用原生 DMABUF 渲染。", + session.compositor_label + )); + } else { + decision.enable_dmabuf = false; + if session.is_wayland && !session.prefer_native_wayland { + decision.force_x11_backend = true; + } + + if intel_gpu.should_disable_dmabuf() && session.is_wayland { + decision.warn = true; + if intel_gpu.inconclusive { + decision.message = Some("Wayland 上检测到 Intel GPU,但缺少 boot_vga 信息:预防性禁用 WebKit DMABUF,若确认非主 GPU 可通过 CLASH_VERGE_DMABUF=1 覆盖。".into()); + } else { + decision.message = Some("Wayland 上检测到 Intel 主 GPU (0x8086):禁用 WebKit DMABUF 以避免帧缓冲失败。".into()); + } + } else if session.is_wayland { + decision.message = Some( + "Wayland 会话未匹配受支持的合成器:禁用 WebKit DMABUF 渲染。".into(), + ); + } else { + decision.message = + Some("禁用 WebKit DMABUF 渲染以获得更稳定的输出。".into()); + } + } + } + } + + decision + } +} + +fn detect_intel_gpu() -> IntelGpuDetection { + let Ok(entries) = fs::read_dir(DRM_PATH) else { + return IntelGpuDetection::default(); + }; + + let mut detection = IntelGpuDetection::default(); + let mut seen_devices: HashSet = HashSet::new(); + let mut missing_boot_vga = false; + + for entry in entries.flatten() { + let name = entry.file_name(); + let name = name.to_string_lossy(); + + if !(name.starts_with("renderD") || name.starts_with("card")) { + continue; + } + + let device_path = entry.path().join("device"); + let device_key = fs::canonicalize(&device_path).unwrap_or(device_path); + + if !seen_devices.insert(device_key.clone()) { + continue; + } + + let vendor_path = device_key.join("vendor"); + let Ok(vendor) = fs::read_to_string(&vendor_path) else { + continue; + }; + + if !vendor.trim().eq_ignore_ascii_case(INTEL_VENDOR_ID) { + continue; + } + + detection.has_intel = true; + + let boot_vga_path = device_key.join("boot_vga"); + match fs::read_to_string(&boot_vga_path) { + Ok(flag) => { + if flag.trim() == "1" { + detection.intel_is_primary = true; + } + } + Err(_) => { + missing_boot_vga = true; + } + } + } + + if detection.has_intel && !detection.intel_is_primary && missing_boot_vga { + detection.inconclusive = true; + } + + detection +} + +fn detect_nvidia_gpu() -> NvidiaGpuDetection { + let mut detection = NvidiaGpuDetection::default(); + let entries = match fs::read_dir(DRM_PATH) { + Ok(entries) => entries, + Err(err) => { + logging!( + info, + Type::Setup, + "无法读取 DRM 设备目录 {}({}),尝试通过 NVIDIA 驱动摘要进行降级检测。", + DRM_PATH, + err + ); + detection.driver_summary = read_nvidia_driver_summary(); + if let Some(summary) = detection.driver_summary.as_ref() { + detection.open_kernel_module = summary_indicates_open_kernel_module(summary); + detection.has_nvidia = true; + detection.missing_boot_vga = true; + } else { + logging!( + info, + Type::Setup, + "降级检测失败:未能读取 NVIDIA 驱动摘要,保留 WebKit DMABUF。" + ); + } + return detection; + } + }; + + let mut seen_devices: HashSet = HashSet::new(); + + for entry in entries.flatten() { + let name = entry.file_name(); + let name = name.to_string_lossy(); + + if !(name.starts_with("renderD") || name.starts_with("card")) { + continue; + } + + let device_path = entry.path().join("device"); + let device_key = fs::canonicalize(&device_path).unwrap_or(device_path); + + if !seen_devices.insert(device_key.clone()) { + continue; + } + + let vendor_path = device_key.join("vendor"); + let Ok(vendor) = fs::read_to_string(&vendor_path) else { + continue; + }; + + if !vendor.trim().eq_ignore_ascii_case(NVIDIA_VENDOR_ID) { + continue; + } + + detection.has_nvidia = true; + + let boot_vga_path = device_key.join("boot_vga"); + match fs::read_to_string(&boot_vga_path) { + Ok(flag) => { + if flag.trim() == "1" { + detection.nvidia_is_primary = true; + } + } + Err(_) => { + detection.missing_boot_vga = true; + } + } + } + + if detection.has_nvidia { + detection.driver_summary = read_nvidia_driver_summary(); + match detection.driver_summary.as_ref() { + Some(summary) => { + detection.open_kernel_module = summary_indicates_open_kernel_module(summary); + } + None => { + logging!( + info, + Type::Setup, + "检测到 NVIDIA 设备,但无法读取 {},默认视为未启用开源内核模块。", + NVIDIA_VERSION_PATH + ); + } + } + } + + detection +} + +fn read_nvidia_driver_summary() -> Option { + match fs::read_to_string(NVIDIA_VERSION_PATH) { + Ok(content) => content + .lines() + .next() + .map(|line| line.trim().to_string()) + .filter(|line| !line.is_empty()), + Err(err) => { + logging!( + info, + Type::Setup, + "读取 {} 失败:{}", + NVIDIA_VERSION_PATH, + err + ); + None + } + } +} + +fn summary_indicates_open_kernel_module(summary: &str) -> bool { + let normalized = summary.to_ascii_lowercase(); + const PATTERNS: [&str; 4] = [ + "open kernel module", + "open kernel modules", + "open gpu kernel module", + "open gpu kernel modules", + ]; + + let is_open = PATTERNS.iter().any(|pattern| normalized.contains(pattern)); + + if !is_open && normalized.contains("open") { + logging!( + info, + Type::Setup, + "检测到 NVIDIA 驱动摘要包含 open 关键字但未匹配已知开源模块格式:{}", + summary + ); + } + + is_open +} + +fn extract_nvidia_driver_version(summary: &str) -> Option<&str> { + summary + .split_whitespace() + .find(|token| token.chars().all(|c| c.is_ascii_digit() || c == '.')) +} + +pub fn configure_environment() { + let session = SessionEnv::gather(); + let overrides = DmabufOverrides::gather(); + let intel_gpu = detect_intel_gpu(); + let nvidia_gpu = detect_nvidia_gpu(); + let decision = DmabufDecision::resolve(&session, &overrides, intel_gpu, &nvidia_gpu); + + if overrides.should_override_env(&decision) { + unsafe { + if decision.enable_dmabuf { + env::remove_var("WEBKIT_DISABLE_DMABUF_RENDERER"); + } else { + env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); + } + } + } + + if let Some(message) = decision.message { + if decision.warn { + logging!(warn, Type::Setup, "{}", message); + } else { + logging!(info, Type::Setup, "{}", message); + } + } + + if decision.force_x11_backend { + unsafe { + env::set_var("GDK_BACKEND", "x11"); + env::remove_var("WAYLAND_DISPLAY"); + } + logging!( + info, + Type::Setup, + "Wayland detected: Forcing X11 backend for WebKit stability." + ); + } + + if session.is_kde_plasma { + unsafe { + env::set_var("GTK_CSD", "0"); + } + logging!( + info, + Type::Setup, + "KDE/Plasma detected: Disabled GTK CSD for better titlebar stability." + ); + } +} diff --git a/src-tauri/src/utils/mod.rs b/src-tauri/src/utils/mod.rs index 787351a0..7c3e1aba 100644 --- a/src-tauri/src/utils/mod.rs +++ b/src-tauri/src/utils/mod.rs @@ -5,6 +5,8 @@ pub mod format; pub mod help; pub mod i18n; pub mod init; +#[cfg(target_os = "linux")] +pub mod linux; pub mod logging; pub mod network; pub mod notification;