atagen

monolith

public
Star0
Watch0

Commit 48af562d968b

tarball

atagen <boss@atagen.co>2026-10-16 12:43 UTC

6 files changed+219-59
render-vk, monolith: stop stalling the event loop on shm commits

Every shm commit copied the whole buffer twice, allocated a new image, and blocked the CPU on a fence until the upload finished, so clients that redraw during spawns and resizes made frames miss vblanks in bursts. Each surface now keeps one texture and only damaged rows are copied, straight from the pool, with the copy ordered against frames by its barrier instead of a wait. New windows also hold on their seed until their first buffer lands, glides scale the client instead of cropping it, and the float toggle takes a window out of its scratchpad.
Modified file: crates/layout/src/lib.rs+12 additions−1 deletions
@@ -678,11 +678,22 @@ impl Layout {
678678 self.place(w);
679679 }
680680
681+ /// Switches a window between tiled and floating. A scratchpad window
682+ /// leaves its scratchpad and tiles on the tag being viewed, since its own
683+ /// tag may well be hidden.
681684 pub fn toggle_floating(&mut self, w: WindowId) {
682685 let Some(win) = self.windows.get(w) else {
683686 return;
684687 };
685- if win.scratchpad.is_some() {
688+ if let Some(s) = win.scratchpad {
689+ let o = win.output.or(self.active);
690+ let tag = o.map_or(self.homeless, |o| self.launch_tag(o));
691+ self.leave_scratchpad(s, w);
692+ let win = &mut self.windows[w];
693+ win.output = o;
694+ win.tag = tag;
695+ win.floating = false;
696+ self.place(w);
686697 return;
687698 }
688699 self.unplace(w);
Modified file: crates/monolith/src/drm.rs+18 additions−1 deletions
@@ -327,7 +327,19 @@ fn on_vblank(state: &mut State, crtc: crtc::Handle, meta: &mut Option<DrmEventMe
327327 }
328328 surface.queued = false;
329329 if let Some(DrmEventTime::Monotonic(t)) = meta.as_ref().map(|m| m.time) {
330- surface.last_vblank_ns = Some(t.as_nanos() as u64);
330+ let t = t.as_nanos() as u64;
331+ if let Some(last) = surface.last_vblank_ns
332+ && surface.redraw
333+ && t - last > surface.refresh_ns * 3 / 2
334+ {
335+ let missed = (t - last) / surface.refresh_ns - 1;
336+ tracing::debug!(
337+ output = surface.output.name(),
338+ missed,
339+ "missed vblanks while animating"
340+ );
341+ }
342+ surface.last_vblank_ns = Some(t);
331343 }
332344 render_surface(state, crtc);
333345 }
@@ -381,6 +393,7 @@ fn render_surface(state: &mut State, crtc: crtc::Handle) {
381393 if surface.queued {
382394 return;
383395 }
396+ let began = std::time::Instant::now();
384397 let now = surface.predict(now_ns());
385398 let Some(area) = layout.output(surface.id).map(|o| o.area) else {
386399 return;
@@ -413,6 +426,10 @@ fn render_surface(state: &mut State, crtc: crtc::Handle) {
413426 Ok(_) => {}
414427 Err(e) => tracing::warn!("rendering failed: {e}"),
415428 }
429+ let took = began.elapsed();
430+ if took.as_nanos() as u64 > surface.refresh_ns / 2 {
431+ tracing::debug!(output = surface.output.name(), ?took, "slow frame");
432+ }
416433 surface.redraw = animating;
417434 if animating && !surface.queued {
418435 let wait = Duration::from_nanos(surface.refresh_ns);
Modified file: crates/monolith/src/handlers.rs+1 additions
@@ -70,6 +70,7 @@ impl CompositorHandler for State {
7070 t.send_configure();
7171 }
7272 }
73+ self.on_buffer(surface);
7374 self.popups.commit(surface);
7475 if let Some(PopupKind::Xdg(popup)) = self.popups.find_popup(surface)
7576 && !popup.is_initial_configure_sent()
Modified file: crates/monolith/src/scene.rs+28 additions−10 deletions
@@ -9,7 +9,7 @@ use smithay::backend::renderer::element::memory::MemoryRenderBufferRenderElement
99 use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
1010 use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;
1111 use smithay::backend::renderer::element::texture::TextureRenderElement;
12-use smithay::backend::renderer::element::utils::CropRenderElement;
12+use smithay::backend::renderer::element::utils::{CropRenderElement, RescaleRenderElement};
1313 use smithay::backend::renderer::element::{AsRenderElements, Element, Id, Kind, RenderElement};
1414 use smithay::backend::renderer::utils::CommitCounter;
1515 use smithay::backend::renderer::utils::with_renderer_surface_state;
@@ -27,6 +27,7 @@ smithay::backend::renderer::element::render_elements! {
2727 Effect=EffectElement,
2828 Border=SolidColorRenderElement,
2929 Memory=MemoryRenderBufferRenderElement<VulkanRenderer>,
30+ Rescaled=RescaleRenderElement<WaylandSurfaceRenderElement<VulkanRenderer>>,
3031 }
3132
3233 /// Border strips per window, kept between frames so unchanged borders cause
@@ -67,28 +68,30 @@ pub fn build(
6768 continue;
6869 }
6970 match anim.gliding(p.window, now) {
70- Some(r) => above.push((p.window, local(r), true)),
71+ Some(r) => above.push((p.window, local(r), true, true)),
7172 None => {
7273 let r = anim.tiled(o, p.window, now).unwrap_or(p.rect);
73- tiled.push((p.window, local(r), true));
74+ tiled.push((p.window, local(r), true, false));
7475 }
7576 }
7677 }
7778 for &(w, r) in &arr.floating {
78- above.push((w, local(anim.gliding(w, now).unwrap_or(r)), true));
79+ let glide = anim.gliding(w, now);
80+ above.push((w, local(glide.unwrap_or(r)), true, glide.is_some()));
7981 }
8082 if let Some(w) = arr.fullscreen {
81- above.push((w, local(anim.gliding(w, now).unwrap_or(area)), false));
83+ let glide = anim.gliding(w, now);
84+ above.push((w, local(glide.unwrap_or(area)), false, glide.is_some()));
8285 }
8386 borders.0.retain(|id, _| windows.contains_key(id));
8487 let focused = layout.focused();
8588 let mut out = Vec::new();
86- for (id, r, framed) in tiled.into_iter().chain(above).rev() {
89+ for (id, r, framed, gliding) in tiled.into_iter().chain(above).rev() {
8790 let Some(window) = windows.get(&id) else {
8891 continue;
8992 };
9093 let effect = anim.effect(id, now);
91- window_elements(&mut out, renderer, window, r, effect);
94+ window_elements(&mut out, renderer, window, r, effect, gliding);
9295 if framed && deco.border > 0 {
9396 let color = if focused == Some(id) {
9497 deco.focused.0
@@ -120,15 +123,17 @@ pub fn build(
120123 out
121124 }
122125
123-/// Draws `window` filling `r`. A buffer bigger than `r` is cropped, and one
124-/// smaller has its last row and column stretched over the rest, so a resize
125-/// in flight never shows a scaled or missing client.
126+/// Draws `window` filling `r`. In a planned move a buffer bigger than `r` is
127+/// cropped and one smaller has its last row and column stretched over the
128+/// rest, keeping content aligned with its neighbours. A gliding window is
129+/// scaled instead, so the client resizing mid-glide does not jump.
126130 fn window_elements(
127131 out: &mut Vec<Draw>,
128132 renderer: &mut VulkanRenderer,
129133 window: &Window,
130134 r: Rect,
131135 effect: Option<Effect>,
136+ scaled: bool,
132137 ) {
133138 let Some(((x, y), (w, h))) = pixels(r) else {
134139 return;
@@ -142,6 +147,19 @@ fn window_elements(
142147 effect_elements(out, renderer, window, elems, ((x, y), (w, h)), effect);
143148 return;
144149 }
150+ if scaled && geo.size.w > 0 && geo.size.h > 0 {
151+ let scale = Scale::from((
152+ f64::from(w) / f64::from(geo.size.w),
153+ f64::from(h) / f64::from(geo.size.h),
154+ ));
155+ let origin = Point::<i32, Physical>::from((x, y));
156+ out.extend(
157+ elems
158+ .into_iter()
159+ .map(|e| Draw::Rescaled(RescaleRenderElement::from_element(e, origin, scale))),
160+ );
161+ return;
162+ }
145163 if (w, h) == (geo.size.w, geo.size.h) {
146164 out.extend(elems.into_iter().map(Draw::Surface));
147165 return;
Modified file: crates/monolith/src/state.rs+52 additions−4 deletions
@@ -13,7 +13,9 @@ use smithay::desktop::{PopupManager, Space, Window, WindowSurfaceType};
1313 use smithay::input::keyboard::{Keycode, XkbConfig};
1414 use smithay::input::pointer::CursorImageStatus;
1515 use smithay::input::{Seat, SeatState};
16+use smithay::reexports::calloop::LoopHandle;
1617 use smithay::reexports::calloop::generic::Generic;
18+use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
1719 use smithay::reexports::calloop::{EventLoop, Interest, LoopSignal, Mode, PostAction};
1820 use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::State as XdgState;
1921 use smithay::reexports::wayland_server::backend::{ClientData, ClientId, DisconnectReason};
@@ -28,7 +30,7 @@ use smithay::wayland::shell::xdg::decoration::XdgDecorationState;
2830 use smithay::wayland::shell::xdg::{XdgShellState, XdgToplevelSurfaceData};
2931 use smithay::wayland::shm::ShmState;
3032 use smithay::wayland::socket::ListeningSocketSource;
31-use std::collections::HashMap;
33+use std::collections::{HashMap, HashSet};
3234 use std::ffi::OsString;
3335 use std::path::PathBuf;
3436 use std::sync::Arc;
@@ -70,6 +72,8 @@ pub struct State {
7072 /// How each shown window was placed by the last layout pass, to spot
7173 /// switches between tiled, floating and fullscreen.
7274 pub last_kind: HashMap<WindowId, (Kind, Rect, OutputId)>,
75+ pub awaiting: HashSet<WindowId>,
76+ pub handle: LoopHandle<'static, State>,
7377
7478 pub compositor_state: CompositorState,
7579 pub xdg_shell_state: XdgShellState,
@@ -85,7 +89,7 @@ pub struct State {
8589
8690 impl State {
8791 pub fn new(
88- event_loop: &mut EventLoop<State>,
92+ event_loop: &mut EventLoop<'static, State>,
8993 display: Display<State>,
9094 config_path: Option<PathBuf>,
9195 ) -> Result<Self, Box<dyn std::error::Error>> {
@@ -157,6 +161,8 @@ impl State {
157161 anim: Animator::new(animation),
158162 borders: Default::default(),
159163 last_kind: HashMap::new(),
164+ awaiting: HashSet::new(),
165+ handle: event_loop.handle(),
160166 compositor_state: CompositorState::new::<State>(&dh),
161167 xdg_shell_state: XdgShellState::new::<State>(&dh),
162168 _xdg_decoration_state: XdgDecorationState::new::<State>(&dh),
@@ -225,13 +231,44 @@ impl State {
225231 self.layout.scratchpad_add(id, name);
226232 }
227233 self.windows.insert(id, window);
228- self.anim.spawned(id, now_ns());
234+ self.await_first_buffer(id);
229235 if self.layout.visible(id) {
230236 self.layout.focus(id);
231237 }
232238 self.apply_layout();
233239 }
234240
241+ /// Holds a new window on its seed until its first buffer lands, so the
242+ /// spawn animation never plays over an empty slot. Clients that are slow
243+ /// to draw get it anyway after a while.
244+ fn await_first_buffer(&mut self, id: WindowId) {
245+ self.awaiting.insert(id);
246+ let timeout = Timer::from_duration(Duration::from_millis(300));
247+ let _ = self.handle.insert_source(timeout, move |_, _, state| {
248+ state.first_buffer(id);
249+ TimeoutAction::Drop
250+ });
251+ }
252+
253+ /// Called on commits, starts the spawn of a window that now has a buffer.
254+ pub fn on_buffer(&mut self, surface: &WlSurface) {
255+ if self.awaiting.is_empty() {
256+ return;
257+ }
258+ let has_buffer =
259+ with_renderer_surface_state(surface, |s| s.buffer_size().is_some()).unwrap_or(false);
260+ if has_buffer && let Some(id) = self.window_id(surface) {
261+ self.first_buffer(id);
262+ }
263+ }
264+
265+ fn first_buffer(&mut self, id: WindowId) {
266+ if self.awaiting.remove(&id) {
267+ self.anim.spawned(id, now_ns());
268+ self.apply_layout();
269+ }
270+ }
271+
235272 pub fn unmap_window(&mut self, surface: &WlSurface) {
236273 self.unmapped
237274 .retain(|w| w.toplevel().is_some_and(|t| t.wl_surface() != surface));
@@ -321,8 +358,19 @@ impl State {
321358 if let Some(w) = arr.fullscreen {
322359 shown.push((w, area, Kind::Fullscreen, o));
323360 }
361+ let held: Vec<WindowId> = arr
362+ .tiled
363+ .iter()
364+ .map(|p| p.window)
365+ .filter(|w| self.awaiting.contains(w))
366+ .collect();
367+ let planned = if held.is_empty() {
368+ arr.tiled.clone()
369+ } else {
370+ self.layout.arrange_hiding(o, &held).tiled
371+ };
324372 let targets: Vec<(WindowId, Rect)> =
325- arr.tiled.iter().map(|p| (p.window, p.rect)).collect();
373+ planned.iter().map(|p| (p.window, p.rect)).collect();
326374 tracks.push((o, targets));
327375 }
328376
Modified file: crates/render-vk/src/renderer.rs+108 additions−43 deletions
@@ -26,12 +26,13 @@ use smithay::wayland::shm::{shm_format_to_fourcc, with_buffer_contents};
2626 use std::collections::HashMap;
2727 use std::fmt;
2828 use std::marker::PhantomData;
29-use std::sync::Arc;
29+use std::sync::{Arc, Mutex};
3030
3131 struct Pending {
3232 fence: Arc<FenceInner>,
3333 cmd: vk::CommandBuffer,
3434 _keep: Vec<Arc<ImageInner>>,
35+ _staging: Option<Staging>,
3536 }
3637
3738 pub struct VulkanRenderer {
@@ -365,10 +366,13 @@ impl VulkanRenderer {
365366 fence: fence.clone(),
366367 cmd,
367368 _keep: sampled,
369+ _staging: None,
368370 });
369371 Ok(SyncPoint::from(VkFence(fence)))
370372 }
371373
374+ /// Copies `region` of a full-image `data` into `image`, rows top-down or
375+ /// flipped.
372376 fn upload(
373377 &mut self,
374378 image: &Arc<ImageInner>,
@@ -376,35 +380,53 @@ impl VulkanRenderer {
376380 region: Rectangle<i32, Buffer>,
377381 flipped: bool,
378382 ) -> Result<(), Error> {
379- if image.external {
380- return Err(Error::ForeignTexture);
381- }
382383 let width = image.width as usize;
383- let needed = width * image.height as usize * BYTES_PER_PIXEL;
384+ let rows = image.height as usize;
385+ let needed = width * rows * BYTES_PER_PIXEL;
384386 if data.len() < needed {
385387 return Err(Error::BufferTooSmall {
386388 needed,
387389 got: data.len(),
388390 });
389391 }
392+ let stride = width * BYTES_PER_PIXEL;
393+ self.upload_with(image, region, |y| {
394+ let from = if flipped { rows - 1 - y } else { y };
395+ let start = from * stride + region.loc.x as usize * BYTES_PER_PIXEL;
396+ &data[start..start + region.size.w as usize * BYTES_PER_PIXEL]
397+ })
398+ }
399+
400+ /// Uploads `region` of `image`, taking each image row `y` in the region
401+ /// from `row(y)`. The CPU does not wait: the copy is ordered before later
402+ /// frames by its barrier, and its staging memory lives until its fence.
403+ fn upload_with<'a>(
404+ &mut self,
405+ image: &Arc<ImageInner>,
406+ region: Rectangle<i32, Buffer>,
407+ row: impl Fn(usize) -> &'a [u8],
408+ ) -> Result<(), Error> {
409+ if image.external {
410+ return Err(Error::ForeignTexture);
411+ }
412+ self.reap();
413+ let (w, h) = (region.size.w as usize, region.size.h as usize);
414+ if w == 0 || h == 0 {
415+ return Ok(());
416+ }
417+ let line = w * BYTES_PER_PIXEL;
390418 let mut staging = Staging::new(
391419 &self.shared,
392- needed,
420+ line * h,
393421 vk::BufferUsageFlags::TRANSFER_SRC,
394422 MemoryLocation::CpuToGpu,
395423 )?;
396- let row = width * BYTES_PER_PIXEL;
397- let rows = image.height as usize;
398424 let dst = staging.bytes_mut();
399- for y in 0..rows {
400- let from = if flipped { rows - 1 - y } else { y };
401- dst[y * row..(y + 1) * row].copy_from_slice(&data[from * row..(from + 1) * row]);
425+ for i in 0..h {
426+ dst[i * line..(i + 1) * line].copy_from_slice(row(region.loc.y as usize + i));
402427 }
403428 let copy = vk::BufferImageCopy::default()
404- .buffer_offset(
405- ((region.loc.y as usize * width + region.loc.x as usize) * BYTES_PER_PIXEL) as u64,
406- )
407- .buffer_row_length(image.width)
429+ .buffer_row_length(w as u32)
408430 .image_subresource(
409431 vk::ImageSubresourceLayers::default()
410432 .aspect_mask(vk::ImageAspectFlags::COLOR)
@@ -416,24 +438,19 @@ impl VulkanRenderer {
416438 z: 0,
417439 })
418440 .image_extent(vk::Extent3D {
419- width: region.size.w as u32,
420- height: region.size.h as u32,
441+ width: w as u32,
442+ height: h as u32,
421443 depth: 1,
422444 });
423- let buffer = staging.buffer;
424- let target = image.clone();
425- self.one_shot(|d, cmd, family| unsafe {
426- acquire(
427- d,
428- cmd,
429- family,
430- &target,
431- vk::ImageLayout::TRANSFER_DST_OPTIMAL,
432- );
445+ let d = &self.shared.device.device;
446+ let family = self.shared.device.queue_family;
447+ let cmd = self.begin()?;
448+ unsafe {
449+ acquire(d, cmd, family, image, vk::ImageLayout::TRANSFER_DST_OPTIMAL);
433450 d.cmd_copy_buffer_to_image(
434451 cmd,
435- buffer,
436- target.image,
452+ staging.buffer,
453+ image.image,
437454 vk::ImageLayout::TRANSFER_DST_OPTIMAL,
438455 &[copy],
439456 );
@@ -441,10 +458,18 @@ impl VulkanRenderer {
441458 d,
442459 cmd,
443460 family,
444- &target,
461+ image,
445462 vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
446463 );
447- })
464+ }
465+ let fence = self.end_and_submit(cmd)?;
466+ self.pending.push(Pending {
467+ fence,
468+ cmd,
469+ _keep: vec![image.clone()],
470+ _staging: Some(staging),
471+ });
472+ Ok(())
448473 }
449474
450475 fn read_back(
@@ -879,13 +904,20 @@ impl ExportMem for VulkanRenderer {
879904 }
880905
881906 impl ImportMemWl for VulkanRenderer {
907+ /// Keeps one texture per surface and uploads only the damaged rows into
908+ /// it, straight from the client's shm pool.
882909 fn import_shm_buffer(
883910 &mut self,
884911 buffer: &WlBuffer,
885- _surface: Option<&SurfaceData>,
886- _damage: &[Rectangle<i32, Buffer>],
912+ surface: Option<&SurfaceData>,
913+ damage: &[Rectangle<i32, Buffer>],
887914 ) -> Result<VulkanTexture, Error> {
888- let (pixels, fourcc, size) = with_buffer_contents(buffer, |ptr, len, data| {
915+ let cache = surface.map(|s| {
916+ s.data_map
917+ .insert_if_missing_threadsafe(|| ShmCache(Mutex::new(None)));
918+ s.data_map.get::<ShmCache>().expect("just inserted")
919+ });
920+ with_buffer_contents(buffer, |ptr, len, data| {
889921 let fourcc = shm_format_to_fourcc(data.format).ok_or(Error::ShmAccess)?;
890922 let (w, h, stride, offset) = (
891923 data.width as usize,
@@ -893,18 +925,51 @@ impl ImportMemWl for VulkanRenderer {
893925 data.stride as usize,
894926 data.offset as usize,
895927 );
896- let row = w * BYTES_PER_PIXEL;
897- if w == 0 || h == 0 || stride < row || offset + stride * (h - 1) + row > len {
928+ let line = w * BYTES_PER_PIXEL;
929+ if w == 0 || h == 0 || stride < line || offset + stride * (h - 1) + line > len {
898930 return Err(Error::ShmAccess);
899931 }
900- let mut pixels = Vec::with_capacity(row * h);
901- for y in 0..h {
902- let line = unsafe { std::slice::from_raw_parts(ptr.add(offset + y * stride), row) };
903- pixels.extend_from_slice(line);
932+ let size = Size::from((data.width, data.height));
933+ let full = Rectangle::from_size(size);
934+ let reused = cache
935+ .and_then(|c| c.0.lock().unwrap_or_else(|e| e.into_inner()).clone())
936+ .filter(|t| {
937+ t.0.fourcc == fourcc
938+ && t.size() == size
939+ && Arc::ptr_eq(&t.0.shared, &self.shared)
940+ });
941+ let (texture, region) = match reused {
942+ Some(t) => {
943+ let dirty = damage
944+ .iter()
945+ .filter_map(|d| d.intersection(full))
946+ .reduce(|a, b| a.merge(b));
947+ (t, if damage.is_empty() { Some(full) } else { dirty })
948+ }
949+ None => {
950+ let usage = vk::ImageUsageFlags::SAMPLED
951+ | vk::ImageUsageFlags::TRANSFER_DST
952+ | vk::ImageUsageFlags::TRANSFER_SRC;
953+ let image =
954+ ImageInner::create(&self.shared, fourcc, w as u32, h as u32, usage)?;
955+ (VulkanTexture(image), Some(full))
956+ }
957+ };
958+ if let Some(region) = region {
959+ let x = region.loc.x as usize * BYTES_PER_PIXEL;
960+ let rw = region.size.w as usize * BYTES_PER_PIXEL;
961+ self.upload_with(&texture.0, region, |y| unsafe {
962+ std::slice::from_raw_parts(ptr.add(offset + y * stride + x), rw)
963+ })?;
904964 }
905- Ok((pixels, fourcc, Size::from((data.width, data.height))))
965+ if let Some(c) = cache {
966+ *c.0.lock().unwrap_or_else(|e| e.into_inner()) = Some(texture.clone());
967+ }
968+ Ok(texture)
906969 })
907- .map_err(|_| Error::ShmAccess)??;
908- self.import_memory(&pixels, fourcc, size, false)
970+ .map_err(|_| Error::ShmAccess)?
909971 }
910972 }
973+
974+/// The texture last imported for a surface's shm buffers.
975+struct ShmCache(Mutex<Option<VulkanTexture>>);