Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/hyprland.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion docs/native-cosmic.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
46 changes: 35 additions & 11 deletions src/session/cosmic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<()> {
Expand Down Expand Up @@ -448,6 +451,12 @@ fn compositor_namespace(runtime: &Path, wrapper: Option<&Path>) -> Result<Comman
} else {
"parent-bin"
});
if wrapper.is_none() {
// --die-with-parent stops Weston only from inside a PID namespace;
// without one Weston outlives a crashed daemon. cosmic-comp keeps the
// host PID view for client credentials and exits with its guard socket.
command.arg("--unshare-pid");
}
command.arg("--tmpfs").arg(&destination);
if let Some(wrapper) = wrapper {
let file = fs::File::open(wrapper)?;
Expand Down Expand Up @@ -594,14 +603,19 @@ impl CosmicSession {
])
.arg(format!("--width={}", options.width))
.arg(format!("--height={}", options.height));
super::private::use_mesa_egl(&mut parent);
prepare_child(&mut parent);
let (stdout, stderr) = log_file(&runtime.join("cosmic-parent.log"))?;
parent.stdout(stdout).stderr(stderr);
session.parent = Some(parent.spawn()?);
wait_for(
Duration::from_secs(10),
|| {
if let Some(status) = session.parent.as_mut().unwrap().try_wait()? {
// Observe without reaping, so shutdown's group signals cannot
// reach a recycled process group.
if let super::private::UnreapedExit::Exited(status) =
super::private::unreaped_exit(session.parent.as_ref().unwrap().id())
{
bail!("private COSMIC parent exited: {status}");
}
Ok(parent_socket.exists())
Expand Down Expand Up @@ -637,6 +651,7 @@ impl CosmicSession {
});
}
session.lifetime = Some(lifetime);
super::private::use_mesa_egl(&mut compositor);
prepare_child(&mut compositor);
let (stdout, stderr) = log_file(&runtime.join("cosmic.log"))?;
compositor.stdout(stdout).stderr(stderr);
Expand Down Expand Up @@ -741,18 +756,27 @@ impl CosmicSession {
}
self.private.shutdown();
if let Some(mut parent) = self.parent.take() {
// Signal the group: Weston survives the death of its bwrap
// supervisor despite --die-with-parent. The unreaped leader keeps
// the group number from being recycled before the SIGKILL.
let group = -(parent.id() as i32);
Comment thread
avifenesh marked this conversation as resolved.
unsafe {
libc::kill(parent.id() as i32, libc::SIGTERM);
libc::kill(group, libc::SIGTERM);
}
if wait_for(
let _ = wait_for(
Duration::from_secs(3),
|| Ok(parent.try_wait()?.is_some()),
|| {
Ok(!matches!(
super::private::unreaped_exit(parent.id()),
super::private::UnreapedExit::Running
))
},
"COSMIC parent stop",
)
.is_err()
{
let _ = parent.kill();
);
unsafe {
libc::kill(group, libc::SIGKILL);
}
let _ = parent.kill();
let _ = parent.wait();
}
// Smithay owns these paths rather than XWayland itself. A compositor
Expand Down
59 changes: 58 additions & 1 deletion src/session/hyprland.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,37 @@ pub(crate) fn blockers() -> Vec<String> {
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<PathBuf> {
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,
Expand Down Expand Up @@ -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")
Expand Down
16 changes: 16 additions & 0 deletions src/session/private.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading