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<WeftDrmData> 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.
This commit is contained in:
Marco Allegretti 2026-03-10 22:32:21 +01:00
parent 7d8b1cfecd
commit 8925ebe3df
6 changed files with 854 additions and 81 deletions

179
Cargo.lock generated
View file

@ -24,6 +24,12 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "aliasable"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
[[package]] [[package]]
name = "android-activity" name = "android-activity"
version = "0.6.0" version = "0.6.0"
@ -78,12 +84,6 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b"
[[package]]
name = "async-task"
version = "4.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
[[package]] [[package]]
name = "atomic-waker" name = "atomic-waker"
version = "1.1.2" version = "1.1.2"
@ -184,7 +184,6 @@ version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7"
dependencies = [ dependencies = [
"async-task",
"bitflags 2.11.0", "bitflags 2.11.0",
"polling", "polling",
"rustix 1.1.4", "rustix 1.1.4",
@ -204,18 +203,6 @@ dependencies = [
"wayland-client", "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]] [[package]]
name = "cc" name = "cc"
version = "1.2.56" version = "1.2.56"
@ -234,6 +221,16 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" 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]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.4" version = "1.0.4"
@ -520,6 +517,8 @@ dependencies = [
"drm-fourcc", "drm-fourcc",
"gbm-sys", "gbm-sys",
"libc", "libc",
"wayland-backend",
"wayland-server",
] ]
[[package]] [[package]]
@ -739,6 +738,40 @@ version = "0.2.183"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" 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]] [[package]]
name = "libloading" name = "libloading"
version = "0.8.9" version = "0.8.9"
@ -839,6 +872,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "memoffset"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
dependencies = [
"autocfg",
]
[[package]] [[package]]
name = "ndk" name = "ndk"
version = "0.9.0" version = "0.9.0"
@ -1389,6 +1431,15 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" 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]] [[package]]
name = "semver" name = "semver"
version = "1.0.27" version = "1.0.27"
@ -1437,6 +1488,15 @@ dependencies = [
"zmij", "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]] [[package]]
name = "sha2" name = "sha2"
version = "0.10.9" version = "0.10.9"
@ -1481,6 +1541,7 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "740cea6927892bc182d5bf70c8f79806c8bc9f68f2fb96e55a30be171b63af98" checksum = "740cea6927892bc182d5bf70c8f79806c8bc9f68f2fb96e55a30be171b63af98"
dependencies = [ dependencies = [
"aliasable",
"appendlist", "appendlist",
"atomic_float", "atomic_float",
"bitflags 2.11.0", "bitflags 2.11.0",
@ -1510,6 +1571,7 @@ dependencies = [
"thiserror 2.0.18", "thiserror 2.0.18",
"tracing", "tracing",
"udev", "udev",
"wayland-backend",
"wayland-client", "wayland-client",
"wayland-cursor", "wayland-cursor",
"wayland-egl", "wayland-egl",
@ -1517,6 +1579,7 @@ dependencies = [
"wayland-protocols-misc", "wayland-protocols-misc",
"wayland-protocols-wlr", "wayland-protocols-wlr",
"wayland-server", "wayland-server",
"wayland-sys",
"winit", "winit",
"xkbcommon", "xkbcommon",
] ]
@ -1529,7 +1592,7 @@ checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016"
dependencies = [ dependencies = [
"bitflags 2.11.0", "bitflags 2.11.0",
"calloop 0.13.0", "calloop 0.13.0",
"calloop-wayland-source 0.3.0", "calloop-wayland-source",
"cursor-icon", "cursor-icon",
"libc", "libc",
"log", "log",
@ -1546,6 +1609,16 @@ dependencies = [
"xkeysym", "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]] [[package]]
name = "smol_str" name = "smol_str"
version = "0.2.2" version = "0.2.2"
@ -1566,6 +1639,25 @@ dependencies = [
"unicode-ident", "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]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.26.0" version = "3.26.0"
@ -1628,6 +1720,30 @@ dependencies = [
"cfg-if", "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]] [[package]]
name = "toml_datetime" name = "toml_datetime"
version = "1.0.0+spec-1.1.0" version = "1.0.0+spec-1.1.0"
@ -1644,7 +1760,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"toml_datetime", "toml_datetime 1.0.0+spec-1.1.0",
"toml_parser", "toml_parser",
"winnow", "winnow",
] ]
@ -1658,6 +1774,12 @@ dependencies = [
"winnow", "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]] [[package]]
name = "tracing" name = "tracing"
version = "0.1.44" version = "0.1.44"
@ -1762,6 +1884,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]]
name = "version-compare"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e"
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.5" version = "0.9.5"
@ -2031,7 +2159,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "374f6b70e8e0d6bf9461a32988fd553b59ff630964924dad6e4a4eb6bd538d17" checksum = "374f6b70e8e0d6bf9461a32988fd553b59ff630964924dad6e4a4eb6bd538d17"
dependencies = [ dependencies = [
"dlib", "dlib",
"libc",
"log", "log",
"memoffset",
"once_cell", "once_cell",
"pkg-config", "pkg-config",
] ]
@ -2065,14 +2195,11 @@ name = "weft-compositor"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"calloop 0.14.4", "sd-notify",
"calloop-wayland-source 0.4.1",
"smithay", "smithay",
"smithay-drm-extras",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"wayland-protocols",
"wayland-protocols-wlr",
"wayland-server",
] ]
[[package]] [[package]]

View file

@ -5,9 +5,6 @@ edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
publish = false publish = false
[lints]
workspace = true
[[bin]] [[bin]]
name = "weft-compositor" name = "weft-compositor"
path = "src/main.rs" path = "src/main.rs"
@ -33,4 +30,8 @@ smithay = { version = "0.7", default-features = false, features = [
"backend_libinput", "backend_libinput",
"backend_udev", "backend_udev",
"backend_session_libseat", "backend_session_libseat",
"renderer_multi",
"use_system_lib",
] } ] }
smithay-drm-extras = "0.1"
sd-notify = "0.4"

View file

@ -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"))] #[cfg(not(target_os = "linux"))]
pub fn run() -> anyhow::Result<()> { pub fn run() -> anyhow::Result<()> {
anyhow::bail!("DRM/KMS backend requires Linux; pass --winit for development on other platforms") anyhow::bail!("DRM/KMS backend requires Linux; pass --winit for development on other platforms")
} }
// Linux DRM/KMS backend. #[cfg(target_os = "linux")]
// GPU enumeration and rendering are deferred; this skeleton establishes the use std::{collections::HashMap, path::Path, sync::Arc, time::Duration};
// session, socket, and event loop that the full implementation will extend.
#[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<GlesRenderer, DrmDeviceFd>,
GbmGlesBackend<GlesRenderer, DrmDeviceFd>,
>;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
pub fn run() -> anyhow::Result<()> { 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::<WeftCompositorState>::new()?;
let display_handle = display.handle();
let mut event_loop: EventLoop<'static, WeftCompositorState> = EventLoop::try_new()?; let mut event_loop: EventLoop<'static, WeftCompositorState> = EventLoop::try_new()?;
let loop_handle = event_loop.handle(); let loop_handle = event_loop.handle();
let mut display = Display::<WeftCompositorState>::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(); let loop_signal = event_loop.get_signal();
// Gain DRM device access without root via libseat. let primary_gpu_node = if let Ok(var) = std::env::var("WEFT_DRM_DEVICE") {
let (session, _notifier) = DrmNode::from_path(var).context("invalid WEFT_DRM_DEVICE path")?
LibSeatSession::new().map_err(|e| anyhow::anyhow!("libseat session failed: {e}"))?; } 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() let gpu_manager: super::drm_device::WeftGpuManager =
.map_err(|e| anyhow::anyhow!("Wayland socket creation failed: {e}"))?; 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(); let socket_name = listening_socket.socket_name().to_os_string();
std::env::set_var("WAYLAND_DISPLAY", &socket_name); 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 loop_handle
.insert_source(listening_socket, |client_stream, _, state| { .insert_source(listening_socket, |stream, _, state| {
state state
.display_handle .display_handle
.insert_client(client_stream, Arc::new(WeftClientState::default())) .insert_client(stream, Arc::new(WeftClientState::default()))
.unwrap(); .unwrap();
}) })
.map_err(|e| anyhow::anyhow!("socket source insertion failed: {e}"))?; .map_err(|e| anyhow::anyhow!("socket source: {e}"))?;
loop_handle loop_handle
.insert_source( .insert_source(
Generic::new(display, Interest::READ, Mode::Level), Generic::new(display, Interest::READ, Mode::Level),
|_, display, state| { |_, display, state| {
// Safety: the display is owned by this Generic source and is never // Safety: Display is owned by this Generic source and outlives the event loop.
// dropped while the event loop runs.
unsafe { unsafe {
display.get_mut().dispatch_clients(state).unwrap(); display.get_mut().dispatch_clients(state).unwrap();
} }
Ok(PostAction::Continue) 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::<LibinputSessionInterface<LibSeatSession>>(
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 loop_handle
.insert_source(udev_backend, move |event, _, _state| match event { .insert_source(
UdevEvent::Added { device_id, path } => { libinput_backend,
tracing::info!(?device_id, ?path, "GPU device added"); 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 } => { UdevEvent::Changed { device_id } => {
tracing::debug!(?device_id, "GPU device changed"); 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 } => { UdevEvent::Removed { device_id } => {
tracing::info!(?device_id, "GPU device removed"); 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 = for (node, path) in existing {
WeftCompositorState::new(display_handle, loop_signal, loop_handle, session.seat()); 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, |_| {})?; event_loop.run(None, &mut state, |_| {})?;
Ok(()) 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<DrmNode> {
// 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::<FormatSet>()
})
.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<DrmScanEvent> = 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::<WeftCompositorState>(&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<WeftMultiRenderer<'_>>] = &[];
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();
}

View file

@ -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<DrmDeviceFd>;
pub type WeftExporter = GbmFramebufferExporter<DrmDeviceFd>;
pub type WeftDrmOutput = DrmOutput<
WeftAllocator,
WeftExporter,
Option<OutputPresentationFeedback>,
DrmDeviceFd,
>;
pub type WeftDrmOutputManager = DrmOutputManager<
WeftAllocator,
WeftExporter,
Option<OutputPresentationFeedback>,
DrmDeviceFd,
>;
pub type WeftGpuManager = GpuManager<GbmGlesBackend<GlesRenderer, DrmDeviceFd>>;
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<crtc::Handle, WeftOutputSurface>,
pub render_node: Option<DrmNode>,
pub registration_token: RegistrationToken,
}
pub struct WeftDrmData {
pub session: LibSeatSession,
pub primary_gpu: DrmNode,
pub gpu_manager: WeftGpuManager,
pub devices: HashMap<DrmNode, WeftDrmDevice>,
pub keyboards: Vec<smithay::reexports::input::Device>,
pub display_handle: DisplayHandle,
}

View file

@ -1,2 +1,5 @@
pub mod drm; pub mod drm;
pub mod winit; pub mod winit;
#[cfg(target_os = "linux")]
pub mod drm_device;

View file

@ -1,3 +1,6 @@
#[cfg(target_os = "linux")]
use crate::backend::drm_device::WeftDrmData;
use smithay::{ use smithay::{
backend::{input::TabletToolDescriptor, renderer::utils::on_commit_buffer_handler}, backend::{input::TabletToolDescriptor, renderer::utils::on_commit_buffer_handler},
delegate_compositor, delegate_cursor_shape, delegate_dmabuf, delegate_input_method_manager, 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. // Set to false when the compositor should exit the event loop.
pub running: bool, pub running: bool,
#[cfg(target_os = "linux")]
pub drm: Option<WeftDrmData>,
} }
impl WeftCompositorState { impl WeftCompositorState {
@ -134,6 +140,8 @@ impl WeftCompositorState {
cursor_image_status: CursorImageStatus::Hidden, cursor_image_status: CursorImageStatus::Hidden,
dmabuf_global: None, dmabuf_global: None,
running: true, running: true,
#[cfg(target_os = "linux")]
drm: None,
} }
} }
} }
@ -291,12 +299,24 @@ impl DmabufHandler for WeftCompositorState {
fn dmabuf_imported( fn dmabuf_imported(
&mut self, &mut self,
_global: &DmabufGlobal, _global: &DmabufGlobal,
_dmabuf: smithay::backend::allocator::dmabuf::Dmabuf, dmabuf: smithay::backend::allocator::dmabuf::Dmabuf,
notifier: ImportNotifier, notifier: ImportNotifier,
) { ) {
// DMA-BUF import requires the renderer, which lives in the backend run function. #[cfg(target_os = "linux")]
// The backend is responsible for creating the global only when it can service imports. if let Some(drm) = self.drm.as_mut() {
// If we reach here without a backend handler wired up, reject. 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::<Self>();
return;
}
}
drop(notifier); drop(notifier);
} }
} }