srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src/screencopy.rs
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2024-04-08 23:54:00 +0200
committersrdusr <[email protected]>2024-04-08 23:54:00 +0200
commit709333908d5b7d3157165d1811c8bc1795ab0028 (patch)
tree92d5616519f63c1e4743add0294ce552e239681c /crates/wayland/src/screencopy.rs
parent592bd3716c0aa5a47a5eb43f45f55844de9db382 (diff)
downloadsrdwm-709333908d5b7d3157165d1811c8bc1795ab0028.tar.gz
srdwm-709333908d5b7d3157165d1811c8bc1795ab0028.zip
Wayland: clipboard, session lock, screencopy, multi-monitor; modularize
Implements the protocols that were blocking srdwm-wayland from being a real session, plus multi-monitor, and splits the backend into modules. Everything here was verified by running it against real clients, not just compiled. Protocols - wlr-layer-shell + xdg-output: bars/launchers/notifications. xdg-output is not optional in practice - without it wofi segfaults rather than degrading, since it calls get_xdg_output without null-checking. - Clipboard: wl_data_device_manager, primary selection, and wlr-data-control. Data-control is what `wl-paste --watch cliphist store` needs, as it reads the selection without holding focus. Selection focus now follows keyboard focus, without which a focused window can neither copy nor paste. - ext-session-lock: `locked` gates rendering and input. No key is treated as a WM binding while locked - the config binds Mod4+Return to spawn a terminal, so honouring bindings at a locked screen would defeat the lock. The lock is confirmed only after a client-content-free frame has actually been presented, never at request time. - wlr-screencopy (hand-written; smithay ships no helper) for grim/slurp. Multi-monitor Every connected connector becomes a head with its own buffers, damage tracker and page-flip state, matched by CRTC so differing refresh rates don't gate each other. Outputs are reached through primary_output/output_at/ output_for_wl rather than a single field, which kept the change to ~11 call sites. Session lock creates one lock surface per output and waits for all of them, so a second monitor can't still show the desktop when the locker is told the session is safe. Modes are picked by the PREFERRED flag, not list order, and CRTCs are never double-assigned. Bugs found by testing, not review - Nothing gave a newly-created window Wayland focus: a freshly-opened app received no keystrokes and could not paste until clicked. - Opening a window at a locked screen stole keyboard focus. Caught by counting wl_keyboard.enter delivered to a client launched while locked: 1 before the guard, 0 after. A killed locker correctly leaves it locked. - Reading back the winit EGL window surface destroyed the GL context on the first screencopy capture, taking the compositor down. Root-caused by A/B-ing the same build with only the readback removed; capture now renders an offscreen pass. - Output mode was resent every frame at 60Hz, flooding any client bound to wl_output with duplicate mode/done events. - general.default_layout was defaulted and validated but never read, so setting it did nothing. srdwm is dynamic-first (Windows/macOS style, with drag-to-edge snapping); tiling is one opt-in layout, and that stays true. - .gitignore's unanchored `srdwm` matched any path component of that name, silently excluding the whole crates/srdwm source crate - the binary crate the workspace lists as a member, so a fresh clone could not build. Modularization lib.rs went from ~1260 lines to 78: state, protocols, input, lock, screencopy, winit and udev now each own one responsibility. lock is grouped by feature rather than kind on purpose, since its security invariant spans state, protocol handling and rendering at once. Multi-monitor was verified in the QEMU VM with a two-output virtio-gpu: both heads screendumped at their own resolution showing srdwm's clear colour, and a window forced to global x=1500 landed on head 1 at head-local x=220 while head 0 stayed empty. Known limitation, now documented: the nested winit backend stalls while its window is occluded, because the host stops scheduling frames and eglSwapBuffers blocks.
Diffstat (limited to 'crates/wayland/src/screencopy.rs')
-rw-r--r--crates/wayland/src/screencopy.rs302
1 files changed, 302 insertions, 0 deletions
diff --git a/crates/wayland/src/screencopy.rs b/crates/wayland/src/screencopy.rs
new file mode 100644
index 0000000..324669c
--- /dev/null
+++ b/crates/wayland/src/screencopy.rs
@@ -0,0 +1,302 @@
+//! `wlr-screencopy-unstable-v1`: lets a client ask the compositor for the
+//! contents of an output (or a region of one). This is what `grim` uses, and
+//! therefore what the user's `Print`/`Alt+Print` screenshot binds
+//! (`grim`, `slurp | grim -g -`) and `wf-recorder` need.
+//!
+//! Unlike every other protocol this backend speaks, smithay 0.7 ships no
+//! helper for this one - there is no `ScreencopyState`/`ScreencopyHandler`
+//! to delegate to - so the `GlobalDispatch`/`Dispatch` plumbing below is
+//! written out by hand against the raw `wayland-protocols-wlr` server
+//! bindings.
+//!
+//! Capture is deferred, not immediate: a `copy` request only *queues* the
+//! frame (`CompState::screencopy_pending`), and the actual pixels are read
+//! back inside the render pass, from the framebuffer that was just drawn
+//! (see `service_pending`). Doing it at request time would either capture
+//! the previous frame or require an extra off-screen render of the whole
+//! scene; reading back the real framebuffer is both cheaper and matches what
+//! is actually on screen.
+
+use std::time::UNIX_EPOCH;
+
+use smithay::backend::allocator::Fourcc;
+use smithay::backend::renderer::{ExportMem, Renderer};
+use smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer;
+use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
+use smithay::reexports::wayland_server::protocol::wl_shm;
+use smithay::reexports::wayland_server::{
+ Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
+};
+use smithay::utils::{Buffer as BufferCoord, Physical, Rectangle, Size};
+use smithay::wayland::shm::with_buffer_contents_mut;
+use wayland_protocols_wlr::screencopy::v1::server::{
+ zwlr_screencopy_frame_v1::{self, ZwlrScreencopyFrameV1},
+ zwlr_screencopy_manager_v1::{self, ZwlrScreencopyManagerV1},
+};
+
+use crate::state::CompState;
+
+/// Every pixel we hand out is 4 bytes; the protocol needs a stride and the
+/// shm format that matches what `copy_framebuffer` is asked to produce.
+const BYTES_PER_PIXEL: u32 = 4;
+const CAPTURE_FOURCC: Fourcc = Fourcc::Xrgb8888;
+const CAPTURE_SHM_FORMAT: wl_shm::Format = wl_shm::Format::Xrgb8888;
+
+/// The screencopy manager global. Held by `CompState` purely to keep the
+/// global alive for the compositor's lifetime.
+#[derive(Debug)]
+pub struct ScreencopyState {
+ _global: smithay::reexports::wayland_server::backend::GlobalId,
+}
+
+impl ScreencopyState {
+ pub fn new<D>(dh: &DisplayHandle) -> Self
+ where
+ D: GlobalDispatch<ZwlrScreencopyManagerV1, ()> + 'static,
+ {
+ // Version 3 advertises `linux_dmabuf`/`buffer_done`, which this
+ // software-readback implementation does not support, so cap at 2 --
+ // that still covers `copy_with_damage`, which `wf-recorder` uses.
+ Self { _global: dh.create_global::<D, ZwlrScreencopyManagerV1, _>(2, ()) }
+ }
+}
+
+/// State attached to each `zwlr_screencopy_frame_v1`.
+#[derive(Debug, Clone)]
+pub struct FrameData {
+ /// Region of the output to capture, in physical pixels.
+ pub region: Rectangle<i32, Physical>,
+ /// Set once `copy`/`copy_with_damage` has been handled, so a second one
+ /// can be rejected with the protocol's `already_used` error.
+ pub used: bool,
+}
+
+/// A capture that has been requested but not yet serviced. Drained by
+/// `service_pending` during the next render pass.
+#[derive(Debug)]
+pub struct PendingCapture {
+ pub frame: ZwlrScreencopyFrameV1,
+ pub buffer: WlBuffer,
+ pub region: Rectangle<i32, Physical>,
+ /// `copy_with_damage` clients expect a `damage` event before `ready`.
+ pub with_damage: bool,
+}
+
+impl GlobalDispatch<ZwlrScreencopyManagerV1, ()> for CompState {
+ fn bind(
+ _state: &mut Self,
+ _dh: &DisplayHandle,
+ _client: &Client,
+ manager: New<ZwlrScreencopyManagerV1>,
+ _data: &(),
+ data_init: &mut DataInit<'_, Self>,
+ ) {
+ data_init.init(manager, ());
+ }
+}
+
+impl Dispatch<ZwlrScreencopyManagerV1, ()> for CompState {
+ fn request(
+ state: &mut Self,
+ _client: &Client,
+ _manager: &ZwlrScreencopyManagerV1,
+ request: zwlr_screencopy_manager_v1::Request,
+ _data: &(),
+ _dh: &DisplayHandle,
+ data_init: &mut DataInit<'_, Self>,
+ ) {
+ use zwlr_screencopy_manager_v1::Request;
+ let (frame, region) = match request {
+ Request::CaptureOutput { frame, overlay_cursor: _, output } => {
+ (frame, state.output_capture_region(&output))
+ }
+ Request::CaptureOutputRegion { frame, overlay_cursor: _, output, x, y, width, height } => {
+ // Clamp to the output: a client is free to ask for a region
+ // hanging off the edge (slurp will, at a screen border), and
+ // `copy_framebuffer` errors out on out-of-bounds reads.
+ let full = state.output_capture_region(&output);
+ let requested = Rectangle::new((x, y).into(), (width.max(0), height.max(0)).into());
+ (frame, full.intersection(requested).unwrap_or_default())
+ }
+ Request::Destroy => return,
+ _ => return,
+ };
+
+ let frame = data_init.init(frame, FrameData { region, used: false });
+ if region.size.w <= 0 || region.size.h <= 0 {
+ // Nothing to capture (empty or fully off-screen region).
+ frame.failed();
+ return;
+ }
+ frame.buffer(
+ CAPTURE_SHM_FORMAT,
+ region.size.w as u32,
+ region.size.h as u32,
+ region.size.w as u32 * BYTES_PER_PIXEL,
+ );
+ }
+}
+
+impl Dispatch<ZwlrScreencopyFrameV1, FrameData> for CompState {
+ fn request(
+ state: &mut Self,
+ _client: &Client,
+ frame: &ZwlrScreencopyFrameV1,
+ request: zwlr_screencopy_frame_v1::Request,
+ data: &FrameData,
+ _dh: &DisplayHandle,
+ _data_init: &mut DataInit<'_, Self>,
+ ) {
+ use zwlr_screencopy_frame_v1::Request;
+ let (buffer, with_damage) = match request {
+ Request::Copy { buffer } => (buffer, false),
+ Request::CopyWithDamage { buffer } => (buffer, true),
+ Request::Destroy => {
+ state.screencopy_pending.retain(|p| &p.frame != frame);
+ return;
+ }
+ _ => return,
+ };
+
+ if data.used {
+ frame.post_error(zwlr_screencopy_frame_v1::Error::AlreadyUsed, "frame was already copied");
+ return;
+ }
+
+ // Validate the buffer really can hold the frame before promising a
+ // capture - a mismatch here would otherwise be a silent short write.
+ let expected_stride = data.region.size.w as u32 * BYTES_PER_PIXEL;
+ let ok = with_buffer_contents_mut(&buffer, |_ptr, len, spec| {
+ spec.format == CAPTURE_SHM_FORMAT
+ && spec.width == data.region.size.w
+ && spec.height == data.region.size.h
+ && spec.stride as u32 == expected_stride
+ && len >= (expected_stride * data.region.size.h as u32) as usize
+ })
+ .unwrap_or(false);
+ if !ok {
+ frame.post_error(
+ zwlr_screencopy_frame_v1::Error::InvalidBuffer,
+ "buffer does not match the advertised format/size/stride",
+ );
+ return;
+ }
+
+ state.screencopy_pending.push(PendingCapture {
+ frame: frame.clone(),
+ buffer,
+ region: data.region,
+ with_damage,
+ });
+ }
+
+ fn destroyed(state: &mut Self, _client: smithay::reexports::wayland_server::backend::ClientId, frame: &ZwlrScreencopyFrameV1, _data: &FrameData) {
+ state.screencopy_pending.retain(|p| &p.frame != frame);
+ }
+}
+
+impl CompState {
+ /// Full-output capture region for the output the client named, in that
+ /// output's own physical pixels (captures read back a single output's
+ /// framebuffer, so coordinates are output-local, not global).
+ fn output_capture_region(&self, output: &WlOutput) -> Rectangle<i32, Physical> {
+ let size: Size<i32, Physical> = self
+ .output_for_wl(output)
+ .and_then(|e| e.output.current_mode())
+ .map(|m| m.size)
+ .unwrap_or_default();
+ Rectangle::from_size(size)
+ }
+}
+
+/// Services every queued capture against the framebuffer that was just
+/// rendered, then answers each client with `flags` + `ready` (or `failed`).
+///
+/// Called from inside both backends' render passes, while `framebuffer` is
+/// still bound and holds the current frame.
+/// Rejects queued captures outright. Used while the session is locked: the
+/// render pass draws only the lock surface, so there is no frame a capture
+/// could legitimately be served from. Failing immediately is both correct
+/// (the client gets an answer instead of hanging) and safer than leaving
+/// requests queued, which would otherwise all fire against the first
+/// *unlocked* frame after the screen is unlocked.
+pub fn fail_pending(pending: Vec<PendingCapture>) {
+ for capture in pending {
+ if capture.frame.is_alive() {
+ capture.frame.failed();
+ }
+ }
+}
+
+/// Takes the queue by value rather than `&mut CompState` so the udev
+/// backend can call it while already holding a `&mut` borrow of the
+/// `UdevOutput` that owns its renderer. Callers drain
+/// `CompState::screencopy_pending` before binding the renderer.
+pub fn service_pending<R>(pending: Vec<PendingCapture>, renderer: &mut R, framebuffer: &R::Framebuffer<'_>)
+where
+ R: Renderer + ExportMem,
+{
+ for pending in pending {
+ if !pending.frame.is_alive() {
+ continue;
+ }
+ match copy_region(renderer, framebuffer, pending.region, &pending.buffer) {
+ Ok(()) => {
+ // No `y_invert`: `copy_framebuffer` hands back rows already
+ // in top-down order for both renderers used here (verified
+ // against a real `grim` capture - an inverted image is the
+ // immediately visible symptom if this is ever wrong).
+ pending.frame.flags(zwlr_screencopy_frame_v1::Flags::empty());
+ if pending.with_damage {
+ pending.frame.damage(0, 0, pending.region.size.w as u32, pending.region.size.h as u32);
+ }
+ let now = UNIX_EPOCH.elapsed().unwrap_or_default();
+ let secs = now.as_secs();
+ pending.frame.ready((secs >> 32) as u32, secs as u32, now.subsec_nanos());
+ }
+ Err(e) => {
+ log::warn!("screencopy: capture failed: {e}");
+ pending.frame.failed();
+ }
+ }
+ }
+}
+
+fn copy_region<R>(
+ renderer: &mut R,
+ framebuffer: &R::Framebuffer<'_>,
+ region: Rectangle<i32, Physical>,
+ buffer: &WlBuffer,
+) -> Result<(), String>
+where
+ R: Renderer + ExportMem,
+{
+ // `copy_framebuffer` works in buffer coordinates; with no output
+ // transform or fractional scale in play (see this backend's single
+ // `Output`), those are the same numbers as the physical ones.
+ let src: Rectangle<i32, BufferCoord> = Rectangle::new((region.loc.x, region.loc.y).into(), (region.size.w, region.size.h).into());
+ let mapping = renderer
+ .copy_framebuffer(framebuffer, src, CAPTURE_FOURCC)
+ .map_err(|e| format!("copy_framebuffer: {e}"))?;
+ let pixels = renderer.map_texture(&mapping).map_err(|e| format!("map_texture: {e}"))?;
+
+ let stride = region.size.w as usize * BYTES_PER_PIXEL as usize;
+ let needed = stride * region.size.h as usize;
+ if pixels.len() < needed {
+ return Err(format!("readback produced {} bytes, need {}", pixels.len(), needed));
+ }
+
+ with_buffer_contents_mut(buffer, |ptr, len, _spec| {
+ if len < needed {
+ return Err(format!("client buffer holds {len} bytes, need {needed}"));
+ }
+ // SAFETY: `with_buffer_contents_mut` guarantees `ptr` is valid for
+ // `len` bytes for the duration of this closure, and `needed <= len`
+ // was just checked. Source and destination are distinct mappings.
+ unsafe {
+ std::ptr::copy_nonoverlapping(pixels.as_ptr(), ptr, needed);
+ }
+ Ok(())
+ })
+ .map_err(|e| format!("client buffer not accessible: {e}"))?
+}