diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index 5b5e7c6e69..15499d38f9 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -51,3 +51,6 @@ wayland-protocols.workspace = true wayland-backend = { version = "0.3.3", features = ["client_system"] } wayland-client = { version = "0.31.2" } wayland-sys = { version = "0.31.1", features = ["dlopen"] } +as-raw-xcb-connection = "1.0.1" +tiny-xlib = "0.2.3" +x11rb = { version = "0.13.1", features = ["allow-unsafe-code", "dl-libxcb", "dri3", "randr"] } \ No newline at end of file diff --git a/wgpu/src/window.rs b/wgpu/src/window.rs index baf8a05458..92f1687372 100644 --- a/wgpu/src/window.rs +++ b/wgpu/src/window.rs @@ -2,6 +2,40 @@ pub mod compositor; #[cfg(all(unix, not(target_os = "macos")))] mod wayland; +#[cfg(all(unix, not(target_os = "macos")))] +mod x11; pub use compositor::Compositor; pub use wgpu::Surface; + +#[cfg(all(unix, not(target_os = "macos")))] +use rustix::fs::{major, minor}; +#[cfg(all(unix, not(target_os = "macos")))] +use std::{fs::File, io::Read, path::PathBuf}; + +#[cfg(all(unix, not(target_os = "macos")))] +fn ids_from_dev(dev: u64) -> Option<(u16, u16)> { + let path = PathBuf::from(format!( + "/sys/dev/char/{}:{}/device", + major(dev), + minor(dev) + )); + let vendor = { + let path = path.join("vendor"); + let mut file = File::open(&path).ok()?; + let mut contents = String::new(); + let _ = file.read_to_string(&mut contents).ok()?; + u16::from_str_radix(contents.trim().trim_start_matches("0x"), 16) + .ok()? + }; + let device = { + let path = path.join("device"); + let mut file = File::open(&path).ok()?; + let mut contents = String::new(); + let _ = file.read_to_string(&mut contents).ok()?; + u16::from_str_radix(contents.trim().trim_start_matches("0x"), 16) + .ok()? + }; + + Some((vendor, device)) +} diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 15919db5e7..c0839451ac 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -8,6 +8,8 @@ use crate::{Backend, Primitive, Renderer, Settings}; #[cfg(all(unix, not(target_os = "macos")))] use super::wayland::get_wayland_device_ids; +#[cfg(all(unix, not(target_os = "macos")))] +use super::x11::get_x11_device_ids; /// A window graphics backend for iced powered by `wgpu`. #[allow(missing_debug_implementations)] @@ -29,7 +31,10 @@ impl Compositor { compatible_window: Option, ) -> Option { #[cfg(all(unix, not(target_os = "macos")))] - let ids = compatible_window.as_ref().and_then(get_wayland_device_ids); + let ids = compatible_window.as_ref().and_then(|window| { + get_wayland_device_ids(window) + .or_else(|| get_x11_device_ids(window)) + }); // HACK: // 1. If we specifically didn't select an nvidia gpu diff --git a/wgpu/src/window/wayland.rs b/wgpu/src/window/wayland.rs index a77b1150cd..b50f5d6909 100644 --- a/wgpu/src/window/wayland.rs +++ b/wgpu/src/window/wayland.rs @@ -1,12 +1,10 @@ use crate::graphics::compositor::Window; use raw_window_handle::{RawDisplayHandle, WaylandDisplayHandle}; -use rustix::fs::{major, minor}; use sctk::{ dmabuf::{DmabufFeedback, DmabufHandler, DmabufState}, registry::{ProvidesRegistryState, RegistryState}, registry_handlers, }; -use std::{fs::File, io::Read, path::PathBuf}; use wayland_client::{ backend::Backend, globals::registry_queue_init, protocol::wl_buffer, Connection, QueueHandle, @@ -70,6 +68,10 @@ impl ProvidesRegistryState for AppData { } pub fn get_wayland_device_ids(window: &W) -> Option<(u16, u16)> { + if !wayland_sys::client::is_lib_available() { + return None; + } + let conn = match window.display_handle().map(|handle| handle.as_raw()) { #[allow(unsafe_code)] Ok(RawDisplayHandle::Wayland(WaylandDisplayHandle { @@ -103,35 +105,7 @@ pub fn get_wayland_device_ids(window: &W) -> Option<(u16, u16)> { }; let dev = feedback.main_device(); - let path = PathBuf::from(format!( - "/sys/dev/char/{}:{}/device", - major(dev), - minor(dev) - )); - let vendor = { - let path = path.join("vendor"); - let mut file = File::open(&path).ok()?; - let mut contents = String::new(); - let _ = file.read_to_string(&mut contents).ok()?; - u16::from_str_radix( - contents.trim().trim_start_matches("0x"), - 16, - ) - .ok()? - }; - let device = { - let path = path.join("device"); - let mut file = File::open(&path).ok()?; - let mut contents = String::new(); - let _ = file.read_to_string(&mut contents).ok()?; - u16::from_str_radix( - contents.trim().trim_start_matches("0x"), - 16, - ) - .ok()? - }; - - Some((vendor, device)) + super::ids_from_dev(dev) } _ => None, } diff --git a/wgpu/src/window/x11.rs b/wgpu/src/window/x11.rs new file mode 100644 index 0000000000..58da401a2e --- /dev/null +++ b/wgpu/src/window/x11.rs @@ -0,0 +1,171 @@ +use std::{ + fs, + io::{BufRead, BufReader}, + path::Path, +}; + +use crate::graphics::compositor::Window; + +use as_raw_xcb_connection::AsRawXcbConnection; +use raw_window_handle::{ + RawDisplayHandle, XcbDisplayHandle, XlibDisplayHandle, +}; +use rustix::fs::{fstat, stat}; +use tiny_xlib::Display; +use x11rb::{ + connection::{Connection, RequestConnection}, + protocol::{ + dri3::{ConnectionExt as _, X11_EXTENSION_NAME as DRI3_NAME}, + randr::{ + ConnectionExt as _, ProviderCapability, + X11_EXTENSION_NAME as RANDR_NAME, + }, + }, + xcb_ffi::XCBConnection, +}; + +pub fn get_x11_device_ids(window: &W) -> Option<(u16, u16)> { + x11rb::xcb_ffi::load_libxcb().ok()?; + + #[allow(unsafe_code)] + let (conn, screen) = match window + .display_handle() + .map(|handle| handle.as_raw()) + { + #[allow(unsafe_code)] + Ok(RawDisplayHandle::Xlib(XlibDisplayHandle { + display, + screen, + .. + })) => match display { + Some(ptr) => unsafe { + let xlib_display = Display::from_ptr(ptr.as_ptr()); + let conn = XCBConnection::from_raw_xcb_connection( + xlib_display.as_raw_xcb_connection() as *mut _, + false, + ) + .ok(); + // intentially leak the display, we don't want to close the connection + + (conn?, screen) + }, + None => (XCBConnection::connect(None).ok()?.0, screen), + }, + Ok(RawDisplayHandle::Xcb(XcbDisplayHandle { + connection, + screen, + .. + })) => match connection { + Some(ptr) => ( + unsafe { + XCBConnection::from_raw_xcb_connection(ptr.as_ptr(), false) + .ok()? + }, + screen, + ), + None => (XCBConnection::connect(None).ok()?.0, screen), + }, + _ => { + return None; + } + }; + let root = conn.setup().roots[screen as usize].root; + + // The nvidia xorg driver advertises DRI2 and DRI3, + // but doesn't really return any useful data for either of them. + // We also can't query EGL, as a display created from an X11 display + // running on the properietary driver won't return an EGLDevice. + // + // So we have to resort to hacks. + + // check for randr + let _ = conn.extension_information(RANDR_NAME).ok()??; + // check version, because we need providers to exist + let version = conn.randr_query_version(1, 4).ok()?.reply().ok()?; + if version.major_version < 1 + || (version.major_version == 1 && version.minor_version < 4) + { + return None; + } + + // get the name of the first Source Output provider, that will be our main device + let randr = conn.randr_get_providers(root).ok()?.reply().ok()?; + let mut name = None; + for provider in randr.providers { + let info = conn + .randr_get_provider_info(provider, randr.timestamp) + .ok()? + .reply() + .ok()?; + if info + .capabilities + .contains(ProviderCapability::SOURCE_OUTPUT) + || name.is_none() + { + name = std::str::from_utf8(&info.name) + .ok() + .map(ToString::to_string); + } + } + + // if that name is formatted `NVIDIA-x`, then x represents the /dev/nvidiaX number, which we can relate to /dev/dri + if let Some(number) = name.and_then(|name| { + name.trim().strip_prefix("NVIDIA-")?.parse::().ok() + }) { + // let it be known, that I hate this "interface"... + for busid in fs::read_dir("/proc/driver/nvidia/gpus") + .ok()? + .map(Result::ok) + .flatten() + { + for line in BufReader::new( + fs::File::open(busid.path().join("information")).ok()?, + ) + .lines() + { + if let Ok(line) = line { + if line.starts_with("Device Minor") { + if let Some((_, num)) = line.split_once(":") { + let minor = num.trim().parse::().ok()?; + if minor == number { + // we found the device + for device in fs::read_dir( + Path::new("/sys/module/nvidia/drivers/pci:nvidia/") + .join(busid.file_name()) + .join("drm"), + ) + .ok()? + .map(Result::ok) + .flatten() + { + let device = device.file_name(); + if device.to_string_lossy().starts_with("card") + || device.to_string_lossy().starts_with("render") + { + let stat = + stat(Path::new("/dev/dri").join(device)).ok()?; + let dev = stat.st_rdev; + return super::ids_from_dev(dev); + } + } + } + } + } + } + } + } + + None + } else { + // check via DRI3 + let _ = conn.extension_information(DRI3_NAME).ok()??; + // we have dri3, dri3_open exists on any version, so skip version checks. + + // provider being NONE tells the X server to use the RandR provider. + let dri3 = conn.dri3_open(root, x11rb::NONE).ok()?.reply().ok()?; + let device_fd = dri3.device_fd; + let stat = fstat(device_fd).ok()?; + let dev = stat.st_rdev; + super::ids_from_dev(dev) + } +}