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
This commit is contained in:
Sline
2025-10-18 12:13:00 +08:00
committed by GitHub
Unverified
parent 210c12a74e
commit fecae38c63
4 changed files with 528 additions and 83 deletions

View File

@@ -59,6 +59,7 @@
- 修复删除订阅时未能实际删除相关文件
- 修复 macOS 连接界面显示异常
- 修复规则配置项在不同配置文件间全局共享导致切换被重置的问题
- 修复 Linux Wayland 下部分 GPU 可能出现的 UI 渲染问题
## v2.4.2

View File

@@ -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())

View File

@@ -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<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum NvidiaDmabufDisableReason {
PrimaryOpenKernelModule,
MissingBootVga,
PreferNativeWayland,
}
impl NvidiaGpuDetection {
fn disable_reason(&self, session: &SessionEnv) -> Option<NvidiaDmabufDisableReason> {
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<bool>,
dmabuf_override: Option<String>,
}
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<String>,
}
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<PathBuf> = 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<PathBuf> = 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<String> {
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."
);
}
}

View File

@@ -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;