diff --git a/docs/hyprland.md b/docs/hyprland.md index 521c241..3881d15 100644 --- a/docs/hyprland.md +++ b/docs/hyprland.md @@ -12,6 +12,12 @@ The backend targets Hyprland 0.53 and KWin 6.6. It requires `Hyprland`, Aquamarine requires a renderer-backed allocator even with headless outputs. The workspace starts a private KWin virtual compositor to supply that allocator, then creates a Hyprland headless output and removes the nested Wayland output. + +KWin's virtual compositor uses the first DRM device that opens, and Hyprland +allocates its buffers on that device. NVIDIA's GBM cannot allocate them. On a +host with an NVIDIA GPU and another GPU, the private KWin compositor runs in a +bubblewrap namespace whose `/dev/dri` holds only the other GPU's render node. +Hosts with one GPU vendor use every render node as before. KWin supplies no desktop shell. Neither compositor connects to a host display, input device, system bus, or DRM card. Weston 14 cannot serve as the parent: Aquamarine requests `wl_compositor` version 6 while Weston advertises version 5. diff --git a/docs/native-cosmic.md b/docs/native-cosmic.md index c19511f..b4ca48d 100644 --- a/docs/native-cosmic.md +++ b/docs/native-cosmic.md @@ -27,7 +27,8 @@ output scale and EI input checks succeed. The backend uses COSMIC toplevel information and management, ext image-copy capture, wlr output management, and COSMIC's private D-Bus EI input socket. -Capture uses shared memory and does not require a GPU. Window identifiers are +Capture uses shared memory and does not require a GPU. The compositor, its Weston parent and XWayland +load only Mesa's EGL vendor, so an installed NVIDIA driver is never used. Window identifiers are COSMIC's stable foreign-toplevel identifiers. PID is unavailable from this protocol and remains unset. diff --git a/src/session/cosmic.rs b/src/session/cosmic.rs index 68eaba5..f4fb08b 100644 --- a/src/session/cosmic.rs +++ b/src/session/cosmic.rs @@ -335,12 +335,15 @@ fn xwayland_exec() -> Result<()> { .create(true) .append(true) .open(runtime.join("xwayland.log"))?; - let error = Command::new(executable) + let mut command = Command::new(executable); + command .args(args) .arg("-auth") .arg(authority) - .stderr(Stdio::from(stderr)) - .exec(); + .stderr(Stdio::from(stderr)); + // The compositor does not pass its environment on to XWayland. + super::private::use_mesa_egl(&mut command); + let error = command.exec(); Err(error).context("exec authenticated COSMIC XWayland") } fn set_cookie(authority: &Path, display: &str, cookie: &str) -> Result<()> { @@ -448,6 +451,12 @@ fn compositor_namespace(runtime: &Path, wrapper: Option<&Path>) -> Result Vec { blockers } +/// KWin's virtual renderer takes the first DRM device that opens, and +/// Hyprland allocates its buffers on that renderer's device. NVIDIA's GBM +/// cannot allocate them, so on a host that also has another GPU the renderer +/// sees only that GPU's render node. Other hosts keep every node. +fn parent_render_node() -> Option { + let mut nodes: Vec<(PathBuf, bool)> = fs::read_dir("/sys/class/drm") + .ok()? + .flatten() + .filter_map(|entry| { + let name = entry.file_name().to_string_lossy().into_owned(); + name.strip_prefix("renderD") + .is_some_and(|n| !n.is_empty() && n.bytes().all(|c| c.is_ascii_digit())) + .then_some(())?; + let driver = fs::read_link(entry.path().join("device/driver")).ok()?; + let nvidia = driver.file_name().is_some_and(|name| name == "nvidia"); + let node = Path::new("/dev/dri").join(&name); + let path = std::ffi::CString::new(node.as_os_str().as_encoded_bytes()).ok()?; + (unsafe { libc::access(path.as_ptr(), libc::R_OK | libc::W_OK) } == 0) + .then_some((node, nvidia)) + }) + .collect(); + nodes.sort(); + if !nodes.iter().any(|(_, nvidia)| *nvidia) { + return None; + } + nodes + .into_iter() + .find(|(_, nvidia)| !nvidia) + .map(|(node, _)| node) +} + pub(crate) fn configure_process_environment( command: &mut Command, runtime: &Path, @@ -206,7 +237,33 @@ impl HyprlandSession { private, filter: None, }; - let mut parent = Command::new("kwin_wayland"); + let mut parent = match parent_render_node() { + Some(node) => { + let bwrap = command_path_check("bwrap"); + if !bwrap.ok { + bail!( + "bwrap is required to keep Hyprland's renderer off the NVIDIA render node" + ); + } + let mut command = Command::new(bwrap.detail); + command + .args([ + "--dev-bind", + "/", + "/", + "--proc", + "/proc", + "--tmpfs", + "/dev/dri", + ]) + .arg("--dev-bind") + .arg(&node) + .arg(&node) + .args(["--unshare-pid", "--die-with-parent", "--", "kwin_wayland"]); + command + } + None => Command::new("kwin_wayland"), + }; configure_process_environment(&mut parent, runtime, "", &options.xauthority_path); parent .env_remove("WAYLAND_DISPLAY") diff --git a/src/session/private.rs b/src/session/private.rs index ededff6..87af4d6 100644 --- a/src/session/private.rs +++ b/src/session/private.rs @@ -337,6 +337,22 @@ fn start_private_bus( Ok(child) } +/// Restrict EGL to Mesa for a software-rendered process. glvnd otherwise +/// tries the NVIDIA vendor first, which fails or crashes without its devices. +/// Hosts without Mesa's vendor file keep the default vendor list. +pub(crate) fn use_mesa_egl(command: &mut Command) { + if let Some(vendor) = [ + "/usr/share/glvnd/egl_vendor.d/50_mesa.json", + "/etc/glvnd/egl_vendor.d/50_mesa.json", + ] + .into_iter() + .map(Path::new) + .find(|path| path.is_file()) + { + command.env("__EGL_VENDOR_LIBRARY_FILENAMES", vendor); + } +} + pub(crate) fn prepare_child(command: &mut Command) { let parent = std::process::id(); command.stdin(Stdio::null()).process_group(0);