atagen

monolith

public
Star0
Watch0

Commit 3ea37d969201

tarball

atagen <boss@atagen.co>2026-10-16 22:30 UTC

5 files changed+265-0
monolith: support ext-image-copy-capture of outputs

A session's first frame is captured at once, and later frames wait until the output draws something new. Recorders and portals then get frames at the rate the screen changes instead of spinning on a static desktop. Captures render the scene again into the client's buffer, since sharing the scanout buffer would tie capture to the DRM backend.
Added file: crates/monolith/src/capture.rs+243 additions
@@ -0,0 +1,243 @@
1+//! ext-image-copy-capture of whole outputs, for grim and screen sharing
2+//! portals.
3+
4+use crate::scene::Draw;
5+use crate::state::{State, now_ns};
6+use monolith_layout::OutputId;
7+use monolith_render_vk::VulkanImage;
8+use smithay::backend::allocator::Fourcc;
9+use smithay::backend::allocator::dmabuf::Dmabuf;
10+use smithay::backend::renderer::damage::OutputDamageTracker;
11+use smithay::backend::renderer::{Bind, ExportMem, Offscreen};
12+use smithay::output::{Output, WeakOutput};
13+use smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer;
14+use smithay::reexports::wayland_server::protocol::wl_shm;
15+use smithay::utils::{Point, Rectangle, Size, Transform};
16+use smithay::wayland::dmabuf::get_dmabuf;
17+use smithay::wayland::image_capture_source::{
18+ ImageCaptureSource, ImageCaptureSourceHandler, OutputCaptureSourceHandler,
19+ OutputCaptureSourceState,
20+};
21+use smithay::wayland::image_copy_capture::{
22+ BufferConstraints, CaptureFailureReason, DmabufConstraints, Frame, ImageCopyCaptureHandler,
23+ ImageCopyCaptureState, Session, SessionRef,
24+};
25+use smithay::wayland::shm::{shm_format_to_fourcc, with_buffer_contents_mut};
26+use std::collections::HashSet;
27+use std::time::Duration;
28+
29+#[derive(Default)]
30+pub struct Captures {
31+ sessions: Vec<Session>,
32+ /// Frames waiting for their output to show something new.
33+ waiting: Vec<(SessionRef, Frame)>,
34+ damaged: HashSet<String>,
35+}
36+
37+impl Captures {
38+ /// Records that `output` drew a frame with new content.
39+ pub fn damaged(&mut self, output: &Output) {
40+ if !self.waiting.is_empty() {
41+ self.damaged.insert(output.name());
42+ }
43+ }
44+}
45+
46+impl ImageCaptureSourceHandler for State {
47+ fn source_destroyed(&mut self, _source: ImageCaptureSource) {}
48+}
49+
50+impl OutputCaptureSourceHandler for State {
51+ fn output_capture_source_state(&mut self) -> &mut OutputCaptureSourceState {
52+ &mut self.output_capture_source_state
53+ }
54+
55+ fn output_source_created(&mut self, source: ImageCaptureSource, output: &Output) {
56+ source.user_data().insert_if_missing(|| output.downgrade());
57+ }
58+}
59+
60+impl ImageCopyCaptureHandler for State {
61+ fn image_copy_capture_state(&mut self) -> &mut ImageCopyCaptureState {
62+ &mut self.image_copy_capture_state
63+ }
64+
65+ 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+ })
83+ }
84+
85+ fn new_session(&mut self, session: Session) {
86+ self.captures.sessions.push(session);
87+ }
88+
89+ fn frame(&mut self, session: &SessionRef, frame: Frame) {
90+ if session.user_data().get::<Delivered>().is_some() {
91+ self.captures.waiting.push((session.clone(), frame));
92+ self.needs_redraw = true;
93+ } else {
94+ self.capture(session, frame);
95+ }
96+ }
97+
98+ fn session_destroyed(&mut self, session: SessionRef) {
99+ let c = &mut self.captures;
100+ c.sessions.retain(|s| *s != session);
101+ c.waiting.retain(|(s, _)| *s != session);
102+ }
103+}
104+
105+impl State {
106+ /// Completes waiting frames on outputs that drew something new.
107+ pub fn complete_captures(&mut self) {
108+ if self.captures.damaged.is_empty() {
109+ return;
110+ }
111+ let damaged = std::mem::take(&mut self.captures.damaged);
112+ let (ready, waiting) = std::mem::take(&mut self.captures.waiting)
113+ .into_iter()
114+ .partition::<Vec<_>, _>(|(s, _)| {
115+ source_output(&s.source()).is_some_and(|o| damaged.contains(&o.name()))
116+ });
117+ self.captures.waiting = waiting;
118+ for (session, frame) in ready {
119+ self.capture(&session, frame);
120+ }
121+ }
122+
123+ fn capture(&mut self, session: &SessionRef, frame: Frame) {
124+ let Some(output) = source_output(&session.source()) else {
125+ frame.fail(CaptureFailureReason::Stopped);
126+ return;
127+ };
128+ let Some(o) = self.layout_output(&output) else {
129+ frame.fail(CaptureFailureReason::Stopped);
130+ return;
131+ };
132+ let Some(mode) = output.current_mode() else {
133+ frame.fail(CaptureFailureReason::Unknown);
134+ return;
135+ };
136+ let elements = self.output_elements(o, &output, session.draw_cursor());
137+ let buffer = frame.buffer();
138+ 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),
141+ };
142+ match result {
143+ Ok(()) => {
144+ session.user_data().insert_if_missing(|| Delivered);
145+ frame.success(Transform::Normal, None, Duration::from_nanos(now_ns()));
146+ }
147+ Err(e) => {
148+ tracing::warn!("screen capture failed: {e}");
149+ frame.fail(CaptureFailureReason::Unknown);
150+ }
151+ }
152+ }
153+
154+ fn capture_dmabuf(
155+ &mut self,
156+ mut dmabuf: Dmabuf,
157+ size: (i32, i32),
158+ elements: &[Draw],
159+ ) -> Result<(), Box<dyn std::error::Error>> {
160+ let mut fb = self.renderer.bind(&mut dmabuf)?;
161+ let mut tracker = OutputDamageTracker::new(size, 1.0, Transform::Normal);
162+ let result = tracker.render_output(&mut self.renderer, &mut fb, 0, elements, CLEAR)?;
163+ result.sync.wait()?;
164+ Ok(())
165+ }
166+
167+ fn capture_shm(
168+ &mut self,
169+ buffer: &WlBuffer,
170+ size: (i32, i32),
171+ elements: &[Draw],
172+ ) -> Result<(), Box<dyn std::error::Error>> {
173+ let format = with_buffer_contents_mut(buffer, |_, _, data| data.format)?;
174+ let fourcc = shm_format_to_fourcc(format).ok_or("unsupported shm format")?;
175+ let mut image: VulkanImage = self.renderer.create_buffer(fourcc, size.into())?;
176+ let mut fb = self.renderer.bind(&mut image)?;
177+ let mut tracker = OutputDamageTracker::new(size, 1.0, Transform::Normal);
178+ let result = tracker.render_output(&mut self.renderer, &mut fb, 0, elements, CLEAR)?;
179+ result.sync.wait()?;
180+ let region = Rectangle::from_size(Size::from(size));
181+ let mapping = self.renderer.copy_framebuffer(&fb, region, fourcc)?;
182+ let pixels = self.renderer.map_texture(&mapping)?;
183+ let row = size.0 as usize * 4;
184+ with_buffer_contents_mut(buffer, |ptr, len, data| {
185+ // SAFETY: smithay maps the whole pool, `len` bytes from `ptr`.
186+ let pool = unsafe { std::slice::from_raw_parts_mut(ptr, len) };
187+ for (y, src) in pixels.chunks_exact(row).enumerate() {
188+ let start = data.offset as usize + y * data.stride as usize;
189+ if let Some(dst) = pool.get_mut(start..start + row) {
190+ dst.copy_from_slice(src);
191+ }
192+ }
193+ })?;
194+ Ok(())
195+ }
196+
197+ /// Everything output `o` shows, as the backends draw it.
198+ pub fn output_elements(&mut self, o: OutputId, output: &Output, cursor: bool) -> Vec<Draw> {
199+ let now = now_ns();
200+ let mut elements = Vec::new();
201+ if cursor && let Some(area) = self.layout.output(o).map(|out| out.area) {
202+ let local = self.pointer_pos - Point::from((area.x, area.y));
203+ if local.x >= 0.0 && local.y >= 0.0 && local.x < area.w && local.y < area.h {
204+ elements.extend(self.cursor.elements(
205+ &mut self.renderer,
206+ &self.cursor_status,
207+ local,
208+ ));
209+ }
210+ }
211+ match crate::lock::elements(&self.lock, &mut self.renderer, output) {
212+ Some(locked) => elements.extend(locked),
213+ None => elements.extend(crate::scene::build(
214+ &self.layout,
215+ o,
216+ &self.windows,
217+ &self.anim,
218+ &mut self.renderer,
219+ now,
220+ &self.config.decoration,
221+ &mut self.borders,
222+ Some(output),
223+ )),
224+ }
225+ elements
226+ }
227+
228+ pub fn layout_output(&self, output: &Output) -> Option<OutputId> {
229+ self.layout
230+ .outputs()
231+ .map(|(o, _)| o)
232+ .find(|&o| self.smithay_output(o).as_ref() == Some(output))
233+ }
234+}
235+
236+/// Marks a session that has had a frame, so later ones wait for damage.
237+struct Delivered;
238+
239+const CLEAR: [f32; 4] = [0.07, 0.075, 0.09, 1.0];
240+
241+fn source_output(source: &ImageCaptureSource) -> Option<Output> {
242+ source.user_data().get::<WeakOutput>()?.upgrade()
243+}
Modified file: crates/monolith/src/drm.rs+3 additions
@@ -202,6 +202,7 @@ pub fn init(
202202 .node_with_type(NodeType::Render)
203203 .and_then(Result::ok)
204204 .unwrap_or(node);
205+ state.render_node = Some(render_node);
205206 let allocator = GbmAllocator::new(
206207 gbm.clone(),
207208 GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT,
@@ -590,6 +591,7 @@ fn render_surface(state: &mut State, crtc: crtc::Handle, target: u64) -> bool {
590591 cursor_status,
591592 pointer_pos,
592593 lock,
594+ captures,
593595 ..
594596 } = state
595597 else {
@@ -668,6 +670,7 @@ fn render_surface(state: &mut State, crtc: crtc::Handle, target: u64) -> bool {
668670 Ok(()) => {
669671 surface.queued = true;
670672 lock.shown(&surface.output);
673+ captures.damaged(&surface.output);
671674 }
672675 Err(e) => tracing::warn!("queueing a frame failed: {e}"),
673676 }
Modified file: crates/monolith/src/main.rs+1 additions
@@ -1,5 +1,6 @@
11 mod activation;
22 mod anim;
3+mod capture;
34 mod commands;
45 mod cursor;
56 mod drm;
Modified file: crates/monolith/src/nested/mod.rs+4 additions
@@ -180,6 +180,7 @@ pub fn render(state: &mut State) {
180180 config,
181181 output_id,
182182 lock,
183+ captures,
183184 ..
184185 } = state;
185186 let Some(host) = host.as_mut() else { return };
@@ -264,6 +265,9 @@ pub fn render(state: &mut State) {
264265 });
265266 }
266267 crate::layers::send_frames(&host.output, elapsed);
268+ if damage.is_some() {
269+ captures.damaged(&host.output);
270+ }
267271 let Some(damage) = damage else {
268272 if *needs_redraw {
269273 host.surface.frame(&host.qh, ());
Modified file: crates/monolith/src/state.rs+14 additions
@@ -26,6 +26,8 @@ use smithay::wayland::compositor::{CompositorClientState, CompositorState, with_
2626 use smithay::wayland::dmabuf::{DmabufGlobal, DmabufState};
2727 use smithay::wayland::idle_inhibit::IdleInhibitManagerState;
2828 use smithay::wayland::idle_notify::IdleNotifierState;
29+use smithay::wayland::image_capture_source::{ImageCaptureSourceState, OutputCaptureSourceState};
30+use smithay::wayland::image_copy_capture::ImageCopyCaptureState;
2931 use smithay::wayland::output::OutputManagerState;
3032 use smithay::wayland::selection::data_device::DataDeviceState;
3133 use smithay::wayland::selection::ext_data_control::DataControlState as ExtDataControlState;
@@ -88,6 +90,9 @@ pub struct State {
8890 pub placed_by_rule: HashSet<WindowId>,
8991 /// A layer surface given the keyboard by a click, like a launcher.
9092 pub layer_focus: Option<WlSurface>,
93+ pub captures: crate::capture::Captures,
94+ /// The render node clients get dmabufs for, known on bare metal.
95+ pub render_node: Option<smithay::backend::drm::DrmNode>,
9196 pub lock: crate::lock::Lock,
9297 pub inhibitors: HashSet<WlSurface>,
9398 pub inhibited: bool,
@@ -96,6 +101,9 @@ pub struct State {
96101 pub compositor_state: CompositorState,
97102 pub xdg_shell_state: XdgShellState,
98103 pub layer_shell_state: WlrLayerShellState,
104+ pub _image_capture_source_state: ImageCaptureSourceState,
105+ pub output_capture_source_state: OutputCaptureSourceState,
106+ pub image_copy_capture_state: ImageCopyCaptureState,
99107 pub activation_state: XdgActivationState,
100108 pub primary_selection_state: PrimarySelectionState,
101109 pub wlr_data_control_state: WlrDataControlState,
@@ -197,6 +205,8 @@ impl State {
197205 awaiting: HashSet::new(),
198206 placed_by_rule: HashSet::new(),
199207 layer_focus: None,
208+ captures: Default::default(),
209+ render_node: None,
200210 lock: Default::default(),
201211 inhibitors: HashSet::new(),
202212 inhibited: false,
@@ -204,6 +214,9 @@ impl State {
204214 compositor_state: CompositorState::new::<State>(&dh),
205215 xdg_shell_state: XdgShellState::new::<State>(&dh),
206216 layer_shell_state: WlrLayerShellState::new::<State>(&dh),
217+ _image_capture_source_state: ImageCaptureSourceState::new(),
218+ output_capture_source_state: OutputCaptureSourceState::new::<State>(&dh),
219+ image_copy_capture_state: ImageCopyCaptureState::new::<State>(&dh),
207220 activation_state: XdgActivationState::new::<State>(&dh),
208221 wlr_data_control_state: WlrDataControlState::new::<State, _>(
209222 &dh,
@@ -238,6 +251,7 @@ impl State {
238251 crate::nested::render(self);
239252 crate::drm::render(self);
240253 self.confirm_lock();
254+ self.complete_captures();
241255 crate::ipc::broadcast(self);
242256 self.publish_workspaces();
243257 let _ = self.display.flush_clients();