srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src/udev/capture.rs
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2024-12-24 20:44:00 +0200
committersrdusr <[email protected]>2024-12-24 20:44:00 +0200
commit3d73ed0f057a555ded67ad58dbe02b36cd4d2d1f (patch)
tree71e9bab462b9073444183e2a969ba512cb94559f /crates/wayland/src/udev/capture.rs
parent331a1a1d3a0d8dfd4c285b755c2e13785c95ee2a (diff)
downloadsrdwm-3d73ed0f057a555ded67ad58dbe02b36cd4d2d1f.tar.gz
srdwm-3d73ed0f057a555ded67ad58dbe02b36cd4d2d1f.zip
Accumulate wayland-crate additions: capture rendering, focus/Space sync, VT resume, plumbing
Bundles the remaining wayland-crate changes built up here, touching both backends (udev and winit) and the shared input/rendering code: - udev/capture.rs: off-screen Pixman render of an arbitrary (not necessarily on-screen) workspace's window content to a PPM file -- what crates/core's capture-request queue drives, for a workspace switcher's thumbnail previews. wlr-screencopy structurally can't do this (it can only see what an output is presenting), which is why this exists as a separate render path rather than reusing it. - input::focus_window now also raises the window in smithay's own Space, not just core's stacking order - Space is what actually renders on top and what pointer hit-testing reads, so any focus path that skipped this (an IPC "focus" dispatch, concretely) left a window genuinely focused while still rendering, and receiving clicks, underneath whatever was already topmost. Both backends' poll loops now re-sync this after any IPC mutation. - udev/session.rs's VT-switch resume fix (drains a stale pending page flip before reasserting CRTCs) already has its own earlier, cleanly isolated commit - not duplicated here. - Assorted decoration/cursor/rounded-corners/output-management/ screencopy/XWayland changes and their cross-backend wiring. Coarser than the repo's usual one-purpose-per-commit convention, deliberately - see the core-crate sweep commit's own message for why.
Diffstat (limited to 'crates/wayland/src/udev/capture.rs')
-rw-r--r--crates/wayland/src/udev/capture.rs129
1 files changed, 129 insertions, 0 deletions
diff --git a/crates/wayland/src/udev/capture.rs b/crates/wayland/src/udev/capture.rs
new file mode 100644
index 0000000..0f87d27
--- /dev/null
+++ b/crates/wayland/src/udev/capture.rs
@@ -0,0 +1,129 @@
+//! Off-screen render of a workspace that isn't necessarily the one
+//! currently on screen - `srd capture workspace <id> <path>`, drained
+//! from `WindowManager::drain_capture_requests` on every poll. See
+//! `srdwm_core::CaptureRequest`'s own doc comment for why this exists at
+//! all: `wlr-screencopy` (`crates/wayland/src/screencopy.rs`, and `grim`)
+//! can only ever see what an output is actually presenting, and a
+//! workspace switcher's thumbnail needs exactly the opposite - a
+//! workspace that, most of the time, is *not* the one presented.
+//!
+//! Deliberately simple, not a small reimplementation of
+//! `render_udev_frame`: only window content is drawn, no borders,
+//! shadows, titlebars, cursor or layer-shell surfaces - every consumer
+//! this was built for (a workspace-switcher tile) draws those tiny, where
+//! that detail is imperceptible, and skipping them keeps this from needing
+//! to duplicate that function's animation/occlusion bookkeeping. Always
+//! renders at the target monitor's native resolution and downscales
+//! afterward if a smaller size was requested, rather than trying to get
+//! smithay's fractional-output-scale rendering path exactly right for a
+//! target with no real `Output` behind it.
+
+use super::*;
+use smithay::backend::allocator::Fourcc;
+use smithay::backend::renderer::element::surface::{render_elements_from_surface_tree, WaylandSurfaceRenderElement};
+use smithay::backend::renderer::element::Kind;
+use smithay::backend::renderer::{Bind, ExportMem, Offscreen};
+use smithay::utils::{Buffer as BufferCoord, Transform};
+
+impl CompState {
+ /// Services every capture request queued since the last poll. Takes
+ /// the `Vec` by value for the same reason `screencopy::service_pending`
+ /// does: the renderer this needs lives behind `self.udev`'s own
+ /// mutable borrow, so the request list has to be lifted out of
+ /// `self.wm` before that borrow starts.
+ pub(crate) fn service_capture_requests(&mut self, requests: Vec<srdwm_core::CaptureRequest>) {
+ for req in requests {
+ if let Err(e) = self.capture_workspace(&req) {
+ log::warn!("capture: workspace {} -> {}: {e}", req.workspace, req.path);
+ }
+ }
+ }
+
+ fn capture_workspace(&mut self, req: &srdwm_core::CaptureRequest) -> Result<(), String> {
+ // The monitor a freshly-placed window on this workspace would land
+ // on: workspaces aren't per-monitor in this compositor (a single
+ // `current_workspace` is shared by every screen - see
+ // `WindowManager`'s own field doc comment), so there's no single
+ // "this workspace's monitor" to ask for; the primary one is the
+ // same reasonable default `arrange_workspace` itself falls back to.
+ let (origin, native): ((i32, i32), (u32, u32)) = {
+ let wm = self.wm.borrow();
+ let monitor = wm.monitors().iter().find(|m| m.primary).or_else(|| wm.monitors().first()).ok_or("no monitor to capture from")?;
+ ((monitor.full_geometry.x, monitor.full_geometry.y), (monitor.full_geometry.width, monitor.full_geometry.height))
+ };
+ if native.0 == 0 || native.1 == 0 {
+ return Err("monitor has zero size".to_string());
+ }
+
+ let ids = self.wm.borrow().window_ids_on_workspace_front_to_back(req.workspace);
+ let mut elements: Vec<WaylandSurfaceRenderElement<PixmanRenderer>> = Vec::new();
+ let Some(udev) = self.udev.as_mut() else { return Err("no udev backend".to_string()) };
+ for id in ids {
+ let Some(w) = self.id_to_window.get(&id) else { continue };
+ let Some(surface) = crate::input::dwindow_wl_surface(w) else { continue };
+ let Some(geom) = self.wm.borrow().window(id).map(|w| w.geometry) else { continue };
+ let loc = (geom.x - origin.0, geom.y - origin.1);
+ elements.extend(render_elements_from_surface_tree(&mut udev.renderer, &surface, loc, 1.0, 1.0, Kind::Unspecified));
+ }
+
+ let (nw, nh) = (native.0 as i32, native.1 as i32);
+ let mut target = udev.renderer.create_buffer(Fourcc::Xrgb8888, (nw, nh).into()).map_err(|e| format!("create_buffer: {e}"))?;
+ let mut framebuffer = udev.renderer.bind(&mut target).map_err(|e| format!("bind: {e}"))?;
+ let mut tracker = OutputDamageTracker::new((nw, nh), 1.0, Transform::Normal);
+ tracker
+ .render_output(&mut udev.renderer, &mut framebuffer, 0, &elements, [0.0, 0.0, 0.0, 1.0])
+ .map_err(|e| format!("render_output: {e:?}"))?;
+
+ let region: Rectangle<i32, BufferCoord> = Rectangle::new((0, 0).into(), (nw, nh).into());
+ let mapping = udev.renderer.copy_framebuffer(&framebuffer, region, Fourcc::Xrgb8888).map_err(|e| format!("copy_framebuffer: {e}"))?;
+ let pixels = udev.renderer.map_texture(&mapping).map_err(|e| format!("map_texture: {e}"))?;
+
+ write_ppm(pixels, native, req.size, &req.path)
+ }
+}
+
+/// `pixels` is `Xrgb8888` - 4 bytes per pixel, little-endian, so byte
+/// order in memory is B, G, R, X. PPM (`P6`) wants tightly-packed R, G, B
+/// with no pad byte, hence the reorder rather than a straight `memcpy`.
+/// Downscales with plain nearest-neighbor sampling when `target` is
+/// smaller than `native` - a thumbnail has no need for anything more
+/// expensive, and this avoids pulling in an image-scaling crate for one
+/// call site.
+fn write_ppm(pixels: &[u8], native: (u32, u32), target: Option<(u32, u32)>, path: &str) -> Result<(), String> {
+ let (nw, nh) = native;
+ let (tw, th) = target.unwrap_or(native);
+ if tw == 0 || th == 0 {
+ return Err("requested capture size is zero".to_string());
+ }
+ let src_stride = nw as usize * 4;
+ let needed = src_stride * nh as usize;
+ if pixels.len() < needed {
+ return Err(format!("readback produced {} bytes, need {needed}", pixels.len()));
+ }
+
+ let mut rgb = Vec::with_capacity(tw as usize * th as usize * 3);
+ for ty in 0..th {
+ // `.min(nh - 1)`/`.min(nw - 1)`: guards the last row/column of a
+ // downscale from ever reading one pixel past the source when an
+ // integer ratio rounds up, not a real expectation of overflow.
+ let sy = (ty as u64 * nh as u64 / th as u64).min(nh as u64 - 1) as usize;
+ for tx in 0..tw {
+ let sx = (tx as u64 * nw as u64 / tw as u64).min(nw as u64 - 1) as usize;
+ let i = sy * src_stride + sx * 4;
+ rgb.push(pixels[i + 2]); // R
+ rgb.push(pixels[i + 1]); // G
+ rgb.push(pixels[i]); // B
+ }
+ }
+
+ let mut out = format!("P6\n{tw} {th}\n255\n").into_bytes();
+ out.extend_from_slice(&rgb);
+ // Written to a `.tmp` sibling and renamed into place: a reader (AGS's
+ // wsPreview poller) racing a partial write is exactly the kind of
+ // flicker/corruption a debounced, event-driven cache is supposed to
+ // avoid - `rename` within the same directory is atomic, a plain
+ // `write` never is.
+ let tmp = format!("{path}.tmp");
+ std::fs::write(&tmp, &out).map_err(|e| format!("write {tmp}: {e}"))?;
+ std::fs::rename(&tmp, path).map_err(|e| format!("rename to {path}: {e}"))
+}