From 8b22255f6edafba4ac9c92d1088e438fa57d286e Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Thu, 19 Mar 2026 22:23:13 +0100 Subject: [PATCH] backends: add headless backend --- src/backends.rs | 1 + src/backends/headless.rs | 305 +++++++++++++++++++++++++++++++++++++++ src/cli.rs | 1 + src/compositor.rs | 11 +- 4 files changed, 317 insertions(+), 1 deletion(-) create mode 100644 src/backends/headless.rs diff --git a/src/backends.rs b/src/backends.rs index 6bf2f9c6..5c06f892 100644 --- a/src/backends.rs +++ b/src/backends.rs @@ -1,3 +1,4 @@ pub mod dummy; +pub mod headless; pub mod metal; pub mod x; diff --git a/src/backends/headless.rs b/src/backends/headless.rs new file mode 100644 index 00000000..7b5ec265 --- /dev/null +++ b/src/backends/headless.rs @@ -0,0 +1,305 @@ +use { + crate::{ + async_engine::SpawnedFuture, + backend::{Backend, BackendDrmDevice, BackendEvent, DrmDeviceId, DrmEvent}, + backends::headless::HeadlessBackendError::{ + CreateDrm, GetDrmNodes, MonitorFdFailed, MonitorFdReadable, NoDrmNodes, OpenDrmNode, + }, + gfx_api::{GfxApi, GfxContext}, + io_uring::IoUringError, + state::State, + udev::{Udev, UdevDevice, UdevError, UdevMonitor}, + utils::{ + bitflags::BitflagsExt, clonecell::CloneCell, copyhashmap::CopyHashMap, + errorfmt::ErrorFmt, hash_map_ext::HashMapExt, on_change::OnChange, oserror::OsError, + }, + video::drm::{Drm, DrmError, DrmVersion, NodeType, get_drm_nodes_from_dev}, + }, + HeadlessBackendError::{ + AddUdevSubsystemMatch, CreateUdev, CreateUdevEnumerator, CreateUdevMonitor, + DupUdevMonitorFd, EnableUdevReceiving, GetUdevEntry, ScanUdevDevices, + }, + std::{cell::Cell, error::Error, rc::Rc}, + thiserror::Error, + uapi::{ + AsUstr, OwnedFd, + c::{self, dev_t}, + major, minor, + }, +}; + +#[derive(Debug, Error)] +pub enum HeadlessBackendError { + #[error("Could not create a udev instance")] + CreateUdev(#[source] UdevError), + #[error("Could not create a udev monitor")] + CreateUdevMonitor(#[source] UdevError), + #[error("Could not add a udev subsystem match")] + AddUdevSubsystemMatch(#[source] UdevError), + #[error("Could not enable udev receiving")] + EnableUdevReceiving(#[source] UdevError), + #[error("Could not dup udev monitor fd")] + DupUdevMonitorFd(#[source] OsError), + #[error("Could not create a udev enumerator")] + CreateUdevEnumerator(#[source] UdevError), + #[error("Could not scan udev device")] + ScanUdevDevices(#[source] UdevError), + #[error("Could not get udev entry")] + GetUdevEntry(#[source] UdevError), + #[error("Could not determine DRM nodes of device")] + GetDrmNodes(#[source] OsError), + #[error("Device has no DRM nodes")] + NoDrmNodes, + #[error("Could not open DRM node")] + OpenDrmNode(#[source] OsError), + #[error("Could not create Drm object")] + CreateDrm(#[source] DrmError), + #[error("Could not wait for monitor FD to become readable")] + MonitorFdReadable(#[source] IoUringError), + #[error("The monitor FD failed")] + MonitorFdFailed, +} + +pub struct HeadlessBackend { + state: Rc, + udev: Rc, + monitor: Rc, + monitor_fd: Rc, + devs: CopyHashMap>, + render_device: Cell>, +} + +struct HeadlessDrmDevice { + backend: Rc, + id: DrmDeviceId, + dev: dev_t, + api: Cell, + drm: Drm, + ctx: CloneCell>>, + events: OnChange, +} + +const DRM: &[u8] = b"drm"; + +pub async fn create(state: &Rc) -> Result, HeadlessBackendError> { + let udev = Rc::new(Udev::new().map_err(CreateUdev)?); + let monitor = Rc::new(udev.create_monitor().map_err(CreateUdevMonitor)?); + monitor + .add_match_subsystem_devtype(Some(DRM), None) + .map_err(AddUdevSubsystemMatch)?; + monitor.enable_receiving().map_err(EnableUdevReceiving)?; + let monitor_fd = uapi::fcntl_dupfd_cloexec(monitor.fd(), 0) + .map(Rc::new) + .map_err(Into::into) + .map_err(DupUdevMonitorFd)?; + Ok(Rc::new(HeadlessBackend { + state: state.clone(), + udev, + monitor, + monitor_fd, + devs: Default::default(), + render_device: Default::default(), + })) +} + +impl Backend for HeadlessBackend { + fn run(self: Rc) -> SpawnedFuture>> { + let slf = self.clone(); + self.state.eng.spawn("headless backend", async move { + slf.run().await?; + Ok(()) + }) + } + + fn clear(&self) { + for dev in self.devs.lock().drain_values() { + dev.ctx.take(); + dev.events.clear(); + } + } +} + +fn is_primary_node(n: &[u8]) -> bool { + match n.strip_prefix(b"card") { + Some(r) => r.iter().copied().all(|c| matches!(c, b'0'..=b'9')), + _ => false, + } +} + +impl HeadlessBackend { + async fn run(self: Rc) -> Result<(), HeadlessBackendError> { + if let Some(acceptor) = self.state.acceptor.get() { + println!("WAYLAND_DISPLAY={}", acceptor.socket_name()); + } + let mut enumerate = self.udev.create_enumerate().map_err(CreateUdevEnumerator)?; + enumerate + .add_match_subsystem(DRM) + .map_err(AddUdevSubsystemMatch)?; + enumerate.scan_devices().map_err(ScanUdevDevices)?; + let mut entry_opt = enumerate.get_list_entry().map_err(GetUdevEntry)?; + while let Some(entry) = entry_opt.take() { + if let Ok(dev) = self.udev.create_device_from_syspath(entry.name()) { + self.handle_device_add(dev); + } + entry_opt = entry.next(); + } + self.state + .backend_events + .push(BackendEvent::DevicesEnumerated); + loop { + let res = self + .state + .ring + .readable(&self.monitor_fd) + .await + .map_err(MonitorFdReadable)?; + if res.intersects(c::POLLERR | c::POLLHUP) { + return Err(MonitorFdFailed); + } + while let Some(dev) = self.monitor.receive_device() { + if let Some(action) = dev.action() + && action.as_ustr() == "add" + { + self.handle_device_add(dev); + } + } + } + } + + fn handle_device_add(self: &Rc, dev: UdevDevice) { + let num = dev.devnum(); + if let Err(e) = self.handle_device_add_(dev) { + log::error!( + "Could not add device {}:{}: {}", + major(num), + minor(num), + ErrorFmt(e), + ); + } + } + + fn handle_device_add_(self: &Rc, dev: UdevDevice) -> Result<(), HeadlessBackendError> { + let Some(subsystem) = dev.subsystem() else { + return Ok(()); + }; + if subsystem.as_ustr() != DRM { + return Ok(()); + } + let Some(sysname) = dev.sysname() else { + return Ok(()); + }; + if !is_primary_node(sysname.to_bytes()) { + return Ok(()); + } + let devnum = dev.devnum(); + if self.devs.contains(&devnum) { + return Ok(()); + } + let nodes = get_drm_nodes_from_dev(major(devnum), minor(devnum)).map_err(GetDrmNodes)?; + let node = nodes + .get(&NodeType::Render) + .or_else(|| nodes.get(&NodeType::Primary)) + .ok_or(NoDrmNodes)?; + let fd = uapi::open(&**node, c::O_RDWR | c::O_CLOEXEC, 0) + .map_err(Into::into) + .map_err(OpenDrmNode)?; + let drm = Drm::open_existing(Rc::new(fd)).map_err(CreateDrm)?; + let dev = Rc::new(HeadlessDrmDevice { + backend: self.clone(), + id: self.state.drm_dev_ids.next(), + dev: devnum, + api: Cell::new(self.state.default_gfx_api.get()), + drm, + ctx: Default::default(), + events: Default::default(), + }); + self.devs.set(devnum, dev.clone()); + self.state + .backend_events + .push(BackendEvent::NewDrmDevice(dev)); + Ok(()) + } +} + +impl HeadlessDrmDevice { + fn create_ctx(&self, api: GfxApi) -> Option> { + match self.backend.state.create_gfx_context(&self.drm, Some(api)) { + Ok(c) => Some(c), + Err(e) => { + log::error!("Could not create GFX context: {}", ErrorFmt(e)); + None + } + } + } +} + +impl BackendDrmDevice for HeadlessDrmDevice { + fn id(&self) -> DrmDeviceId { + self.id + } + + fn event(&self) -> Option { + self.events.events.pop() + } + + fn on_change(&self, cb: Rc) { + self.events.on_change.set(Some(cb)); + } + + fn dev_t(&self) -> dev_t { + self.dev + } + + fn make_render_device(&self) { + if self.is_render_device() { + return; + } + let ctx = match self.ctx.get() { + Some(ctx) => ctx, + _ => { + let Some(ctx) = self.create_ctx(self.gtx_api()) else { + return; + }; + self.ctx.set(Some(ctx.clone())); + ctx + } + }; + self.backend.render_device.set(Some(self.id)); + self.backend.state.set_render_ctx(Some(ctx)); + } + + fn set_gfx_api(&self, api: GfxApi) { + if self.api.get() == api { + return; + } + if self.ctx.is_none() { + self.api.set(api); + return; + } + let Some(ctx) = self.create_ctx(api) else { + return; + }; + self.ctx.set(Some(ctx.clone())); + self.api.set(api); + self.events.send_event(DrmEvent::GfxApiChanged); + if self.is_render_device() { + self.backend.state.set_render_ctx(Some(ctx)); + } + } + + fn gtx_api(&self) -> GfxApi { + self.api.get() + } + + fn version(&self) -> Result { + self.drm.version() + } + + fn set_direct_scanout_enabled(&self, enabled: bool) { + let _ = enabled; + } + + fn is_render_device(&self) -> bool { + self.backend.render_device.get() == Some(self.id) + } +} diff --git a/src/cli.rs b/src/cli.rs index a650e8b3..00c66e15 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -197,6 +197,7 @@ pub struct SeatTestArgs { pub enum CliBackend { X11, Metal, + Headless, } #[derive(Args, Debug)] diff --git a/src/compositor.rs b/src/compositor.rs index 01940220..43416809 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -7,7 +7,7 @@ use { backend::{Backend, Connector}, backends::{ dummy::{DummyBackend, DummyOutput}, - metal, x, + headless, metal, x, }, cli::{CliBackend, GlobalArgs, RunArgs}, client::{ClientId, Clients}, @@ -644,6 +644,15 @@ async fn create_backend( } } } + CliBackend::Headless => { + log::info!("Trying to create headless backend"); + match headless::create(state).await { + Ok(b) => return Some(b), + Err(e) => { + log::error!("Could not create headless backend: {}", ErrorFmt(e)); + } + } + } } } None