atagen

monolith

public
Star0
Watch0

Commit ba17141501b4

tarball

atagen <boss@atagen.co>2026-10-17 03:37 UTC

8 files changed+84-7
monolith: stop asking hidden surfaces to draw under fullscreen

Windows and lower layers covered by a fullscreen window kept getting frame callbacks, so any that animate kept committing and held the output at full refresh. Under VRR that defeats the point, since the panel should follow the fullscreen client alone. A debug line now names the clients committing most each second, and the frame rate line no longer invents a vblank count from the nominal refresh while VRR is on.
Modified file: crates/monolith/src/drm.rs+5 additions−2 deletions
@@ -500,11 +500,14 @@ fn on_vblank(state: &mut State, crtc: crtc::Handle, meta: &mut Option<DrmEventMe
500500 surface.presented += 1;
501501 let window = t.saturating_sub(surface.stats_since);
502502 if window >= 1_000_000_000 {
503- let vblanks = window / surface.refresh_ns.max(1);
503+ // Under VRR the panel waits for each flip, so the nominal refresh
504+ // says nothing about how many vblanks there were.
505+ let vblanks = (!surface.vrr_on).then(|| window / surface.refresh_ns.max(1));
504506 tracing::debug!(
505507 output = surface.output.name(),
506508 presented = surface.presented,
507509 vblanks,
510+ vrr = surface.vrr_on,
508511 budget_us = surface.budget.ns() / 1000,
509512 "frame rate"
510513 );
@@ -715,7 +718,7 @@ fn render_surface(state: &mut State, crtc: crtc::Handle, target: u64) -> bool {
715718 }
716719 surface.redraw = animating;
717720 let elapsed = core.start.elapsed();
718- crate::frame::send_frame_callbacks(wm, lock, &surface.output, elapsed);
721+ crate::frame::send_frame_callbacks(wm, lock, surface.id, &surface.output, now, elapsed);
719722 animating && !surface.queued
720723 }
721724
Modified file: crates/monolith/src/frame.rs+22 additions−3 deletions
@@ -60,16 +60,35 @@ pub fn compose(
6060 elements
6161 }
6262
63-/// Lets clients shown on `output` draw their next frame.
64-pub fn send_frame_callbacks(wm: &Wm, lock: &Lock, output: &Output, elapsed: Duration) {
63+/// Lets clients shown on `output` draw their next frame. Under a fullscreen
64+/// window at rest only it and the overlay layer are shown, and anything
65+/// else that kept drawing would hold the output, or a VRR panel, at full
66+/// refresh for frames nobody sees.
67+pub fn send_frame_callbacks(
68+ wm: &Wm,
69+ lock: &Lock,
70+ o: OutputId,
71+ output: &Output,
72+ now: u64,
73+ elapsed: Duration,
74+) {
6575 if lock.active() {
6676 crate::lock::send_frames(lock, output, elapsed);
6777 return;
6878 }
79+ let covering = wm
80+ .layout
81+ .arrange(o)
82+ .fullscreen
83+ .filter(|&w| wm.anim.gliding(w, now).is_none())
84+ .and_then(|w| wm.window(w));
6985 for window in wm.space.elements_for_output(output) {
86+ if covering.is_some_and(|c| c != window) {
87+ continue;
88+ }
7089 window.send_frame(output, elapsed, Some(Duration::ZERO), |_, _| {
7190 Some(output.clone())
7291 });
7392 }
74- crate::layers::send_frames(output, elapsed);
93+ crate::layers::send_frames(output, elapsed, covering.is_some());
7594 }
Modified file: crates/monolith/src/handlers.rs+1 additions
@@ -83,6 +83,7 @@ impl CompositorHandler for State {
8383 if let Some(window) = window {
8484 window.on_commit();
8585 }
86+ self.count_commit(&root);
8687 if let Some(id) = self.window_id(&root) {
8788 self.refresh_identity(id);
8889 if self.services.captures.wants_damage() {
Modified file: crates/monolith/src/layers.rs+6 additions−1 deletions
@@ -210,8 +210,13 @@ pub fn elements(out: &mut Vec<Draw>, renderer: &mut VulkanRenderer, output: &Out
210210 }
211211 }
212212
213-pub fn send_frames(output: &Output, elapsed: Duration) {
213+/// With `covered`, a fullscreen window hides everything below the overlay
214+/// layer, so only overlay surfaces are asked to draw.
215+pub fn send_frames(output: &Output, elapsed: Duration, covered: bool) {
214216 for l in layer_map_for_output(output).layers() {
217+ if covered && l.layer() != Layer::Overlay {
218+ continue;
219+ }
215220 l.send_frame(output, elapsed, Some(Duration::ZERO), |_, _| {
216221 Some(output.clone())
217222 });
Modified file: crates/monolith/src/main.rs+1 additions
@@ -19,6 +19,7 @@ mod lock;
1919 mod nested;
2020 mod scene;
2121 mod state;
22+mod stats;
2223 mod toplevels;
2324 mod wm;
2425 mod xwayland;
Modified file: crates/monolith/src/nested/mod.rs+1 additions−1 deletions
@@ -259,7 +259,7 @@ pub fn render(state: &mut State) {
259259 dirty.redraw = wm.anim.active(now);
260260 lock.shown(&host.output);
261261 let elapsed = core.start.elapsed();
262- crate::frame::send_frame_callbacks(wm, lock, &host.output, elapsed);
262+ crate::frame::send_frame_callbacks(wm, lock, host.output_id, &host.output, now, elapsed);
263263 if damage.is_some() {
264264 captures.output_damaged(&host.output);
265265 }
Modified file: crates/monolith/src/state.rs+2 additions
@@ -85,6 +85,7 @@ pub struct Services {
8585 pub launches: crate::launch::Launches,
8686 pub ipc: crate::ipc::Server,
8787 pub idle: crate::idle::Idle,
88+ pub commits: crate::stats::CommitStats,
8889 }
8990
9091 /// The config and what is derived from it, swapped whole on reload.
@@ -183,6 +184,7 @@ impl State {
183184 launches: Default::default(),
184185 ipc: Default::default(),
185186 idle,
187+ commits: Default::default(),
186188 },
187189 })
188190 }
Added file: crates/monolith/src/stats.rs+46 additions
@@ -0,0 +1,46 @@
1+//! Which clients commit most, logged once a second at debug level. Every
2+//! visible commit costs a frame, so this names whoever keeps an idle
3+//! desktop, or a VRR panel, at full refresh.
4+
5+use crate::state::{State, now_ns};
6+use smithay::desktop::{WindowSurfaceType, layer_map_for_output};
7+use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
8+use std::collections::HashMap;
9+
10+#[derive(Default)]
11+pub struct CommitStats {
12+ counts: HashMap<String, u32>,
13+ since: u64,
14+}
15+
16+impl State {
17+ /// Counts a commit on `root` against the window or layer it belongs to.
18+ pub fn count_commit(&mut self, root: &WlSurface) {
19+ if !tracing::enabled!(tracing::Level::DEBUG) {
20+ return;
21+ }
22+ let label = match self.wm.id_of(root).and_then(|id| self.wm.get(id)) {
23+ Some(w) => w.app_id.clone(),
24+ None => self
25+ .wm
26+ .space
27+ .outputs()
28+ .find_map(|o| {
29+ let map = layer_map_for_output(o);
30+ let layer = map.layer_for_surface(root, WindowSurfaceType::TOPLEVEL)?;
31+ Some(format!("layer:{}", layer.namespace()))
32+ })
33+ .unwrap_or_else(|| "other".into()),
34+ };
35+ let stats = &mut self.services.commits;
36+ *stats.counts.entry(label).or_default() += 1;
37+ let now = now_ns();
38+ if now.saturating_sub(stats.since) >= 1_000_000_000 {
39+ let mut top: Vec<(String, u32)> = stats.counts.drain().collect();
40+ top.sort_by_key(|e| std::cmp::Reverse(e.1));
41+ top.truncate(3);
42+ tracing::debug!(?top, "commits per second");
43+ stats.since = now;
44+ }
45+ }
46+}