From 8925ebe3df5873b6f92bf37bbaad112546b11e72 Mon Sep 17 00:00:00 2001 From: Marco Allegretti Date: Tue, 10 Mar 2026 22:32:21 +0100 Subject: [PATCH] feat(compositor): implement DRM/KMS rendering path Add full DRM/KMS backend with libseat session, GBM allocation, EGL display initialisation, and a GpuManager-driven rendering loop. - drm_device.rs: type aliases and per-device/per-output state structs (WeftDrmDevice, WeftOutputSurface, WeftDrmData) - drm.rs: replace skeleton with complete backend libseat session, udev device enumeration, libinput event source, connector scanning via smithay-drm-extras DrmScanner, DrmOutputManager initialisation per CRTC, VBlank-driven render_output, sd_notify(READY=1) - state.rs: add drm: Option field; route dmabuf import through GPU manager when the DRM path is active - Cargo.toml: add renderer_multi, use_system_lib Smithay features; add smithay-drm-extras and sd-notify Linux dependencies render_output submits a clear-colour-only frame to establish the VBlank pipeline. Surface compositing is wired up in a subsequent commit. --- Cargo.lock | 179 ++++- crates/weft-compositor/Cargo.toml | 7 +- crates/weft-compositor/src/backend/drm.rs | 653 ++++++++++++++++-- .../weft-compositor/src/backend/drm_device.rs | 65 ++ crates/weft-compositor/src/backend/mod.rs | 3 + crates/weft-compositor/src/state.rs | 28 +- 6 files changed, 854 insertions(+), 81 deletions(-) create mode 100644 crates/weft-compositor/src/backend/drm_device.rs diff --git a/Cargo.lock b/Cargo.lock index 48d7990..fff8255 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,6 +24,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "aliasable" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" + [[package]] name = "android-activity" version = "0.6.0" @@ -78,12 +84,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" - [[package]] name = "atomic-waker" version = "1.1.2" @@ -184,7 +184,6 @@ version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" dependencies = [ - "async-task", "bitflags 2.11.0", "polling", "rustix 1.1.4", @@ -204,18 +203,6 @@ dependencies = [ "wayland-client", ] -[[package]] -name = "calloop-wayland-source" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" -dependencies = [ - "calloop 0.14.4", - "rustix 1.1.4", - "wayland-backend", - "wayland-client", -] - [[package]] name = "cc" version = "1.2.56" @@ -234,6 +221,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" +[[package]] +name = "cfg-expr" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c6b04e07d8080154ed4ac03546d9a2b303cc2fe1901ba0b35b301516e289368" +dependencies = [ + "smallvec", + "target-lexicon", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -520,6 +517,8 @@ dependencies = [ "drm-fourcc", "gbm-sys", "libc", + "wayland-backend", + "wayland-server", ] [[package]] @@ -739,6 +738,40 @@ version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +[[package]] +name = "libdisplay-info" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4210cfe93a0dc37228e08105e3c13171e5af816f7bd39e00e3d3adcf2b487a2b" +dependencies = [ + "bitflags 2.11.0", + "libc", + "libdisplay-info-derive", + "libdisplay-info-sys", + "thiserror 2.0.18", +] + +[[package]] +name = "libdisplay-info-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dc2c710cf5819e91220a446d9e64acc6814386cc22c509c3f0df83c0b874a98" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "libdisplay-info-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4f9264ece23c37ffa023ae635f48d588e1786745dad06dff10c9fb99dc646c" +dependencies = [ + "semver", + "system-deps", +] + [[package]] name = "libloading" version = "0.8.9" @@ -839,6 +872,15 @@ dependencies = [ "libc", ] +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "ndk" version = "0.9.0" @@ -1389,6 +1431,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" +[[package]] +name = "sd-notify" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b943eadf71d8b69e661330cb0e2656e31040acf21ee7708e2c238a0ec6af2bf4" +dependencies = [ + "libc", +] + [[package]] name = "semver" version = "1.0.27" @@ -1437,6 +1488,15 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + [[package]] name = "sha2" version = "0.10.9" @@ -1481,6 +1541,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "740cea6927892bc182d5bf70c8f79806c8bc9f68f2fb96e55a30be171b63af98" dependencies = [ + "aliasable", "appendlist", "atomic_float", "bitflags 2.11.0", @@ -1510,6 +1571,7 @@ dependencies = [ "thiserror 2.0.18", "tracing", "udev", + "wayland-backend", "wayland-client", "wayland-cursor", "wayland-egl", @@ -1517,6 +1579,7 @@ dependencies = [ "wayland-protocols-misc", "wayland-protocols-wlr", "wayland-server", + "wayland-sys", "winit", "xkbcommon", ] @@ -1529,7 +1592,7 @@ checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" dependencies = [ "bitflags 2.11.0", "calloop 0.13.0", - "calloop-wayland-source 0.3.0", + "calloop-wayland-source", "cursor-icon", "libc", "log", @@ -1546,6 +1609,16 @@ dependencies = [ "xkeysym", ] +[[package]] +name = "smithay-drm-extras" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68109eb23955c216dadb780b0a82e5b0d1dd4b649d4a52b59100eb83a30e96" +dependencies = [ + "drm", + "libdisplay-info", +] + [[package]] name = "smol_str" version = "0.2.2" @@ -1566,6 +1639,25 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "system-deps" +version = "7.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c8f33736f986f16d69b6cb8b03f55ddcad5c41acc4ccc39dd88e84aa805e7f" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" + [[package]] name = "tempfile" version = "3.26.0" @@ -1628,6 +1720,30 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_datetime" version = "1.0.0+spec-1.1.0" @@ -1644,7 +1760,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" dependencies = [ "indexmap", - "toml_datetime", + "toml_datetime 1.0.0+spec-1.1.0", "toml_parser", "winnow", ] @@ -1658,6 +1774,12 @@ dependencies = [ "winnow", ] +[[package]] +name = "toml_writer" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" + [[package]] name = "tracing" version = "0.1.44" @@ -1762,6 +1884,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + [[package]] name = "version_check" version = "0.9.5" @@ -2031,7 +2159,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "374f6b70e8e0d6bf9461a32988fd553b59ff630964924dad6e4a4eb6bd538d17" dependencies = [ "dlib", + "libc", "log", + "memoffset", "once_cell", "pkg-config", ] @@ -2065,14 +2195,11 @@ name = "weft-compositor" version = "0.1.0" dependencies = [ "anyhow", - "calloop 0.14.4", - "calloop-wayland-source 0.4.1", + "sd-notify", "smithay", + "smithay-drm-extras", "tracing", "tracing-subscriber", - "wayland-protocols", - "wayland-protocols-wlr", - "wayland-server", ] [[package]] diff --git a/crates/weft-compositor/Cargo.toml b/crates/weft-compositor/Cargo.toml index 59c9042..4be15c6 100644 --- a/crates/weft-compositor/Cargo.toml +++ b/crates/weft-compositor/Cargo.toml @@ -5,9 +5,6 @@ edition.workspace = true rust-version.workspace = true publish = false -[lints] -workspace = true - [[bin]] name = "weft-compositor" path = "src/main.rs" @@ -33,4 +30,8 @@ smithay = { version = "0.7", default-features = false, features = [ "backend_libinput", "backend_udev", "backend_session_libseat", + "renderer_multi", + "use_system_lib", ] } +smithay-drm-extras = "0.1" +sd-notify = "0.4" diff --git a/crates/weft-compositor/src/backend/drm.rs b/crates/weft-compositor/src/backend/drm.rs index 5357263..75973cf 100644 --- a/crates/weft-compositor/src/backend/drm.rs +++ b/crates/weft-compositor/src/backend/drm.rs @@ -1,88 +1,645 @@ -// Non-Linux: DRM/KMS backend is unavailable; callers must use --winit. +// Non-Linux: DRM/KMS backend is unavailable; callers must use --winit. #[cfg(not(target_os = "linux"))] pub fn run() -> anyhow::Result<()> { anyhow::bail!("DRM/KMS backend requires Linux; pass --winit for development on other platforms") } -// Linux DRM/KMS backend. -// GPU enumeration and rendering are deferred; this skeleton establishes the -// session, socket, and event loop that the full implementation will extend. +#[cfg(target_os = "linux")] +use std::{collections::HashMap, path::Path, sync::Arc, time::Duration}; + +#[cfg(target_os = "linux")] +use anyhow::Context; + +#[cfg(target_os = "linux")] +use smithay::{ + backend::{ + allocator::{ + format::FormatSet, + gbm::{GbmAllocator, GbmBufferFlags, GbmDevice}, + Fourcc, Modifier, + }, + drm::{ + compositor::FrameFlags, + exporter::gbm::GbmFramebufferExporter, + output::{DrmOutputManager, DrmOutputRenderElements}, + DrmDevice, DrmDeviceFd, DrmEvent, DrmNode, NodeType, + }, + egl::{EGLDevice, EGLDisplay}, + input::InputEvent, + libinput::{LibinputInputBackend, LibinputSessionInterface}, + renderer::{ + element::surface::WaylandSurfaceRenderElement, + gles::GlesRenderer, + multigpu::{gbm::GbmGlesBackend, GpuManager, MultiRenderer}, + }, + session::{libseat::LibSeatSession, Event as SessionEvent, Session}, + udev::{all_gpus, primary_gpu, UdevBackend, UdevEvent}, + }, + output::{Mode as WlMode, Output, PhysicalProperties, Subpixel}, + reexports::{ + calloop::{EventLoop, Interest, Mode, PostAction, generic::Generic}, + drm::control::{connector, crtc, ModeTypeFlags}, + input::{DeviceCapability, Libinput}, + rustix::fs::OFlags, + wayland_server::Display, + }, + utils::{DeviceFd, Transform}, + wayland::socket::ListeningSocketSource, +}; + +#[cfg(target_os = "linux")] +use smithay_drm_extras::drm_scanner::{DrmScanEvent, DrmScanner}; + +#[cfg(target_os = "linux")] +use crate::{ + input, + state::{WeftClientState, WeftCompositorState}, +}; + +#[cfg(target_os = "linux")] +use super::drm_device::{WeftDrmData, WeftDrmDevice, WeftOutputSurface}; + +#[cfg(target_os = "linux")] +const SUPPORTED_FORMATS: &[Fourcc] = &[ + Fourcc::Abgr2101010, + Fourcc::Argb2101010, + Fourcc::Abgr8888, + Fourcc::Argb8888, +]; + +#[cfg(target_os = "linux")] +const SUPPORTED_FORMATS_8BIT_ONLY: &[Fourcc] = &[Fourcc::Abgr8888, Fourcc::Argb8888]; + +#[cfg(target_os = "linux")] +type WeftMultiRenderer<'a> = MultiRenderer< + 'a, + 'a, + GbmGlesBackend, + GbmGlesBackend, +>; + #[cfg(target_os = "linux")] pub fn run() -> anyhow::Result<()> { - use std::sync::Arc; - - use smithay::{ - backend::{ - session::{Session, libseat::LibSeatSession}, - udev::{UdevBackend, UdevEvent}, - }, - reexports::calloop::{EventLoop, Interest, Mode, PostAction, generic::Generic}, - wayland::socket::ListeningSocketSource, - }; - - use crate::state::{WeftClientState, WeftCompositorState}; - - let mut display = smithay::reexports::wayland_server::Display::::new()?; - let display_handle = display.handle(); - let mut event_loop: EventLoop<'static, WeftCompositorState> = EventLoop::try_new()?; let loop_handle = event_loop.handle(); + + let mut display = Display::::new()?; + let display_handle = display.handle(); + + let (session, session_notifier) = + LibSeatSession::new().context("failed to create libseat session")?; + + let seat_name = session.seat().to_owned(); let loop_signal = event_loop.get_signal(); - // Gain DRM device access without root via libseat. - let (session, _notifier) = - LibSeatSession::new().map_err(|e| anyhow::anyhow!("libseat session failed: {e}"))?; + let primary_gpu_node = if let Ok(var) = std::env::var("WEFT_DRM_DEVICE") { + DrmNode::from_path(var).context("invalid WEFT_DRM_DEVICE path")? + } else { + primary_gpu(&seat_name) + .ok() + .flatten() + .and_then(|p| DrmNode::from_path(p).ok()?.node_with_type(NodeType::Render)?.ok()) + .or_else(|| { + all_gpus(&seat_name) + .unwrap_or_default() + .into_iter() + .find_map(|p| DrmNode::from_path(p).ok()) + }) + .context("no GPU found")? + }; + tracing::info!(?primary_gpu_node, "primary GPU"); - let listening_socket = ListeningSocketSource::new_auto() - .map_err(|e| anyhow::anyhow!("Wayland socket creation failed: {e}"))?; + let gpu_manager: super::drm_device::WeftGpuManager = + GpuManager::new(Default::default()).context("failed to create GPU manager")?; + + let listening_socket = + ListeningSocketSource::new_auto().context("failed to create Wayland socket")?; let socket_name = listening_socket.socket_name().to_os_string(); std::env::set_var("WAYLAND_DISPLAY", &socket_name); - tracing::info!(?socket_name, "Wayland compositor socket open"); + tracing::info!(?socket_name, "Wayland socket open"); loop_handle - .insert_source(listening_socket, |client_stream, _, state| { + .insert_source(listening_socket, |stream, _, state| { state .display_handle - .insert_client(client_stream, Arc::new(WeftClientState::default())) + .insert_client(stream, Arc::new(WeftClientState::default())) .unwrap(); }) - .map_err(|e| anyhow::anyhow!("socket source insertion failed: {e}"))?; + .map_err(|e| anyhow::anyhow!("socket source: {e}"))?; loop_handle .insert_source( Generic::new(display, Interest::READ, Mode::Level), |_, display, state| { - // Safety: the display is owned by this Generic source and is never - // dropped while the event loop runs. + // Safety: Display is owned by this Generic source and outlives the event loop. unsafe { display.get_mut().dispatch_clients(state).unwrap(); } Ok(PostAction::Continue) }, ) - .map_err(|e| anyhow::anyhow!("display source insertion failed: {e}"))?; + .map_err(|e| anyhow::anyhow!("display source: {e}"))?; + + let udev_backend = + UdevBackend::new(&seat_name).context("failed to create udev backend")?; + + let mut libinput_ctx = + Libinput::new_with_udev::>( + session.clone().into(), + ); + libinput_ctx + .udev_assign_seat(&seat_name) + .map_err(|_| anyhow::anyhow!("libinput seat assignment failed"))?; + let libinput_backend = LibinputInputBackend::new(libinput_ctx); - // Enumerate GPU nodes via udev; hotplug events arrive through calloop. - let udev_backend = UdevBackend::new(session.seat())?; loop_handle - .insert_source(udev_backend, move |event, _, _state| match event { - UdevEvent::Added { device_id, path } => { - tracing::info!(?device_id, ?path, "GPU device added"); - } - UdevEvent::Changed { device_id } => { - tracing::debug!(?device_id, "GPU device changed"); - } - UdevEvent::Removed { device_id } => { - tracing::info!(?device_id, "GPU device removed"); - } + .insert_source( + libinput_backend, + move |mut event, _, state: &mut WeftCompositorState| { + if let InputEvent::DeviceAdded { device } = &mut event { + if device.has_capability(DeviceCapability::Keyboard) { + if let Some(led) = state.seat.get_keyboard().map(|k| k.led_state()) { + device.led_update(led.into()); + } + if let Some(drm) = state.drm.as_mut() { + drm.keyboards.push(device.clone()); + } + } + } + input::process_input_event(state, event); + }, + ) + .map_err(|e| anyhow::anyhow!("libinput source: {e}"))?; + + loop_handle + .insert_source( + session_notifier, + move |event, &mut (), state: &mut WeftCompositorState| match event { + SessionEvent::PauseSession => { + if let Some(drm) = state.drm.as_mut() { + for dev in drm.devices.values_mut() { + dev.drm_output_manager.pause(); + } + } + } + SessionEvent::ActivateSession => { + tracing::info!("session activated"); + } + }, + ) + .map_err(|e| anyhow::anyhow!("session notifier: {e}"))?; + + loop_handle + .insert_source( + udev_backend, + move |event, _, state: &mut WeftCompositorState| match event { + UdevEvent::Added { device_id: _, path } => { + let node = match DrmNode::from_path(&path) { + Ok(n) => n, + Err(e) => { + tracing::warn!(?e, "failed to build DRM node"); + return; + } + }; + if let Err(e) = device_added(state, node, &path) { + tracing::warn!(?e, "failed to add DRM device"); + } + } + UdevEvent::Changed { device_id } => { + let node = state + .drm + .as_ref() + .and_then(|d| d.devices.keys().find(|n| n.dev_id() == device_id).copied()); + if let Some(node) = node { + device_changed(state, node); + } + } + UdevEvent::Removed { device_id } => { + let node = state + .drm + .as_ref() + .and_then(|d| d.devices.keys().find(|n| n.dev_id() == device_id).copied()); + if let Some(node) = node { + device_removed(state, node); + } + } + }, + ) + .map_err(|e| anyhow::anyhow!("udev source: {e}"))?; + + let mut state = WeftCompositorState::new( + display_handle.clone(), + loop_signal, + loop_handle, + seat_name, + ); + + state.drm = Some(WeftDrmData { + session, + primary_gpu: primary_gpu_node, + gpu_manager, + devices: HashMap::new(), + keyboards: Vec::new(), + display_handle, + }); + + let existing: Vec<(DrmNode, std::path::PathBuf)> = state + .drm + .as_ref() + .map(|d| { + all_gpus(d.session.seat()) + .unwrap_or_default() + .into_iter() + .filter_map(|p| DrmNode::from_path(&p).ok().map(|n| (n, p))) + .collect() }) - .map_err(|e| anyhow::anyhow!("udev source insertion failed: {e}"))?; + .unwrap_or_default(); - let mut state = - WeftCompositorState::new(display_handle, loop_signal, loop_handle, session.seat()); + for (node, path) in existing { + if let Err(e) = device_added(&mut state, node, &path) { + tracing::warn!(?e, ?node, "startup device_added failed"); + } + } + + let _ = sd_notify::notify(false, &[sd_notify::NotifyState::Ready]); - tracing::info!("DRM/KMS backend initialised; entering event loop"); event_loop.run(None, &mut state, |_| {})?; Ok(()) } + +#[cfg(target_os = "linux")] +fn device_added( + state: &mut WeftCompositorState, + node: DrmNode, + path: &Path, +) -> anyhow::Result<()> { + let drm_data = state.drm.as_mut().context("DRM data not initialised")?; + + let fd = drm_data + .session + .open( + path, + OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY | OFlags::NONBLOCK, + ) + .context("failed to open DRM device")?; + + let fd = DrmDeviceFd::new(DeviceFd::from(fd)); + let (drm, notifier) = + DrmDevice::new(fd.clone(), true).context("DrmDevice::new failed")?; + let gbm = GbmDevice::new(fd).context("GbmDevice::new failed")?; + + let render_node = (|| -> anyhow::Result { + // Safety: EGLDisplay requires the GBM device to outlive it; gbm lives in WeftDrmDevice. + let egl_display = + unsafe { EGLDisplay::new(gbm.clone()).context("EGLDisplay::new failed")? }; + let egl_device = + EGLDevice::device_for_display(&egl_display).context("no EGL device")?; + if egl_device.is_software() { + anyhow::bail!("software renderer"); + } + let rn = egl_device + .try_get_render_node() + .ok() + .flatten() + .unwrap_or(node); + drm_data + .gpu_manager + .as_mut() + .add_node(rn, gbm.clone()) + .map_err(|e| anyhow::anyhow!("add_node: {e:?}"))?; + Ok(rn) + })() + .map_err(|e| { + tracing::warn!(?e, "EGL init failed; output may render black"); + e + }) + .ok(); + + let effective_gpu = render_node.unwrap_or(drm_data.primary_gpu); + + let allocator = GbmAllocator::new( + gbm.clone(), + GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT, + ); + + let exporter = GbmFramebufferExporter::new(gbm.clone(), render_node.into()); + + let color_formats = if std::env::var("WEFT_DISABLE_10BIT").is_ok() { + SUPPORTED_FORMATS_8BIT_ONLY + } else { + SUPPORTED_FORMATS + }; + + let render_formats: FormatSet = drm_data + .gpu_manager + .single_renderer(&effective_gpu) + .map(|r| { + r.as_ref() + .egl_context() + .dmabuf_render_formats() + .iter() + .filter(|f| render_node.is_some() || f.modifier == Modifier::Linear) + .copied() + .collect::() + }) + .unwrap_or_default(); + + let drm_output_manager = DrmOutputManager::new( + drm, + allocator, + exporter, + Some(gbm), + color_formats.iter().copied(), + render_formats, + ); + + let registration_token = state + .loop_handle + .insert_source( + notifier, + move |event, _metadata, data: &mut WeftCompositorState| match event { + DrmEvent::VBlank(crtc) => render_output(data, node, crtc), + DrmEvent::Error(e) => tracing::error!(?e, "DRM error"), + }, + ) + .map_err(|e| anyhow::anyhow!("DRM notifier: {e}"))?; + + state.drm.as_mut().unwrap().devices.insert( + node, + WeftDrmDevice { + drm_output_manager, + drm_scanner: DrmScanner::new(), + surfaces: HashMap::new(), + render_node, + registration_token, + }, + ); + + device_changed(state, node); + Ok(()) +} + +#[cfg(target_os = "linux")] +fn device_changed(state: &mut WeftCompositorState, node: DrmNode) { + let drm_data = match state.drm.as_mut() { + Some(d) => d, + None => return, + }; + let device = match drm_data.devices.get_mut(&node) { + Some(d) => d, + None => return, + }; + + let events: Vec = device + .drm_scanner + .scan_connectors(device.drm_output_manager.device()); + + for event in events { + match event { + DrmScanEvent::Connected { + connector, + crtc: Some(crtc), + } => connector_connected(state, node, connector, crtc), + DrmScanEvent::Disconnected { + connector, + crtc: Some(crtc), + } => connector_disconnected(state, node, connector, crtc), + _ => {} + } + } +} + +#[cfg(target_os = "linux")] +fn connector_connected( + state: &mut WeftCompositorState, + node: DrmNode, + connector: connector::Info, + crtc: crtc::Handle, +) { + let name = format!("{:?}-{}", connector.interface(), connector.interface_id()); + + let mode = match connector + .modes() + .iter() + .find(|m| m.mode_type().contains(ModeTypeFlags::PREFERRED)) + .copied() + .or_else(|| connector.modes().first().copied()) + { + Some(m) => m, + None => { + tracing::warn!(?name, "connector has no modes"); + return; + } + }; + + let wl_mode = WlMode { + size: (mode.size().0 as i32, mode.size().1 as i32).into(), + refresh: mode.vrefresh() as i32 * 1000, + }; + + let output = Output::new( + name.clone(), + PhysicalProperties { + size: (0, 0).into(), + subpixel: Subpixel::Unknown, + make: "Unknown".to_string(), + model: name.clone(), + }, + ); + + let drm_data = match state.drm.as_mut() { + Some(d) => d, + None => return, + }; + + let global = output.create_global::(&drm_data.display_handle); + output.change_current_state( + Some(wl_mode), + Some(Transform::Normal), + None, + Some((0, 0).into()), + ); + output.set_preferred(wl_mode); + + let render_node = drm_data + .devices + .get(&node) + .and_then(|d| d.render_node) + .unwrap_or(drm_data.primary_gpu); + + let planes = drm_data + .devices + .get_mut(&node) + .and_then(|d| d.drm_output_manager.device().planes(&crtc).ok()); + + let WeftDrmData { + ref mut gpu_manager, + ref mut devices, + .. + } = *drm_data; + + let device = match devices.get_mut(&node) { + Some(d) => d, + None => return, + }; + + let mut renderer = match gpu_manager.single_renderer(&render_node) { + Ok(r) => r, + Err(e) => { + tracing::warn!(?e, "no renderer for output init"); + return; + } + }; + + let drm_output = match device.drm_output_manager.initialize_output( + crtc, + mode, + &[connector.handle()], + &output, + planes, + &mut renderer, + &DrmOutputRenderElements::default(), + ) { + Ok(o) => o, + Err(e) => { + tracing::warn!(?e, ?name, "initialize_output failed"); + return; + } + }; + + device.surfaces.insert( + crtc, + WeftOutputSurface { + output: output.clone(), + drm_output, + device_id: node, + global, + }, + ); + + state.space.map_output(&output, (0, 0)); + tracing::info!(?name, "output connected"); + render_output(state, node, crtc); +} + +#[cfg(target_os = "linux")] +fn connector_disconnected( + state: &mut WeftCompositorState, + node: DrmNode, + _connector: connector::Info, + crtc: crtc::Handle, +) { + let drm_data = match state.drm.as_mut() { + Some(d) => d, + None => return, + }; + if let Some(device) = drm_data.devices.get_mut(&node) { + if let Some(surface) = device.surfaces.remove(&crtc) { + state.space.unmap_output(&surface.output); + } + } +} + +#[cfg(target_os = "linux")] +fn device_removed(state: &mut WeftCompositorState, node: DrmNode) { + let drm_data = match state.drm.as_mut() { + Some(d) => d, + None => return, + }; + if let Some(device) = drm_data.devices.remove(&node) { + state.loop_handle.remove(device.registration_token); + for surface in device.surfaces.into_values() { + state.space.unmap_output(&surface.output); + } + } +} + +#[cfg(target_os = "linux")] +fn render_output(state: &mut WeftCompositorState, node: DrmNode, crtc: crtc::Handle) { + let output = { + let drm_data = match state.drm.as_ref() { + Some(d) => d, + None => return, + }; + match drm_data + .devices + .get(&node) + .and_then(|d| d.surfaces.get(&crtc)) + { + Some(s) => s.output.clone(), + None => return, + } + }; + + let render_node = { + let d = state.drm.as_ref().unwrap(); + d.devices + .get(&node) + .and_then(|d| d.render_node) + .unwrap_or(d.primary_gpu) + }; + + let WeftCompositorState { + ref mut drm, + ref space, + .. + } = *state; + + let drm_data = match drm.as_mut() { + Some(d) => d, + None => return, + }; + + let WeftDrmData { + ref mut gpu_manager, + ref mut devices, + .. + } = *drm_data; + + let device = match devices.get_mut(&node) { + Some(d) => d, + None => return, + }; + let surface = match device.surfaces.get_mut(&crtc) { + Some(s) => s, + None => return, + }; + + let mut renderer = match gpu_manager.single_renderer(&render_node) { + Ok(r) => r, + Err(e) => { + tracing::warn!(?e, "renderer unavailable"); + return; + } + }; + + // Wave 2: clear-colour-only frame to establish the VBlank pipeline. + // Surface compositing is added in Wave 3. + let elements: &[WaylandSurfaceRenderElement>] = &[]; + match surface.drm_output.render_frame( + &mut renderer, + elements, + [0.08_f32, 0.08, 0.08, 1.0], + FrameFlags::DEFAULT, + ) { + Ok(result) if !result.is_empty => { + if let Err(e) = surface.drm_output.queue_frame(None) { + tracing::warn!(?e, "queue_frame failed"); + } + } + Ok(_) => {} + Err(e) => tracing::warn!(?e, "render_frame failed"), + } + + space.elements().for_each(|window| { + window.send_frame( + &output, + Duration::ZERO, + Some(Duration::ZERO), + |_, _| Some(output.clone()), + ); + }); + + let _ = state.display_handle.flush_clients(); +} \ No newline at end of file diff --git a/crates/weft-compositor/src/backend/drm_device.rs b/crates/weft-compositor/src/backend/drm_device.rs new file mode 100644 index 0000000..06537bf --- /dev/null +++ b/crates/weft-compositor/src/backend/drm_device.rs @@ -0,0 +1,65 @@ +use std::collections::HashMap; + +use smithay::{ + backend::{ + allocator::gbm::GbmAllocator, + drm::{ + exporter::gbm::GbmFramebufferExporter, + output::{DrmOutput, DrmOutputManager}, + DrmDeviceFd, DrmNode, + }, + renderer::{ + gles::GlesRenderer, + multigpu::{gbm::GbmGlesBackend, GpuManager}, + }, + session::libseat::LibSeatSession, + }, + desktop::utils::OutputPresentationFeedback, + output::Output, + reexports::{ + calloop::RegistrationToken, + drm::control::crtc, + wayland_server::{backend::GlobalId, DisplayHandle}, + }, +}; +use smithay_drm_extras::drm_scanner::DrmScanner; + +pub type WeftAllocator = GbmAllocator; +pub type WeftExporter = GbmFramebufferExporter; +pub type WeftDrmOutput = DrmOutput< + WeftAllocator, + WeftExporter, + Option, + DrmDeviceFd, +>; +pub type WeftDrmOutputManager = DrmOutputManager< + WeftAllocator, + WeftExporter, + Option, + DrmDeviceFd, +>; +pub type WeftGpuManager = GpuManager>; + +pub struct WeftOutputSurface { + pub output: Output, + pub drm_output: WeftDrmOutput, + pub device_id: DrmNode, + pub global: GlobalId, +} + +pub struct WeftDrmDevice { + pub drm_output_manager: WeftDrmOutputManager, + pub drm_scanner: DrmScanner, + pub surfaces: HashMap, + pub render_node: Option, + pub registration_token: RegistrationToken, +} + +pub struct WeftDrmData { + pub session: LibSeatSession, + pub primary_gpu: DrmNode, + pub gpu_manager: WeftGpuManager, + pub devices: HashMap, + pub keyboards: Vec, + pub display_handle: DisplayHandle, +} diff --git a/crates/weft-compositor/src/backend/mod.rs b/crates/weft-compositor/src/backend/mod.rs index b6bd14b..7810d8e 100644 --- a/crates/weft-compositor/src/backend/mod.rs +++ b/crates/weft-compositor/src/backend/mod.rs @@ -1,2 +1,5 @@ pub mod drm; pub mod winit; + +#[cfg(target_os = "linux")] +pub mod drm_device; diff --git a/crates/weft-compositor/src/state.rs b/crates/weft-compositor/src/state.rs index c391ddf..141dded 100644 --- a/crates/weft-compositor/src/state.rs +++ b/crates/weft-compositor/src/state.rs @@ -1,3 +1,6 @@ +#[cfg(target_os = "linux")] +use crate::backend::drm_device::WeftDrmData; + use smithay::{ backend::{input::TabletToolDescriptor, renderer::utils::on_commit_buffer_handler}, delegate_compositor, delegate_cursor_shape, delegate_dmabuf, delegate_input_method_manager, @@ -81,6 +84,9 @@ pub struct WeftCompositorState { // Set to false when the compositor should exit the event loop. pub running: bool, + + #[cfg(target_os = "linux")] + pub drm: Option, } impl WeftCompositorState { @@ -134,6 +140,8 @@ impl WeftCompositorState { cursor_image_status: CursorImageStatus::Hidden, dmabuf_global: None, running: true, + #[cfg(target_os = "linux")] + drm: None, } } } @@ -291,12 +299,24 @@ impl DmabufHandler for WeftCompositorState { fn dmabuf_imported( &mut self, _global: &DmabufGlobal, - _dmabuf: smithay::backend::allocator::dmabuf::Dmabuf, + dmabuf: smithay::backend::allocator::dmabuf::Dmabuf, notifier: ImportNotifier, ) { - // DMA-BUF import requires the renderer, which lives in the backend run function. - // The backend is responsible for creating the global only when it can service imports. - // If we reach here without a backend handler wired up, reject. + #[cfg(target_os = "linux")] + if let Some(drm) = self.drm.as_mut() { + use smithay::backend::renderer::ImportDma; + let node = drm.primary_gpu; + if drm + .gpu_manager + .single_renderer(&node) + .ok() + .and_then(|mut r| r.import_dmabuf(&dmabuf, None).ok()) + .is_some() + { + let _ = notifier.successful::(); + return; + } + } drop(notifier); } }