atagen

monolith

public
Star0
Watch0

Commit 867b15d7c12f

tarball

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

6 files changed+115-87
monolith: share one frame path between the backends and capture

DRM, nested and screen capture each assembled cursor plus lock-or-scene and sent frame callbacks their own way, and had already drifted. Nested sent callbacks to every window rather than those on its output. compose and send_frame_callbacks are now the only copies. Animation pruning moved out of each output's render into after_dispatch at the current time. With two outputs at different rates, the one rendering later could otherwise prune state the other still had to draw.
Modified file: crates/monolith/src/capture.rs+14 additions−35 deletions
@@ -1,6 +1,7 @@
11 //! ext-image-copy-capture of outputs and single windows, for grim and screen
22 //! sharing portals.
33
4+use crate::frame::{CLEAR, FrameCtx};
45 use crate::scene::Draw;
56 use crate::state::{State, now_ns};
67 use monolith_layout::{OutputId, WindowId};
@@ -204,7 +205,19 @@ impl State {
204205 frame.fail(CaptureFailureReason::Stopped);
205206 return;
206207 };
207- self.output_elements(o, &output, session.draw_cursor())
208+ let ctx = FrameCtx {
209+ renderer: &mut self.renderer,
210+ wm: &mut self.wm,
211+ input: &mut self.input,
212+ lock: &self.services.lock,
213+ deco: &self.settings.config.decoration,
214+ unmanaged: self
215+ .services
216+ .xwayland
217+ .as_ref()
218+ .map_or(&[], |x| &x.unmanaged),
219+ };
220+ crate::frame::compose(ctx, o, &output, now_ns(), session.draw_cursor())
208221 }
209222 Target::Window(id) => match self.wm.window(*id) {
210223 Some(window) => window_elements(&mut self.renderer, window),
@@ -274,38 +287,6 @@ impl State {
274287 Ok(())
275288 }
276289
277- /// Everything output `o` shows, as the backends draw it.
278- pub fn output_elements(&mut self, o: OutputId, output: &Output, cursor: bool) -> Vec<Draw> {
279- let now = now_ns();
280- let mut elements = Vec::new();
281- if cursor && let Some(area) = self.wm.layout.output(o).map(|out| out.area) {
282- let local = self.input.pointer_pos - Point::from((area.x, area.y));
283- if local.x >= 0.0 && local.y >= 0.0 && local.x < area.w && local.y < area.h {
284- elements.extend(self.input.cursor.elements(
285- &mut self.renderer,
286- &self.input.cursor_status,
287- local,
288- ));
289- }
290- }
291- match crate::lock::elements(&self.services.lock, &mut self.renderer, output) {
292- Some(locked) => elements.extend(locked),
293- None => elements.extend(crate::scene::build(
294- &mut self.wm,
295- o,
296- &mut self.renderer,
297- now,
298- &self.settings.config.decoration,
299- Some(output),
300- self.services
301- .xwayland
302- .as_ref()
303- .map_or(&[], |x| &x.unmanaged),
304- )),
305- }
306- elements
307- }
308-
309290 fn output_named(&self, name: &str) -> Option<(OutputId, Output)> {
310291 self.wm
311292 .layout
@@ -325,5 +306,3 @@ fn window_elements(renderer: &mut VulkanRenderer, window: &smithay::desktop::Win
325306
326307 /// Marks a session that has had a frame, so later ones wait for damage.
327308 struct Delivered;
328-
329-const CLEAR: [f32; 4] = [0.07, 0.075, 0.09, 1.0];
Modified file: crates/monolith/src/drm.rs+12 additions−31 deletions
@@ -41,7 +41,7 @@ use std::collections::{HashMap, VecDeque};
4141 use std::path::{Path, PathBuf};
4242 use std::time::Duration;
4343
44-const CLEAR: [f32; 4] = [0.07, 0.075, 0.09, 1.0];
44+use crate::frame::{CLEAR, FrameCtx};
4545 /// Frames that only move the cursor stay at or below 60 Hz under VRR, so the
4646 /// mouse does not override the refresh rate a game is pacing.
4747 const CURSOR_FRAME_NS: u64 = 16_666_667;
@@ -658,30 +658,21 @@ fn render_surface(state: &mut State, crtc: crtc::Handle, target: u64) -> bool {
658658 }
659659 let began = std::time::Instant::now();
660660 let now = target;
661- let Some(area) = wm.layout.output(surface.id).map(|o| o.area) else {
661+ if wm.layout.output(surface.id).is_none() {
662662 return false;
663- };
664- let mut elements = Vec::new();
665- let local = input.pointer_pos - Point::from((area.x, area.y));
666- if local.x >= 0.0 && local.y >= 0.0 && local.x < area.w && local.y < area.h {
667- elements.extend(input.cursor.elements(renderer, &input.cursor_status, local));
668663 }
669664 let scene_began = std::time::Instant::now();
670- match crate::lock::elements(lock, renderer, &surface.output) {
671- Some(locked) => elements.extend(locked),
672- None => elements.extend(crate::scene::build(
673- wm,
674- surface.id,
675- renderer,
676- now,
677- &settings.config.decoration,
678- Some(&surface.output),
679- xwayland.as_ref().map_or(&[], |x| &x.unmanaged),
680- )),
681- }
665+ let ctx = FrameCtx {
666+ renderer: &mut *renderer,
667+ wm: &mut *wm,
668+ input: &mut *input,
669+ lock: &*lock,
670+ deco: &settings.config.decoration,
671+ unmanaged: xwayland.as_ref().map_or(&[], |x| &x.unmanaged),
672+ };
673+ let elements = crate::frame::compose(ctx, surface.id, &surface.output, now, true);
682674 let scene_ns = scene_began.elapsed().as_nanos() as u64;
683675 let animating = wm.anim.active(now);
684- wm.anim.prune(now);
685676 let render_began = std::time::Instant::now();
686677 let cpu_began = thread_cpu_ns();
687678 let mut queue_ns = 0;
@@ -724,17 +715,7 @@ fn render_surface(state: &mut State, crtc: crtc::Handle, target: u64) -> bool {
724715 }
725716 surface.redraw = animating;
726717 let elapsed = core.start.elapsed();
727- let output = surface.output.clone();
728- if lock.active() {
729- crate::lock::send_frames(lock, &output, elapsed);
730- return animating && !surface.queued;
731- }
732- for window in wm.space.elements_for_output(&output) {
733- window.send_frame(&output, elapsed, Some(Duration::ZERO), |_, _| {
734- Some(output.clone())
735- });
736- }
737- crate::layers::send_frames(&output, elapsed);
718+ crate::frame::send_frame_callbacks(wm, lock, &surface.output, elapsed);
738719 animating && !surface.queued
739720 }
740721
Added file: crates/monolith/src/frame.rs+75 additions
@@ -0,0 +1,75 @@
1+//! What every output shows, shared by the backends and screen capture.
2+//! Backends only decide when to draw and how to present.
3+
4+use crate::lock::Lock;
5+use crate::scene::Draw;
6+use crate::state::Input;
7+use crate::wm::Wm;
8+use monolith_config::Decoration;
9+use monolith_layout::OutputId;
10+use monolith_render_vk::VulkanRenderer;
11+use smithay::output::Output;
12+use smithay::utils::Point;
13+use smithay::xwayland::X11Surface;
14+use std::time::Duration;
15+
16+pub const CLEAR: [f32; 4] = [0.07, 0.075, 0.09, 1.0];
17+
18+/// Everything a frame reads, borrowed apart from the rest of the state.
19+pub struct FrameCtx<'a> {
20+ pub renderer: &'a mut VulkanRenderer,
21+ pub wm: &'a mut Wm,
22+ pub input: &'a mut Input,
23+ pub lock: &'a Lock,
24+ pub deco: &'a Decoration,
25+ pub unmanaged: &'a [X11Surface],
26+}
27+
28+/// The elements output `o` shows at `now`, front to back. `cursor` draws the
29+/// pointer, which a nested session leaves to its host.
30+pub fn compose(
31+ ctx: FrameCtx<'_>,
32+ o: OutputId,
33+ output: &Output,
34+ now: u64,
35+ cursor: bool,
36+) -> Vec<Draw> {
37+ let mut elements = Vec::new();
38+ if cursor && let Some(area) = ctx.wm.layout.output(o).map(|out| out.area) {
39+ let local = ctx.input.pointer_pos - Point::from((area.x, area.y));
40+ if local.x >= 0.0 && local.y >= 0.0 && local.x < area.w && local.y < area.h {
41+ elements.extend(ctx.input.cursor.elements(
42+ ctx.renderer,
43+ &ctx.input.cursor_status,
44+ local,
45+ ));
46+ }
47+ }
48+ match crate::lock::elements(ctx.lock, ctx.renderer, output) {
49+ Some(locked) => elements.extend(locked),
50+ None => elements.extend(crate::scene::build(
51+ ctx.wm,
52+ o,
53+ ctx.renderer,
54+ now,
55+ ctx.deco,
56+ Some(output),
57+ ctx.unmanaged,
58+ )),
59+ }
60+ elements
61+}
62+
63+/// Lets clients shown on `output` draw their next frame.
64+pub fn send_frame_callbacks(wm: &Wm, lock: &Lock, output: &Output, elapsed: Duration) {
65+ if lock.active() {
66+ crate::lock::send_frames(lock, output, elapsed);
67+ return;
68+ }
69+ for window in wm.space.elements_for_output(output) {
70+ window.send_frame(output, elapsed, Some(Duration::ZERO), |_, _| {
71+ Some(output.clone())
72+ });
73+ }
74+ crate::layers::send_frames(output, elapsed);
75+}
Modified file: crates/monolith/src/main.rs+1 additions
@@ -6,6 +6,7 @@ mod commands;
66 mod cursor;
77 mod drm;
88 mod focus;
9+mod frame;
910 mod globals;
1011 mod handlers;
1112 mod idle;
Modified file: crates/monolith/src/nested/mod.rs+12 additions−21 deletions
@@ -18,7 +18,6 @@ use smithay::backend::renderer::damage::OutputDamageTracker;
1818 use smithay::output::{Mode, Output, PhysicalProperties, Subpixel};
1919 use smithay::reexports::calloop::EventLoop;
2020 use smithay::utils::{Logical, Size, Transform};
21-use std::time::Duration;
2221 use wayland_client::globals::{GlobalListContents, registry_queue_init};
2322 use wayland_client::protocol::{wl_buffer, wl_callback, wl_compositor, wl_registry, wl_surface};
2423 use wayland_client::{Connection, Dispatch, QueueHandle, delegate_noop};
@@ -31,7 +30,7 @@ use wayland_protocols::xdg::decoration::zv1::client::{
3130 };
3231 use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base};
3332
34-const CLEAR: [f32; 4] = [0.07, 0.075, 0.09, 1.0];
33+use crate::frame::{CLEAR, FrameCtx};
3534 const DEFAULT_SIZE: (i32, i32) = (1280, 800);
3635 const CLOCK_MONOTONIC: u32 = 1;
3736
@@ -175,6 +174,7 @@ pub fn render(state: &mut State) {
175174 backend,
176175 renderer,
177176 wm,
177+ input,
178178 dirty,
179179 core,
180180 settings,
@@ -231,17 +231,15 @@ pub fn render(state: &mut State) {
231231 }
232232 };
233233 let o = host.output_id;
234- let elements = crate::lock::elements(lock, renderer, &host.output).unwrap_or_else(|| {
235- crate::scene::build(
236- wm,
237- o,
238- renderer,
239- now,
240- &settings.config.decoration,
241- Some(&host.output),
242- xwayland.as_ref().map_or(&[], |x| &x.unmanaged),
243- )
244- });
234+ let ctx = FrameCtx {
235+ renderer: &mut *renderer,
236+ wm: &mut *wm,
237+ input: &mut *input,
238+ lock: &*lock,
239+ deco: &settings.config.decoration,
240+ unmanaged: xwayland.as_ref().map_or(&[], |x| &x.unmanaged),
241+ };
242+ let elements = crate::frame::compose(ctx, o, &host.output, now, false);
245243 match host
246244 .damage
247245 .render_output(renderer, &mut fb, slot.age() as usize, &elements, CLEAR)
@@ -259,16 +257,9 @@ pub fn render(state: &mut State) {
259257 }
260258 };
261259 dirty.redraw = wm.anim.active(now);
262- wm.anim.prune(now);
263260 lock.shown(&host.output);
264261 let elapsed = core.start.elapsed();
265- crate::lock::send_frames(lock, &host.output, elapsed);
266- for window in wm.space.elements().filter(|_| !lock.active()) {
267- window.send_frame(&host.output, elapsed, Some(Duration::ZERO), |_, _| {
268- Some(host.output.clone())
269- });
270- }
271- crate::layers::send_frames(&host.output, elapsed);
262+ crate::frame::send_frame_callbacks(wm, lock, &host.output, elapsed);
272263 if damage.is_some() {
273264 captures.output_damaged(&host.output);
274265 }
Modified file: crates/monolith/src/state.rs+1 additions
@@ -193,6 +193,7 @@ impl State {
193193 crate::backend::Backend::Nested(_) => crate::nested::render(self),
194194 crate::backend::Backend::Starting => {}
195195 }
196+ self.wm.anim.prune(now_ns());
196197 self.confirm_lock();
197198 self.complete_captures();
198199 crate::ipc::broadcast(self);