use super::*; use super::drm::{bring_up_head, pick_crtc, probe_connected}; use super::session::{register_drm_fd, register_libinput, register_session_notifier, register_udev_monitor}; pub struct UdevPlatform { event_loop: EventLoop<'static, CompState>, display: Display, state: CompState, listener: ListeningSocket, clients: Vec, pending: Rc>>, ipc: Option, } impl UdevPlatform { pub fn connect(wm: Rc>, bound_keys: &[String], repeat_keys: &[String]) -> PlatformResult { let event_loop: EventLoop<'static, CompState> = EventLoop::try_new().map_err(err)?; let (session, notifier) = LibSeatSession::new().map_err(err)?; let seat_name = session.seat(); let gpu_path = udev::primary_gpu(&seat_name) .ok() .flatten() .unwrap_or_else(|| std::path::PathBuf::from("/dev/dri/card0")); log::info!("udev: using {} as primary GPU", gpu_path.display()); let mut session_for_open = session.clone(); let fd = session_for_open .open(&gpu_path, rustix::fs::OFlags::RDWR | rustix::fs::OFlags::CLOEXEC) .map_err(err)?; let card = Rc::new(Card(fd)); // Every connected connector becomes a head, laid out left-to-right. let connected = probe_connected(&card)?; log::info!("udev: {} connected output(s)", connected.len()); let renderer = PixmanRenderer::new().map_err(err)?; let dh = Display::::new().map_err(err)?; let display_handle = dh.handle(); // `zwp_linux_dmabuf_v1` - see `protocols.rs`'s `DmabufHandler` for // why `PixmanRenderer`, a pure software renderer, can still import // these (mmap, not GPU). `create_global` (v3) rather than the v4 // `..._with_default_feedback` variant: the latter needs a // `main_device` `dev_t` to steer multi-GPU clients toward the // right render node, which is a real gap worth closing later but // not required for a single-GPU client to allocate and hand over a // Linear-modifier buffer, which is all this backend can use anyway. let mut dmabuf_state = DmabufState::new(); dmabuf_state.create_global::(&display_handle, renderer.dmabuf_formats()); let mut heads: Vec = Vec::new(); let mut output_entries: Vec = Vec::new(); let mut used_crtcs: Vec = Vec::new(); let mut x_offset = 0; for probe in &connected { let Some(crtc) = pick_crtc(&card, probe, &used_crtcs) else { log::warn!("udev: no free CRTC left for connector {}; not driving it", probe.name); continue; }; let (head, entry) = bring_up_head(&card, &display_handle, probe, crtc, x_offset)?; log::info!("udev: head {}: {} {}x{} at x={x_offset}", heads.len(), probe.name, head.size.0, head.size.1); used_crtcs.push(crtc); x_offset += head.size.0; heads.push(head); output_entries.push(entry); } let Some(first) = heads.first() else { return Err(PlatformError::Other("udev: no usable outputs".into())); }; // Pointer starts centred on the first head. let (width, height) = first.size; // xdg-output - see the matching comment in `lib.rs`'s // `WaylandPlatform::connect` for why this isn't optional. smithay::wayland::output::OutputManagerState::new_with_xdg_output::(&display_handle); let compositor_state = CompositorState::new::(&display_handle); let xdg_shell_state = XdgShellState::new::(&display_handle); let xdg_decoration_state = XdgDecorationState::new::(&display_handle); let shm_state = ShmState::new::(&display_handle, Vec::new()); // Selection (clipboard) protocols - see the matching block in // `lib.rs`'s `WaylandPlatform::connect` for the ordering constraint. let primary_selection_state = PrimarySelectionState::new::(&display_handle); let data_control_state = DataControlState::new::(&display_handle, Some(&primary_selection_state), |_| true); let mut seat_state = SeatState::new(); let mut seat = seat_state.new_wl_seat(&display_handle, "seat0"); let system_xkb = crate::xkb_config::read(); let xkb_config = smithay::input::keyboard::XkbConfig { rules: "", model: system_xkb.model.as_deref().unwrap_or(""), layout: system_xkb.layout.as_deref().unwrap_or(""), variant: system_xkb.variant.as_deref().unwrap_or(""), options: system_xkb.options.clone(), }; // 600ms delay, not 200 - see `state/mod.rs`'s `REPEAT_DELAY` doc // comment for why. seat.add_keyboard(xkb_config, 600, 25).map_err(err)?; seat.add_pointer(); // Each output occupies its own slice of the global space, so a // window's coordinates say which monitor it is on. let mut space = Space::default(); for entry in &output_entries { space.map_output(&entry.output, (entry.location.x, entry.location.y)); } let pending = Rc::new(RefCell::new(Vec::new())); let udev_state = UdevState { card: card.clone(), renderer, heads, active: true, pointer_pos: (width as f64 / 2.0, height as f64 / 2.0).into(), }; let state = CompState { compositor_state, xdg_shell_state, _xdg_decoration_state: xdg_decoration_state, shm_state, dmabuf_state, xdg_activation_state: XdgActivationState::new::(&display_handle), _text_input_manager_state: smithay::wayland::text_input::TextInputManagerState::new::(&display_handle), _input_method_manager_state: smithay::wayland::input_method::InputMethodManagerState::new::(&display_handle, |_client| true), _gtk_shell_state: crate::gtk_shell::GtkShellState::new::(&display_handle), seat_state, seat, space, popups: PopupManager::default(), outputs: output_entries, layer_shell_state: smithay::wayland::shell::wlr_layer::WlrLayerShellState::new::(&display_handle), dh: display_handle.clone(), data_device_state: DataDeviceState::new::(&display_handle), primary_selection_state, data_control_state, session_lock_state: smithay::wayland::session_lock::SessionLockManagerState::new::( &display_handle, |_| true, ), _screencopy_state: crate::screencopy::ScreencopyState::new::(&display_handle), screencopy_pending: Vec::new(), _foreign_toplevel_state: crate::foreign_toplevel::ForeignToplevelState::new::(&display_handle), foreign_toplevel_managers: Vec::new(), foreign_toplevel_handles: HashMap::new(), _workspace_state: crate::workspace::WorkspaceManagerState::new::(&display_handle), _output_power_state: Some(crate::output_power::OutputPowerManagerState::new::(&display_handle)), _gamma_control_state: Some(crate::gamma_control::GammaControlManagerState::new::(&display_handle)), _output_management_state: crate::output_management::OutputManagementState::new::(&display_handle), output_managers: Vec::new(), output_heads: HashMap::new(), output_modes: HashMap::new(), output_serial: 0, last_broadcast_outputs: Vec::new(), workspace_managers: Vec::new(), workspace_groups: Vec::new(), workspace_handles: HashMap::new(), _viewporter_state: smithay::wayland::viewporter::ViewporterState::new::(&display_handle), _fractional_scale_state: smithay::wayland::fractional_scale::FractionalScaleManagerState::new::(&display_handle), _cursor_shape_state: smithay::wayland::cursor_shape::CursorShapeManagerState::new::(&display_handle), idle_notifier_state: smithay::wayland::idle_notify::IdleNotifierState::new(&display_handle, event_loop.handle()), _idle_inhibit_manager_state: smithay::wayland::idle_inhibit::IdleInhibitManagerState::new::(&display_handle), idle_inhibiting_surfaces: Vec::new(), last_idle_notify: None, window_anims: HashMap::new(), last_broadcast_flags: HashMap::new(), last_broadcast_workspace: None, lock: Default::default(), cursor_status: smithay::input::pointer::CursorImageStatus::default_named(), cursor_buffers: crate::cursor::make_buffers(), last_titlebar_click: None, context_menu: None, context_menu_buffer: None, wm: wm.clone(), surface_to_id: HashMap::new(), id_to_window: HashMap::new(), dead_layer_surfaces: HashSet::new(), decorations: HashMap::new(), border_top_decorations: HashMap::new(), shadow_buffers: HashMap::new(), rounded_corners_program: None, content_epoch: HashMap::new(), rounded_content_buffers: HashMap::new(), border_side_buffers: HashMap::new(), last_synced_size: HashMap::new(), pending: pending.clone(), bound_keys: Rc::new(bound_keys.iter().cloned().collect::>()), repeat_keys: Rc::new(repeat_keys.iter().cloned().collect::>()), repeat: None, start_time: Instant::now(), udev: Some(udev_state), xwayland_shell_state: smithay::wayland::xwayland_shell::XWaylandShellState::new::(&display_handle), xwm: None, xwayland_windows: HashMap::new(), xwayland_pending: Vec::new(), ewmh: None, }; let listener = ListeningSocket::bind_auto("wayland", 0..32).map_err(err)?; if let Some(name) = listener.socket_name() { std::env::set_var("WAYLAND_DISPLAY", name); log::info!("wayland socket: {}", name.to_string_lossy()); } // Otherwise this is whatever the session inherited - typically // stale from a *previous* login's compositor (a shell's exported // `XDG_CURRENT_DESKTOP=Hyprland` surviving into this one), since // nothing else ever sets it. `xdg-desktop-portal` and any client // that sniffs this value to pick a desktop-specific integration // (screenshot/file-picker backends, etc.) get actively misrouted by // the stale value rather than just seeing "unknown". Only affects // processes spawned from here on (autostart, `srd.spawn`) - an // env var set mid-process doesn't retroactively reach anything // already running. std::env::set_var("XDG_CURRENT_DESKTOP", "srdwm"); let ipc = match listener.socket_name().map(|n| n.to_string_lossy().into_owned()) { Some(name) => match srdwm_platform::IpcServer::bind(&name) { Ok(ipc) => Some(ipc), Err(e) => { log::warn!("control socket unavailable ({e}); srd and scripts that use it won't work"); None } }, None => None, }; let handle = event_loop.handle(); register_drm_fd(&handle, &card)?; register_libinput(&handle, &session, &seat_name)?; register_session_notifier(&handle, notifier)?; if let Err(e) = register_udev_monitor(&handle, &seat_name) { log::warn!("udev: connector hotplug unavailable ({e}); monitors are fixed at startup"); } if let Err(e) = crate::xwayland::spawn(&handle, &display_handle) { log::warn!("XWayland unavailable ({e}); X11-only clients will not run"); } Ok(Self { event_loop, display: dh, state, listener, clients: Vec::new(), pending, ipc }) } fn accept_clients(&mut self) -> PlatformResult<()> { if let Some(stream) = self.listener.accept().map_err(err)? { let client = self.display.handle().insert_client(stream, std::sync::Arc::new(ClientState::default())).map_err(err)?; self.clients.push(client); } Ok(()) } } impl Platform for UdevPlatform { fn kind(&self) -> PlatformKind { PlatformKind::Wayland } fn poll_events(&mut self) -> PlatformResult> { self.accept_clients()?; self.event_loop.dispatch(Some(Duration::from_millis(16)), &mut self.state).map_err(err)?; // Held bindings that repeat - see `CompState::tick_repeat`. self.state.tick_repeat(); self.display.dispatch_clients(&mut self.state).map_err(err)?; self.display.flush_clients().map_err(err)?; if let Some(ipc) = self.ipc.as_mut() { if ipc.poll(&self.state.wm) { self.pending.borrow_mut().push(CoreEvent::WorkspaceChanged); } } self.state.render_udev_frame(); Ok(self.pending.borrow_mut().drain(..).collect()) } /// One `srdwm_core::Monitor` per head, positioned in the global space. /// This is what makes core's layout engine multi-monitor-aware in /// practice: `arrange_workspace` groups windows by `monitor` and lays /// each group out inside that monitor's rectangle. fn monitors(&mut self) -> PlatformResult> { let Some(udev) = self.state.udev.as_ref() else { return Ok(Vec::new()) }; Ok(udev .heads .iter() .enumerate() .map(|(i, head)| { // Shrunk by whatever a layer-shell surface (bar, dock) has // reserved via `set_exclusive_zone` - reporting the full // head size here otherwise means core's placement/tiling // treats that strip as ordinary free space, so a new // window's titlebar lands right where the bar renders on // top of it, unreachable to drag. `non_exclusive_zone()` is // output-local, so it's translated into this head's // position in the shared global space the same way // `head.location` already is. let zone = layer_map_for_output(&head.output).non_exclusive_zone(); let rect = srdwm_core::Rect::new( head.location.x + zone.loc.x, head.location.y + zone.loc.y, zone.size.w as u32, zone.size.h as u32, ); let mut m = srdwm_core::Monitor::new(i as u32, head.output.name(), rect); // `Monitor::new` defaults `full_geometry` to whatever // `geometry` was constructed with - correct for a monitor // with no layer-shell client at all, wrong the moment one // exists, since `rect` above is already zone-shrunk. Without // this, `full_geometry` was silently identical to `geometry` // for every real monitor this backend ever reported, which // made `toggle_fullscreen`'s whole "ignore the reserved // zone" design a no-op in practice: fullscreen still // stopped at the bar/dock exactly like maximize does. // Reported live as "fullscreen isn't actually going // fullscreen" - confirmed by triggering it and reading // the resulting geometry back over IPC, not just from // reading this code. m.full_geometry = srdwm_core::Rect::new(head.location.x, head.location.y, head.size.0 as u32, head.size.1 as u32); m.primary = i == 0; m }) .collect()) } fn apply_geometry(&mut self, window: srdwm_core::WindowId, _geometry: srdwm_core::Rect) -> PlatformResult<()> { self.state.sync_geometry(window); Ok(()) } fn set_title(&mut self, _window: srdwm_core::WindowId, _title: &str) -> PlatformResult<()> { Ok(()) } /// Was `wm.focus_window(window)` alone - core-only, so a caller that /// only has `Platform` to go through (`crates/platform`'s `IpcServer`, /// which can't reach `CompState`/real Wayland focus at all) could make /// a window *look* focused (rendering already reads live core state /// for the highlighted-border/titlebar-text colour) without it ever /// actually receiving a keystroke - confirmed live: `srd dispatch /// focus ` changed core's own focused-window /// bookkeeping but left `_NET_ACTIVE_WINDOW` at `0x0` and real /// keyboard input going nowhere. `crate::input::focus_window` is the /// same full path a real mouse click already goes through. fn focus(&mut self, window: srdwm_core::WindowId) -> PlatformResult<()> { crate::input::focus_window(&mut self.state, window); Ok(()) } fn minimize(&mut self, window: srdwm_core::WindowId) -> PlatformResult<()> { if let Some(w) = self.state.id_to_window.get(&window) { self.state.space.unmap_elem(w); } Ok(()) } fn restore(&mut self, window: srdwm_core::WindowId) -> PlatformResult<()> { self.state.sync_geometry(window); Ok(()) } fn close(&mut self, window: srdwm_core::WindowId) -> PlatformResult<()> { let Some(w) = self.state.id_to_window.get(&window) else { return Ok(()) }; if let Some(toplevel) = w.toplevel() { toplevel.send_close(); } else if let Some(x11) = w.x11_surface() { // `w.toplevel()` is `None` for an XWayland window - without // this arm, closing one (the WM's own close binding, or `srd // dispatch close`) silently did nothing at all. `close()` itself // handles both cases: a polite WM_DELETE_WINDOW for a // cooperating client, outright `destroy_window` for one that // doesn't support it. let _ = x11.close(); } Ok(()) } fn set_decorated(&mut self, _window: srdwm_core::WindowId, _decorated: bool) -> PlatformResult<()> { Ok(()) } fn set_border_color(&mut self, _window: srdwm_core::WindowId, _rgb: (u8, u8, u8)) -> PlatformResult<()> { Ok(()) } fn set_border_width(&mut self, _window: srdwm_core::WindowId, _width: u32) -> PlatformResult<()> { Ok(()) } fn redraw_decoration(&mut self, window: srdwm_core::WindowId, _win: &srdwm_core::Window, _focused: bool) -> PlatformResult<()> { self.state.redraw_decoration_buffer(window); self.state.sync_geometry(window); Ok(()) } fn grab_keyboard(&mut self) -> PlatformResult<()> { Ok(()) } fn ungrab_keyboard(&mut self) -> PlatformResult<()> { Ok(()) } }