atagen

monolith

public
Star0
Watch0

Commit 0f11410be91b

tarball

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

5 files changed+264-42
monolith: copy large shm damage on the import worker

The spans log showed shm uploads of 1 to 3.5 ms inside the commit handler on the main thread, and the frames missed without a slow render each followed one: the render timer fired late. A commit whose damage is over 512 KiB now waits on a blocker while the worker copies those rows into staging memory, and the import only records the GPU copy. The client may not touch a committed buffer until it is released, so reading it before the commit applies is safe. Smaller damage stays inline, where the round trip would cost more than the copy.
Modified file: crates/monolith/src/handlers.rs+43 additions−2 deletions
@@ -15,7 +15,7 @@ use smithay::reexports::wayland_server::{Client, Resource};
1515 use smithay::utils::Serial;
1616 use smithay::wayland::buffer::BufferHandler;
1717 use smithay::wayland::compositor::{
18- BufferAssignment, CompositorClientState, CompositorHandler, CompositorState,
18+ BufferAssignment, CompositorClientState, CompositorHandler, CompositorState, Damage,
1919 SurfaceAttributes, add_blocker, add_pre_commit_hook, get_parent, is_sync_subsurface,
2020 with_states,
2121 };
@@ -38,7 +38,8 @@ use smithay::wayland::shell::xdg::{
3838 PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState,
3939 };
4040 use monolith_protocols::workspace::{Action, WorkspaceHandler, WorkspaceState};
41-use smithay::wayland::shm::{ShmHandler, ShmState};
41+use smithay::utils::{Rectangle, Transform};
42+use smithay::wayland::shm::{ShmHandler, ShmState, with_buffer_contents};
4243
4344 impl CompositorHandler for State {
4445 fn compositor_state(&mut self) -> &mut CompositorState {
@@ -294,6 +295,7 @@ impl DrmSyncobjHandler for State {
294295 /// stalling on the client's GPU work: until its explicit acquire point
295296 /// signals, or for implicitly synced buffers until their writes finish.
296297 fn hold_until_readable(state: &mut State, surface: &WlSurface) {
298+ stage_shm(state, surface);
297299 let (acquire, dmabuf) = with_states(surface, |s| {
298300 let acquire = s
299301 .cached_state
@@ -348,6 +350,45 @@ fn hold_until_readable(state: &mut State, surface: &WlSurface) {
348350 }
349351 }
350352
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+
351392 impl XdgDecorationHandler for State {
352393 fn new_decoration(&mut self, toplevel: ToplevelSurface) {
353394 server_side(&toplevel);
Modified file: crates/monolith/src/import.rs+94 additions−24 deletions
@@ -1,14 +1,18 @@
1-//! Dmabuf imports on a worker thread. A first import takes 15 to 27 ms on
2-//! NVIDIA, which on the main thread drops frames. Buffer creation answers
3-//! once the import is done, and commits of buffers still importing wait on
4-//! a blocker.
1+//! Buffer work on a worker thread. A first dmabuf import takes 15 to 27 ms
2+//! on NVIDIA, and copying a large shm damage takes 1 to 3.5 ms, either of
3+//! which on the main thread makes frames miss. Buffer creation answers once
4+//! an import is done, and commits wait on a blocker until their buffer is
5+//! ready.
56
67 use crate::state::State;
7-use monolith_render_vk::{DmabufImporter, VulkanTexture};
8+use monolith_render_vk::{DmabufImporter, ShmStager, StagedShm, VulkanTexture};
89 use smithay::backend::allocator::dmabuf::{Dmabuf, WeakDmabuf};
910 use smithay::reexports::calloop::LoopHandle;
1011 use smithay::reexports::calloop::channel::{Event, channel};
11-use smithay::reexports::wayland_server::Client;
12+use smithay::reexports::wayland_server::backend::ObjectId;
13+use smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer;
14+use smithay::reexports::wayland_server::{Client, Resource};
15+use smithay::utils::{Buffer, Rectangle};
1216 use smithay::wayland::compositor::{Blocker, BlockerState, CompositorHandler};
1317 use smithay::wayland::dmabuf::ImportNotifier;
1418 use std::collections::HashMap;
@@ -16,9 +20,14 @@ use std::sync::Arc;
1620 use std::sync::atomic::{AtomicBool, Ordering};
1721 use std::sync::mpsc;
1822
23+/// Below this many damaged bytes a copy is cheaper than the round trip to
24+/// the worker and back.
25+const OFFLOAD_BYTES: i64 = 512 * 1024;
26+
1927 pub struct Imports {
20- jobs: mpsc::Sender<Dmabuf>,
21- waiting: HashMap<WeakDmabuf, Waiters>,
28+ jobs: mpsc::Sender<Job>,
29+ dmabufs: HashMap<WeakDmabuf, Waiters>,
30+ shm: HashMap<ObjectId, Waiters>,
2231 }
2332
2433 #[derive(Default)]
@@ -40,42 +49,61 @@ impl Blocker for ImportBlocker {
4049 }
4150 }
4251
43-type Done = (Dmabuf, Result<VulkanTexture, String>);
52+enum Job {
53+ Dmabuf(Dmabuf),
54+ Shm(WlBuffer, Option<Rectangle<i32, Buffer>>),
55+}
56+
57+enum Done {
58+ Dmabuf(Dmabuf, Result<VulkanTexture, String>),
59+ Shm(WlBuffer, Result<StagedShm, String>),
60+}
4461
4562 impl Imports {
4663 pub fn new(
4764 handle: &LoopHandle<'static, State>,
4865 importer: DmabufImporter,
66+ stager: ShmStager,
4967 ) -> std::io::Result<Self> {
5068 let (done, results) = channel::<Done>();
51- let (jobs, queue) = mpsc::channel::<Dmabuf>();
69+ let (jobs, queue) = mpsc::channel::<Job>();
5270 std::thread::Builder::new()
53- .name("dmabuf-import".into())
71+ .name("buffer-import".into())
5472 .spawn(move || {
55- for buffer in queue {
56- let result = importer.import(&buffer).map_err(|e| e.to_string());
57- if done.send((buffer, result)).is_err() {
73+ for job in queue {
74+ let result = match job {
75+ Job::Dmabuf(b) => {
76+ let r = importer.import(&b).map_err(|e| e.to_string());
77+ Done::Dmabuf(b, r)
78+ }
79+ Job::Shm(b, region) => {
80+ let r = stager.stage(&b, region).map_err(|e| e.to_string());
81+ Done::Shm(b, r)
82+ }
83+ };
84+ if done.send(result).is_err() {
5885 break;
5986 }
6087 }
6188 })?;
6289 handle
63- .insert_source(results, |event, _, state| {
64- if let Event::Msg((buffer, result)) = event {
65- state.imported(buffer, result);
66- }
90+ .insert_source(results, |event, _, state| match event {
91+ Event::Msg(Done::Dmabuf(buffer, result)) => state.imported(buffer, result),
92+ Event::Msg(Done::Shm(buffer, result)) => state.staged(buffer, result),
93+ Event::Closed => {}
6794 })
6895 .map_err(|e| std::io::Error::other(e.to_string()))?;
6996 Ok(Self {
7097 jobs,
71- waiting: HashMap::new(),
98+ dmabufs: HashMap::new(),
99+ shm: HashMap::new(),
72100 })
73101 }
74102
75103 fn start(&mut self, buffer: &Dmabuf) -> &mut Waiters {
76104 let jobs = &self.jobs;
77- self.waiting.entry(buffer.weak()).or_insert_with(|| {
78- let _ = jobs.send(buffer.clone());
105+ self.dmabufs.entry(buffer.weak()).or_insert_with(|| {
106+ let _ = jobs.send(Job::Dmabuf(buffer.clone()));
79107 Waiters::default()
80108 })
81109 }
@@ -101,8 +129,33 @@ impl State {
101129 Some(ImportBlocker(waiters.done.clone()))
102130 }
103131
132+ /// A blocker for a commit of an shm buffer with enough damage to copy on
133+ /// the worker. `region` is the damage, `None` for all of it.
134+ pub fn stage_blocker(
135+ &mut self,
136+ buffer: &WlBuffer,
137+ region: Option<Rectangle<i32, Buffer>>,
138+ size: (i32, i32),
139+ client: Client,
140+ ) -> Option<ImportBlocker> {
141+ let area = region.map_or(i64::from(size.0) * i64::from(size.1), |r| {
142+ i64::from(r.size.w) * i64::from(r.size.h)
143+ });
144+ if area * 4 < OFFLOAD_BYTES {
145+ return None;
146+ }
147+ let imports = &mut self.services.imports;
148+ let jobs = &imports.jobs;
149+ let waiters = imports.shm.entry(buffer.id()).or_insert_with(|| {
150+ let _ = jobs.send(Job::Shm(buffer.clone(), region));
151+ Waiters::default()
152+ });
153+ waiters.clients.push(client);
154+ Some(ImportBlocker(waiters.done.clone()))
155+ }
156+
104157 fn imported(&mut self, buffer: Dmabuf, result: Result<VulkanTexture, String>) {
105- let Some(waiters) = self.services.imports.waiting.remove(&buffer.weak()) else {
158+ let Some(mut waiters) = self.services.imports.dmabufs.remove(&buffer.weak()) else {
106159 return;
107160 };
108161 let result = result.and_then(|t| {
@@ -110,19 +163,36 @@ impl State {
110163 .adopt_dmabuf(&buffer, t)
111164 .map_err(|e| e.to_string())
112165 });
166+ let notifiers = std::mem::take(&mut waiters.notifiers);
113167 match result {
114168 Ok(()) => {
115- for n in waiters.notifiers {
169+ for n in notifiers {
116170 let _ = n.successful::<State>();
117171 }
118172 }
119173 Err(e) => {
120174 tracing::warn!("dmabuf import failed: {e}");
121- for n in waiters.notifiers {
175+ for n in notifiers {
122176 n.failed();
123177 }
124178 }
125179 }
180+ self.release(waiters);
181+ }
182+
183+ /// A failed copy only costs the import doing it on the main thread.
184+ fn staged(&mut self, buffer: WlBuffer, result: Result<StagedShm, String>) {
185+ let Some(waiters) = self.services.imports.shm.remove(&buffer.id()) else {
186+ return;
187+ };
188+ match result {
189+ Ok(staged) => self.renderer.adopt_staged(&buffer, staged),
190+ Err(e) => tracing::debug!("staging an shm buffer failed: {e}"),
191+ }
192+ self.release(waiters);
193+ }
194+
195+ fn release(&mut self, waiters: Waiters) {
126196 waiters.done.store(true, Ordering::Release);
127197 let dh = self.core.display.clone();
128198 for client in waiters.clients {
Modified file: crates/monolith/src/state.rs+5 additions−1 deletions
@@ -142,7 +142,11 @@ impl State {
142142 let mut layout = Layout::default();
143143 layout.gaps = gaps(&config);
144144 let animation = config.animation.clone();
145- let imports = crate::import::Imports::new(&handle, renderer.dmabuf_importer())?;
145+ let imports = crate::import::Imports::new(
146+ &handle,
147+ renderer.dmabuf_importer(),
148+ renderer.shm_stager(),
149+ )?;
146150 let idle = crate::idle::Idle::new(&dh, handle.clone());
147151 Ok(Self {
148152 protocols: Protocols::new(&dh, &renderer),
Modified file: crates/render-vk/src/lib.rs+1 additions−1 deletions
@@ -18,4 +18,4 @@ pub use dmabuf::VulkanAllocator;
1818 pub use error::Error;
1919 pub use frame::{Effect, Transition, VulkanFrame};
2020 pub use image::{VulkanImage, VulkanMapping, VulkanTarget, VulkanTexture};
21-pub use renderer::{DmabufImporter, VulkanRenderer};
21+pub use renderer::{DmabufImporter, ShmStager, StagedShm, VulkanRenderer};
Modified file: crates/render-vk/src/renderer.rs+121 additions−14 deletions
@@ -19,6 +19,8 @@ use smithay::backend::renderer::{
1919 Bind, ContextId, DebugFlags, ExportMem, ImportDma, ImportDmaWl, ImportMem, Offscreen, Renderer,
2020 RendererSuper, Texture, TextureFilter,
2121 };
22+use smithay::reexports::wayland_server::Resource;
23+use smithay::reexports::wayland_server::backend::ObjectId;
2224 use smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer;
2325 use smithay::utils::{Buffer, Physical, Rectangle, Size, Transform};
2426 use smithay::wayland::compositor::SurfaceData;
@@ -53,6 +55,8 @@ pub struct VulkanRenderer {
5355 caps: Arc<DmabufCaps>,
5456 shared: Arc<Shared>,
5557 submissions: Cell<u64>,
58+ /// Shm uploads staged off the main thread, by buffer.
59+ staged: HashMap<ObjectId, (WlBuffer, StagedShm)>,
5660 stall: Cell<StallPhase>,
5761 }
5862
@@ -149,6 +153,7 @@ impl VulkanRenderer {
149153 pool,
150154 spare_cmds: Vec::new(),
151155 submissions: Cell::new(0),
156+ staged: HashMap::new(),
152157 stall: Cell::new(StallPhase {
153158 // Measured on an RTX 3060, see research/nvidia.md.
154159 phase: shared.device.name.contains("NVIDIA").then_some(1015),
@@ -513,21 +518,21 @@ impl VulkanRenderer {
513518 return Err(Error::ForeignTexture);
514519 }
515520 self.reap();
516- let (w, h) = (region.size.w as usize, region.size.h as usize);
517- if w == 0 || h == 0 {
521+ if region.size.w <= 0 || region.size.h <= 0 {
518522 return Ok(());
519523 }
520- let line = w * BYTES_PER_PIXEL;
521- let mut staging = Staging::new(
522- &self.shared,
523- line * h,
524- vk::BufferUsageFlags::TRANSFER_SRC,
525- MemoryLocation::CpuToGpu,
526- )?;
527- let dst = staging.bytes_mut();
528- for i in 0..h {
529- dst[i * line..(i + 1) * line].copy_from_slice(row(region.loc.y as usize + i));
530- }
524+ let staging = stage_rows(&self.shared, region, row)?;
525+ self.copy_staged(image, region, staging)
526+ }
527+
528+ /// Copies staged pixels into `region` of `image` on the GPU.
529+ fn copy_staged(
530+ &mut self,
531+ image: &Arc<ImageInner>,
532+ region: Rectangle<i32, Buffer>,
533+ staging: Staging,
534+ ) -> Result<(), Error> {
535+ let (w, h) = (region.size.w as usize, region.size.h as usize);
531536 let copy = vk::BufferImageCopy::default()
532537 .buffer_row_length(w as u32)
533538 .image_subresource(
@@ -1050,6 +1055,8 @@ impl ImportMemWl for VulkanRenderer {
10501055 surface: Option<&SurfaceData>,
10511056 damage: &[Rectangle<i32, Buffer>],
10521057 ) -> Result<VulkanTexture, Error> {
1058+ self.staged.retain(|_, (b, _)| b.is_alive());
1059+ let staged = self.staged.remove(&buffer.id()).map(|(_, s)| s);
10531060 let cache = surface.map(|s| {
10541061 s.data_map
10551062 .insert_if_missing_threadsafe(|| ShmCache(Mutex::new(None)));
@@ -1093,7 +1100,15 @@ impl ImportMemWl for VulkanRenderer {
10931100 (VulkanTexture(image), Some(full))
10941101 }
10951102 };
1096- if let Some(region) = region {
1103+ let staged = staged.filter(|s| {
1104+ s.fourcc == fourcc
1105+ && s.size == size
1106+ && region.is_some_and(|r| s.region.contains_rect(r))
1107+ });
1108+ if let Some(s) = staged {
1109+ tracing::trace!(region = ?s.region, "uploading a staged shm copy");
1110+ self.copy_staged(&texture.0, s.region, s.staging)?;
1111+ } else if let Some(region) = region {
10971112 let x = region.loc.x as usize * BYTES_PER_PIXEL;
10981113 let rw = region.size.w as usize * BYTES_PER_PIXEL;
10991114 self.upload_with(&texture.0, region, |y| unsafe {
@@ -1111,3 +1126,95 @@ impl ImportMemWl for VulkanRenderer {
11111126
11121127 /// The texture last imported for a surface's shm buffers.
11131128 struct ShmCache(Mutex<Option<VulkanTexture>>);
1129+
1130+/// Copies `region` of a full-image `row` source into new staging memory.
1131+fn stage_rows<'a>(
1132+ shared: &Arc<Shared>,
1133+ region: Rectangle<i32, Buffer>,
1134+ row: impl Fn(usize) -> &'a [u8],
1135+) -> Result<Staging, Error> {
1136+ let (w, h) = (region.size.w as usize, region.size.h as usize);
1137+ let line = w * BYTES_PER_PIXEL;
1138+ let mut staging = Staging::new(
1139+ shared,
1140+ line * h,
1141+ vk::BufferUsageFlags::TRANSFER_SRC,
1142+ MemoryLocation::CpuToGpu,
1143+ )?;
1144+ let dst = staging.bytes_mut();
1145+ for i in 0..h {
1146+ dst[i * line..(i + 1) * line].copy_from_slice(row(region.loc.y as usize + i));
1147+ }
1148+ Ok(staging)
1149+}
1150+
1151+/// Shm pixels copied off the main thread, waiting for the import of the
1152+/// commit that shows them.
1153+pub struct StagedShm {
1154+ staging: Staging,
1155+ region: Rectangle<i32, Buffer>,
1156+ fourcc: Fourcc,
1157+ size: Size<i32, Buffer>,
1158+}
1159+
1160+/// Copies shm buffers into staging memory on another thread. The renderer
1161+/// it came from takes the result back with `adopt_staged`.
1162+#[derive(Clone)]
1163+pub struct ShmStager {
1164+ shared: Arc<Shared>,
1165+}
1166+
1167+impl ShmStager {
1168+ /// Stages `region` of `buffer`, all of it when the region is `None`.
1169+ pub fn stage(
1170+ &self,
1171+ buffer: &WlBuffer,
1172+ region: Option<Rectangle<i32, Buffer>>,
1173+ ) -> Result<StagedShm, Error> {
1174+ with_buffer_contents(buffer, |ptr, len, data| {
1175+ let fourcc = shm_format_to_fourcc(data.format).ok_or(Error::ShmAccess)?;
1176+ let (w, h, stride, offset) = (
1177+ data.width as usize,
1178+ data.height as usize,
1179+ data.stride as usize,
1180+ data.offset as usize,
1181+ );
1182+ let line = w * BYTES_PER_PIXEL;
1183+ if w == 0 || h == 0 || stride < line || offset + stride * (h - 1) + line > len {
1184+ return Err(Error::ShmAccess);
1185+ }
1186+ let size = Size::from((data.width, data.height));
1187+ let full = Rectangle::from_size(size);
1188+ let region = region.and_then(|r| r.intersection(full)).unwrap_or(full);
1189+ let x = region.loc.x as usize * BYTES_PER_PIXEL;
1190+ let rw = region.size.w as usize * BYTES_PER_PIXEL;
1191+ // SAFETY: the bounds check above keeps every row inside the
1192+ // `len` bytes smithay mapped at `ptr`.
1193+ let staging = stage_rows(&self.shared, region, |y| unsafe {
1194+ std::slice::from_raw_parts(ptr.add(offset + y * stride + x), rw)
1195+ })?;
1196+ Ok(StagedShm {
1197+ staging,
1198+ region,
1199+ fourcc,
1200+ size,
1201+ })
1202+ })
1203+ .map_err(|_| Error::ShmAccess)?
1204+ }
1205+}
1206+
1207+impl VulkanRenderer {
1208+ pub fn shm_stager(&self) -> ShmStager {
1209+ ShmStager {
1210+ shared: self.shared.clone(),
1211+ }
1212+ }
1213+
1214+ /// Hands back pixels a stager copied, used by the next import of `buffer`.
1215+ pub fn adopt_staged(&mut self, buffer: &WlBuffer, staged: StagedShm) {
1216+ if Arc::ptr_eq(&staged.staging.shared, &self.shared) {
1217+ self.staged.insert(buffer.id(), (buffer.clone(), staged));
1218+ }
1219+ }
1220+}