srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src/winit.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/winit.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/winit.rs')
-rw-r--r--crates/wayland/src/winit.rs412
1 files changed, 412 insertions, 0 deletions
diff --git a/crates/wayland/src/winit.rs b/crates/wayland/src/winit.rs
new file mode 100644
index 0000000..85be223
--- /dev/null
+++ b/crates/wayland/src/winit.rs
@@ -0,0 +1,412 @@
+//! Nested ("winit") backend: srdwm as a window on an existing
+//! compositor/X server, the Wayland analogue of running the X11 backend
+//! under Xephyr. Used for development and for the case where srdwm is
+//! started from inside another session; the real bare-TTY path is
+//! [`crate::udev`].
+//!
+//! Both backends share all protocol state ([`crate::state::CompState`]),
+//! input routing ([`crate::input`]) and lock behaviour ([`crate::lock`]);
+//! what differs is only how a frame reaches a screen.
+
+use std::cell::RefCell;
+use std::collections::HashMap;
+use std::rc::Rc;
+use std::time::Instant;
+
+use smithay::backend::allocator::Fourcc;
+use smithay::backend::input::{AbsolutePositionEvent, ButtonState as BackendButtonState, Event as InputEventTrait, InputEvent, PointerButtonEvent};
+use smithay::backend::renderer::damage::OutputDamageTracker;
+use smithay::backend::renderer::element::memory::MemoryRenderBufferRenderElement;
+use smithay::backend::renderer::element::Kind;
+use smithay::backend::renderer::gles::GlesRenderer;
+use smithay::backend::winit::{self, WinitEvent, WinitEventLoop, WinitGraphicsBackend};
+use smithay::desktop::space::render_output;
+use smithay::desktop::{layer_map_for_output, Space};
+use smithay::input::SeatState;
+use smithay::output::{Mode as OutputMode, Output, PhysicalProperties, Subpixel};
+use smithay::reexports::wayland_server::{Client, Display, ListeningSocket};
+use smithay::reexports::winit::platform::pump_events::PumpStatus;
+use smithay::utils::Transform;
+use smithay::wayland::compositor::CompositorState;
+use smithay::wayland::selection::data_device::DataDeviceState;
+use smithay::wayland::selection::primary_selection::PrimarySelectionState;
+use smithay::wayland::selection::wlr_data_control::DataControlState;
+use smithay::wayland::session_lock::SessionLockManagerState;
+use smithay::wayland::shell::wlr_layer::WlrLayerShellState;
+use smithay::wayland::shell::xdg::decoration::XdgDecorationState;
+use smithay::wayland::shell::xdg::XdgShellState;
+use smithay::wayland::shm::ShmState;
+
+use srdwm_core::{Event as CoreEvent, Window as CoreWindow, WindowId, WindowManager};
+use srdwm_platform::{Platform, PlatformError, PlatformKind, Result as PlatformResult};
+
+use crate::input::{handle_keyboard_key_event, handle_pointer_button, handle_pointer_position, last_pointer_pos};
+use crate::lock::{lock_render_elements, send_lock_frame};
+use crate::lock::SessionLock;
+use crate::state::{ClientState, CompState, OutputEntry};
+use crate::{err, screencopy};
+
+pub struct WaylandPlatform {
+ display: Display<CompState>,
+ state: CompState,
+ backend: WinitGraphicsBackend<GlesRenderer>,
+ winit_events: WinitEventLoop,
+ damage_tracker: OutputDamageTracker,
+ output: Output,
+ listener: ListeningSocket,
+ clients: Vec<Client>,
+ pending: Rc<RefCell<Vec<CoreEvent>>>,
+ wm: Rc<RefCell<WindowManager>>,
+}
+
+impl WaylandPlatform {
+ /// `bound_keys` are the config's `"Mod4+Shift+Return"`-style combo
+ /// strings (see `srdwm_core::key_combo_string`) - the same set the X11
+ /// backend grabs individually via `XGrabKey`. Only a keypress matching
+ /// one of these is withheld from the focused client.
+ pub fn connect(wm: Rc<RefCell<WindowManager>>, bound_keys: &[String]) -> PlatformResult<Self> {
+ let display: Display<CompState> = Display::new().map_err(err)?;
+ let dh = display.handle();
+
+ let (backend, winit_events) = winit::init::<GlesRenderer>().map_err(err)?;
+ let size = backend.window_size();
+
+ let output = Output::new(
+ "srdwm-wayland".to_string(),
+ PhysicalProperties { size: (0, 0).into(), subpixel: Subpixel::Unknown, make: "srdwm".into(), model: "winit".into() },
+ );
+ output.change_current_state(
+ Some(OutputMode { size, refresh: 60_000 }),
+ Some(Transform::Normal),
+ None,
+ Some((0, 0).into()),
+ );
+ output.create_global::<CompState>(&dh);
+ // xdg-output (zxdg_output_manager_v1): several real layer-shell
+ // clients (confirmed live: wofi 1.5.3) unconditionally call
+ // `zxdg_output_manager_v1.get_xdg_output` while setting up a layer
+ // surface and don't null-check the manager proxy if the global was
+ // never advertised - so without this, those clients don't just
+ // fail gracefully, they segfault. Real wlroots compositors (e.g.
+ // Hyprland) always advertise it, which is why this only surfaced
+ // once a real Wayland-native client could connect at all (see
+ // `docs/IMPLEMENTATION_STATUS.md`).
+ smithay::wayland::output::OutputManagerState::new_with_xdg_output::<CompState>(&dh);
+
+ let compositor_state = CompositorState::new::<CompState>(&dh);
+ let xdg_shell_state = XdgShellState::new::<CompState>(&dh);
+ let xdg_decoration_state = XdgDecorationState::new::<CompState>(&dh);
+ let shm_state = ShmState::new::<CompState>(&dh, Vec::new());
+ // Order matters: data-control piggybacks on primary-selection's
+ // client filter, so it needs the already-built state by reference.
+ let primary_selection_state = PrimarySelectionState::new::<CompState>(&dh);
+ let data_control_state = DataControlState::new::<CompState, _>(&dh, Some(&primary_selection_state), |_| true);
+ let mut seat_state = SeatState::new();
+ let mut seat = seat_state.new_wl_seat(&dh, "seat0");
+ seat.add_keyboard(Default::default(), 200, 25).map_err(err)?;
+ seat.add_pointer();
+
+ let mut space = Space::default();
+ space.map_output(&output, (0, 0));
+
+ let pending = Rc::new(RefCell::new(Vec::new()));
+ let state = CompState {
+ compositor_state,
+ xdg_shell_state,
+ _xdg_decoration_state: xdg_decoration_state,
+ shm_state,
+ seat_state,
+ seat,
+ space,
+ // The nested backend is inherently one output: a single window on
+ // the host. Multi-output is a udev/DRM concern.
+ outputs: vec![OutputEntry { output: output.clone(), location: (0, 0).into() }],
+ layer_shell_state: WlrLayerShellState::new::<CompState>(&dh),
+ dh: dh.clone(),
+ data_device_state: DataDeviceState::new::<CompState>(&dh),
+ primary_selection_state,
+ data_control_state,
+ session_lock_state: SessionLockManagerState::new::<CompState, _>(&dh, |_| true),
+ _screencopy_state: screencopy::ScreencopyState::new::<CompState>(&dh),
+ screencopy_pending: Vec::new(),
+ lock: SessionLock::default(),
+ wm: wm.clone(),
+ surface_to_id: HashMap::new(),
+ id_to_window: HashMap::new(),
+ decorations: HashMap::new(),
+ pending: pending.clone(),
+ bound_keys: Rc::new(bound_keys.iter().cloned().collect()),
+ start_time: Instant::now(),
+ udev: None,
+ xwayland_shell_state: smithay::wayland::xwayland_shell::XWaylandShellState::new::<CompState>(&dh),
+ xwm: None,
+ xwayland_windows: HashMap::new(),
+ xwayland_pending: Vec::new(),
+ };
+
+ let listener = ListeningSocket::bind_auto("wayland", 0..32).map_err(err)?;
+ if let Some(name) = listener.socket_name() {
+ std::env::set_var("WAYLAND_DISPLAY", name);
+ log::info!("wayland socket: {}", name.to_string_lossy());
+ }
+
+ let damage_tracker = OutputDamageTracker::from_output(&output);
+
+ Ok(Self { display, state, backend, winit_events, damage_tracker, output, listener, clients: Vec::new(), pending, wm })
+ }
+
+ fn accept_clients(&mut self) -> PlatformResult<()> {
+ if let Some(stream) = self.listener.accept().map_err(err)? {
+ let client = self.display.handle().insert_client(stream, std::sync::Arc::new(ClientState::default())).map_err(err)?;
+ self.clients.push(client);
+ }
+ Ok(())
+ }
+
+ fn pump_winit(&mut self) -> PlatformResult<bool> {
+ let mut closed = false;
+ let state = &mut self.state;
+ let output = &self.output;
+ let pump = self.winit_events.dispatch_new_events(|event| {
+ handle_winit_event(state, output, event, &mut closed);
+ });
+ if matches!(pump, PumpStatus::Exit(_)) {
+ closed = true;
+ }
+ Ok(closed)
+ }
+
+ fn render_frame(&mut self) -> PlatformResult<()> {
+ let size = self.backend.window_size();
+ let resized = self.output.current_mode().map(|m| m.size) != Some(size);
+ if resized {
+ // Only push a new output mode - and thus emit `wl_output.mode`/
+ // `done` - when the size actually changed. This used to run
+ // unconditionally every frame; harmless with no Wayland-native
+ // client connected (the only way this was ever exercised before
+ // real layer-shell clients existed), but a real client bound to
+ // `wl_output` would otherwise be flooded with duplicate
+ // mode/done events at the render loop's full frame rate.
+ self.output.change_current_state(Some(OutputMode { size, refresh: 60_000 }), None, None, None);
+ layer_map_for_output(&self.output).arrange();
+ }
+
+ let age = self.backend.buffer_age().unwrap_or(0);
+ let (renderer, mut framebuffer) = self.backend.bind().map_err(err)?;
+
+ // Locked: the lock surface over an opaque black clear, and nothing
+ // else - no windows, no decorations, no layer surfaces.
+ if self.state.lock.locked {
+ let lock_surface = self.state.lock_surface_for(&self.output).cloned();
+ let elements = lock_render_elements(lock_surface.as_ref(), renderer);
+ self.damage_tracker
+ .render_output(renderer, &mut framebuffer, age, &elements, [0.0, 0.0, 0.0, 1.0])
+ .map_err(err)?;
+ drop(framebuffer);
+ self.backend.submit(None).map_err(err)?;
+ send_lock_frame(lock_surface.as_ref(), &self.output, self.state.start_time.elapsed());
+ self.state.confirm_lock_if_presented(&self.output);
+ screencopy::fail_pending(std::mem::take(&mut self.state.screencopy_pending));
+ return Ok(());
+ }
+
+ let mut custom_elements: Vec<MemoryRenderBufferRenderElement<GlesRenderer>> = Vec::new();
+ for (&id, deco) in self.state.decorations.iter() {
+ let Some(geom) = self.wm.borrow().window(id).map(|w| w.geometry) else { continue };
+ match MemoryRenderBufferRenderElement::from_buffer(renderer, (geom.x as f64, geom.y as f64), deco, None, None, None, Kind::Unspecified) {
+ Ok(elem) => custom_elements.push(elem),
+ Err(e) => log::warn!("failed to import titlebar buffer for window {id}: {e}"),
+ }
+ }
+
+ render_output(
+ &self.output,
+ renderer,
+ &mut framebuffer,
+ 1.0,
+ age,
+ [&self.state.space],
+ &custom_elements,
+ &mut self.damage_tracker,
+ [0.05, 0.05, 0.08, 1.0],
+ )
+ .map_err(err)?;
+ drop(framebuffer);
+ self.backend.submit(None).map_err(err)?;
+ self.state.space.elements().for_each(|w| w.send_frame(&self.output, self.state.start_time.elapsed(), None, |_, _| Some(self.output.clone())));
+
+ // Screencopy is serviced *after* the on-screen frame is submitted,
+ // into its own offscreen buffer - never by reading back the window
+ // surface. Reading the winit backend's EGL window surface (what an
+ // earlier version did) reliably killed the GL context: the first
+ // `grim` capture produced `eglSwapBuffers: BAD_SURFACE` followed by
+ // `BAD_ALLOC` and "context has been lost", confirmed by A/B-ing the
+ // same build with only the readback call removed. The offscreen
+ // detour costs a second scene render, but only on frames where a
+ // capture was actually requested.
+ //
+ // Deliberately placed after the locked-session early return above,
+ // so a capture requested while the screen is locked can never see
+ // client content.
+ let captures = std::mem::take(&mut self.state.screencopy_pending);
+ if !captures.is_empty() {
+ if let Err(e) = self.capture_offscreen(captures) {
+ log::warn!("screencopy: offscreen capture pass failed: {e}");
+ }
+ }
+ Ok(())
+ }
+
+ /// Re-renders the current scene into an offscreen GLES renderbuffer and
+ /// serves the queued screencopy captures from it. See the call site for
+ /// why capture cannot read the window surface directly.
+ fn capture_offscreen(&mut self, captures: Vec<screencopy::PendingCapture>) -> PlatformResult<()> {
+ use smithay::backend::renderer::{Bind, Offscreen};
+
+ let size = self.output.current_mode().map(|m| m.size).unwrap_or_default();
+ if size.w <= 0 || size.h <= 0 {
+ return Ok(());
+ }
+ let renderer = self.backend.renderer();
+ let mut target: smithay::backend::renderer::gles::GlesRenderbuffer =
+ renderer.create_buffer(Fourcc::Abgr8888, (size.w, size.h).into()).map_err(err)?;
+ let mut framebuffer = renderer.bind(&mut target).map_err(err)?;
+
+ let mut custom_elements: Vec<MemoryRenderBufferRenderElement<GlesRenderer>> = Vec::new();
+ for (&id, deco) in self.state.decorations.iter() {
+ let Some(geom) = self.wm.borrow().window(id).map(|w| w.geometry) else { continue };
+ if let Ok(elem) = MemoryRenderBufferRenderElement::from_buffer(renderer, (geom.x as f64, geom.y as f64), deco, None, None, None, Kind::Unspecified) {
+ custom_elements.push(elem);
+ }
+ }
+
+ // A throwaway damage tracker, so this pass always draws the whole
+ // scene (age 0) and never perturbs the on-screen tracker's history.
+ let mut tracker = OutputDamageTracker::from_output(&self.output);
+ render_output(
+ &self.output,
+ renderer,
+ &mut framebuffer,
+ 1.0,
+ 0,
+ [&self.state.space],
+ &custom_elements,
+ &mut tracker,
+ [0.05, 0.05, 0.08, 1.0],
+ )
+ .map_err(err)?;
+
+ screencopy::service_pending(captures, renderer, &framebuffer);
+ Ok(())
+ }
+}
+
+fn handle_winit_event(state: &mut CompState, output: &Output, event: WinitEvent, closed: &mut bool) {
+ match event {
+ WinitEvent::CloseRequested => *closed = true,
+ WinitEvent::Input(InputEvent::Keyboard { event }) => handle_keyboard_key_event(state, &event),
+ WinitEvent::Input(InputEvent::PointerMotionAbsolute { event }) => {
+ let size = output.current_mode().map(|m| m.size).unwrap_or_default().to_logical(1);
+ let pos = event.position_transformed(size);
+ handle_pointer_position(state, pos, event.time_msec());
+ }
+ WinitEvent::Input(InputEvent::PointerButton { event }) => {
+ let pos = last_pointer_pos(state);
+ let button = event.button_code();
+ let pressed = event.state() == BackendButtonState::Pressed;
+ handle_pointer_button(state, pos, button, pressed, event.time_msec());
+ }
+ WinitEvent::Resized { .. } => {}
+ _ => {}
+ }
+}
+
+impl Platform for WaylandPlatform {
+ fn kind(&self) -> PlatformKind {
+ PlatformKind::Wayland
+ }
+
+ fn poll_events(&mut self) -> PlatformResult<Vec<CoreEvent>> {
+ self.accept_clients()?;
+ let closed = self.pump_winit()?;
+ if closed {
+ return Err(PlatformError::Other("compositor window closed".into()));
+ }
+ self.display.dispatch_clients(&mut self.state).map_err(err)?;
+ self.display.flush_clients().map_err(err)?;
+ self.render_frame()?;
+ Ok(self.pending.borrow_mut().drain(..).collect())
+ }
+
+ fn monitors(&mut self) -> PlatformResult<Vec<srdwm_core::Monitor>> {
+ let size = self.backend.window_size();
+ Ok(vec![{
+ let mut m = srdwm_core::Monitor::new(0, "winit", srdwm_core::Rect::new(0, 0, size.w as u32, size.h as u32));
+ m.primary = true;
+ m
+ }])
+ }
+
+ fn apply_geometry(&mut self, window: WindowId, geometry: srdwm_core::Rect) -> PlatformResult<()> {
+ let _ = geometry;
+ self.state.sync_geometry(window);
+ Ok(())
+ }
+
+ fn set_title(&mut self, _window: WindowId, _title: &str) -> PlatformResult<()> {
+ Ok(())
+ }
+
+ fn focus(&mut self, window: WindowId) -> PlatformResult<()> {
+ self.state.wm.borrow_mut().focus_window(window);
+ Ok(())
+ }
+
+ fn minimize(&mut self, window: WindowId) -> PlatformResult<()> {
+ if let Some(w) = self.state.id_to_window.get(&window) {
+ self.state.space.unmap_elem(w);
+ }
+ Ok(())
+ }
+
+ fn restore(&mut self, window: WindowId) -> PlatformResult<()> {
+ self.state.sync_geometry(window);
+ Ok(())
+ }
+
+ fn close(&mut self, window: WindowId) -> PlatformResult<()> {
+ if let Some(w) = self.state.id_to_window.get(&window).and_then(|w| w.toplevel()) {
+ w.send_close();
+ }
+ Ok(())
+ }
+
+ fn set_decorated(&mut self, _window: WindowId, _decorated: bool) -> PlatformResult<()> {
+ Ok(())
+ }
+
+ fn set_border_color(&mut self, _window: WindowId, _rgb: (u8, u8, u8)) -> PlatformResult<()> {
+ Ok(())
+ }
+
+ fn set_border_width(&mut self, _window: WindowId, _width: u32) -> PlatformResult<()> {
+ Ok(())
+ }
+
+ fn redraw_decoration(&mut self, window: WindowId, _win: &CoreWindow, _focused: bool) -> PlatformResult<()> {
+ // Re-renders the title/focus-color band and re-syncs geometry;
+ // `sync_geometry` re-renders the decoration too, but only if one
+ // already exists, so this also covers first paint.
+ self.state.redraw_decoration_buffer(window);
+ self.state.sync_geometry(window);
+ Ok(())
+ }
+
+ fn grab_keyboard(&mut self) -> PlatformResult<()> {
+ Ok(())
+ }
+
+ fn ungrab_keyboard(&mut self) -> PlatformResult<()> {
+ Ok(())
+ }
+}