atagen

monolith

public
Star0
Watch0

Commit 6f4f29570be0

tarball

atagen <boss@atagen.co>2026-10-17 01:31 UTC

6 files changed+136-47
monolith: capture single windows through toplevel sources

Portals share a window by capturing the foreign toplevel the user picked. A window capture draws the window and its popups alone, sized to its geometry, and waits for that window's commits rather than an output's damage. When the window resizes, the session's constraints change and the waiting frame fails with buffer_constraints, so the client reallocates instead of getting a cropped image.
Modified file: crates/monolith/src/capture.rs+115 additions−44 deletions
@@ -1,22 +1,25 @@
1-//! ext-image-copy-capture of whole outputs, for grim and screen sharing
2-//! portals.
1+//! ext-image-copy-capture of outputs and single windows, for grim and screen
2+//! sharing portals.
33
44 use crate::scene::Draw;
55 use crate::state::{State, now_ns};
6-use monolith_layout::OutputId;
7-use monolith_render_vk::VulkanImage;
6+use monolith_layout::{OutputId, WindowId};
7+use monolith_render_vk::{VulkanImage, VulkanRenderer};
88 use smithay::backend::allocator::Fourcc;
99 use smithay::backend::allocator::dmabuf::Dmabuf;
1010 use smithay::backend::renderer::damage::OutputDamageTracker;
11+use smithay::backend::renderer::element::AsRenderElements;
12+use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;
1113 use smithay::backend::renderer::{Bind, ExportMem, Offscreen};
1214 use smithay::output::{Output, WeakOutput};
1315 use smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer;
1416 use smithay::reexports::wayland_server::protocol::wl_shm;
15-use smithay::utils::{Point, Rectangle, Size, Transform};
17+use smithay::utils::{Point, Rectangle, Scale, Size, Transform};
1618 use smithay::wayland::dmabuf::get_dmabuf;
19+use smithay::wayland::foreign_toplevel_list::ForeignToplevelHandle;
1720 use smithay::wayland::image_capture_source::{
1821 ImageCaptureSource, ImageCaptureSourceHandler, OutputCaptureSourceHandler,
19- OutputCaptureSourceState,
22+ OutputCaptureSourceState, ToplevelCaptureSourceHandler, ToplevelCaptureSourceState,
2023 };
2124 use smithay::wayland::image_copy_capture::{
2225 BufferConstraints, CaptureFailureReason, DmabufConstraints, Frame, ImageCopyCaptureHandler,
@@ -26,21 +29,38 @@ use smithay::wayland::shm::{shm_format_to_fourcc, with_buffer_contents_mut};
2629 use std::collections::HashSet;
2730 use std::time::Duration;
2831
32+/// What a capture source shows.
33+#[derive(Clone, Debug, PartialEq, Eq, Hash)]
34+enum Target {
35+ Output(String),
36+ Window(WindowId),
37+}
38+
2939 #[derive(Default)]
3040 pub struct Captures {
3141 sessions: Vec<Session>,
32- /// Frames waiting for their output to show something new.
42+ /// Frames waiting for their target to show something new.
3343 waiting: Vec<(SessionRef, Frame)>,
34- damaged: HashSet<String>,
44+ damaged: HashSet<Target>,
3545 }
3646
3747 impl Captures {
3848 /// Records that `output` drew a frame with new content.
39- pub fn damaged(&mut self, output: &Output) {
49+ pub fn output_damaged(&mut self, output: &Output) {
50+ if !self.waiting.is_empty() {
51+ self.damaged.insert(Target::Output(output.name()));
52+ }
53+ }
54+
55+ pub fn window_damaged(&mut self, id: WindowId) {
4056 if !self.waiting.is_empty() {
41- self.damaged.insert(output.name());
57+ self.damaged.insert(Target::Window(id));
4258 }
4359 }
60+
61+ pub fn wants_damage(&self) -> bool {
62+ !self.waiting.is_empty()
63+ }
4464 }
4565
4666 impl ImageCaptureSourceHandler for State {
@@ -57,29 +77,30 @@ impl OutputCaptureSourceHandler for State {
5777 }
5878 }
5979
80+impl ToplevelCaptureSourceHandler for State {
81+ fn toplevel_capture_source_state(&mut self) -> &mut ToplevelCaptureSourceState {
82+ &mut self.toplevel_capture_source_state
83+ }
84+
85+ fn toplevel_source_created(
86+ &mut self,
87+ source: ImageCaptureSource,
88+ toplevel: ForeignToplevelHandle,
89+ ) {
90+ if let Some(id) = self.list_handles.window(&toplevel) {
91+ source.user_data().insert_if_missing(|| id);
92+ }
93+ }
94+}
95+
6096 impl ImageCopyCaptureHandler for State {
6197 fn image_copy_capture_state(&mut self) -> &mut ImageCopyCaptureState {
6298 &mut self.image_copy_capture_state
6399 }
64100
65101 fn capture_constraints(&mut self, source: &ImageCaptureSource) -> Option<BufferConstraints> {
66- let output = source.user_data().get::<WeakOutput>()?.upgrade()?;
67- let mode = output.current_mode()?;
68- let dma = self.render_node.map(|node| {
69- let mut formats: Vec<(Fourcc, Vec<_>)> = Vec::new();
70- for f in self.renderer.render_formats().iter() {
71- match formats.iter_mut().find(|(c, _)| *c == f.code) {
72- Some((_, mods)) => mods.push(f.modifier),
73- None => formats.push((f.code, vec![f.modifier])),
74- }
75- }
76- DmabufConstraints { node, formats }
77- });
78- Some(BufferConstraints {
79- size: (mode.size.w, mode.size.h).into(),
80- shm: vec![wl_shm::Format::Argb8888, wl_shm::Format::Xrgb8888],
81- dma,
82- })
102+ let size = self.target_size(&self.target(source)?)?;
103+ Some(self.constraints(size))
83104 }
84105
85106 fn new_session(&mut self, session: Session) {
@@ -103,7 +124,7 @@ impl ImageCopyCaptureHandler for State {
103124 }
104125
105126 impl State {
106- /// Completes waiting frames on outputs that drew something new.
127+ /// Completes waiting frames whose target showed something new.
107128 pub fn complete_captures(&mut self) {
108129 if self.captures.damaged.is_empty() {
109130 return;
@@ -112,7 +133,8 @@ impl State {
112133 let (ready, waiting) = std::mem::take(&mut self.captures.waiting)
113134 .into_iter()
114135 .partition::<Vec<_>, _>(|(s, _)| {
115- source_output(&s.source()).is_some_and(|o| damaged.contains(&o.name()))
136+ self.target(&s.source())
137+ .is_some_and(|t| damaged.contains(&t))
116138 });
117139 self.captures.waiting = waiting;
118140 for (session, frame) in ready {
@@ -120,24 +142,69 @@ impl State {
120142 }
121143 }
122144
145+ fn target(&self, source: &ImageCaptureSource) -> Option<Target> {
146+ let data = source.user_data();
147+ if let Some(output) = data.get::<WeakOutput>() {
148+ return Some(Target::Output(output.upgrade()?.name()));
149+ }
150+ let id = *data.get::<WindowId>()?;
151+ self.windows.contains_key(&id).then_some(Target::Window(id))
152+ }
153+
154+ fn target_size(&self, target: &Target) -> Option<(i32, i32)> {
155+ let size: (i32, i32) = match target {
156+ Target::Output(name) => self.output_named(name)?.1.current_mode()?.size.into(),
157+ Target::Window(id) => self.windows.get(id)?.geometry().size.into(),
158+ };
159+ (size.0 > 0 && size.1 > 0).then_some(size)
160+ }
161+
162+ fn constraints(&self, size: (i32, i32)) -> BufferConstraints {
163+ let dma = self.render_node.map(|node| {
164+ let mut formats: Vec<(Fourcc, Vec<_>)> = Vec::new();
165+ for f in self.renderer.render_formats().iter() {
166+ match formats.iter_mut().find(|(c, _)| *c == f.code) {
167+ Some((_, mods)) => mods.push(f.modifier),
168+ None => formats.push((f.code, vec![f.modifier])),
169+ }
170+ }
171+ DmabufConstraints { node, formats }
172+ });
173+ BufferConstraints {
174+ size: size.into(),
175+ shm: vec![wl_shm::Format::Argb8888, wl_shm::Format::Xrgb8888],
176+ dma,
177+ }
178+ }
179+
123180 fn capture(&mut self, session: &SessionRef, frame: Frame) {
124- let Some(output) = source_output(&session.source()) else {
181+ let Some(target) = self.target(&session.source()) else {
125182 frame.fail(CaptureFailureReason::Stopped);
126183 return;
127184 };
128- let Some(o) = self.layout_output(&output) else {
129- frame.fail(CaptureFailureReason::Stopped);
185+ let Some(size) = self.target_size(&target) else {
186+ frame.fail(CaptureFailureReason::Unknown);
130187 return;
131188 };
132- let Some(mode) = output.current_mode() else {
133- frame.fail(CaptureFailureReason::Unknown);
189+ if session.current_constraints().map(|c| (c.size.w, c.size.h)) != Some(size) {
190+ session.update_constraints(self.constraints(size));
191+ frame.fail(CaptureFailureReason::BufferConstraints);
134192 return;
193+ }
194+ let elements = match &target {
195+ Target::Output(name) => {
196+ let Some((o, output)) = self.output_named(name) else {
197+ frame.fail(CaptureFailureReason::Stopped);
198+ return;
199+ };
200+ self.output_elements(o, &output, session.draw_cursor())
201+ }
202+ Target::Window(id) => window_elements(&mut self.renderer, &self.windows[id]),
135203 };
136- let elements = self.output_elements(o, &output, session.draw_cursor());
137204 let buffer = frame.buffer();
138205 let result = match get_dmabuf(&buffer) {
139- Ok(dmabuf) => self.capture_dmabuf(dmabuf.clone(), mode.size.into(), &elements),
140- Err(_) => self.capture_shm(&buffer, mode.size.into(), &elements),
206+ Ok(dmabuf) => self.capture_dmabuf(dmabuf.clone(), size, &elements),
207+ Err(_) => self.capture_shm(&buffer, size, &elements),
141208 };
142209 match result {
143210 Ok(()) => {
@@ -226,19 +293,23 @@ impl State {
226293 elements
227294 }
228295
229- pub fn layout_output(&self, output: &Output) -> Option<OutputId> {
296+ fn output_named(&self, name: &str) -> Option<(OutputId, Output)> {
230297 self.layout
231298 .outputs()
232- .map(|(o, _)| o)
233- .find(|&o| self.smithay_output(o).as_ref() == Some(output))
299+ .filter_map(|(o, _)| Some((o, self.smithay_output(o)?)))
300+ .find(|(_, out)| out.name() == name)
234301 }
235302 }
236303
304+/// A window alone, with its popups, its geometry at the origin.
305+fn window_elements(renderer: &mut VulkanRenderer, window: &smithay::desktop::Window) -> Vec<Draw> {
306+ let at = Point::from((-window.geometry().loc.x, -window.geometry().loc.y));
307+ let elems: Vec<WaylandSurfaceRenderElement<VulkanRenderer>> =
308+ window.render_elements(renderer, at, Scale::from(1.0), 1.0);
309+ elems.into_iter().map(Draw::Surface).collect()
310+}
311+
237312 /// Marks a session that has had a frame, so later ones wait for damage.
238313 struct Delivered;
239314
240315 const CLEAR: [f32; 4] = [0.07, 0.075, 0.09, 1.0];
241-
242-fn source_output(source: &ImageCaptureSource) -> Option<Output> {
243- source.user_data().get::<WeakOutput>()?.upgrade()
244-}
Modified file: crates/monolith/src/drm.rs+1 additions−1 deletions
@@ -678,7 +678,7 @@ fn render_surface(state: &mut State, crtc: crtc::Handle, target: u64) -> bool {
678678 Ok(()) => {
679679 surface.queued = true;
680680 lock.shown(&surface.output);
681- captures.damaged(&surface.output);
681+ captures.output_damaged(&surface.output);
682682 }
683683 Err(e) => tracing::warn!("queueing a frame failed: {e}"),
684684 }
Modified file: crates/monolith/src/handlers.rs+5 additions
@@ -80,6 +80,11 @@ impl CompositorHandler for State {
8080 {
8181 window.on_commit();
8282 }
83+ if self.captures.wants_damage()
84+ && let Some(id) = self.window_id(&root)
85+ {
86+ self.captures.window_damaged(id);
87+ }
8388 }
8489 if let Some(i) = self
8590 .unmapped
Modified file: crates/monolith/src/nested/mod.rs+1 additions−1 deletions
@@ -268,7 +268,7 @@ pub fn render(state: &mut State) {
268268 }
269269 crate::layers::send_frames(&host.output, elapsed);
270270 if damage.is_some() {
271- captures.damaged(&host.output);
271+ captures.output_damaged(&host.output);
272272 }
273273 let Some(damage) = damage else {
274274 if *needs_redraw {
Modified file: crates/monolith/src/state.rs+5 additions−1 deletions
@@ -28,7 +28,9 @@ use smithay::wayland::compositor::{CompositorClientState, CompositorState, with_
2828 use smithay::wayland::dmabuf::{DmabufGlobal, DmabufState};
2929 use smithay::wayland::idle_inhibit::IdleInhibitManagerState;
3030 use smithay::wayland::idle_notify::IdleNotifierState;
31-use smithay::wayland::image_capture_source::{ImageCaptureSourceState, OutputCaptureSourceState};
31+use smithay::wayland::image_capture_source::{
32+ ImageCaptureSourceState, OutputCaptureSourceState, ToplevelCaptureSourceState,
33+};
3234 use smithay::wayland::image_copy_capture::ImageCopyCaptureState;
3335 use smithay::wayland::output::OutputManagerState;
3436 use smithay::wayland::seat::WaylandFocus;
@@ -113,6 +115,7 @@ pub struct State {
113115 pub layer_shell_state: WlrLayerShellState,
114116 pub _image_capture_source_state: ImageCaptureSourceState,
115117 pub output_capture_source_state: OutputCaptureSourceState,
118+ pub toplevel_capture_source_state: ToplevelCaptureSourceState,
116119 pub image_copy_capture_state: ImageCopyCaptureState,
117120 pub activation_state: XdgActivationState,
118121 pub primary_selection_state: PrimarySelectionState,
@@ -238,6 +241,7 @@ impl State {
238241 layer_shell_state: WlrLayerShellState::new::<State>(&dh),
239242 _image_capture_source_state: ImageCaptureSourceState::new(),
240243 output_capture_source_state: OutputCaptureSourceState::new::<State>(&dh),
244+ toplevel_capture_source_state: ToplevelCaptureSourceState::new::<State>(&dh),
241245 image_copy_capture_state: ImageCopyCaptureState::new::<State>(&dh),
242246 activation_state: XdgActivationState::new::<State>(&dh),
243247 wlr_data_control_state: WlrDataControlState::new::<State, _>(
Modified file: crates/monolith/src/toplevels.rs+9 additions
@@ -14,6 +14,15 @@ use std::collections::HashMap;
1414 #[derive(Default)]
1515 pub struct ListHandles(HashMap<WindowId, ForeignToplevelHandle>);
1616
17+impl ListHandles {
18+ pub fn window(&self, toplevel: &ForeignToplevelHandle) -> Option<WindowId> {
19+ self.0
20+ .iter()
21+ .find(|(_, h)| h.identifier() == toplevel.identifier())
22+ .map(|(id, _)| *id)
23+ }
24+}
25+
1726 pub fn key(id: WindowId) -> u64 {
1827 slotmap::Key::data(&id).as_ffi()
1928 }