srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src/udev/platform.rs
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2025-02-15 14:56:00 +0200
committersrdusr <[email protected]>2025-02-15 14:56:00 +0200
commit0a4c4b5941fe982ccb3d3175e26d9d83f0025ffd (patch)
tree7672d0af277664f457c6c9462925c0005fe35dcf /crates/wayland/src/udev/platform.rs
parent413daa7ba2ea0ebd1424c024fd0566423aaea3f8 (diff)
downloadsrdwm-0a4c4b5941fe982ccb3d3175e26d9d83f0025ffd.tar.gz
srdwm-0a4c4b5941fe982ccb3d3175e26d9d83f0025ffd.zip
Checkpoint: preserve all uncommitted rust-rewrite worktree work
Safety commit before reconciling this worktree with main, which has diverged with its own separate fixes today. Nothing here is reviewed or curated yet - this exists purely so none of this work can be lost to a git operation, disk issue, or worktree cleanup while that reconciliation happens.
Diffstat (limited to 'crates/wayland/src/udev/platform.rs')
-rw-r--r--crates/wayland/src/udev/platform.rs316
1 files changed, 264 insertions, 52 deletions
diff --git a/crates/wayland/src/udev/platform.rs b/crates/wayland/src/udev/platform.rs
index 88c0d8f..cbcee64 100644
--- a/crates/wayland/src/udev/platform.rs
+++ b/crates/wayland/src/udev/platform.rs
@@ -10,6 +10,12 @@ pub struct UdevPlatform {
clients: Vec<Client>,
pending: Rc<RefCell<Vec<CoreEvent>>>,
ipc: Option<srdwm_platform::IpcServer>,
+ /// Last time `ipc.poll()` actually ran - see its call site in
+ /// `poll_events` for why this exists at all.
+ last_ipc_poll: Instant,
+ /// Last time the unconditional end-of-cycle `render_udev_frame()` call
+ /// actually ran - see its own call site for why.
+ last_render: Instant,
}
impl UdevPlatform {
@@ -52,16 +58,24 @@ impl UdevPlatform {
let mut heads: Vec<UdevHead> = Vec::new();
let mut output_entries: Vec<crate::state::OutputEntry> = Vec::new();
let mut used_crtcs: Vec<crtc::Handle> = Vec::new();
+ // Two accumulators - see `bring_up_head`'s own doc comment on its
+ // `logical_x` parameter for why a second head's logical position
+ // can't just be derived from the physical offset and its own
+ // scale alone once an earlier head has a *different* scale.
let mut x_offset = 0;
+ let mut logical_x = 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);
+ let scale = wm.borrow().monitor_scale(&probe.name);
+ let (head, entry) = bring_up_head(&card, &display_handle, probe, crtc, x_offset, logical_x, scale)?;
+ log::info!("udev: head {}: {} {}x{} at x={x_offset} (logical x={logical_x})", heads.len(), probe.name, head.size.0, head.size.1);
used_crtcs.push(crtc);
+ let resolved_scale = head.output.current_scale().fractional_scale();
x_offset += head.size.0;
+ logical_x += (head.size.0 as f64 / resolved_scale).round() as i32;
heads.push(head);
output_entries.push(entry);
}
@@ -114,9 +128,11 @@ impl UdevPlatform {
active: true,
pointer_pos: (width as f64 / 2.0, height as f64 / 2.0).into(),
session: session.clone(),
+ disabled_connectors: std::collections::HashSet::new(),
+ last_rendered_workspace: None,
};
- let state = CompState {
+ let mut state = CompState {
compositor_state,
xdg_shell_state,
_xdg_decoration_state: xdg_decoration_state,
@@ -143,6 +159,7 @@ impl UdevPlatform {
_screencopy_state: crate::screencopy::ScreencopyState::new::<CompState>(&display_handle),
screencopy_pending: Vec::new(),
_appmenu_state: crate::appmenu::AppmenuManagerState::new::<CompState>(&display_handle),
+ _virtual_keyboard_state: smithay::wayland::virtual_keyboard::VirtualKeyboardManagerState::new::<CompState, _>(&display_handle, |_client| true),
_foreign_toplevel_state: crate::foreign_toplevel::ForeignToplevelState::new::<CompState>(&display_handle),
foreign_toplevel_managers: Vec::new(),
foreign_toplevel_handles: HashMap::new(),
@@ -172,6 +189,7 @@ impl UdevPlatform {
last_broadcast_workspace: None,
lock: Default::default(),
cursor_status: smithay::input::pointer::CursorImageStatus::default_named(),
+ decoration_cursor_active: false,
cursor_buffers: crate::cursor::make_buffers(),
last_titlebar_click: None,
gesture_swipe: None,
@@ -189,12 +207,15 @@ impl UdevPlatform {
border_top_decorations: HashMap::new(),
border_bottom_decorations: HashMap::new(),
decoration_signatures: HashMap::new(),
+ hovered_titlebar_button: None,
shadow_buffers: HashMap::new(),
rounded_corners_program: None,
content_epoch: HashMap::new(),
rounded_content_buffers: HashMap::new(),
border_side_buffers: HashMap::new(),
+ color_filter_buffers: HashMap::new(),
last_synced_size: HashMap::new(),
+ pending_size_configure: HashMap::new(),
pending: pending.clone(),
bound_keys: Rc::new(bound_keys.iter().cloned().collect::<HashSet<_>>()),
repeat_keys: Rc::new(repeat_keys.iter().cloned().collect::<HashSet<_>>()),
@@ -209,6 +230,15 @@ impl UdevPlatform {
appmenu_registrar: None,
};
+ // Before the Wayland socket even binds, deliberately - see
+ // `restore_monitor_layout`'s and `monitor_layout`'s own doc
+ // comments for why this compositor restores its own remembered
+ // layout itself rather than leaving it to whichever panel happens
+ // to be running: no client can possibly connect and see the
+ // default, un-restored arrangement, not even for one frame, since
+ // the socket a client would need to connect to doesn't exist yet.
+ state.restore_monitor_layout();
+
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);
@@ -248,7 +278,7 @@ impl UdevPlatform {
log::warn!("XWayland unavailable ({e}); X11-only clients will not run");
}
- Ok(Self { event_loop, display: dh, state, listener, clients: Vec::new(), pending, ipc })
+ Ok(Self { event_loop, display: dh, state, listener, clients: Vec::new(), pending, ipc, last_ipc_poll: Instant::now(), last_render: Instant::now() })
}
fn accept_clients(&mut self) -> PlatformResult<()> {
@@ -268,13 +298,97 @@ impl Platform for UdevPlatform {
fn poll_events(&mut self) -> PlatformResult<Vec<CoreEvent>> {
self.accept_clients()?;
+ let dispatch_start = Instant::now();
self.event_loop.dispatch(Some(Duration::from_millis(16)), &mut self.state).map_err(err)?;
+ // `dispatch`'s `Duration::from_millis(16)` argument is a *maximum*
+ // wait, not a guarantee - calloop returns the moment any
+ // registered source looks ready, however long or short that takes.
+ // A source stuck permanently "ready" (an fd calloop never removes
+ // even though every read on it comes back EOF/HUP - confirmed live
+ // via `strace`, traced to the libseat session notifier's internal
+ // ping channel, and reproducible on a bare tty1 login within the
+ // first second of every single srdwm start, independent of which
+ // libseat backend - seatd or the logind fallback - is active)
+ // makes `dispatch` return in microseconds forever, turning this
+ // loop into an unthrottled spin that burns 70-90% of a core doing
+ // nothing: `accept_clients`/`tick_repeat`/`dispatch_clients` all
+ // still run their own (cheap) work on every single one of those
+ // spurious wakeups, thousands of times a second, instead of the
+ // ~60 times a second the 16ms figure was meant to cap it at.
+ //
+ // This doesn't fix *why* that source never goes away - that's
+ // upstream, in calloop/libseat's own channel-notification internals
+ // - but it puts a floor under the symptom regardless of which
+ // source eventually turns out to cause it.
+ //
+ // Sleeping the full remainder of a 16ms cycle on *every* fast
+ // return (an earlier version of this did exactly that) blocks this
+ // thread against everything, not just the next spurious wakeup --
+ // a genuine DRM page-flip completion or a client committing its
+ // next video frame that becomes ready *during* the sleep sits
+ // unprocessed until the sleep ends, instead of being picked up
+ // immediately. Reported live as choppy/laggy video playback: up to
+ // 16ms of pure, avoidable latency added to every frame's worth of
+ // real work that happened to land in that window.
+ //
+ // A per-iteration streak counter was tried first, throttling only
+ // once several fast returns in a row looked like true idle
+ // spinning rather than one-off real work - but `dispatch`'s
+ // return time can't actually distinguish the two here: the dead
+ // pipe is *always* ready, so every call returns in microseconds
+ // whether or not it also picked up something real, and a streak
+ // built on that timing never resets during genuine activity
+ // either. Telling real work apart from the spurious wakeup would
+ // need a signal from *inside* dispatch (e.g. the render path
+ // flagging "a frame actually went out this tick"), which is real
+ // plumbing, not a one-line fix.
+ //
+ // Short of that: cap the sleep itself far below 16ms instead of
+ // trying to skip it selectively. `MIN_CYCLE` (~3ms) still turns
+ // the true spin (unbounded, thousands of empty iterations/sec)
+ // into a bounded few hundred/sec - a real, if smaller, win over
+ // no floor at all - while capping how long any genuinely-ready
+ // event can ever sit blocked to something well under one frame at
+ // 60Hz, rather than up to a full frame's worth of latency.
+ const MIN_CYCLE: Duration = Duration::from_millis(3);
+ let elapsed = dispatch_start.elapsed();
+ if elapsed < MIN_CYCLE {
+ std::thread::sleep(MIN_CYCLE - elapsed);
+ }
// 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)?;
self.state.apply_registrar_events();
- if let Some(ipc) = self.ipc.as_mut() {
+ self.state.poll_global_menu_properties();
+ // Throttled to ~60Hz, not run on every single `poll_events` cycle --
+ // `IpcServer::poll` unconditionally rebuilds and diffs a full
+ // `client_snapshot`/`workspace_snapshot` on every call (cloning each
+ // window's title, app_id, global-menu data, ...) even when nothing
+ // has changed and nobody is subscribed, purely so a real change is
+ // never missed. Cheap at a sane call rate; not cheap at the rate
+ // this loop actually runs at - see `MIN_CYCLE`'s own doc comment
+ // just above: the dead libseat pipe that makes `dispatch` return in
+ // microseconds forever means this whole function's "rest of the
+ // cycle" work already runs at whatever `dispatch` gets bounced to
+ // (a few hundred times a second, floor-capped by `MIN_CYCLE`, not
+ // the ~60 times a second one `Duration::from_millis(16)` above was
+ // meant to imply), and that snapshot/diff cost was riding along at
+ // that same needlessly high rate - measured live as a continuous,
+ // unwavering ~20% of a core even at complete idle, unaffected by
+ // toggling shadows/rounded_corners/animations (all purely per-
+ // render-frame costs, not per-cycle ones, so none of them could
+ // have explained a cost that never budged with the screen doing
+ // nothing). A real `srd dispatch`/`srd set` command still lands
+ // within one throttled window (well under a human's own reaction
+ // time), not delayed by anything close to what would read as
+ // input lag.
+ const IPC_POLL_INTERVAL: Duration = Duration::from_millis(16);
+ let ipc_due = self.last_ipc_poll.elapsed() >= IPC_POLL_INTERVAL;
+ if ipc_due {
+ self.last_ipc_poll = Instant::now();
+ }
+ if let Some(ipc) = self.ipc.as_mut().filter(|_| ipc_due) {
if ipc.poll(&self.state.wm) {
self.pending.borrow_mut().push(CoreEvent::WorkspaceChanged);
// `ipc.rs`'s `handle_request` (`"focus"`, `"toggle
@@ -295,9 +409,16 @@ impl Platform for UdevPlatform {
// unconditionally on any IPC mutation, not just ones that
// are definitely focus changes - raising an already-topmost
// element is a no-op reinsertion.
+ //
+ // `raise_in_space`, not the full `focus_window` - that one
+ // also re-runs `WindowManager::focus_window`'s workspace-
+ // follow side effect on the already-focused window, which
+ // silently reverted any `activate_workspace` IPC dispatch
+ // within this same cycle (see `raise_in_space`'s own doc
+ // comment for the full story).
let focused = self.state.wm.borrow().focused_id();
if let Some(id) = focused {
- crate::input::focus_window(&mut self.state, id);
+ crate::input::raise_in_space(&mut self.state, id);
}
}
}
@@ -344,6 +465,10 @@ impl Platform for UdevPlatform {
log::warn!("udev: set_output_position: no head at index {id}");
continue;
};
+ // `(x, y)` is whatever `srd dispatch set output position`
+ // sent, unconverted - that command's own contract is to
+ // match `srd monitors`' `full_x`/`full_y` (physical),
+ // which is exactly what `apply_output_position` wants.
crate::output_management::apply_output_position(&mut self.state, &output, (x, y).into());
any_applied = true;
}
@@ -361,57 +486,144 @@ impl Platform for UdevPlatform {
self.pending.borrow_mut().push(CoreEvent::MonitorAdded(srdwm_core::Monitor::new(0, "", srdwm_core::Rect::new(0, 0, 0, 0))));
}
}
- self.state.render_udev_frame();
+ // Applies any `srd set_output_enabled` IPC requests queued since
+ // the last poll - `disable_connector_by_name`/`enable_connector_
+ // by_name` already push their own `MonitorRemoved`/`MonitorAdded`
+ // event, so nothing further is needed here beyond calling them.
+ let enable_requests = self.state.wm.borrow_mut().drain_output_enable_requests();
+ for (name, enabled) in enable_requests {
+ if enabled {
+ self.state.enable_connector_by_name(&name);
+ } else {
+ self.state.disable_connector_by_name(&name);
+ }
+ }
+ // Throttled the same way and for the same underlying reason as the
+ // `ipc.poll()` call above - this is the *other*, larger half of
+ // this cycle's needless work at the dead-pipe-driven spin rate.
+ // `render_udev_frame` isn't only called from here: a real DRM
+ // page-flip completion (`session.rs`), a VT-switch resume, and an
+ // output hotplug each call it directly, immediately, completely
+ // unthrottled by this - those are genuine, comparatively rare
+ // events that should redraw the instant they happen. This one
+ // specific call site is different: it's the unconditional catch-
+ // all that used to run at the end of *every* cycle regardless of
+ // whether `dispatch` actually picked up anything real, which at
+ // this loop's dead-pipe-driven rate meant re-walking every visible
+ // window, rebuilding the whole `custom_elements` list, and running
+ // Pixman's own damage tracking against it a few hundred times a
+ // second, forever - `has_damage` already meant an idle desktop's
+ // *page flip* was skipped, but computing "no, still nothing to
+ // flip" this often is itself most of the cost this whole function
+ // was found burning at idle. `RENDER_INTERVAL` (~8ms, ~120Hz) is
+ // comfortably above any real display's refresh rate - a head can
+ // never actually present faster than its own vblank allows
+ // regardless (`flip_pending` already gates that) - so this cannot
+ // cap real, on-screen frame rate on any hardware this backend
+ // targets; it only stops the redundant "check again" calls in
+ // between.
+ const RENDER_INTERVAL: Duration = Duration::from_millis(8);
+ if self.last_render.elapsed() >= RENDER_INTERVAL {
+ self.last_render = Instant::now();
+ 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.
+ /// One `srdwm_core::Monitor` per head, positioned in the global space
+ /// - or several, when `srd.monitor.split` has requested that head be
+ /// divided into logical sub-monitors ("monitors inside monitors"; see
+ /// `srdwm_core::monitor::MonitorSplit`'s own doc comment). 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 - a split just means more,
+ /// smaller rectangles feeding the same grouping, no other core-side
+ /// change needed.
fn monitors(&mut self) -> PlatformResult<Vec<srdwm_core::Monitor>> {
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.maximize_geometry = crate::input::maximize_geometry_for(&head.output, m.full_geometry);
- m.primary = i == 0;
- m
- })
- .collect())
+ let wm = self.state.wm.clone();
+ let wm = wm.borrow();
+ let mut out = Vec::new();
+ let mut next_id: u32 = 0;
+ for head in udev.heads.iter() {
+ // 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.
+ //
+ // `non_exclusive_zone()` is in *logical* (scale-divided)
+ // units - a bar reports its own reserved strip the way every
+ // layer-shell client does, in logical points - while `head.
+ // location`/`head.size` are raw physical pixels straight from
+ // the DRM mode, never touched by `srd.monitor.scale`. Left
+ // unconverted, `usable` silently mixed the two units on any
+ // output with a scale other than exactly `1.0`: at scale
+ // `0.712`, a 1920-physical-pixel-wide head's own `zone.size.w`
+ // came back as ~2697 (logical), reported as this monitor's
+ // *usable* width - larger than its own *full* width, and
+ // large enough to overlap whichever real monitor sat next to
+ // it in the shared global space. Reported live as "Firefox
+ // maximized on one monitor also shows partially on the
+ // other" and general visual glitching on the scaled output --
+ // both are this: placement math trusting an oversized rect
+ // that reached into a neighboring monitor's real screen.
+ // Scaling `zone` back into physical pixels here keeps `usable`
+ // in the same unit as `full`/`maximize`/`head.location`
+ // everywhere else in this compositor.
+ let zone = layer_map_for_output(&head.output).non_exclusive_zone();
+ let scale = head.output.current_scale().fractional_scale();
+ let zone_physical = |v: i32| (v as f64 * scale).round() as i32;
+ let usable = srdwm_core::Rect::new(
+ head.location.x + zone_physical(zone.loc.x),
+ head.location.y + zone_physical(zone.loc.y),
+ zone_physical(zone.size.w).max(0) as u32,
+ zone_physical(zone.size.h).max(0) as u32,
+ );
+ // The head's true full rect, ignoring any exclusive zone --
+ // deliberately *not* defaulted from `usable` the way `Monitor::
+ // new` alone would (see the fullscreen note below).
+ let full = srdwm_core::Rect::new(head.location.x, head.location.y, head.size.0 as u32, head.size.1 as u32);
+ let maximize = crate::input::maximize_geometry_for(&head.output, full);
+ let name = head.output.name();
+ let split = wm.monitor_split(&name);
+ let parts = split.map(|s| s.parts).unwrap_or(1).max(1);
+ let rows = split.map(|s| s.rows).unwrap_or(false);
+ for part in 0..parts {
+ let sub_name = if parts <= 1 { name.clone() } else { format!("{name}-{}", part + 1) };
+ let mut m = srdwm_core::Monitor::new(next_id, sub_name, srdwm_core::monitor::split_rect(usable, part, parts, rows));
+ // `Monitor::new` defaults `full_geometry`/`maximize_
+ // geometry` to whatever `geometry` was constructed with --
+ // correct for a monitor with no layer-shell client and no
+ // split at all, wrong the moment either exists, since the
+ // rect above may already be zone-shrunk and/or a sub-
+ // region. 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. Each split
+ // part gets its *own* full/maximize rect too - without
+ // this, fullscreening a window in either half of a split
+ // head would cover the *entire* physical panel, silently
+ // erasing the split it was placed to respect.
+ m.full_geometry = srdwm_core::monitor::split_rect(full, part, parts, rows);
+ m.maximize_geometry = srdwm_core::monitor::split_rect(maximize, part, parts, rows);
+ m.primary = next_id == 0;
+ m.split = parts > 1;
+ m.scale = scale;
+ out.push(m);
+ next_id += 1;
+ }
+ }
+ Ok(out)
}
fn apply_geometry(&mut self, window: srdwm_core::WindowId, _geometry: srdwm_core::Rect) -> PlatformResult<()> {