srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src/udev/mod.rs
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2024-07-16 21:25:00 +0200
committersrdusr <[email protected]>2024-07-16 21:25:00 +0200
commit6fad44b3ecffb22f77a78661ce179b304e9a6ccc (patch)
tree4bba4624f0be3a2aa34965740ae5c9ebcc12b759 /crates/wayland/src/udev/mod.rs
parent2994f622b2998dc6aebbd38f52a6e6612ed8e7c8 (diff)
downloadsrdwm-6fad44b3ecffb22f77a78661ce179b304e9a6ccc.tar.gz
srdwm-6fad44b3ecffb22f77a78661ce179b304e9a6ccc.zip
Split crates/wayland/src/udev.rs (1643 lines) into udev/
Pure reorganization, no behavior change - verified by diffing the function-name and struct-name sets before/after (both identical) plus a full cargo test pass. Struct definitions (Card, DrmBuffer, UdevHead, UdevState) and their small impls stay in mod.rs, since default (crate- scoped) privacy there is visible to every descendant submodule without further changes. The rest splits by concern: - render.rs: the per-frame impl CompState block (render_udev_frame and the gamma/output-power methods) - still one ~520-line function, left intact rather than decomposed, given how much of its structure (the self.udev.as_mut() disjoint-borrow pattern threaded through it) is deliberate and already documented inline. - outputs.rs: hotplug reprobe/relayout (impl CompState). - platform.rs: UdevPlatform's struct/connect logic and its `impl Platform for UdevPlatform`, previously split apart in the flat file by ~250 lines of unrelated DRM/session code sitting between them. - drm.rs: mode/CRTC/framebuffer probing and setup. - session.rs: libseat/libinput/udev-monitor calloop registration and the libinput event handler. A few free functions and one struct (ConnectorProbe, bring_up_head, probe_connected, pick_crtc, the register_* functions) went from module-private to pub(crate): called across what are now sibling submodules, which - unlike a defining module's own descendants -- Rust's privacy model doesn't let see each other's private items. Matches this crate's existing pub(crate) convention rather than introducing pub(super), which crates/core's manager/ split used instead to match *that* crate's plain-private convention.
Diffstat (limited to 'crates/wayland/src/udev/mod.rs')
-rw-r--r--crates/wayland/src/udev/mod.rs235
1 files changed, 235 insertions, 0 deletions
diff --git a/crates/wayland/src/udev/mod.rs b/crates/wayland/src/udev/mod.rs
new file mode 100644
index 0000000..2d9f9ff
--- /dev/null
+++ b/crates/wayland/src/udev/mod.rs
@@ -0,0 +1,235 @@
+//! DRM/udev backend: runs srdwm as the real compositor on a bare TTY (no
+//! host session to nest under), unlike the `backend_winit`-based path in
+//! `lib.rs`.
+//!
+//! Scope:
+//! - Single primary GPU, but **every** connected connector on it: each
+//! becomes a [`UdevHead`] with its own scanout buffers, damage tracker
+//! and page-flip state, laid out left-to-right in the global coordinate
+//! space. Connectors are re-probed on hotplug (see `reprobe_outputs`);
+//! a second GPU is not supported.
+//! - Rendering is **software**, via smithay's `PixmanRenderer` compositing
+//! into plain KMS "dumb buffers" through the legacy (non-atomic) mode-set
+//! API (`set_crtc`/`page_flip`). This deliberately avoids the
+//! GBM/EGL/`DrmCompositor` pipeline real hardware-accelerated compositors
+//! (and smithay's own `anvil` example) use: that path needs a GPU with
+//! working KMS+3D driver support, which is not guaranteed in a low-spec
+//! machine's VM (QEMU's plainest virtual display devices only support
+//! dumb-buffer scanout). Dumb buffers work on essentially any DRM driver.
+//! - Session/seat handling is real, via `libseat` (VT-switch-safe device
+//! access, no root required if the seatd/logind + libseat setup is
+//! present) - not a raw `/dev/dri/cardN` open.
+//! - Input is real, via `libinput`, sharing the exact same precise
+//! keybinding matching and pointer/titlebar hit-testing code paths
+//! `handle_keyboard_key_event`/`handle_pointer_position`/
+//! `handle_pointer_button` in `lib.rs` use for the nested winit backend.
+//! - Session pause/resume (VT switch away/back) stops/resumes rendering,
+//! but does not yet re-probe connectors on resume.
+
+use std::cell::RefCell;
+use std::collections::{HashMap, HashSet};
+use std::os::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd};
+use std::rc::Rc;
+use std::time::{Duration, Instant};
+
+use smithay::backend::input::{
+ Axis, ButtonState as BackendButtonState, Event as InputEventTrait, InputEvent, PointerAxisEvent,
+ PointerButtonEvent, PointerMotionEvent,
+};
+use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface};
+use smithay::backend::renderer::damage::OutputDamageTracker;
+use smithay::backend::renderer::element::memory::MemoryRenderBufferRenderElement;
+use smithay::backend::renderer::element::Kind;
+use smithay::backend::renderer::pixman::PixmanRenderer;
+use smithay::backend::renderer::{Bind, ImportDma};
+use smithay::backend::session::{libseat::LibSeatSession, libseat::LibSeatSessionNotifier, Event as SessionEvent, Session};
+use smithay::backend::udev::{self, UdevBackend, UdevEvent};
+use smithay::desktop::{layer_map_for_output, PopupManager, Space};
+use smithay::backend::input::AxisSource;
+use smithay::wayland::shell::wlr_layer::Layer;
+use smithay::input::pointer::AxisFrame;
+use smithay::input::SeatState;
+use smithay::output::{Mode as OutputMode, Output, PhysicalProperties, Subpixel};
+use smithay::reexports::calloop::generic::{FdWrapper, Generic};
+use smithay::reexports::calloop::{EventLoop, Interest, LoopHandle, Mode as CalloopMode, PostAction};
+use smithay::reexports::drm::buffer::{Buffer as DrmBufferTrait, DrmFourcc};
+use smithay::reexports::drm::control::{
+ connector, crtc, dumbbuffer::DumbBuffer, framebuffer, Device as ControlDevice, Event as DrmEvent, Mode as DrmMode,
+ ModeTypeFlags, PageFlipFlags,
+};
+use smithay::reexports::drm::Device as BasicDevice;
+use smithay::reexports::input::Libinput;
+use smithay::reexports::pixman::{FormatCode, Image};
+use smithay::reexports::rustix;
+use smithay::reexports::wayland_server::backend::GlobalId;
+use smithay::reexports::wayland_server::{Client, Display, DisplayHandle, ListeningSocket};
+use smithay::utils::{Logical, Physical, Point, Rectangle, Scale, Size, Transform};
+use smithay::wayland::compositor::CompositorState;
+use smithay::wayland::dmabuf::DmabufState;
+use smithay::wayland::selection::data_device::DataDeviceState;
+use smithay::wayland::selection::primary_selection::PrimarySelectionState;
+use smithay::wayland::selection::wlr_data_control::DataControlState;
+use smithay::wayland::shell::xdg::decoration::XdgDecorationState;
+use smithay::wayland::shell::xdg::XdgShellState;
+use smithay::wayland::shm::ShmState;
+use smithay::wayland::xdg_activation::XdgActivationState;
+
+use srdwm_core::{Event as CoreEvent, WindowManager};
+use srdwm_platform::{Platform, PlatformError, PlatformKind, Result as PlatformResult};
+
+use crate::decoration;
+use crate::err;
+use crate::input::{handle_keyboard_key_event, handle_pointer_button, handle_pointer_position};
+use crate::state::{ClientState, CompState};
+
+/// A DRM device node, opened through the session (not a raw `File::open`)
+/// so access is properly gated by logind/seatd and revoked on VT switch.
+pub(crate) struct Card(OwnedFd);
+
+impl AsFd for Card {
+ fn as_fd(&self) -> BorrowedFd<'_> {
+ self.0.as_fd()
+ }
+}
+impl BasicDevice for Card {}
+impl ControlDevice for Card {}
+
+pub(crate) struct DrmBuffer {
+ dumb: DumbBuffer,
+ fb: framebuffer::Handle,
+ image: Image<'static, 'static>,
+}
+
+/// One connector+CRTC pair srdwm scans out to - i.e. one physical monitor.
+///
+/// Each head owns its own scanout buffers, damage tracker and flip state,
+/// because monitors have independent resolutions and refresh cycles: a flip
+/// completing on one says nothing about the others. The *renderer* is not
+/// here but on [`UdevState`], since all heads on one GPU share it.
+pub(crate) struct UdevHead {
+ pub(crate) crtc: crtc::Handle,
+ /// Which connector this head drives - the key hotplug diffs against.
+ pub(crate) connector: connector::Handle,
+ pub(crate) output: Output,
+ /// The `wl_output` global, kept so it can be destroyed when the monitor
+ /// is unplugged; leaving it advertised would show clients a screen that
+ /// no longer exists.
+ pub(crate) global: GlobalId,
+ pub(crate) damage_tracker: OutputDamageTracker,
+ pub(crate) buffers: [DrmBuffer; 2],
+ pub(crate) front: usize,
+ /// A flip is in flight; the next frame for this head waits for the DRM
+ /// page-flip event (matched by `crtc`) before starting.
+ pub(crate) flip_pending: bool,
+ /// Per-buffer-slot age passed to `damage_tracker.render_output`: how
+ /// many *damage-producing* renders ago that exact buffer was last
+ /// brought fully up to date. 0 means "never rendered, contents
+ /// undefined" and forces a full redraw. This used to be hardcoded to 0
+ /// on every single call regardless - which, per
+ /// `OutputDamageTracker::damage_output_internal`, forces the entire
+ /// output geometry to be treated as damaged every time, so every frame
+ /// was a full-screen software (pixman) recomposite plus a page-flip,
+ /// nonstop, at whatever rate the event loop's 16ms dispatch timeout
+ /// allowed - continuously, even with a fully idle desktop. That
+ /// competes for the same single thread's CPU time as libinput event
+ /// processing and is exactly what `client bug: event processing
+ /// lagging behind` (logged for both the keyboard and the mouse) was
+ /// reporting. With correct ages, a call that finds no real damage
+ /// returns near-free (`damage_output_internal`'s own element/geometry
+ /// comparison, no pixel work) and skips the flip entirely instead of
+ /// always finding "damage".
+ pub(crate) ages: [usize; 2],
+ /// Origin of this head in the global coordinate space.
+ pub(crate) location: Point<i32, Logical>,
+ pub(crate) size: (i32, i32),
+}
+
+/// Everything the DRM/udev backend needs that the nested winit backend
+/// doesn't. Lives as a field on `CompState` (rather than a separate struct)
+/// because calloop callbacks registered against the event loop only ever
+/// get `&mut CompState` - see the module docs in `lib.rs` for why the
+/// protocol-handler state itself has to be backend-agnostic.
+pub(crate) struct UdevState {
+ pub(crate) card: Rc<Card>,
+ /// Shared by every head: one GPU, one software renderer.
+ pub(crate) renderer: PixmanRenderer,
+ pub(crate) heads: Vec<UdevHead>,
+ pub(crate) active: bool,
+ /// Pointer position in the *global* space, so it can cross between
+ /// monitors; clamped to the union of all head rectangles.
+ pub(crate) pointer_pos: Point<f64, Logical>,
+}
+
+impl UdevState {
+ /// Bounding box of every head, used to clamp pointer motion.
+ fn bounds(&self) -> (f64, f64) {
+ let w = self.heads.iter().map(|h| h.location.x + h.size.0).max().unwrap_or(0);
+ let h = self.heads.iter().map(|h| h.location.y + h.size.1).max().unwrap_or(0);
+ (w as f64, h as f64)
+ }
+}
+
+
+impl UdevHead {
+ /// Frees the DRM resources this head owns. Dropping the Rust structs
+ /// alone would leak the kernel-side framebuffers and dumb buffers,
+ /// which matters when a monitor is plugged and unplugged repeatedly.
+ fn release(self, card: &Card) {
+ for buffer in self.buffers {
+ if let Err(e) = card.destroy_framebuffer(buffer.fb) {
+ log::warn!("udev: destroy_framebuffer failed: {e}");
+ }
+ if let Err(e) = card.destroy_dumb_buffer(buffer.dumb) {
+ log::warn!("udev: destroy_dumb_buffer failed: {e}");
+ }
+ }
+ }
+
+ /// Copies the just-rendered pixman image into buffer `back`'s dumb
+ /// buffer (software rendering writes into its own owned image, not the
+ /// scanout memory directly, to avoid tying that image's lifetime to an
+ /// mmap - see this module's docs) and flips to it.
+ fn copy_and_flip(&mut self, card: &Card, back: usize) -> std::io::Result<()> {
+ let (src_stride, height) = (self.buffers[back].image.stride(), self.buffers[back].image.height());
+ let byte_len = src_stride * height;
+ // SAFETY: `image` owns this memory and outlives the byte slice we
+ // construct from it here; we only read, and only for the duration
+ // of this call.
+ let src: &[u8] = unsafe { std::slice::from_raw_parts(self.buffers[back].image.data() as *const u8, byte_len) };
+ // The dumb buffer's pitch is whatever the kernel driver actually
+ // allocated, which the DRM API does not guarantee equals pixman's
+ // own `src_stride` (drivers are free to pad each row for
+ // alignment). This used to be a single flat `copy_from_slice` sized
+ // off `src_stride` alone; on any driver that pads, that copies each
+ // source row into the wrong offset in the destination, shearing the
+ // image diagonally by one row per `dst_stride - src_stride` bytes of
+ // padding. Copying row by row, each clamped to the narrower of the
+ // two strides, is correct regardless of whether the strides happen
+ // to match.
+ let dst_stride = self.buffers[back].dumb.pitch() as usize;
+ {
+ let mut mapping = card.map_dumb_buffer(&mut self.buffers[back].dumb)?;
+ let dst = mapping.as_mut();
+ let row_len = src_stride.min(dst_stride);
+ for row in 0..height {
+ let s = row * src_stride;
+ let d = row * dst_stride;
+ if s + row_len > src.len() || d + row_len > dst.len() {
+ break;
+ }
+ dst[d..d + row_len].copy_from_slice(&src[s..s + row_len]);
+ }
+ }
+ card.page_flip(self.crtc, self.buffers[back].fb, PageFlipFlags::EVENT, None)?;
+ self.flip_pending = true;
+ Ok(())
+ }
+}
+
+mod drm;
+mod outputs;
+mod platform;
+mod render;
+mod session;
+
+pub use platform::UdevPlatform;