atagen

monolith

public
Star0
Watch0

Commit 57e81cf6ceb2

tarball

atagen <boss@atagen.co>2026-10-17 04:49 UTC

12 files changed+807-765
monolith: split handlers by protocol and pacing out of drm

handlers.rs had grown into every protocol the early milestones needed, while each later protocol got its own module. Frame pacing moves to pacing.rs as pure functions of timestamps so the Budget behaviour found over several bare-metal rounds can be tested without a DRM device, and nested's prediction shares the vblank arithmetic.
Added file: crates/monolith/src/commit.rs+225 additions
@@ -0,0 +1,225 @@
1+use crate::state::{ClientState, State};
2+use smithay::backend::allocator::dmabuf::Dmabuf;
3+use smithay::backend::renderer::utils::{import_surface_tree, on_commit_buffer_handler};
4+use smithay::desktop::PopupKind;
5+use smithay::reexports::calloop::Interest;
6+use smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer;
7+use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
8+use smithay::reexports::wayland_server::{Client, Resource};
9+use smithay::utils::{Rectangle, Transform};
10+use smithay::wayland::buffer::BufferHandler;
11+use smithay::wayland::compositor::{
12+ BufferAssignment, CompositorClientState, CompositorHandler, CompositorState, Damage,
13+ SurfaceAttributes, add_blocker, add_pre_commit_hook, get_parent, is_sync_subsurface,
14+ with_states,
15+};
16+use smithay::wayland::dmabuf::{
17+ DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier, get_dmabuf,
18+};
19+use smithay::wayland::drm_syncobj::{DrmSyncobjCachedState, DrmSyncobjHandler, DrmSyncobjState};
20+use smithay::wayland::shm::{ShmHandler, ShmState, with_buffer_contents};
21+
22+impl CompositorHandler for State {
23+ fn compositor_state(&mut self) -> &mut CompositorState {
24+ &mut self.protocols.compositor_state
25+ }
26+
27+ fn new_surface(&mut self, surface: &WlSurface) {
28+ add_pre_commit_hook::<Self, _>(surface, |state, _dh, surface| {
29+ hold_until_readable(state, surface);
30+ });
31+ }
32+
33+ fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState {
34+ if let Some(x) = client.get_data::<smithay::xwayland::XWaylandClientData>() {
35+ return &x.compositor_state;
36+ }
37+ &client
38+ .get_data::<ClientState>()
39+ .expect("every client has ClientState")
40+ .compositor_state
41+ }
42+
43+ fn commit(&mut self, surface: &WlSurface) {
44+ on_commit_buffer_handler::<Self>(surface);
45+ // Importing at commit keeps first-time dmabuf imports, which take
46+ // milliseconds on NVIDIA, out of the render racing the next vblank.
47+ if let Err(e) = import_surface_tree(&mut self.renderer, surface) {
48+ tracing::debug!("early import failed: {e}");
49+ }
50+ if !is_sync_subsurface(surface) {
51+ let mut root = surface.clone();
52+ while let Some(parent) = get_parent(&root) {
53+ root = parent;
54+ }
55+ let mapped = self.wm.id_of(&root).and_then(|id| self.wm.window(id));
56+ let window = mapped.or_else(|| {
57+ self.wm
58+ .unmapped
59+ .iter()
60+ .find(|w| crate::wm::root(w).as_ref() == Some(&root))
61+ });
62+ if let Some(window) = window {
63+ window.on_commit();
64+ }
65+ self.count_commit(&root);
66+ if let Some(id) = self.window_id(&root) {
67+ self.refresh_identity(id);
68+ if self.services.captures.wants_damage() {
69+ self.services.captures.window_damaged(id);
70+ }
71+ }
72+ }
73+ if let Some(i) = self
74+ .wm
75+ .unmapped
76+ .iter()
77+ .position(|w| w.toplevel().is_some_and(|t| t.wl_surface() == surface))
78+ {
79+ let window = self.wm.unmapped.remove(i);
80+ let toplevel = window.toplevel().cloned();
81+ self.map_window(window, None);
82+ if let Some(t) = toplevel {
83+ t.send_configure();
84+ }
85+ }
86+ self.on_buffer(surface);
87+ self.wm.popups.commit(surface);
88+ if let Some(PopupKind::Xdg(popup)) = self.wm.popups.find_popup(surface)
89+ && !popup.is_initial_configure_sent()
90+ {
91+ let _ = popup.send_configure();
92+ }
93+ self.layer_commit(surface);
94+ self.dirty.redraw = true;
95+ }
96+}
97+
98+impl BufferHandler for State {
99+ fn buffer_destroyed(&mut self, _buffer: &WlBuffer) {}
100+}
101+
102+impl ShmHandler for State {
103+ fn shm_state(&self) -> &ShmState {
104+ &self.protocols.shm_state
105+ }
106+}
107+
108+impl DmabufHandler for State {
109+ fn dmabuf_state(&mut self) -> &mut DmabufState {
110+ &mut self.protocols.dmabuf_state
111+ }
112+
113+ fn dmabuf_imported(
114+ &mut self,
115+ _global: &DmabufGlobal,
116+ dmabuf: Dmabuf,
117+ notifier: ImportNotifier,
118+ ) {
119+ self.import_for(&dmabuf, notifier);
120+ }
121+}
122+
123+impl DrmSyncobjHandler for State {
124+ fn drm_syncobj_state(&mut self) -> Option<&mut DrmSyncobjState> {
125+ self.backend.syncobj_mut()
126+ }
127+}
128+
129+/// Holds a dmabuf commit until the buffer can be read without the renderer
130+/// stalling on the client's GPU work: until its explicit acquire point
131+/// signals, or for implicitly synced buffers until their writes finish.
132+fn hold_until_readable(state: &mut State, surface: &WlSurface) {
133+ stage_shm(state, surface);
134+ let (acquire, dmabuf) = with_states(surface, |s| {
135+ let acquire = s
136+ .cached_state
137+ .get::<DrmSyncobjCachedState>()
138+ .pending()
139+ .acquire_point
140+ .clone();
141+ let dmabuf = match s
142+ .cached_state
143+ .get::<SurfaceAttributes>()
144+ .pending()
145+ .buffer
146+ .as_ref()
147+ {
148+ Some(BufferAssignment::NewBuffer(buffer)) => get_dmabuf(buffer).cloned().ok(),
149+ _ => None,
150+ };
151+ (acquire, dmabuf)
152+ });
153+ let (Some(dmabuf), Some(client)) = (dmabuf, surface.client()) else {
154+ return;
155+ };
156+ if let Some(blocker) = state.import_blocker(&dmabuf, client.clone()) {
157+ add_blocker(surface, blocker);
158+ }
159+ if let Some(point) = acquire
160+ && let Ok((blocker, source)) = point.generate_blocker()
161+ {
162+ let inserted = state.core.handle.insert_source(source, move |_, _, state| {
163+ let dh = state.core.display.clone();
164+ state
165+ .client_compositor_state(&client)
166+ .blocker_cleared(state, &dh);
167+ Ok(())
168+ });
169+ if inserted.is_ok() {
170+ add_blocker(surface, blocker);
171+ }
172+ return;
173+ }
174+ if let Ok((blocker, source)) = dmabuf.generate_blocker(Interest::READ) {
175+ let inserted = state.core.handle.insert_source(source, move |_, _, state| {
176+ let dh = state.core.display.clone();
177+ state
178+ .client_compositor_state(&client)
179+ .blocker_cleared(state, &dh);
180+ Ok(())
181+ });
182+ if inserted.is_ok() {
183+ add_blocker(surface, blocker);
184+ }
185+ }
186+}
187+
188+/// Copies a committed shm buffer's damage on the worker, holding the commit
189+/// until it is staged. Surface damage equals buffer damage at scale 1 with no
190+/// transform, otherwise the whole buffer is staged so it covers whatever the
191+/// import uploads.
192+fn stage_shm(state: &mut State, surface: &WlSurface) {
193+ let job = with_states(surface, |s| {
194+ let mut cached = s.cached_state.get::<SurfaceAttributes>();
195+ let attrs = cached.pending();
196+ let Some(BufferAssignment::NewBuffer(buffer)) = attrs.buffer.as_ref() else {
197+ return None;
198+ };
199+ if get_dmabuf(buffer).is_ok() {
200+ return None;
201+ }
202+ let plain = attrs.buffer_scale == 1 && attrs.buffer_transform == Transform::Normal.into();
203+ let region = attrs
204+ .damage
205+ .iter()
206+ .map(|d| match d {
207+ Damage::Buffer(r) => *r,
208+ Damage::Surface(r) => {
209+ Rectangle::new((r.loc.x, r.loc.y).into(), (r.size.w, r.size.h).into())
210+ }
211+ })
212+ .reduce(|a, b| a.merge(b))
213+ .filter(|_| plain);
214+ Some((buffer.clone(), region))
215+ });
216+ let (Some((buffer, region)), Some(client)) = (job, surface.client()) else {
217+ return;
218+ };
219+ let Ok(size) = with_buffer_contents(&buffer, |_, _, d| (d.width, d.height)) else {
220+ return;
221+ };
222+ if let Some(blocker) = state.stage_blocker(&buffer, region, size, client) {
223+ add_blocker(surface, blocker);
224+ }
225+}
Modified file: crates/monolith/src/drm.rs+4 additions−76 deletions
@@ -37,7 +37,7 @@ use smithay::reexports::rustix::fs::OFlags;
3737 use smithay::utils::{DeviceFd, Point, Transform};
3838 use smithay::wayland::drm_syncobj::{DrmSyncobjState, supports_syncobj_eventfd};
3939 use smithay_drm_extras::drm_scanner::{DrmScanEvent, DrmScanner};
40-use std::collections::{HashMap, VecDeque};
40+use std::collections::HashMap;
4141 use std::path::{Path, PathBuf};
4242 use std::time::Duration;
4343
@@ -78,7 +78,7 @@ struct Surface {
7878 scheduled: Option<RegistrationToken>,
7979 /// The vblank the frame being drawn or queued is meant for.
8080 target_ns: Option<u64>,
81- budget: Budget,
81+ budget: crate::pacing::Budget,
8282 /// Frames presented since `stats_since`, for the once-a-second debug line.
8383 presented: u32,
8484 stats_since: u64,
@@ -92,79 +92,7 @@ impl Surface {
9292 /// The first vblank a render starting at `now` can still make, and the
9393 /// latest moment to start it.
9494 fn next_deadline(&self, now: u64) -> Option<(u64, u64)> {
95- let last = self.last_vblank_ns?;
96- let refresh = self.refresh_ns;
97- if refresh == 0 {
98- return None;
99- }
100- let budget = self.budget.ns().min(refresh);
101- let periods = (now + budget).saturating_sub(last).div_ceil(refresh).max(1);
102- let vblank = last + periods * refresh;
103- Some((vblank, vblank - budget))
104- }
105-}
106-
107-/// How long before a vblank rendering must start. A high percentile of
108-/// recent CPU render times covers the CPU side without one spike inflating
109-/// it, and a slack that grows on every missed vblank and shrinks over seconds
110-/// while frames land covers GPU time and the commit.
111-struct Budget {
112- renders: VecDeque<u64>,
113- slack_ns: u64,
114- /// The slack that last proved enough after a miss. Decay stops there
115- /// for `floor_frames` frames, so it does not keep rediscovering the same
116- /// limit one dropped frame at a time.
117- floor_ns: u64,
118- floor_frames: u64,
119-}
120-
121-impl Budget {
122- const KEEP: usize = 32;
123- const MIN_SLACK_NS: u64 = 1_000_000;
124- const STEP_NS: u64 = 500_000;
125- const DECAY_NS: u64 = 1_000;
126- const FLOOR_NS: u64 = 30_000_000_000;
127-
128- fn new() -> Self {
129- Self {
130- renders: VecDeque::with_capacity(Self::KEEP),
131- slack_ns: 2_000_000,
132- floor_ns: 0,
133- floor_frames: 0,
134- }
135- }
136-
137- fn ns(&self) -> u64 {
138- let mut renders: Vec<u64> = self.renders.iter().copied().collect();
139- renders.sort_unstable();
140- let p90 = renders
141- .get(renders.len().saturating_sub(1) * 9 / 10)
142- .copied()
143- .unwrap_or(0);
144- p90 + self.slack_ns
145- }
146-
147- fn record(&mut self, render_ns: u64) {
148- if self.renders.len() == Self::KEEP {
149- self.renders.pop_front();
150- }
151- self.renders.push_back(render_ns);
152- }
153-
154- fn missed(&mut self, refresh_ns: u64) {
155- self.slack_ns = (self.slack_ns + Self::STEP_NS).min(refresh_ns / 2);
156- self.floor_ns = self.slack_ns;
157- self.floor_frames = Self::FLOOR_NS / refresh_ns.max(1);
158- }
159-
160- fn landed(&mut self) {
161- self.floor_frames = self.floor_frames.saturating_sub(1);
162- let floor = if self.floor_frames > 0 {
163- self.floor_ns
164- } else {
165- Self::MIN_SLACK_NS
166- };
167- self.slack_ns = self.slack_ns.saturating_sub(Self::DECAY_NS).max(floor);
95+ crate::pacing::deadline(self.last_vblank_ns?, self.refresh_ns, self.budget.ns(), now)
16896 }
16997 }
17098
@@ -424,7 +352,7 @@ fn connect(state: &mut State, connector: connector::Info, crtc: crtc::Handle) {
424352 last_vblank_ns: None,
425353 scheduled: None,
426354 target_ns: None,
427- budget: Budget::new(),
355+ budget: crate::pacing::Budget::new(),
428356 presented: 0,
429357 stats_since: 0,
430358 vrr_capable,
Modified file: crates/monolith/src/globals.rs+2 additions
@@ -97,3 +97,5 @@ impl Protocols {
9797 }
9898 }
9999 }
100+
101+smithay::delegate_dispatch2!(State);
Deleted file: crates/monolith/src/handlers.rs−463 deletions
@@ -1,463 +0,0 @@
1-use crate::state::{ClientState, State};
2-use smithay::backend::allocator::dmabuf::Dmabuf;
3-use smithay::backend::renderer::utils::{import_surface_tree, on_commit_buffer_handler};
4-use smithay::desktop::{PopupKind, Window};
5-use smithay::input::dnd::{DndGrabHandler, GrabType, Source};
6-use smithay::input::pointer::CursorImageStatus;
7-use smithay::input::{Seat, SeatHandler, SeatState};
8-use smithay::reexports::calloop::Interest;
9-use smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer;
10-use smithay::reexports::wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode as DecorationMode;
11-use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
12-use smithay::reexports::wayland_server::protocol::wl_seat::WlSeat;
13-use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
14-use smithay::reexports::wayland_server::{Client, Resource};
15-use smithay::utils::Serial;
16-use smithay::wayland::buffer::BufferHandler;
17-use smithay::wayland::compositor::{
18- BufferAssignment, CompositorClientState, CompositorHandler, CompositorState, Damage,
19- SurfaceAttributes, add_blocker, add_pre_commit_hook, get_parent, is_sync_subsurface,
20- with_states,
21-};
22-use smithay::wayland::drm_syncobj::{DrmSyncobjCachedState, DrmSyncobjHandler, DrmSyncobjState};
23-use smithay::wayland::dmabuf::{
24- DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier, get_dmabuf,
25-};
26-use smithay::output::Output;
27-use smithay::wayland::output::OutputHandler;
28-use smithay::wayland::seat::WaylandFocus;
29-use smithay::wayland::selection::SelectionHandler;
30-use smithay::wayland::selection::primary_selection::{
31- PrimarySelectionHandler, PrimarySelectionState, set_primary_focus,
32-};
33-use smithay::wayland::selection::data_device::{
34- DataDeviceHandler, DataDeviceState, WaylandDndGrabHandler, set_data_device_focus,
35-};
36-use smithay::wayland::shell::xdg::decoration::XdgDecorationHandler;
37-use smithay::wayland::shell::xdg::{
38- PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState,
39-};
40-use monolith_protocols::workspace::{Action, WorkspaceHandler, WorkspaceState};
41-use smithay::utils::{Rectangle, Transform};
42-use smithay::wayland::shm::{ShmHandler, ShmState, with_buffer_contents};
43-
44-impl CompositorHandler for State {
45- fn compositor_state(&mut self) -> &mut CompositorState {
46- &mut self.protocols.compositor_state
47- }
48-
49- fn new_surface(&mut self, surface: &WlSurface) {
50- add_pre_commit_hook::<Self, _>(surface, |state, _dh, surface| {
51- hold_until_readable(state, surface);
52- });
53- }
54-
55- fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState {
56- if let Some(x) = client.get_data::<smithay::xwayland::XWaylandClientData>() {
57- return &x.compositor_state;
58- }
59- &client
60- .get_data::<ClientState>()
61- .expect("every client has ClientState")
62- .compositor_state
63- }
64-
65- fn commit(&mut self, surface: &WlSurface) {
66- on_commit_buffer_handler::<Self>(surface);
67- // Importing at commit keeps first-time dmabuf imports, which take
68- // milliseconds on NVIDIA, out of the render racing the next vblank.
69- if let Err(e) = import_surface_tree(&mut self.renderer, surface) {
70- tracing::debug!("early import failed: {e}");
71- }
72- if !is_sync_subsurface(surface) {
73- let mut root = surface.clone();
74- while let Some(parent) = get_parent(&root) {
75- root = parent;
76- }
77- let mapped = self.wm.id_of(&root).and_then(|id| self.wm.window(id));
78- let window = mapped.or_else(|| {
79- self.wm
80- .unmapped
81- .iter()
82- .find(|w| crate::wm::root(w).as_ref() == Some(&root))
83- });
84- if let Some(window) = window {
85- window.on_commit();
86- }
87- self.count_commit(&root);
88- if let Some(id) = self.window_id(&root) {
89- self.refresh_identity(id);
90- if self.services.captures.wants_damage() {
91- self.services.captures.window_damaged(id);
92- }
93- }
94- }
95- if let Some(i) = self
96- .wm
97- .unmapped
98- .iter()
99- .position(|w| w.toplevel().is_some_and(|t| t.wl_surface() == surface))
100- {
101- let window = self.wm.unmapped.remove(i);
102- let toplevel = window.toplevel().cloned();
103- self.map_window(window, None);
104- if let Some(t) = toplevel {
105- t.send_configure();
106- }
107- }
108- self.on_buffer(surface);
109- self.wm.popups.commit(surface);
110- if let Some(PopupKind::Xdg(popup)) = self.wm.popups.find_popup(surface)
111- && !popup.is_initial_configure_sent()
112- {
113- let _ = popup.send_configure();
114- }
115- self.layer_commit(surface);
116- self.dirty.redraw = true;
117- }
118-}
119-
120-impl BufferHandler for State {
121- fn buffer_destroyed(&mut self, _buffer: &WlBuffer) {}
122-}
123-
124-impl ShmHandler for State {
125- fn shm_state(&self) -> &ShmState {
126- &self.protocols.shm_state
127- }
128-}
129-
130-impl DmabufHandler for State {
131- fn dmabuf_state(&mut self) -> &mut DmabufState {
132- &mut self.protocols.dmabuf_state
133- }
134-
135- fn dmabuf_imported(
136- &mut self,
137- _global: &DmabufGlobal,
138- dmabuf: Dmabuf,
139- notifier: ImportNotifier,
140- ) {
141- self.import_for(&dmabuf, notifier);
142- }
143-}
144-
145-impl XdgShellHandler for State {
146- fn xdg_shell_state(&mut self) -> &mut XdgShellState {
147- &mut self.protocols.xdg_shell_state
148- }
149-
150- fn new_toplevel(&mut self, surface: ToplevelSurface) {
151- self.wm.unmapped.push(Window::new_wayland_window(surface));
152- }
153-
154- fn toplevel_destroyed(&mut self, surface: ToplevelSurface) {
155- self.unmap_window(surface.wl_surface());
156- }
157-
158- fn fullscreen_request(&mut self, surface: ToplevelSurface, _output: Option<WlOutput>) {
159- self.set_fullscreen(surface.wl_surface(), true);
160- }
161-
162- fn unfullscreen_request(&mut self, surface: ToplevelSurface) {
163- self.set_fullscreen(surface.wl_surface(), false);
164- }
165-
166- fn new_popup(&mut self, surface: PopupSurface, positioner: PositionerState) {
167- surface.with_pending_state(|s| s.geometry = positioner.get_geometry());
168- let _ = self.wm.popups.track_popup(PopupKind::Xdg(surface));
169- }
170-
171- fn reposition_request(
172- &mut self,
173- surface: PopupSurface,
174- positioner: PositionerState,
175- token: u32,
176- ) {
177- surface.with_pending_state(|s| {
178- s.geometry = positioner.get_geometry();
179- s.positioner = positioner;
180- });
181- surface.send_repositioned(token);
182- }
183-
184- fn grab(&mut self, _surface: PopupSurface, _seat: WlSeat, _serial: Serial) {}
185-}
186-
187-impl SeatHandler for State {
188- type KeyboardFocus = crate::focus::Focus;
189- type PointerFocus = WlSurface;
190- type TouchFocus = WlSurface;
191-
192- fn seat_state(&mut self) -> &mut SeatState<State> {
193- &mut self.input.seat_state
194- }
195-
196- fn cursor_image(&mut self, _seat: &Seat<Self>, image: CursorImageStatus) {
197- self.input.cursor_status = image;
198- self.dirty.redraw = true;
199- }
200-
201- fn focus_changed(&mut self, seat: &Seat<Self>, focused: Option<&crate::focus::Focus>) {
202- let client = focused
203- .and_then(|f| f.wl_surface())
204- .and_then(|s| self.core.display.get_client(s.id()).ok());
205- set_data_device_focus(&self.core.display, seat, client.clone());
206- set_primary_focus(&self.core.display, seat, client);
207- }
208-}
209-
210-impl SelectionHandler for State {
211- type SelectionUserData = ();
212-}
213-
214-impl DataDeviceHandler for State {
215- fn data_device_state(&mut self) -> &mut DataDeviceState {
216- &mut self.protocols.data_device_state
217- }
218-}
219-
220-impl PrimarySelectionHandler for State {
221- fn primary_selection_state(&mut self) -> &mut PrimarySelectionState {
222- &mut self.protocols.primary_selection_state
223- }
224-}
225-
226-impl smithay::wayland::selection::wlr_data_control::DataControlHandler for State {
227- fn data_control_state(
228- &mut self,
229- ) -> &mut smithay::wayland::selection::wlr_data_control::DataControlState {
230- &mut self.protocols.wlr_data_control_state
231- }
232-}
233-
234-impl smithay::wayland::selection::ext_data_control::DataControlHandler for State {
235- fn data_control_state(
236- &mut self,
237- ) -> &mut smithay::wayland::selection::ext_data_control::DataControlState {
238- &mut self.protocols.ext_data_control_state
239- }
240-}
241-
242-impl DndGrabHandler for State {}
243-
244-impl smithay::input::tablet::TabletSeatHandler for State {
245- type ToolFocus = WlSurface;
246-}
247-
248-impl WaylandDndGrabHandler for State {
249- fn dnd_requested<S: Source>(
250- &mut self,
251- source: S,
252- _icon: Option<WlSurface>,
253- _seat: Seat<Self>,
254- _serial: Serial,
255- _type: GrabType,
256- ) {
257- source.cancel();
258- }
259-}
260-
261-impl OutputHandler for State {
262- /// Workspace and toplevel lists name outputs by the client's wl_output,
263- /// so a newly bound one needs announcing.
264- fn output_bound(&mut self, _output: Output, _wl_output: WlOutput) {
265- self.dirty.snapshot = true;
266- }
267-}
268-
269-impl smithay::wayland::pointer_constraints::PointerConstraintsHandler for State {
270- fn new_constraint(
271- &mut self,
272- _surface: &WlSurface,
273- _pointer: &smithay::input::pointer::PointerHandle<Self>,
274- ) {
275- }
276-
277- fn cursor_position_hint(
278- &mut self,
279- _surface: &WlSurface,
280- _pointer: &smithay::input::pointer::PointerHandle<Self>,
281- _location: smithay::utils::Point<f64, smithay::utils::Logical>,
282- ) {
283- }
284-}
285-
286-smithay::delegate_dispatch2!(State);
287-
288-impl DrmSyncobjHandler for State {
289- fn drm_syncobj_state(&mut self) -> Option<&mut DrmSyncobjState> {
290- self.backend.syncobj_mut()
291- }
292-}
293-
294-/// Holds a dmabuf commit until the buffer can be read without the renderer
295-/// stalling on the client's GPU work: until its explicit acquire point
296-/// signals, or for implicitly synced buffers until their writes finish.
297-fn hold_until_readable(state: &mut State, surface: &WlSurface) {
298- stage_shm(state, surface);
299- let (acquire, dmabuf) = with_states(surface, |s| {
300- let acquire = s
301- .cached_state
302- .get::<DrmSyncobjCachedState>()
303- .pending()
304- .acquire_point
305- .clone();
306- let dmabuf = match s
307- .cached_state
308- .get::<SurfaceAttributes>()
309- .pending()
310- .buffer
311- .as_ref()
312- {
313- Some(BufferAssignment::NewBuffer(buffer)) => get_dmabuf(buffer).cloned().ok(),
314- _ => None,
315- };
316- (acquire, dmabuf)
317- });
318- let (Some(dmabuf), Some(client)) = (dmabuf, surface.client()) else {
319- return;
320- };
321- if let Some(blocker) = state.import_blocker(&dmabuf, client.clone()) {
322- add_blocker(surface, blocker);
323- }
324- if let Some(point) = acquire
325- && let Ok((blocker, source)) = point.generate_blocker()
326- {
327- let inserted = state.core.handle.insert_source(source, move |_, _, state| {
328- let dh = state.core.display.clone();
329- state
330- .client_compositor_state(&client)
331- .blocker_cleared(state, &dh);
332- Ok(())
333- });
334- if inserted.is_ok() {
335- add_blocker(surface, blocker);
336- }
337- return;
338- }
339- if let Ok((blocker, source)) = dmabuf.generate_blocker(Interest::READ) {
340- let inserted = state.core.handle.insert_source(source, move |_, _, state| {
341- let dh = state.core.display.clone();
342- state
343- .client_compositor_state(&client)
344- .blocker_cleared(state, &dh);
345- Ok(())
346- });
347- if inserted.is_ok() {
348- add_blocker(surface, blocker);
349- }
350- }
351-}
352-
353-/// Copies a committed shm buffer's damage on the worker, holding the commit
354-/// until it is staged. Surface damage equals buffer damage at scale 1 with no
355-/// transform, otherwise the whole buffer is staged so it covers whatever the
356-/// import uploads.
357-fn stage_shm(state: &mut State, surface: &WlSurface) {
358- let job = with_states(surface, |s| {
359- let mut cached = s.cached_state.get::<SurfaceAttributes>();
360- let attrs = cached.pending();
361- let Some(BufferAssignment::NewBuffer(buffer)) = attrs.buffer.as_ref() else {
362- return None;
363- };
364- if get_dmabuf(buffer).is_ok() {
365- return None;
366- }
367- let plain = attrs.buffer_scale == 1 && attrs.buffer_transform == Transform::Normal.into();
368- let region = attrs
369- .damage
370- .iter()
371- .map(|d| match d {
372- Damage::Buffer(r) => *r,
373- Damage::Surface(r) => {
374- Rectangle::new((r.loc.x, r.loc.y).into(), (r.size.w, r.size.h).into())
375- }
376- })
377- .reduce(|a, b| a.merge(b))
378- .filter(|_| plain);
379- Some((buffer.clone(), region))
380- });
381- let (Some((buffer, region)), Some(client)) = (job, surface.client()) else {
382- return;
383- };
384- let Ok(size) = with_buffer_contents(&buffer, |_, _, d| (d.width, d.height)) else {
385- return;
386- };
387- if let Some(blocker) = state.stage_blocker(&buffer, region, size, client) {
388- add_blocker(surface, blocker);
389- }
390-}
391-
392-impl XdgDecorationHandler for State {
393- fn new_decoration(&mut self, toplevel: ToplevelSurface) {
394- server_side(&toplevel);
395- }
396-
397- fn request_mode(&mut self, toplevel: ToplevelSurface, _mode: DecorationMode) {
398- server_side(&toplevel);
399- }
400-
401- fn unset_mode(&mut self, toplevel: ToplevelSurface) {
402- server_side(&toplevel);
403- }
404-}
405-
406-/// monolith draws no titlebars, and clients are told so whatever they ask for.
407-fn server_side(toplevel: &ToplevelSurface) {
408- toplevel.with_pending_state(|s| s.decoration_mode = Some(DecorationMode::ServerSide));
409- if toplevel.is_initial_configure_sent() {
410- toplevel.send_pending_configure();
411- }
412-}
413-
414-impl State {
415- fn set_fullscreen(&mut self, surface: &WlSurface, on: bool) {
416- if let Some(id) = self.window_id(surface)
417- && self
418- .wm
419- .layout
420- .window(id)
421- .is_some_and(|w| w.fullscreen != on)
422- {
423- self.wm.layout.toggle_fullscreen(id);
424- self.apply_layout();
425- }
426- }
427-}
428-
429-impl WorkspaceHandler for State {
430- fn workspace_state(&mut self) -> &mut WorkspaceState {
431- &mut self.protocols.workspaces
432- }
433-
434- fn workspace_actions(&mut self, actions: Vec<Action>) {
435- for action in actions {
436- let (group, tag) = match &action {
437- Action::Activate { group, workspace } | Action::Deactivate { group, workspace } => {
438- (group, workspace)
439- }
440- Action::Create { group, name } => (group, name),
441- };
442- let Some(o) = self
443- .wm
444- .layout
445- .outputs()
446- .find(|(_, out)| out.name == *group)
447- .map(|(o, _)| o)
448- else {
449- continue;
450- };
451- let t = self.wm.layout.tag(tag);
452- match action {
453- Action::Activate { .. } | Action::Create { .. } => self.wm.layout.view(o, t),
454- Action::Deactivate { .. } => {
455- if self.wm.layout.is_viewed(o, t) {
456- self.wm.layout.toggle_view(o, t);
457- }
458- }
459- }
460- }
461- self.apply_layout();
462- }
463-}
Modified file: crates/monolith/src/main.rs+6 additions−1 deletions
@@ -3,12 +3,12 @@ mod anim;
33 mod backend;
44 mod capture;
55 mod commands;
6+mod commit;
67 mod cursor;
78 mod drm;
89 mod focus;
910 mod frame;
1011 mod globals;
11-mod handlers;
1212 mod idle;
1313 mod import;
1414 mod input;
@@ -17,11 +17,16 @@ mod launch;
1717 mod layers;
1818 mod lock;
1919 mod nested;
20+mod pacing;
2021 mod scene;
22+mod seat;
2123 mod state;
2224 mod stats;
2325 mod toplevels;
26+mod windows;
2427 mod wm;
28+mod workspaces;
29+mod xdg;
2530 mod xwayland;
2631
2732 #[cfg(feature = "spans")]
Modified file: crates/monolith/src/nested/mod.rs+2 additions−4 deletions
@@ -63,10 +63,8 @@ impl Host {
6363 /// vblank after `now`.
6464 pub fn predict(&self, now: u64) -> u64 {
6565 match self.last_present_ns.filter(|_| self.monotonic) {
66- Some(last) if self.refresh_ns > 0 && last <= now => {
67- last + ((now - last) / self.refresh_ns + 1) * self.refresh_ns
68- }
69- _ => now,
66+ Some(last) => crate::pacing::next_vblank(last, self.refresh_ns, now),
67+ None => now,
7068 }
7169 }
7270 }
Added file: crates/monolith/src/pacing.rs+87 additions
@@ -0,0 +1,87 @@
1+//! Frame timing that needs no backend state, shared by DRM and nested.
2+
3+use std::collections::VecDeque;
4+
5+/// The first vblank after `last` that a render starting at `now` with
6+/// `budget` to spare can still make, and the latest moment to start it.
7+pub fn deadline(last: u64, refresh: u64, budget: u64, now: u64) -> Option<(u64, u64)> {
8+ if refresh == 0 {
9+ return None;
10+ }
11+ let budget = budget.min(refresh);
12+ let periods = (now + budget).saturating_sub(last).div_ceil(refresh).max(1);
13+ let vblank = last + periods * refresh;
14+ Some((vblank, vblank - budget))
15+}
16+
17+/// The first vblank strictly after `now`.
18+pub fn next_vblank(last: u64, refresh: u64, now: u64) -> u64 {
19+ if refresh == 0 || last > now {
20+ return now;
21+ }
22+ last + ((now - last) / refresh + 1) * refresh
23+}
24+
25+/// How long before a vblank rendering must start. A high percentile of
26+/// recent CPU render times covers the CPU side without one spike inflating
27+/// it, and a slack that grows on every missed vblank and shrinks over seconds
28+/// while frames land covers GPU time and the commit.
29+pub struct Budget {
30+ renders: VecDeque<u64>,
31+ slack_ns: u64,
32+ /// The slack that last proved enough after a miss. Decay stops there
33+ /// for `floor_frames` frames, so it does not keep rediscovering the same
34+ /// limit one dropped frame at a time.
35+ floor_ns: u64,
36+ floor_frames: u64,
37+}
38+
39+impl Budget {
40+ const KEEP: usize = 32;
41+ const MIN_SLACK_NS: u64 = 1_000_000;
42+ const STEP_NS: u64 = 500_000;
43+ const DECAY_NS: u64 = 1_000;
44+ const FLOOR_NS: u64 = 30_000_000_000;
45+
46+ pub fn new() -> Self {
47+ Self {
48+ renders: VecDeque::with_capacity(Self::KEEP),
49+ slack_ns: 2_000_000,
50+ floor_ns: 0,
51+ floor_frames: 0,
52+ }
53+ }
54+
55+ pub fn ns(&self) -> u64 {
56+ let mut renders: Vec<u64> = self.renders.iter().copied().collect();
57+ renders.sort_unstable();
58+ let p90 = renders
59+ .get(renders.len().saturating_sub(1) * 9 / 10)
60+ .copied()
61+ .unwrap_or(0);
62+ p90 + self.slack_ns
63+ }
64+
65+ pub fn record(&mut self, render_ns: u64) {
66+ if self.renders.len() == Self::KEEP {
67+ self.renders.pop_front();
68+ }
69+ self.renders.push_back(render_ns);
70+ }
71+
72+ pub fn missed(&mut self, refresh_ns: u64) {
73+ self.slack_ns = (self.slack_ns + Self::STEP_NS).min(refresh_ns / 2);
74+ self.floor_ns = self.slack_ns;
75+ self.floor_frames = Self::FLOOR_NS / refresh_ns.max(1);
76+ }
77+
78+ pub fn landed(&mut self) {
79+ self.floor_frames = self.floor_frames.saturating_sub(1);
80+ let floor = if self.floor_frames > 0 {
81+ self.floor_ns
82+ } else {
83+ Self::MIN_SLACK_NS
84+ };
85+ self.slack_ns = self.slack_ns.saturating_sub(Self::DECAY_NS).max(floor);
86+ }
87+}
Added file: crates/monolith/src/seat.rs+106 additions
@@ -0,0 +1,106 @@
1+use crate::state::State;
2+use smithay::input::dnd::{DndGrabHandler, GrabType, Source};
3+use smithay::input::pointer::CursorImageStatus;
4+use smithay::input::{Seat, SeatHandler, SeatState};
5+use smithay::reexports::wayland_server::Resource;
6+use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
7+use smithay::utils::Serial;
8+use smithay::wayland::seat::WaylandFocus;
9+use smithay::wayland::selection::SelectionHandler;
10+use smithay::wayland::selection::data_device::{
11+ DataDeviceHandler, DataDeviceState, WaylandDndGrabHandler, set_data_device_focus,
12+};
13+use smithay::wayland::selection::primary_selection::{
14+ PrimarySelectionHandler, PrimarySelectionState, set_primary_focus,
15+};
16+
17+impl SeatHandler for State {
18+ type KeyboardFocus = crate::focus::Focus;
19+ type PointerFocus = WlSurface;
20+ type TouchFocus = WlSurface;
21+
22+ fn seat_state(&mut self) -> &mut SeatState<State> {
23+ &mut self.input.seat_state
24+ }
25+
26+ fn cursor_image(&mut self, _seat: &Seat<Self>, image: CursorImageStatus) {
27+ self.input.cursor_status = image;
28+ self.dirty.redraw = true;
29+ }
30+
31+ fn focus_changed(&mut self, seat: &Seat<Self>, focused: Option<&crate::focus::Focus>) {
32+ let client = focused
33+ .and_then(|f| f.wl_surface())
34+ .and_then(|s| self.core.display.get_client(s.id()).ok());
35+ set_data_device_focus(&self.core.display, seat, client.clone());
36+ set_primary_focus(&self.core.display, seat, client);
37+ }
38+}
39+
40+impl SelectionHandler for State {
41+ type SelectionUserData = ();
42+}
43+
44+impl DataDeviceHandler for State {
45+ fn data_device_state(&mut self) -> &mut DataDeviceState {
46+ &mut self.protocols.data_device_state
47+ }
48+}
49+
50+impl PrimarySelectionHandler for State {
51+ fn primary_selection_state(&mut self) -> &mut PrimarySelectionState {
52+ &mut self.protocols.primary_selection_state
53+ }
54+}
55+
56+impl smithay::wayland::selection::wlr_data_control::DataControlHandler for State {
57+ fn data_control_state(
58+ &mut self,
59+ ) -> &mut smithay::wayland::selection::wlr_data_control::DataControlState {
60+ &mut self.protocols.wlr_data_control_state
61+ }
62+}
63+
64+impl smithay::wayland::selection::ext_data_control::DataControlHandler for State {
65+ fn data_control_state(
66+ &mut self,
67+ ) -> &mut smithay::wayland::selection::ext_data_control::DataControlState {
68+ &mut self.protocols.ext_data_control_state
69+ }
70+}
71+
72+impl DndGrabHandler for State {}
73+
74+impl smithay::input::tablet::TabletSeatHandler for State {
75+ type ToolFocus = WlSurface;
76+}
77+
78+impl WaylandDndGrabHandler for State {
79+ fn dnd_requested<S: Source>(
80+ &mut self,
81+ source: S,
82+ _icon: Option<WlSurface>,
83+ _seat: Seat<Self>,
84+ _serial: Serial,
85+ _type: GrabType,
86+ ) {
87+ source.cancel();
88+ }
89+}
90+
91+impl smithay::wayland::pointer_constraints::PointerConstraintsHandler for State {
92+ fn new_constraint(
93+ &mut self,
94+ _surface: &WlSurface,
95+ _pointer: &smithay::input::pointer::PointerHandle<Self>,
96+ ) {
97+ }
98+
99+ fn cursor_position_hint(
100+ &mut self,
101+ _surface: &WlSurface,
102+ _pointer: &smithay::input::pointer::PointerHandle<Self>,
103+ _location: smithay::utils::Point<f64, smithay::utils::Logical>,
104+ ) {
105+ }
106+}
Modified file: crates/monolith/src/state.rs+5 additions−221 deletions
@@ -1,15 +1,11 @@
1-use crate::anim::{Animator, Ghost};
1+use crate::anim::Animator;
22 use crate::commands::Binds;
33 use crate::focus::Focus;
44 use crate::globals::Protocols;
5-use crate::wm::{Kind, Wm, configure, identity, root};
5+use crate::wm::{Kind, Wm, configure};
66 use monolith_config::Config;
77 use monolith_layout::{Layout, OutputId, Rect, WindowId};
8-use monolith_protocols::workspace::{Group, Snapshot, Workspace};
98 use monolith_render_vk::VulkanRenderer;
10-use monolith_render_vk::VulkanTexture;
11-use smithay::backend::renderer::Renderer;
12-use smithay::backend::renderer::utils::{import_surface_tree, with_renderer_surface_state};
139 use smithay::desktop::utils::under_from_surface_tree;
1410 use smithay::desktop::{Window, WindowSurfaceType};
1511 use smithay::input::keyboard::{Keycode, XkbConfig};
@@ -17,16 +13,15 @@ use smithay::input::pointer::CursorImageStatus;
1713 use smithay::input::{Seat, SeatState};
1814 use smithay::reexports::calloop::LoopHandle;
1915 use smithay::reexports::calloop::generic::Generic;
20-use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
2116 use smithay::reexports::calloop::{EventLoop, Interest, LoopSignal, Mode, PostAction};
2217 use smithay::reexports::wayland_server::backend::{ClientData, ClientId, DisconnectReason};
2318 use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
24-use smithay::reexports::wayland_server::{Display, DisplayHandle, Resource};
25-use smithay::utils::{Clock, Logical, Monotonic, Point, Rectangle, SERIAL_COUNTER, Size};
19+use smithay::reexports::wayland_server::{Display, DisplayHandle};
20+use smithay::utils::{Clock, Logical, Monotonic, Point, SERIAL_COUNTER, Size};
2621 use smithay::wayland::compositor::CompositorClientState;
2722 use smithay::wayland::shell::wlr_layer::Layer;
2823 use smithay::wayland::socket::ListeningSocketSource;
29-use std::collections::{HashMap, HashSet};
24+use std::collections::HashMap;
3025 use std::ffi::OsString;
3126 use std::path::PathBuf;
3227 use std::sync::Arc;
@@ -230,180 +225,6 @@ impl State {
230225 }
231226 }
232227
233- /// Adds a toplevel to the layout on its first commit, where its app_id
234- /// and title are known and the initial configure can carry its size.
235- pub fn map_window(&mut self, window: Window, float: Option<Rectangle<i32, Logical>>) {
236- let (app_id, title) = identity(&window);
237- let rule = self.settings.config.placement(&app_id, &title);
238- let pid = match window.x11_surface() {
239- Some(x) => x.pid(),
240- None => window
241- .toplevel()
242- .and_then(|t| t.wl_surface().client())
243- .and_then(|c| c.get_credentials(&self.core.display).ok())
244- .map(|c| c.pid as u32),
245- };
246- let launch = pid.and_then(|pid| self.services.launches.find(pid));
247- let output = rule
248- .output
249- .as_deref()
250- .and_then(|name| {
251- self.wm
252- .layout
253- .outputs()
254- .find(|(_, o)| o.name == name)
255- .map(|(id, _)| id)
256- })
257- .or(launch.and_then(|l| l.0));
258- let tag = match rule.tag.as_deref() {
259- Some(t) => Some(self.wm.layout.tag(t)),
260- None => launch.map(|l| l.1),
261- };
262- let float = float.map(|r| {
263- Rect::new(
264- f64::from(r.loc.x),
265- f64::from(r.loc.y),
266- f64::from(r.size.w),
267- f64::from(r.size.h),
268- )
269- });
270- let id = self.wm.layout.add_window(output, tag, float);
271- if rule.floating == Some(true) {
272- self.wm.layout.toggle_floating(id);
273- }
274- if rule.sticky == Some(true) {
275- self.wm.layout.toggle_sticky(id);
276- }
277- if let Some(name) = &rule.scratchpad {
278- self.wm.layout.scratchpad_add(id, name);
279- }
280- let by_rule = rule.tag.is_some() || rule.output.is_some();
281- self.wm.insert(id, window, by_rule, app_id, title);
282- self.await_first_buffer(id);
283- if self.wm.layout.visible(id) {
284- self.wm.layout.focus(id);
285- }
286- self.apply_layout();
287- }
288-
289- /// Holds a new window on its seed until its first buffer lands, so the
290- /// spawn animation never plays over an empty slot. Clients that are slow
291- /// to draw get it anyway after a while.
292- fn await_first_buffer(&mut self, id: WindowId) {
293- let timeout = Timer::from_duration(Duration::from_millis(300));
294- let _ = self.core.handle.insert_source(timeout, move |_, _, state| {
295- state.first_buffer(id);
296- TimeoutAction::Drop
297- });
298- }
299-
300- /// Called on commits, starts the spawn of a window that now has a buffer.
301- pub fn on_buffer(&mut self, surface: &WlSurface) {
302- if !self.wm.any_awaiting() {
303- return;
304- }
305- let has_buffer =
306- with_renderer_surface_state(surface, |s| s.buffer_size().is_some()).unwrap_or(false);
307- if has_buffer && let Some(id) = self.window_id(surface) {
308- self.first_buffer(id);
309- }
310- }
311-
312- fn first_buffer(&mut self, id: WindowId) {
313- let Some(w) = self.wm.get_mut(id) else {
314- return;
315- };
316- w.placed_by_rule = false;
317- if std::mem::take(&mut w.awaiting_buffer) {
318- self.wm.anim.spawned(id, now_ns());
319- self.apply_layout();
320- }
321- }
322-
323- pub fn unmap_window(&mut self, surface: &WlSurface) {
324- self.wm
325- .unmapped
326- .retain(|w| root(w).as_ref() != Some(surface));
327- if let Some(id) = self.window_id(surface) {
328- self.remove_window(id);
329- }
330- }
331-
332- pub fn remove_window(&mut self, id: WindowId) {
333- if let Some(state) = self.wm.remove(id) {
334- self.wm.space.unmap_elem(&state.window);
335- self.leave_ghost(id, &state.window);
336- }
337- self.wm.layout.remove_window(id);
338- self.apply_layout();
339- }
340-
341- /// Keeps a visible tiled window's last frame shrinking to its seed while
342- /// its neighbours close over it.
343- fn leave_ghost(&mut self, id: WindowId, window: &Window) {
344- let Some(o) = self.wm.layout.window(id).and_then(|w| w.output) else {
345- return;
346- };
347- let tiled = self
348- .wm
349- .layout
350- .output(o)
351- .is_some_and(|out| out.tree.contains(id));
352- if !tiled || !self.wm.layout.visible(id) {
353- return;
354- }
355- let Some(seed) = self
356- .wm
357- .layout
358- .arrange_hiding(o, &[id])
359- .tiled
360- .into_iter()
361- .find(|p| p.window == id)
362- else {
363- return;
364- };
365- let Some(surface) = root(window) else {
366- return;
367- };
368- let _ = import_surface_tree(&mut self.renderer, &surface);
369- let context = self.renderer.context_id();
370- let snapshot = with_renderer_surface_state(&surface, |s| {
371- s.texture::<VulkanTexture>(context.clone())
372- .cloned()
373- .map(|t| (t, s.buffer_scale(), s.buffer_transform()))
374- })
375- .flatten();
376- if let Some((texture, scale, transform)) = snapshot {
377- self.wm.anim.add_ghost(Ghost {
378- id,
379- output: o,
380- seed: seed.rect,
381- texture,
382- scale,
383- transform,
384- geometry: window.geometry(),
385- born: now_ns(),
386- });
387- }
388- }
389-
390- /// Picks up a changed title or app id, which clients send at any time.
391- pub fn refresh_identity(&mut self, id: WindowId) {
392- let Some(w) = self.wm.get_mut(id) else {
393- return;
394- };
395- let (app_id, title) = identity(&w.window);
396- if w.app_id != app_id || w.title != title {
397- w.app_id = app_id;
398- w.title = title;
399- self.dirty.snapshot = true;
400- }
401- }
402-
403- pub fn window_id(&self, surface: &WlSurface) -> Option<WindowId> {
404- self.wm.id_of(surface)
405- }
406-
407228 /// Pushes the layout's target geometry into the space and to clients.
408229 pub fn apply_layout(&mut self) {
409230 let now = now_ns();
@@ -556,43 +377,6 @@ impl State {
556377 }
557378 }
558379
559- fn publish_workspaces(&mut self) {
560- let l = &self.wm.layout;
561- let occupied: HashSet<(OutputId, monolith_layout::TagId)> = l
562- .windows()
563- .filter(|(_, w)| w.scratchpad.is_none())
564- .filter_map(|(_, w)| Some((w.output?, w.tag)))
565- .collect();
566- let groups = l
567- .outputs()
568- .filter_map(|(o, _)| {
569- let output = self.backend.output(o)?;
570- // Bars read a tag's existence as having windows, as with
571- // workspaces that come and go, so empty unviewed tags are left out.
572- let workspaces = l
573- .tags()
574- .enumerate()
575- .filter(|&(_, (t, _))| {
576- occupied.contains(&(o, t)) || (t != l.homeless() && l.is_viewed(o, t))
577- })
578- .map(|(i, (t, tag))| Workspace {
579- key: tag.name.clone(),
580- name: tag.name.clone(),
581- coordinates: vec![i as u32],
582- active: l.is_viewed(o, t),
583- urgent: tag.urgent,
584- hidden: false,
585- })
586- .collect();
587- Some(Group { output, workspaces })
588- })
589- .collect();
590- let dh = self.core.display.clone();
591- self.protocols
592- .workspaces
593- .update::<State>(&dh, Snapshot { groups });
594- }
595-
596380 pub fn reconfigure_keyboard(&mut self) {
597381 let kb = self.settings.config.keyboard.clone();
598382 if let Some(keyboard) = self.input.seat.get_keyboard() {
Added file: crates/monolith/src/windows.rs+189 additions
@@ -0,0 +1,189 @@
1+use crate::anim::Ghost;
2+use crate::state::{State, now_ns};
3+use crate::wm::{identity, root};
4+use monolith_layout::{Rect, WindowId};
5+use monolith_render_vk::VulkanTexture;
6+use smithay::backend::renderer::Renderer;
7+use smithay::backend::renderer::utils::{import_surface_tree, with_renderer_surface_state};
8+use smithay::desktop::Window;
9+use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
10+use smithay::reexports::wayland_server::Resource;
11+use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
12+use smithay::utils::{Logical, Rectangle};
13+use std::time::Duration;
14+
15+impl State {
16+ /// Adds a toplevel to the layout on its first commit, where its app_id
17+ /// and title are known and the initial configure can carry its size.
18+ pub fn map_window(&mut self, window: Window, float: Option<Rectangle<i32, Logical>>) {
19+ let (app_id, title) = identity(&window);
20+ let rule = self.settings.config.placement(&app_id, &title);
21+ let pid = match window.x11_surface() {
22+ Some(x) => x.pid(),
23+ None => window
24+ .toplevel()
25+ .and_then(|t| t.wl_surface().client())
26+ .and_then(|c| c.get_credentials(&self.core.display).ok())
27+ .map(|c| c.pid as u32),
28+ };
29+ let launch = pid.and_then(|pid| self.services.launches.find(pid));
30+ let output = rule
31+ .output
32+ .as_deref()
33+ .and_then(|name| {
34+ self.wm
35+ .layout
36+ .outputs()
37+ .find(|(_, o)| o.name == name)
38+ .map(|(id, _)| id)
39+ })
40+ .or(launch.and_then(|l| l.0));
41+ let tag = match rule.tag.as_deref() {
42+ Some(t) => Some(self.wm.layout.tag(t)),
43+ None => launch.map(|l| l.1),
44+ };
45+ let float = float.map(|r| {
46+ Rect::new(
47+ f64::from(r.loc.x),
48+ f64::from(r.loc.y),
49+ f64::from(r.size.w),
50+ f64::from(r.size.h),
51+ )
52+ });
53+ let id = self.wm.layout.add_window(output, tag, float);
54+ if rule.floating == Some(true) {
55+ self.wm.layout.toggle_floating(id);
56+ }
57+ if rule.sticky == Some(true) {
58+ self.wm.layout.toggle_sticky(id);
59+ }
60+ if let Some(name) = &rule.scratchpad {
61+ self.wm.layout.scratchpad_add(id, name);
62+ }
63+ let by_rule = rule.tag.is_some() || rule.output.is_some();
64+ self.wm.insert(id, window, by_rule, app_id, title);
65+ self.await_first_buffer(id);
66+ if self.wm.layout.visible(id) {
67+ self.wm.layout.focus(id);
68+ }
69+ self.apply_layout();
70+ }
71+
72+ /// Holds a new window on its seed until its first buffer lands, so the
73+ /// spawn animation never plays over an empty slot. Clients that are slow
74+ /// to draw get it anyway after a while.
75+ fn await_first_buffer(&mut self, id: WindowId) {
76+ let timeout = Timer::from_duration(Duration::from_millis(300));
77+ let _ = self.core.handle.insert_source(timeout, move |_, _, state| {
78+ state.first_buffer(id);
79+ TimeoutAction::Drop
80+ });
81+ }
82+
83+ /// Called on commits, starts the spawn of a window that now has a buffer.
84+ pub fn on_buffer(&mut self, surface: &WlSurface) {
85+ if !self.wm.any_awaiting() {
86+ return;
87+ }
88+ let has_buffer =
89+ with_renderer_surface_state(surface, |s| s.buffer_size().is_some()).unwrap_or(false);
90+ if has_buffer && let Some(id) = self.window_id(surface) {
91+ self.first_buffer(id);
92+ }
93+ }
94+
95+ fn first_buffer(&mut self, id: WindowId) {
96+ let Some(w) = self.wm.get_mut(id) else {
97+ return;
98+ };
99+ w.placed_by_rule = false;
100+ if std::mem::take(&mut w.awaiting_buffer) {
101+ self.wm.anim.spawned(id, now_ns());
102+ self.apply_layout();
103+ }
104+ }
105+
106+ pub fn unmap_window(&mut self, surface: &WlSurface) {
107+ self.wm
108+ .unmapped
109+ .retain(|w| root(w).as_ref() != Some(surface));
110+ if let Some(id) = self.window_id(surface) {
111+ self.remove_window(id);
112+ }
113+ }
114+
115+ pub fn remove_window(&mut self, id: WindowId) {
116+ if let Some(state) = self.wm.remove(id) {
117+ self.wm.space.unmap_elem(&state.window);
118+ self.leave_ghost(id, &state.window);
119+ }
120+ self.wm.layout.remove_window(id);
121+ self.apply_layout();
122+ }
123+
124+ /// Keeps a visible tiled window's last frame shrinking to its seed while
125+ /// its neighbours close over it.
126+ fn leave_ghost(&mut self, id: WindowId, window: &Window) {
127+ let Some(o) = self.wm.layout.window(id).and_then(|w| w.output) else {
128+ return;
129+ };
130+ let tiled = self
131+ .wm
132+ .layout
133+ .output(o)
134+ .is_some_and(|out| out.tree.contains(id));
135+ if !tiled || !self.wm.layout.visible(id) {
136+ return;
137+ }
138+ let Some(seed) = self
139+ .wm
140+ .layout
141+ .arrange_hiding(o, &[id])
142+ .tiled
143+ .into_iter()
144+ .find(|p| p.window == id)
145+ else {
146+ return;
147+ };
148+ let Some(surface) = root(window) else {
149+ return;
150+ };
151+ let _ = import_surface_tree(&mut self.renderer, &surface);
152+ let context = self.renderer.context_id();
153+ let snapshot = with_renderer_surface_state(&surface, |s| {
154+ s.texture::<VulkanTexture>(context.clone())
155+ .cloned()
156+ .map(|t| (t, s.buffer_scale(), s.buffer_transform()))
157+ })
158+ .flatten();
159+ if let Some((texture, scale, transform)) = snapshot {
160+ self.wm.anim.add_ghost(Ghost {
161+ id,
162+ output: o,
163+ seed: seed.rect,
164+ texture,
165+ scale,
166+ transform,
167+ geometry: window.geometry(),
168+ born: now_ns(),
169+ });
170+ }
171+ }
172+
173+ /// Picks up a changed title or app id, which clients send at any time.
174+ pub fn refresh_identity(&mut self, id: WindowId) {
175+ let Some(w) = self.wm.get_mut(id) else {
176+ return;
177+ };
178+ let (app_id, title) = identity(&w.window);
179+ if w.app_id != app_id || w.title != title {
180+ w.app_id = app_id;
181+ w.title = title;
182+ self.dirty.snapshot = true;
183+ }
184+ }
185+
186+ pub fn window_id(&self, surface: &WlSurface) -> Option<WindowId> {
187+ self.wm.id_of(surface)
188+ }
189+}
Added file: crates/monolith/src/workspaces.rs+91 additions
@@ -0,0 +1,91 @@
1+use crate::state::State;
2+use monolith_layout::OutputId;
3+use monolith_protocols::workspace::{Action, WorkspaceHandler, WorkspaceState};
4+use monolith_protocols::workspace::{Group, Snapshot, Workspace};
5+use smithay::output::Output;
6+use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
7+use smithay::wayland::output::OutputHandler;
8+use std::collections::HashSet;
9+
10+impl OutputHandler for State {
11+ /// Workspace and toplevel lists name outputs by the client's wl_output,
12+ /// so a newly bound one needs announcing.
13+ fn output_bound(&mut self, _output: Output, _wl_output: WlOutput) {
14+ self.dirty.snapshot = true;
15+ }
16+}
17+
18+impl WorkspaceHandler for State {
19+ fn workspace_state(&mut self) -> &mut WorkspaceState {
20+ &mut self.protocols.workspaces
21+ }
22+
23+ fn workspace_actions(&mut self, actions: Vec<Action>) {
24+ for action in actions {
25+ let (group, tag) = match &action {
26+ Action::Activate { group, workspace } | Action::Deactivate { group, workspace } => {
27+ (group, workspace)
28+ }
29+ Action::Create { group, name } => (group, name),
30+ };
31+ let Some(o) = self
32+ .wm
33+ .layout
34+ .outputs()
35+ .find(|(_, out)| out.name == *group)
36+ .map(|(o, _)| o)
37+ else {
38+ continue;
39+ };
40+ let t = self.wm.layout.tag(tag);
41+ match action {
42+ Action::Activate { .. } | Action::Create { .. } => self.wm.layout.view(o, t),
43+ Action::Deactivate { .. } => {
44+ if self.wm.layout.is_viewed(o, t) {
45+ self.wm.layout.toggle_view(o, t);
46+ }
47+ }
48+ }
49+ }
50+ self.apply_layout();
51+ }
52+}
53+
54+impl State {
55+ pub(crate) fn publish_workspaces(&mut self) {
56+ let l = &self.wm.layout;
57+ let occupied: HashSet<(OutputId, monolith_layout::TagId)> = l
58+ .windows()
59+ .filter(|(_, w)| w.scratchpad.is_none())
60+ .filter_map(|(_, w)| Some((w.output?, w.tag)))
61+ .collect();
62+ let groups = l
63+ .outputs()
64+ .filter_map(|(o, _)| {
65+ let output = self.backend.output(o)?;
66+ // Bars read a tag's existence as having windows, as with
67+ // workspaces that come and go, so empty unviewed tags are left out.
68+ let workspaces = l
69+ .tags()
70+ .enumerate()
71+ .filter(|&(_, (t, _))| {
72+ occupied.contains(&(o, t)) || (t != l.homeless() && l.is_viewed(o, t))
73+ })
74+ .map(|(i, (t, tag))| Workspace {
75+ key: tag.name.clone(),
76+ name: tag.name.clone(),
77+ coordinates: vec![i as u32],
78+ active: l.is_viewed(o, t),
79+ urgent: tag.urgent,
80+ hidden: false,
81+ })
82+ .collect();
83+ Some(Group { output, workspaces })
84+ })
85+ .collect();
86+ let dh = self.core.display.clone();
87+ self.protocols
88+ .workspaces
89+ .update::<State>(&dh, Snapshot { groups });
90+ }
91+}
Added file: crates/monolith/src/xdg.rs+90 additions
@@ -0,0 +1,90 @@
1+use crate::state::State;
2+use smithay::desktop::{PopupKind, Window};
3+use smithay::reexports::wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode as DecorationMode;
4+use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
5+use smithay::reexports::wayland_server::protocol::wl_seat::WlSeat;
6+use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
7+use smithay::utils::Serial;
8+use smithay::wayland::shell::xdg::decoration::XdgDecorationHandler;
9+use smithay::wayland::shell::xdg::{
10+ PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState,
11+};
12+
13+impl XdgShellHandler for State {
14+ fn xdg_shell_state(&mut self) -> &mut XdgShellState {
15+ &mut self.protocols.xdg_shell_state
16+ }
17+
18+ fn new_toplevel(&mut self, surface: ToplevelSurface) {
19+ self.wm.unmapped.push(Window::new_wayland_window(surface));
20+ }
21+
22+ fn toplevel_destroyed(&mut self, surface: ToplevelSurface) {
23+ self.unmap_window(surface.wl_surface());
24+ }
25+
26+ fn fullscreen_request(&mut self, surface: ToplevelSurface, _output: Option<WlOutput>) {
27+ self.set_fullscreen(surface.wl_surface(), true);
28+ }
29+
30+ fn unfullscreen_request(&mut self, surface: ToplevelSurface) {
31+ self.set_fullscreen(surface.wl_surface(), false);
32+ }
33+
34+ fn new_popup(&mut self, surface: PopupSurface, positioner: PositionerState) {
35+ surface.with_pending_state(|s| s.geometry = positioner.get_geometry());
36+ let _ = self.wm.popups.track_popup(PopupKind::Xdg(surface));
37+ }
38+
39+ fn reposition_request(
40+ &mut self,
41+ surface: PopupSurface,
42+ positioner: PositionerState,
43+ token: u32,
44+ ) {
45+ surface.with_pending_state(|s| {
46+ s.geometry = positioner.get_geometry();
47+ s.positioner = positioner;
48+ });
49+ surface.send_repositioned(token);
50+ }
51+
52+ fn grab(&mut self, _surface: PopupSurface, _seat: WlSeat, _serial: Serial) {}
53+}
54+
55+impl XdgDecorationHandler for State {
56+ fn new_decoration(&mut self, toplevel: ToplevelSurface) {
57+ server_side(&toplevel);
58+ }
59+
60+ fn request_mode(&mut self, toplevel: ToplevelSurface, _mode: DecorationMode) {
61+ server_side(&toplevel);
62+ }
63+
64+ fn unset_mode(&mut self, toplevel: ToplevelSurface) {
65+ server_side(&toplevel);
66+ }
67+}
68+
69+/// monolith draws no titlebars, and clients are told so whatever they ask for.
70+fn server_side(toplevel: &ToplevelSurface) {
71+ toplevel.with_pending_state(|s| s.decoration_mode = Some(DecorationMode::ServerSide));
72+ if toplevel.is_initial_configure_sent() {
73+ toplevel.send_pending_configure();
74+ }
75+}
76+
77+impl State {
78+ fn set_fullscreen(&mut self, surface: &WlSurface, on: bool) {
79+ if let Some(id) = self.window_id(surface)
80+ && self
81+ .wm
82+ .layout
83+ .window(id)
84+ .is_some_and(|w| w.fullscreen != on)
85+ {
86+ self.wm.layout.toggle_fullscreen(id);
87+ self.apply_layout();
88+ }
89+ }
90+}