srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src/udev.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/wayland/src/udev.rs')
-rw-r--r--crates/wayland/src/udev.rs508
1 files changed, 364 insertions, 144 deletions
diff --git a/crates/wayland/src/udev.rs b/crates/wayland/src/udev.rs
index dcd5f76..2b491e7 100644
--- a/crates/wayland/src/udev.rs
+++ b/crates/wayland/src/udev.rs
@@ -2,10 +2,12 @@
//! host session to nest under), unlike the `backend_winit`-based path in
//! `lib.rs`.
//!
-//! Scope, kept deliberately narrow for a first real (not faked) pass:
-//! - Single primary GPU, first connected connector, its preferred (first
-//! listed) mode, positioned at `(0, 0)` - no hotplug of connectors or
-//! GPUs after startup, no multi-monitor layout.
+//! 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 probed once at startup - no hotplug, and no
+//! second GPU.
//! - 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
@@ -36,7 +38,7 @@ use smithay::backend::input::{
};
use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface};
use smithay::backend::renderer::damage::OutputDamageTracker;
-use smithay::backend::renderer::element::memory::MemoryRenderBufferRenderElement;
+use smithay::backend::renderer::element::memory::{MemoryRenderBuffer, MemoryRenderBufferRenderElement};
use smithay::backend::renderer::element::Kind;
use smithay::backend::renderer::pixman::PixmanRenderer;
use smithay::backend::renderer::Bind;
@@ -51,7 +53,8 @@ use smithay::reexports::calloop::generic::{FdWrapper, Generic};
use smithay::reexports::calloop::{EventLoop, Interest, LoopHandle, Mode as CalloopMode, PostAction};
use smithay::reexports::drm::buffer::DrmFourcc;
use smithay::reexports::drm::control::{
- connector, crtc, dumbbuffer::DumbBuffer, framebuffer, Device as ControlDevice, Event as DrmEvent, Mode as DrmMode, PageFlipFlags,
+ 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;
@@ -60,6 +63,9 @@ use smithay::reexports::rustix;
use smithay::reexports::wayland_server::{Client, Display, ListeningSocket};
use smithay::utils::{Logical, Point, Transform};
use smithay::wayland::compositor::CompositorState;
+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;
@@ -67,13 +73,13 @@ use smithay::wayland::shm::ShmState;
use srdwm_core::{Event as CoreEvent, WindowManager};
use srdwm_platform::{Platform, PlatformError, PlatformKind, Result as PlatformResult};
-use crate::{
- err, handle_keyboard_key_event, handle_pointer_button, handle_pointer_position, ClientState, CompState,
-};
+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.
-struct Card(OwnedFd);
+pub(crate) struct Card(OwnedFd);
impl AsFd for Card {
fn as_fd(&self) -> BorrowedFd<'_> {
@@ -83,98 +89,198 @@ impl AsFd for Card {
impl BasicDevice for Card {}
impl ControlDevice for Card {}
-struct DrmBuffer {
+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,
+ pub(crate) output: Output,
+ 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,
+ /// 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 UdevOutput {
- card: Rc<Card>,
- crtc: crtc::Handle,
- renderer: PixmanRenderer,
- damage_tracker: OutputDamageTracker,
- output: Output,
- buffers: [DrmBuffer; 2],
- front: usize,
- flip_pending: bool,
- active: bool,
- pointer_pos: Point<f64, Logical>,
- size: (i32, i32),
+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 CompState {
- /// Renders and (if there was damage) page-flips a new frame. No-op if
- /// a flip is already in flight (we wait for the DRM page-flip event
- /// before starting the next frame) or the session is paused.
+ /// Renders and (if there was damage) page-flips a new frame on every
+ /// head that is ready for one. A head with a flip still in flight is
+ /// skipped this pass and picked up when its page-flip event arrives, so
+ /// monitors on different refresh rates each run at their own pace
+ /// instead of the slowest one gating the rest.
pub(crate) fn render_udev_frame(&mut self) {
- let Some(udev) = self.udev.as_mut() else { return };
- if udev.flip_pending || !udev.active {
+ let locked = self.lock.locked;
+ let elapsed = self.start_time.elapsed();
+ // Drained before the `&mut self.udev` borrow below, so screencopy can
+ // be serviced with the renderer that borrow owns.
+ let mut captures = std::mem::take(&mut self.screencopy_pending);
+
+ // Which heads are eligible, and what each needs, gathered before the
+ // mutable borrow of `self.udev`.
+ let Some(udev) = self.udev.as_ref() else { return };
+ if !udev.active {
return;
}
-
- let back = 1 - udev.front;
-
- let mut custom_elements: Vec<MemoryRenderBufferRenderElement<PixmanRenderer>> = Vec::new();
- for (&id, deco) in self.decorations.iter() {
- let Some(geom) = self.wm.borrow().window(id).map(|w| w.geometry) else { continue };
- match MemoryRenderBufferRenderElement::from_buffer(&mut udev.renderer, (geom.x as f64, geom.y as f64), deco, None, None, None, Kind::Unspecified) {
- Ok(elem) => custom_elements.push(elem),
- Err(e) => log::warn!("udev: failed to import titlebar buffer for window {id}: {e}"),
+ let ready: Vec<(usize, Output)> = udev
+ .heads
+ .iter()
+ .enumerate()
+ .filter(|(_, h)| !h.flip_pending)
+ .map(|(i, h)| (i, h.output.clone()))
+ .collect();
+
+ let mut presented: Vec<Output> = Vec::new();
+ for (index, output) in ready {
+ let lock_surface = self.lock_surface_for(&output).cloned();
+
+ // Decoration elements are built per head: `from_buffer` needs the
+ // renderer, and geometry is translated into head-local space.
+ let origin = self.udev.as_ref().map(|u| u.heads[index].location).unwrap_or_default();
+ let decorations: Vec<(srdwm_core::Rect, MemoryRenderBuffer)> = if locked {
+ Vec::new()
+ } else {
+ self.decorations
+ .iter()
+ .filter_map(|(&id, deco)| self.wm.borrow().window(id).map(|w| (w.geometry, deco.clone())))
+ .collect()
+ };
+
+ let Some(udev) = self.udev.as_mut() else { return };
+ let head = &mut udev.heads[index];
+ let back = 1 - head.front;
+
+ let mut custom_elements: Vec<MemoryRenderBufferRenderElement<PixmanRenderer>> = Vec::new();
+ if !locked {
+ for (geom, deco) in &decorations {
+ let pos = ((geom.x - origin.x) as f64, (geom.y - origin.y) as f64);
+ match MemoryRenderBufferRenderElement::from_buffer(&mut udev.renderer, pos, deco, None, None, None, Kind::Unspecified) {
+ Ok(elem) => custom_elements.push(elem),
+ Err(e) => log::warn!("udev: failed to import titlebar buffer: {e}"),
+ }
+ }
}
- }
-
- let mut framebuffer = match udev.renderer.bind(&mut udev.buffers[back].image) {
- Ok(fb) => fb,
- Err(e) => {
- log::error!("udev: pixman bind failed: {e}");
- return;
+ let lock_elements = if locked {
+ crate::lock::lock_render_elements(lock_surface.as_ref(), &mut udev.renderer)
+ } else {
+ Vec::new()
+ };
+
+ let head = &mut udev.heads[index];
+ let mut framebuffer = match udev.renderer.bind(&mut head.buffers[back].image) {
+ Ok(fb) => fb,
+ Err(e) => {
+ log::error!("udev: pixman bind failed: {e}");
+ continue;
+ }
+ };
+
+ // Locked heads draw the lock surface over opaque black and
+ // nothing else; unlocked heads draw the normal scene.
+ let result = if locked {
+ head.damage_tracker
+ .render_output(&mut udev.renderer, &mut framebuffer, 0, &lock_elements, [0.0, 0.0, 0.0, 1.0])
+ .map(|r| r.damage.is_some())
+ .map_err(|e| e.to_string())
+ } else {
+ render_output(
+ &head.output,
+ &mut udev.renderer,
+ &mut framebuffer,
+ 1.0,
+ 0, // always a full redraw: buffer "age" tracking isn't worth the complexity for a software-only backend
+ [&self.space],
+ &custom_elements,
+ &mut head.damage_tracker,
+ [0.05, 0.05, 0.08, 1.0],
+ )
+ .map(|r| r.damage.is_some())
+ // Both arms reduce to "was there damage"; the two error types
+ // differ, so they are flattened to a message here.
+ .map_err(|e| e.to_string())
+ };
+ if !locked {
+ crate::screencopy::service_pending(std::mem::take(&mut captures), &mut udev.renderer, &framebuffer);
}
- };
+ drop(framebuffer);
- let result = render_output(
- &udev.output,
- &mut udev.renderer,
- &mut framebuffer,
- 1.0,
- 0, // always a full redraw: buffer "age" tracking isn't worth the complexity for a software-only, single-output backend
- [&self.space],
- &custom_elements,
- &mut udev.damage_tracker,
- [0.05, 0.05, 0.08, 1.0],
- );
- drop(framebuffer);
-
- let has_damage = match result {
- Ok(res) => res.damage.is_some(),
- Err(e) => {
- log::error!("udev: render_output failed: {e}");
- false
+ let has_damage = match result {
+ Ok(d) => d,
+ Err(e) => {
+ log::error!("udev: render_output failed: {e}");
+ continue;
+ }
+ };
+ if has_damage {
+ let head = &mut udev.heads[index];
+ if let Err(e) = head.copy_and_flip(&udev.card, back) {
+ log::error!("udev: page flip failed: {e}");
+ continue;
+ }
}
- };
- if !has_damage {
- return;
+ presented.push(output);
}
- if let Err(e) = udev.copy_and_flip(back) {
- log::error!("udev: page flip failed: {e}");
- return;
+ // Frame callbacks + lock confirmation, once the `udev` borrow is done.
+ for output in presented {
+ if locked {
+ let surface = self.lock_surface_for(&output).cloned();
+ crate::lock::send_lock_frame(surface.as_ref(), &output, elapsed);
+ self.confirm_lock_if_presented(&output);
+ } else {
+ let out = output.clone();
+ self.space.elements().for_each(|w| w.send_frame(&out, elapsed, None, |_, _| Some(out.clone())));
+ }
+ }
+ if locked {
+ crate::screencopy::fail_pending(captures);
}
- self.space.elements().for_each(|w| w.send_frame(&udev.output, self.start_time.elapsed(), None, |_, _| Some(udev.output.clone())));
}
}
-impl UdevOutput {
+impl UdevHead {
/// 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 - seem `crates/wayland/src/udev.rs` module docs) and flips to it.
- fn copy_and_flip(&mut self, back: usize) -> std::io::Result<()> {
+ /// mmap - see this module's docs) and flips to it.
+ fn copy_and_flip(&mut self, card: &Card, back: usize) -> std::io::Result<()> {
let (stride, height) = (self.buffers[back].image.stride(), self.buffers[back].image.height());
let byte_len = stride * height;
// SAFETY: `image` owns this memory and outlives the byte slice we
@@ -182,12 +288,12 @@ impl UdevOutput {
// of this call.
let src: &[u8] = unsafe { std::slice::from_raw_parts(self.buffers[back].image.data() as *const u8, byte_len) };
{
- let mut mapping = self.card.map_dumb_buffer(&mut self.buffers[back].dumb)?;
+ let mut mapping = card.map_dumb_buffer(&mut self.buffers[back].dumb)?;
let dst = mapping.as_mut();
let len = byte_len.min(dst.len());
dst[..len].copy_from_slice(&src[..len]);
}
- self.card.page_flip(self.crtc, self.buffers[back].fb, PageFlipFlags::EVENT, None)?;
+ card.page_flip(self.crtc, self.buffers[back].fb, PageFlipFlags::EVENT, None)?;
self.flip_pending = true;
Ok(())
}
@@ -221,57 +327,93 @@ impl UdevPlatform {
.map_err(err)?;
let card = Rc::new(Card(fd));
- let (crtc, connector, mode) = find_connected_output(&card)?;
- let (width, height) = mode.size();
- let (width, height) = (width as i32, height as i32);
-
- let buffers = [make_drm_buffer(&card, width, height)?, make_drm_buffer(&card, width, height)?];
- card.set_crtc(crtc, Some(buffers[0].fb), (0, 0), &[connector], Some(mode)).map_err(err)?;
-
- let output = Output::new(
- "srdwm-udev".to_string(),
- PhysicalProperties { size: (0, 0).into(), subpixel: Subpixel::Unknown, make: "srdwm".into(), model: "drm".into() },
- );
- let refresh = mode_refresh_mhz(&mode);
- output.change_current_state(
- Some(OutputMode { size: (width, height).into(), refresh }),
- Some(Transform::Normal),
- None,
- Some((0, 0).into()),
- );
+ // Every connected connector becomes a head, laid out left-to-right.
+ let connected = find_connected_outputs(&card)?;
+ log::info!("udev: {} connected output(s)", connected.len());
let renderer = PixmanRenderer::new().map_err(err)?;
- let damage_tracker = OutputDamageTracker::from_output(&output);
-
let dh = Display::<CompState>::new().map_err(err)?;
let display_handle = dh.handle();
- output.create_global::<CompState>(&display_handle);
+
+ let mut heads: Vec<UdevHead> = Vec::new();
+ let mut output_entries: Vec<crate::state::OutputEntry> = Vec::new();
+ let mut x_offset = 0;
+ for (index, probe) in connected.iter().enumerate() {
+ let (width, height) = probe.mode.size();
+ let (width, height) = (width as i32, height as i32);
+
+ let buffers = [make_drm_buffer(&card, width, height)?, make_drm_buffer(&card, width, height)?];
+ card.set_crtc(probe.crtc, Some(buffers[0].fb), (0, 0), &[probe.connector], Some(probe.mode))
+ .map_err(err)?;
+
+ // Named after the real connector (eDP-1, HDMI-A-1, ...) so
+ // clients and the user can tell monitors apart; `wl_output.name`
+ // is what a bar's per-monitor config keys off.
+ let output = Output::new(
+ probe.name.clone(),
+ PhysicalProperties { size: (0, 0).into(), subpixel: Subpixel::Unknown, make: "srdwm".into(), model: "drm".into() },
+ );
+ output.change_current_state(
+ Some(OutputMode { size: (width, height).into(), refresh: mode_refresh_mhz(&probe.mode) }),
+ Some(Transform::Normal),
+ None,
+ Some((x_offset, 0).into()),
+ );
+ output.set_preferred(OutputMode { size: (width, height).into(), refresh: mode_refresh_mhz(&probe.mode) });
+ output.create_global::<CompState>(&display_handle);
+
+ let location: Point<i32, Logical> = (x_offset, 0).into();
+ heads.push(UdevHead {
+ crtc: probe.crtc,
+ output: output.clone(),
+ damage_tracker: OutputDamageTracker::from_output(&output),
+ buffers,
+ front: 0,
+ flip_pending: false,
+ location,
+ size: (width, height),
+ });
+ output_entries.push(crate::state::OutputEntry { output, location });
+ log::info!("udev: head {index}: {} {width}x{height} at x={x_offset}", probe.name);
+ x_offset += width;
+ }
+ 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::<CompState>(&display_handle);
let compositor_state = CompositorState::new::<CompState>(&display_handle);
let xdg_shell_state = XdgShellState::new::<CompState>(&display_handle);
let xdg_decoration_state = XdgDecorationState::new::<CompState>(&display_handle);
let shm_state = ShmState::new::<CompState>(&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::<CompState>(&display_handle);
+ let data_control_state =
+ DataControlState::new::<CompState, _>(&display_handle, Some(&primary_selection_state), |_| true);
let mut seat_state = SeatState::new();
let mut seat = seat_state.new_wl_seat(&display_handle, "seat0");
seat.add_keyboard(Default::default(), 200, 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();
- space.map_output(&output, (0, 0));
+ 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_output = UdevOutput {
+ let udev_state = UdevState {
card: card.clone(),
- crtc,
renderer,
- damage_tracker,
- output: output.clone(),
- buffers,
- front: 0,
- flip_pending: false,
+ heads,
active: true,
pointer_pos: (width as f64 / 2.0, height as f64 / 2.0).into(),
- size: (width, height),
};
let state = CompState {
@@ -282,6 +424,19 @@ impl UdevPlatform {
seat_state,
seat,
space,
+ outputs: output_entries,
+ layer_shell_state: smithay::wayland::shell::wlr_layer::WlrLayerShellState::new::<CompState>(&display_handle),
+ dh: display_handle.clone(),
+ data_device_state: DataDeviceState::new::<CompState>(&display_handle),
+ primary_selection_state,
+ data_control_state,
+ session_lock_state: smithay::wayland::session_lock::SessionLockManagerState::new::<CompState, _>(
+ &display_handle,
+ |_| true,
+ ),
+ _screencopy_state: crate::screencopy::ScreencopyState::new::<CompState>(&display_handle),
+ screencopy_pending: Vec::new(),
+ lock: Default::default(),
wm: wm.clone(),
surface_to_id: HashMap::new(),
id_to_window: HashMap::new(),
@@ -289,7 +444,7 @@ impl UdevPlatform {
pending: pending.clone(),
bound_keys: Rc::new(bound_keys.iter().cloned().collect::<HashSet<_>>()),
start_time: Instant::now(),
- udev: Some(udev_output),
+ udev: Some(udev_state),
xwayland_shell_state: smithay::wayland::xwayland_shell::XWaylandShellState::new::<CompState>(&display_handle),
xwm: None,
xwayland_windows: HashMap::new(),
@@ -331,27 +486,70 @@ fn mode_refresh_mhz(mode: &DrmMode) -> i32 {
}
}
-// TODO: multi-monitor - returns only the first connected connector; no
-// hotplug, no independent per-output layout. See
-// docs/IMPLEMENTATION_STATUS.md's "Not implemented anywhere yet" section.
-fn find_connected_output(card: &Card) -> PlatformResult<(crtc::Handle, connector::Handle, DrmMode)> {
+/// A connector we intend to drive, paired with the CRTC that will scan it
+/// out. Produced once at startup by [`find_connected_outputs`].
+struct OutputProbe {
+ crtc: crtc::Handle,
+ connector: connector::Handle,
+ mode: DrmMode,
+ /// Connector name as the kernel reports it (`eDP-1`, `HDMI-A-1`, ...).
+ name: String,
+}
+
+/// Every connected connector, each assigned a distinct CRTC.
+///
+/// CRTCs are a finite hardware resource and cannot be shared, so a CRTC
+/// already claimed by an earlier connector is skipped - a machine with more
+/// connected monitors than CRTCs drives as many as the hardware allows and
+/// logs the rest rather than failing outright.
+///
+/// Still no hotplug: connectors are probed once at startup. Plugging a
+/// monitor in later needs a udev event handler, which this backend does not
+/// register yet (see `docs/IMPLEMENTATION_STATUS.md`).
+fn find_connected_outputs(card: &Card) -> PlatformResult<Vec<OutputProbe>> {
let res = card.resource_handles().map_err(err)?;
let connectors: Vec<connector::Info> = res.connectors().iter().flat_map(|&h| card.get_connector(h, true)).collect();
- let con = connectors
- .iter()
- .find(|c| c.state() == connector::State::Connected)
- .ok_or_else(|| PlatformError::Other("udev: no connected connector found".into()))?;
- let mode = *con.modes().first().ok_or_else(|| PlatformError::Other("udev: connected connector has no modes".into()))?;
-
- let crtcs: Vec<crtc::Handle> = res.crtcs().to_vec();
- let crtc = con
- .current_encoder()
- .and_then(|enc| card.get_encoder(enc).ok())
- .and_then(|enc| res.filter_crtcs(enc.possible_crtcs()).into_iter().next())
- .or_else(|| crtcs.first().copied())
- .ok_or_else(|| PlatformError::Other("udev: no usable crtc found".into()))?;
-
- Ok((crtc, con.handle(), mode))
+
+ let mut used: Vec<crtc::Handle> = Vec::new();
+ let mut probes = Vec::new();
+ for con in connectors.iter().filter(|c| c.state() == connector::State::Connected) {
+ let name = format!("{:?}-{}", con.interface(), con.interface_id());
+ // Prefer the mode the display advertises as PREFERRED (its native
+ // resolution) rather than whatever happens to be listed first --
+ // the list order is not guaranteed, and picking wrong means running
+ // a monitor at the wrong resolution. Falls back to the first mode
+ // for connectors that flag none.
+ let Some(&mode) = con
+ .modes()
+ .iter()
+ .find(|m| m.mode_type().contains(ModeTypeFlags::PREFERRED))
+ .or_else(|| con.modes().first())
+ else {
+ log::warn!("udev: connector {name} is connected but reports no modes; skipping");
+ continue;
+ };
+ // Prefer the CRTC already driving this connector, else any free one
+ // the encoder can reach.
+ let candidates: Vec<crtc::Handle> = con
+ .current_encoder()
+ .and_then(|enc| card.get_encoder(enc).ok())
+ .map(|enc| res.filter_crtcs(enc.possible_crtcs()))
+ .unwrap_or_default()
+ .into_iter()
+ .chain(res.crtcs().iter().copied())
+ .collect();
+ let Some(crtc) = candidates.into_iter().find(|c| !used.contains(c)) else {
+ log::warn!("udev: no free CRTC left for connector {name}; not driving it");
+ continue;
+ };
+ used.push(crtc);
+ probes.push(OutputProbe { crtc, connector: con.handle(), mode, name });
+ }
+
+ if probes.is_empty() {
+ return Err(PlatformError::Other("udev: no connected connector found".into()));
+ }
+ Ok(probes)
}
fn make_drm_buffer(card: &Card, width: i32, height: i32) -> PlatformResult<DrmBuffer> {
@@ -374,17 +572,21 @@ fn register_drm_fd(handle: &LoopHandle<'static, CompState>, card: &Rc<Card>) ->
let card = udev.card.clone();
match card.receive_events() {
Ok(events) => {
+ // The event names the CRTC it came from, so with several
+ // monitors only that head advances - flipping all of
+ // them would desynchronise the others' buffers.
let mut flipped = false;
for event in events {
- if let DrmEvent::PageFlip(_) = event {
- flipped = true;
+ let DrmEvent::PageFlip(flip) = event else { continue };
+ if let Some(udev) = data.udev.as_mut() {
+ if let Some(head) = udev.heads.iter_mut().find(|h| h.crtc == flip.crtc) {
+ head.front = 1 - head.front;
+ head.flip_pending = false;
+ flipped = true;
+ }
}
}
if flipped {
- if let Some(udev) = data.udev.as_mut() {
- udev.front = 1 - udev.front;
- udev.flip_pending = false;
- }
data.render_udev_frame();
}
}
@@ -422,10 +624,16 @@ fn register_session_notifier(handle: &LoopHandle<'static, CompState>, notifier:
log::info!("udev: session resumed (VT switch back)");
udev.active = true;
// Some drivers reset mode-setting state across a VT
- // switch; reassert it before rendering again.
- let fb = udev.buffers[udev.front].fb;
- if let Err(e) = udev.card.set_crtc(udev.crtc, Some(fb), (0, 0), &[], None) {
- log::warn!("udev: failed to reassert crtc on resume: {e}");
+ // switch; reassert every head before rendering again.
+ let card = udev.card.clone();
+ for head in &mut udev.heads {
+ let fb = head.buffers[head.front].fb;
+ if let Err(e) = card.set_crtc(head.crtc, Some(fb), (0, 0), &[], None) {
+ log::warn!("udev: failed to reassert crtc on resume: {e}");
+ }
+ // Force a full repaint: contents are undefined after
+ // the VT switch.
+ head.flip_pending = false;
}
data.render_udev_frame();
}
@@ -441,9 +649,11 @@ fn handle_libinput_event(state: &mut CompState, event: InputEvent<LibinputInputB
InputEvent::PointerMotion { event } => {
let Some(udev) = state.udev.as_mut() else { return };
let delta = event.delta();
- let (w, h) = (udev.size.0 as f64, udev.size.1 as f64);
- udev.pointer_pos.x = (udev.pointer_pos.x + delta.x).clamp(0.0, w - 1.0);
- udev.pointer_pos.y = (udev.pointer_pos.y + delta.y).clamp(0.0, h - 1.0);
+ // Clamped to the union of every head, so the pointer travels
+ // between monitors instead of stopping at the first one's edge.
+ let (w, h) = udev.bounds();
+ udev.pointer_pos.x = (udev.pointer_pos.x + delta.x).clamp(0.0, (w - 1.0).max(0.0));
+ udev.pointer_pos.y = (udev.pointer_pos.y + delta.y).clamp(0.0, (h - 1.0).max(0.0));
let pos = udev.pointer_pos;
handle_pointer_position(state, pos, event.time_msec());
}
@@ -486,14 +696,24 @@ impl Platform for UdevPlatform {
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<Vec<srdwm_core::Monitor>> {
let Some(udev) = self.state.udev.as_ref() else { return Ok(Vec::new()) };
- let (w, h) = udev.size;
- Ok(vec![{
- let mut m = srdwm_core::Monitor::new(0, "drm", srdwm_core::Rect::new(0, 0, w as u32, h as u32));
- m.primary = true;
- m
- }])
+ Ok(udev
+ .heads
+ .iter()
+ .enumerate()
+ .map(|(i, head)| {
+ let (w, h) = head.size;
+ let rect = srdwm_core::Rect::new(head.location.x, head.location.y, w as u32, h as u32);
+ let mut m = srdwm_core::Monitor::new(i as u32, head.output.name(), rect);
+ m.primary = i == 0;
+ m
+ })
+ .collect())
}
fn apply_geometry(&mut self, window: srdwm_core::WindowId, _geometry: srdwm_core::Rect) -> PlatformResult<()> {