diff options
| author | srdusr <[email protected]> | 2024-12-24 20:44:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2024-12-24 20:44:00 +0200 |
| commit | 3d73ed0f057a555ded67ad58dbe02b36cd4d2d1f (patch) | |
| tree | 71e9bab462b9073444183e2a969ba512cb94559f /crates/wayland/src/udev/capture.rs | |
| parent | 331a1a1d3a0d8dfd4c285b755c2e13785c95ee2a (diff) | |
| download | srdwm-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.rs | 129 |
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}")) +} |