From c50dd98685b757140676bc8c9d6d726d1d8e6fcb Mon Sep 17 00:00:00 2001 From: Samuel Onoja Date: Thu, 24 Sep 2026 16:15:11 +0100 Subject: [PATCH] feat: initialize direct session backend --- compositor/src/backends/direct.rs | 65 ++++++++++++++ compositor/src/backends/mod.rs | 143 ++++++++++++++++++++++++++++++ compositor/src/main.rs | 21 +++-- compositor/src/state.rs | 5 ++ 4 files changed, 226 insertions(+), 8 deletions(-) create mode 100644 compositor/src/backends/direct.rs create mode 100644 compositor/src/backends/mod.rs diff --git a/compositor/src/backends/direct.rs b/compositor/src/backends/direct.rs new file mode 100644 index 0000000..5b8e748 --- /dev/null +++ b/compositor/src/backends/direct.rs @@ -0,0 +1,65 @@ +use std::{error::Error, io}; + +use smithay::{ + backend::{ + libinput::{LibinputInputBackend, LibinputSessionInterface}, + session::{Event as SessionEvent, Session, libseat::LibSeatSession}, + udev::{UdevBackend, UdevEvent}, + }, + reexports::{calloop::EventLoop, input::Libinput}, +}; + +use crate::Ferese; + +pub fn init(event_loop: &mut EventLoop, state: &mut Ferese) -> Result<(), Box> { + let (session, notifier) = LibSeatSession::new()?; + let seat_name = session.seat(); + let udev_backend = UdevBackend::new(&seat_name)?; + let mut libinput_context = + Libinput::new_with_udev::>(session.clone().into()); + libinput_context + .udev_assign_seat(&seat_name) + .map_err(|()| io::Error::other(format!("failed to assign libinput seat {seat_name}")))?; + let libinput_backend = LibinputInputBackend::new(libinput_context.clone()); + + state.direct_session = Some(session); + state.session_active = true; + + event_loop + .handle() + .insert_source(libinput_backend, |event, _, state| { + state.process_input_event(event); + })?; + event_loop + .handle() + .insert_source(notifier, move |event, _, state| match event { + SessionEvent::PauseSession => { + state.session_active = false; + libinput_context.suspend(); + tracing::info!("direct session paused"); + } + SessionEvent::ActivateSession => { + if libinput_context.resume().is_err() { + tracing::error!("failed to resume libinput"); + } + state.session_active = true; + tracing::info!("direct session activated"); + } + })?; + event_loop + .handle() + .insert_source(udev_backend, |event, _, _state| match event { + UdevEvent::Added { device_id, path } => { + tracing::info!(?device_id, ?path, "DRM device added"); + } + UdevEvent::Changed { device_id } => { + tracing::debug!(?device_id, "DRM device changed"); + } + UdevEvent::Removed { device_id } => { + tracing::info!(?device_id, "DRM device removed"); + } + })?; + + tracing::info!(seat = %seat_name, "initialized direct session input and device discovery"); + Ok(()) +} diff --git a/compositor/src/backends/mod.rs b/compositor/src/backends/mod.rs new file mode 100644 index 0000000..6ab46d0 --- /dev/null +++ b/compositor/src/backends/mod.rs @@ -0,0 +1,143 @@ +mod direct; + +use std::ffi::OsString; + +use smithay::reexports::calloop::EventLoop; + +use crate::Ferese; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum BackendKind { + Nested, + Drm, +} + +#[derive(Debug, Eq, PartialEq)] +pub struct LaunchConfig { + pub backend: BackendKind, + pub client: Vec, +} + +impl LaunchConfig { + pub fn from_environment() -> Result { + let graphical_session = + std::env::var_os("WAYLAND_DISPLAY").is_some() || std::env::var_os("DISPLAY").is_some(); + + Self::parse(std::env::args_os().skip(1), graphical_session) + } + + fn parse( + args: impl IntoIterator, + graphical_session: bool, + ) -> Result { + let mut args = args.into_iter().peekable(); + let mut requested_backend = None; + let mut client = Vec::new(); + + while let Some(argument) = args.next() { + if argument == "--" { + client.extend(args); + break; + } + if argument == "--backend" { + let value = args + .next() + .ok_or_else(|| "--backend requires auto, nested, or drm".to_owned())?; + requested_backend = Some(parse_backend(&value)?); + continue; + } + if let Some(value) = argument + .to_str() + .and_then(|argument| argument.strip_prefix("--backend=")) + { + requested_backend = Some(parse_backend(&OsString::from(value))?); + continue; + } + + client.push(argument); + client.extend(args); + break; + } + + let backend = requested_backend.flatten().unwrap_or(if graphical_session { + BackendKind::Nested + } else { + BackendKind::Drm + }); + + Ok(Self { backend, client }) + } +} + +pub fn init( + backend: BackendKind, + event_loop: &mut EventLoop, + state: &mut Ferese, +) -> Result<(), Box> { + match backend { + BackendKind::Nested => crate::winit::init(event_loop, state), + BackendKind::Drm => direct::init(event_loop, state), + } +} + +fn parse_backend(value: &OsString) -> Result, String> { + match value.to_str() { + Some("auto") => Ok(None), + Some("nested") => Ok(Some(BackendKind::Nested)), + Some("drm") => Ok(Some(BackendKind::Drm)), + _ => Err(format!( + "unknown backend {:?}; expected auto, nested, or drm", + value + )), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn auto_selects_nested_inside_a_graphical_session() { + let config = LaunchConfig::parse([OsString::from("foot")], true).unwrap(); + + assert_eq!(config.backend, BackendKind::Nested); + assert_eq!(config.client, [OsString::from("foot")]); + } + + #[test] + fn auto_selects_drm_without_a_graphical_session() { + let config = LaunchConfig::parse(Vec::::new(), false).unwrap(); + + assert_eq!(config.backend, BackendKind::Drm); + assert!(config.client.is_empty()); + } + + #[test] + fn explicit_backend_and_separator_are_consumed() { + let config = LaunchConfig::parse( + [ + OsString::from("--backend=drm"), + OsString::from("--"), + OsString::from("foot"), + OsString::from("--title=Ferese"), + ], + true, + ) + .unwrap(); + + assert_eq!(config.backend, BackendKind::Drm); + assert_eq!( + config.client, + [OsString::from("foot"), OsString::from("--title=Ferese")] + ); + } + + #[test] + fn rejects_an_unknown_backend() { + let error = + LaunchConfig::parse([OsString::from("--backend"), OsString::from("other")], true) + .unwrap_err(); + + assert!(error.contains("unknown backend")); + } +} diff --git a/compositor/src/main.rs b/compositor/src/main.rs index 5ce2d36..9ff456f 100644 --- a/compositor/src/main.rs +++ b/compositor/src/main.rs @@ -1,31 +1,36 @@ #![allow(irrefutable_let_patterns)] +mod backends; mod grabs; mod handlers; mod input; mod state; mod winit; -use std::{error::Error, process::Command}; +use std::{error::Error, io, process::Command}; use smithay::reexports::{calloop::EventLoop, wayland_server::Display}; pub use state::Ferese; use tracing::{info, warn}; +use crate::backends::LaunchConfig; + fn main() -> Result<(), Box> { init_logging(); + let launch = LaunchConfig::from_environment() + .map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?; let mut event_loop = EventLoop::try_new()?; let display = Display::new()?; let mut state = Ferese::new(&mut event_loop, display)?; - winit::init(&mut event_loop, &mut state)?; + backends::init(launch.backend, &mut event_loop, &mut state)?; // SAFETY: the backend has already read the host display and Ferese is // single-threaded before the event loop and optional child start. unsafe { std::env::set_var("WAYLAND_DISPLAY", &state.socket_name) }; - info!(socket = ?state.socket_name, "Ferese is accepting Wayland clients"); - spawn_client_from_args(); + info!(socket = ?state.socket_name, backend = ?launch.backend, "Ferese is accepting Wayland clients"); + spawn_client(launch.client); event_loop.run(None, &mut state, |_| {})?; Ok(()) } @@ -36,12 +41,12 @@ fn init_logging() { tracing_subscriber::fmt().with_env_filter(filter).init(); } -fn spawn_client_from_args() { - let mut args = std::env::args_os().skip(1); - let Some(program) = args.next() else { +fn spawn_client(mut args: Vec) { + if args.is_empty() { info!("no client requested; pass one after `--`, for example `-- foot`"); return; - }; + } + let program = args.remove(0); match Command::new(&program).args(args).spawn() { Ok(child) => info!(program = ?program, pid = child.id(), "spawned nested client"), diff --git a/compositor/src/state.rs b/compositor/src/state.rs index 3d69242..07176ca 100644 --- a/compositor/src/state.rs +++ b/compositor/src/state.rs @@ -11,6 +11,7 @@ use ferese_core::{WindowPlacement, WorkspaceSet}; use ferese_layout::{Axis, Direction, GapConfig, LayoutResult, Rect, SizeConstraints, WindowId}; use smithay::{ + backend::session::libseat::LibSeatSession, desktop::{PopupManager, Space, Window, WindowSurfaceType}, input::{Seat, SeatState}, reexports::{ @@ -51,6 +52,8 @@ pub struct Ferese { pub window_geometry: HashMap, pub focused_window: Option, pub intercepted_keys: HashSet, + pub direct_session: Option, + pub session_active: bool, next_window_id: u64, last_animation_tick: Instant, pub popups: PopupManager, @@ -110,6 +113,8 @@ impl Ferese { window_geometry: HashMap::new(), focused_window: None, intercepted_keys: HashSet::new(), + direct_session: None, + session_active: true, next_window_id: 1, last_animation_tick: start_time, popups: PopupManager::default(),