srdusr
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2024-07-31 17:34:00 +0200
committersrdusr <[email protected]>2024-07-31 17:34:00 +0200
commitf166df4d979162ae09b439c87cdc64f2b0981308 (patch)
treeb337d88dec627ab1e49b91f1f3c91de06a7af692
parentc180bfc34e4c74ba7cac9e0220e9c172c239fe71 (diff)
downloadsrdwm-f166df4d979162ae09b439c87cdc64f2b0981308.tar.gz
srdwm-f166df4d979162ae09b439c87cdc64f2b0981308.zip
Split crates/wayland/src/winit.rs (889 lines) into winit/
Pure reorganization, no behavior change - verified by diffing the function-name and struct-name sets before/after (both identical) plus a full cargo test pass. mod.rs keeps the module doc comment, imports, WaylandPlatform's struct definition, and TARGET_FRAME_TIME, plus mod declarations. The rest splits by concern: - connect.rs: connect(), the ~200-line setup/init function. - run.rs: accept_clients, pump_winit - the small per-poll pair. - render.rs: render_frame, the per-frame render loop (left as one intact ~330-line function, same reasoning as udev's render.rs: its structure is deliberate and already documented inline, not a target for further decomposition in a pure reorganization pass). - capture.rs: capture_offscreen, the screencopy path. - events.rs: handle_winit_event. - platform.rs: `impl Platform for WaylandPlatform`. No tests module existed in the original file, so none was split out. A handful of methods/functions (accept_clients, pump_winit, render_frame, capture_offscreen, handle_winit_event) went from private to pub(super): called across what are now sibling submodules, which Rust's privacy model doesn't let see each other's private items.
-rw-r--r--crates/wayland/src/winit.rs889
-rw-r--r--crates/wayland/src/winit/capture.rs60
-rw-r--r--crates/wayland/src/winit/connect.rs206
-rw-r--r--crates/wayland/src/winit/events.rs48
-rw-r--r--crates/wayland/src/winit/mod.rs99
-rw-r--r--crates/wayland/src/winit/platform.rs142
-rw-r--r--crates/wayland/src/winit/render.rs331
-rw-r--r--crates/wayland/src/winit/run.rs26
8 files changed, 912 insertions, 889 deletions
diff --git a/crates/wayland/src/winit.rs b/crates/wayland/src/winit.rs
deleted file mode 100644
index f50d095..0000000
--- a/crates/wayland/src/winit.rs
+++ /dev/null
@@ -1,889 +0,0 @@
-//! 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, HashSet};
-use std::rc::Rc;
-use std::time::{Duration, Instant};
-
-use smithay::backend::allocator::Fourcc;
-use smithay::backend::input::{
- AbsolutePositionEvent, Axis, AxisSource, ButtonState as BackendButtonState, Event as InputEventTrait, InputEvent, PointerAxisEvent, 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::renderer::ImportDma;
-use smithay::backend::winit::{self, WinitEvent, WinitEventLoop, WinitGraphicsBackend};
-use smithay::reexports::winit::dpi::LogicalSize as WinitLogicalSize;
-use smithay::reexports::winit::window::Window as WinitWindow;
-use smithay::desktop::{layer_map_for_output, PopupManager, Space};
-use smithay::wayland::shell::wlr_layer::Layer;
-use smithay::input::SeatState;
-use smithay::output::{Mode as OutputMode, Output, PhysicalProperties, Subpixel};
-use smithay::reexports::calloop::EventLoop as CalloopEventLoop;
-use smithay::reexports::wayland_server::{Client, Display, ListeningSocket};
-use smithay::reexports::winit::platform::pump_events::PumpStatus;
-use smithay::utils::{Physical, Point, Rectangle, Scale, Size, Transform};
-use smithay::wayland::compositor::CompositorState;
-use smithay::wayland::dmabuf::DmabufState;
-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 smithay::wayland::xdg_activation::XdgActivationState;
-
-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 srdwm_platform::IpcServer;
-use crate::state::{ClientState, CompState, OutputEntry};
-use crate::{decoration, 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>>,
- ipc: Option<IpcServer>,
- /// Exists solely to host `IdleNotifierState`'s internal per-notification
- /// timers - this backend otherwise has no `calloop` loop of its own at
- /// all (see `ipc.rs`'s module doc comment), drawing everything instead
- /// from `winit_events`'s manual poll and this struct's own per-tick
- /// work. `ext_idle_notify_v1` needs a real `LoopHandle` to construct
- /// (`smithay::wayland::idle_notify::IdleNotifierState::new`), and the
- /// alternative - constructing the global without ever dispatching the
- /// loop backing it - would advertise a protocol whose `idled`/`resumed`
- /// events then simply never fire, a worse trap than the small addition
- /// of a second, narrowly-scoped loop dispatched non-blocking once per
- /// tick in `poll_events`.
- idle_event_loop: CalloopEventLoop<'static, CompState>,
- /// When the last frame was rendered - see `poll_events`' doc comment
- /// on why this backend has to pace itself.
- last_frame: Instant,
-}
-
-/// Target frame budget for the winit (nested) backend's self-imposed pacing
-/// - see `poll_events`' doc comment. 60fps to match `OutputMode`'s own
-/// `refresh: 60_000` a few lines below, not because either number is
-/// special.
-const TARGET_FRAME_TIME: Duration = Duration::from_micros(1_000_000 / 60);
-
-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], repeat_keys: &[String]) -> PlatformResult<Self> {
- let display: Display<CompState> = Display::new().map_err(err)?;
- let dh = display.handle();
- // See `idle_event_loop`'s own doc comment on `WaylandPlatform`.
- let idle_event_loop: CalloopEventLoop<'static, CompState> = CalloopEventLoop::try_new().map_err(err)?;
-
- let (mut backend, winit_events) = winit::init_from_attributes::<GlesRenderer>(
- WinitWindow::default_attributes()
- .with_inner_size(WinitLogicalSize::new(1280.0, 800.0))
- .with_title("srdwm")
- .with_visible(true),
- )
- .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");
- let system_xkb = crate::xkb_config::read();
- let xkb_config = smithay::input::keyboard::XkbConfig {
- rules: "",
- model: system_xkb.model.as_deref().unwrap_or(""),
- layout: system_xkb.layout.as_deref().unwrap_or(""),
- variant: system_xkb.variant.as_deref().unwrap_or(""),
- options: system_xkb.options.clone(),
- };
- // 600ms delay, not 200 - see `state.rs`'s `REPEAT_DELAY` doc
- // comment for why.
- seat.add_keyboard(xkb_config, 600, 25).map_err(err)?;
- seat.add_pointer();
-
- let mut space = Space::default();
- space.map_output(&output, (0, 0));
-
- // `zwp_linux_dmabuf_v1` - see `protocols.rs`'s `DmabufHandler` for
- // the udev-vs-winit split on eager import validation, and udev.rs's
- // matching global for why v3 (`create_global`) rather than v4's
- // feedback variant.
- let mut dmabuf_state = DmabufState::new();
- dmabuf_state.create_global::<CompState>(&dh, backend.renderer().dmabuf_formats());
-
- // See `state.rs`'s `rounded_corners_program` doc comment: `None` on
- // any failure (an old/software GL driver missing something the
- // shader needs) rather than refusing to start over a cosmetic
- // feature - content just renders unrounded in that case.
- let rounded_corners_program = match crate::rounded_corners::compile(backend.renderer()) {
- Ok(program) => Some(program),
- Err(e) => {
- log::warn!("winit: rounded-corner shader failed to compile, content will render unrounded: {e}");
- None
- }
- };
-
- let pending = Rc::new(RefCell::new(Vec::new()));
- let state = CompState {
- compositor_state,
- xdg_shell_state,
- _xdg_decoration_state: xdg_decoration_state,
- shm_state,
- dmabuf_state,
- xdg_activation_state: XdgActivationState::new::<CompState>(&dh),
- _text_input_manager_state: smithay::wayland::text_input::TextInputManagerState::new::<CompState>(&dh),
- _input_method_manager_state: smithay::wayland::input_method::InputMethodManagerState::new::<CompState, _>(&dh, |_client| true),
- _gtk_shell_state: crate::gtk_shell::GtkShellState::new::<CompState>(&dh),
- seat_state,
- seat,
- space,
- popups: PopupManager::default(),
- // 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(),
- _foreign_toplevel_state: crate::foreign_toplevel::ForeignToplevelState::new::<CompState>(&dh),
- foreign_toplevel_managers: Vec::new(),
- foreign_toplevel_handles: HashMap::new(),
- _workspace_state: crate::workspace::WorkspaceManagerState::new::<CompState>(&dh),
- _output_power_state: None,
- _gamma_control_state: None,
- _output_management_state: crate::output_management::OutputManagementState::new::<CompState>(&dh),
- output_managers: Vec::new(),
- output_heads: HashMap::new(),
- output_modes: HashMap::new(),
- output_serial: 0,
- last_broadcast_outputs: Vec::new(),
- workspace_managers: Vec::new(),
- workspace_groups: Vec::new(),
- workspace_handles: HashMap::new(),
- _viewporter_state: smithay::wayland::viewporter::ViewporterState::new::<CompState>(&dh),
- _fractional_scale_state: smithay::wayland::fractional_scale::FractionalScaleManagerState::new::<CompState>(&dh),
- _cursor_shape_state: smithay::wayland::cursor_shape::CursorShapeManagerState::new::<CompState>(&dh),
- idle_notifier_state: smithay::wayland::idle_notify::IdleNotifierState::new(&dh, idle_event_loop.handle()),
- _idle_inhibit_manager_state: smithay::wayland::idle_inhibit::IdleInhibitManagerState::new::<CompState>(&dh),
- idle_inhibiting_surfaces: Vec::new(),
- last_idle_notify: None,
- window_anims: HashMap::new(),
- last_broadcast_flags: HashMap::new(),
- last_broadcast_workspace: None,
- lock: SessionLock::default(),
- cursor_status: smithay::input::pointer::CursorImageStatus::default_named(),
- cursor_buffers: crate::cursor::make_buffers(),
- last_titlebar_click: None,
- context_menu: None,
- context_menu_buffer: None,
- wm: wm.clone(),
- surface_to_id: HashMap::new(),
- id_to_window: HashMap::new(),
- dead_layer_surfaces: HashSet::new(),
- decorations: HashMap::new(),
- border_top_decorations: HashMap::new(),
- shadow_buffers: HashMap::new(),
- rounded_corners_program,
- content_epoch: HashMap::new(),
- rounded_content_buffers: HashMap::new(),
- border_side_buffers: HashMap::new(),
- last_synced_size: HashMap::new(),
- pending: pending.clone(),
- bound_keys: Rc::new(bound_keys.iter().cloned().collect()),
- repeat_keys: Rc::new(repeat_keys.iter().cloned().collect()),
- repeat: None,
- 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(),
- ewmh: None,
- };
-
- 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);
-
- let ipc = match listener.socket_name().map(|n| n.to_string_lossy().into_owned()) {
- Some(name) => match IpcServer::bind(&name) {
- Ok(ipc) => Some(ipc),
- Err(e) => {
- log::warn!("control socket unavailable ({e}); srd and scripts that use it won't work");
- None
- }
- },
- None => None,
- };
-
- Ok(Self {
- display,
- state,
- backend,
- winit_events,
- damage_tracker,
- output,
- listener,
- clients: Vec::new(),
- pending,
- wm,
- ipc,
- idle_event_loop,
- last_frame: Instant::now(),
- })
- }
-
- 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<()> {
- self.state.tick_animations();
- self.state.tick_dirty_broadcasts();
- 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(());
- }
-
- // `WinitElement`, not `OverlayElement<GlesRenderer>` directly - see
- // `rounded_corners.rs`'s own doc comment on why content that gets
- // rounded needs a wider element type than everything else here,
- // and why that couldn't just be added as a new `OverlayElement`
- // variant instead. Every existing push below wraps its
- // `OverlayElement` in `WinitElement::Base`; only the new
- // rounded-content push (further down) uses `WinitElement::Rounded`
- // directly.
- let mut custom_elements: Vec<crate::rounded_corners::WinitElement> = Vec::new();
- // The right-click titlebar menu, if open - pushed first so it's
- // topmost over every window (this backend draws no cursor of its
- // own, see this module's doc comment, so there's no "stay under
- // the pointer" ordering concern like udev.rs's matching push has).
- if let (Some(menu), Some(buffer)) = (self.state.context_menu.as_ref(), self.state.context_menu_buffer.as_ref()) {
- let pos = (menu.pos.0 as f64, menu.pos.1 as f64);
- match MemoryRenderBufferRenderElement::from_buffer(renderer, pos, buffer, None, None, None, Kind::Unspecified) {
- Ok(elem) => custom_elements.push(crate::rounded_corners::WinitElement::Base(crate::elements::OverlayElement::Memory(elem))),
- Err(e) => log::warn!("failed to import context menu buffer: {e}"),
- }
- }
- // Content now renders here too, one window at a time, not through
- // `render_output`'s own `spaces` argument - see this function's
- // own call to `damage_tracker.render_output` further down for why,
- // and `elements.rs`'s `surface_content_elements` doc comment for
- // what it's actually for (per-window opacity, impossible through
- // `spaces`, which takes one `alpha` for the whole frame).
- //
- // An earlier version of this exact change was reverted: with two or
- // more native Wayland toplevels on screen, whichever was created
- // *first* always painted in front of later ones regardless of real
- // focus/stacking order. That bug's real root cause (found by
- // instrumenting a locally vendored smithay copy directly) turned
- // out to be `sync_geometry`'s `Space::map_element` call silently
- // re-stacking windows to the top of `Space`'s *own* internal
- // order as a side effect of updating position - see `state.rs`'s
- // `resync_stacking_order` doc comment for the full story and the
- // fix that landed for it (called after every `map_element` since).
- // This loop never reads `Space`'s order at all: `ids` below comes
- // from `WindowManager.order` (`visible_windows_front_to_back`),
- // srdwm's own stacking model, the same source `hit_test` already
- // trusts - so the specific bug that sank the earlier attempt
- // can't recur here regardless of whether `resync_stacking_order`
- // ever drifts again. `self.state.space` stays mapped and
- // `resync_stacking_order`-maintained exactly as before; only the
- // render step stopped reading from it.
- let ids: Vec<WindowId> = self.wm.borrow().visible_windows_front_to_back().map(|w| w.id).collect();
- let focused = self.wm.borrow().focused_id();
- // Popups next: always above every window's own content - see the
- // matching comment in `udev.rs`'s render loop for why this has to
- // be pushed ahead of both the bar/dock and every window now that
- // content shares this same list.
- let popup_targets = crate::elements::popup_targets(&self.state);
- custom_elements.extend(crate::elements::popup_render_elements(&popup_targets, renderer, (0, 0)).into_iter().map(crate::rounded_corners::WinitElement::Base));
- // The bar/dock/launcher, skipped entirely for a fullscreen window --
- // see `udev.rs`'s matching push for the full reasoning.
- let hide_top_layers = self.wm.borrow().visible_windows_front_to_back().any(|w| w.fullscreen);
- // `None` (the user's config never touched `general.rounded_corners`)
- // defaults to *on* here - this backend has an actual GPU shader
- // behind the feature (see `rounded_corners.rs`), no untested
- // per-frame CPU cost to weigh the way the udev backend's own
- // default has to.
- let rounded_corners_enabled = self.wm.borrow().rounded_corners_enabled.unwrap_or(true);
- if !hide_top_layers {
- custom_elements.extend(
- crate::elements::output_layer_elements(renderer, &self.output, (0, 0), |layer| matches!(layer, Layer::Top | Layer::Overlay))
- .into_iter()
- .map(crate::rounded_corners::WinitElement::Base),
- );
- }
- // Windows stacked in front of whichever one border/decoration is
- // being built right now - `ids` is already front-to-back, so this
- // only ever needs appending to, not recomputing. A window's own
- // *content*, pushed inside this same loop below, needs no separate
- // occlusion test - see the matching comment in `udev.rs`'s render
- // loop for why ordinary front-to-back push order already occludes
- // it correctly. The border strips and titlebar bitmap are
- // different: outside `geometry`, so they still need `occluders`'
- // explicit clip against whichever window is stacked in front.
- let mut occluders: Vec<srdwm_core::Rect> = Vec::with_capacity(ids.len());
- for id in ids {
- let Some(w) = self.wm.borrow().window(id).cloned() else { continue };
- // `w.geometry` is the animation's target, not necessarily where
- // the window is actually drawn this frame - see the matching
- // comment in `udev.rs`'s render loop for the full story
- // (reported live as the border "not flush" with the window
- // during an animated maximize/fullscreen/open-slide transition).
- let geom = self.state.window_anims.get(&id).map(crate::state::WindowAnim::current_rect).unwrap_or(w.geometry);
- // Same reasoning as udev.rs's matching push: positioned from
- // `geom`, not `w.geometry`, and not fragment-clipped against
- // `occluders` - see that comment.
- if let Some(shadow) = self.state.shadow_buffers.get(&id) {
- let rect = decoration::shadow_rect(geom);
- let pos = (rect.x as f64, rect.y as f64);
- match MemoryRenderBufferRenderElement::from_buffer(renderer, pos, shadow, None, None, None, Kind::Unspecified) {
- Ok(elem) => custom_elements.push(crate::rounded_corners::WinitElement::Base(crate::elements::OverlayElement::Memory(elem))),
- Err(e) => log::warn!("failed to import shadow buffer for window {id}: {e}"),
- }
- }
- if let Some(deco) = self.state.decorations.get(&id) {
- // Fragment-clipped, same as udev.rs's matching titlebar
- // push - see that comment for why all-or-nothing (skip
- // only once *fully* covered) wasn't enough: a titlebar
- // only partially covered, the common case for cascaded
- // windows, still bled through the covered part.
- let titlebar_rect = srdwm_core::Rect::new(geom.x, geom.y, geom.width, srdwm_core::TITLEBAR_HEIGHT);
- for fragment in crate::elements::visible_border_fragments(titlebar_rect, &occluders) {
- let pos = (fragment.x as f64, fragment.y as f64);
- let src = Rectangle::new(
- Point::from(((fragment.x - titlebar_rect.x) as f64, (fragment.y - titlebar_rect.y) as f64)),
- Size::from((fragment.width as f64, fragment.height as f64)),
- );
- match MemoryRenderBufferRenderElement::from_buffer(renderer, pos, deco, None, Some(src), None, Kind::Unspecified) {
- Ok(elem) => custom_elements.push(crate::rounded_corners::WinitElement::Base(crate::elements::OverlayElement::Memory(elem))),
- Err(e) => log::warn!("failed to import titlebar buffer for window {id}: {e}"),
- }
- }
- }
- // Border strips sit entirely outside `geometry` (see
- // `decoration::border_strips`), so they never overlap this same
- // window's own decoration/content pixels - draw order relative
- // to those doesn't matter, only relative to other windows'.
- if w.border_width > 0 {
- let color = crate::state::effective_border_color(w.border_color, focused == Some(id));
- let strips = decoration::border_strips(geom, w.border_width);
- // Strip 0 (top) is rounded to match the titlebar underneath
- // it - see `render_border_top`'s doc comment - so it's a
- // cached bitmap (rebuilt only in `redraw_decoration_buffer`,
- // same as the titlebar itself), not rasterized fresh here
- // every frame; the other three don't touch a rounded corner
- // and stay persistent solid-colour buffers instead - see
- // `elements::border_side_render_element`'s doc comment for
- // why a per-frame rebuild of either was a real, continuous
- // cost, not a cosmetic one. Not fragment-clipped like the
- // other three below - see the matching comment in
- // `udev.rs` for why the top strip only gets the cheaper
- // all-or-nothing occlusion check.
- if strips[0].width > 0 && strips[0].height > 0 && !strips[0].subtract_all(&occluders).is_empty() {
- if let Some(buffer) = self.state.border_top_decorations.get(&id) {
- match MemoryRenderBufferRenderElement::from_buffer(renderer, (strips[0].x as f64, strips[0].y as f64), buffer, None, None, None, Kind::Unspecified) {
- Ok(elem) => custom_elements.push(crate::rounded_corners::WinitElement::Base(crate::elements::OverlayElement::Memory(elem))),
- Err(e) => log::warn!("failed to import top border buffer for window {id}: {e}"),
- }
- }
- }
- let pool = self.state.border_side_buffers.entry(id).or_default();
- let mut buf_index = 0;
- for strip in &strips[1..] {
- if strip.width == 0 || strip.height == 0 {
- continue;
- }
- for fragment in crate::elements::visible_border_fragments(*strip, &occluders) {
- let buf = crate::elements::border_fragment_buffer(pool, buf_index);
- buf_index += 1;
- custom_elements.push(crate::rounded_corners::WinitElement::Base(crate::elements::OverlayElement::Solid(crate::elements::border_side_render_element(buf, fragment, color, (0, 0)))));
- }
- }
- }
- // The window's own content, at its own `opacity` - see the
- // matching push in `udev.rs`'s render loop for why. Single
- // output at the global origin, so no offset to subtract (see
- // `elements.rs`'s doc comment on why `udev.rs`'s per-head call
- // does). Rounded via `rounded_corners::rounded_content_element`
- // when the feature's on and the shader compiled - a decorated
- // window only rounds its bottom two corners (the top two are
- // already rounded, on the titlebar's own bitmap, by
- // `decoration.rs`), an undecorated/CSD one rounds all four,
- // since its content *is* the window's whole visible extent.
- // Falls back to the plain, unrounded `surface_content_elements`
- // on any failure - no committed buffer yet, a single-pixel
- // solid-colour buffer, or the feature simply being off - same
- // "always show something over a prettier maybe-nothing"
- // reasoning `cursor.rs`'s built-in-arrow fallback already uses.
- if let Some(dwindow) = self.state.id_to_window.get(&id) {
- if let Some(surface) = crate::elements::window_wl_surface(dwindow) {
- let band = if w.decorated { srdwm_core::TITLEBAR_HEIGHT as i32 } else { 0 };
- let pos = (geom.x, geom.y + band);
- let rounded = rounded_corners_enabled.then_some(self.state.rounded_corners_program.as_ref()).flatten().and_then(|program| {
- let corners = if w.decorated { crate::rounded_corners::RoundedCorners::BOTTOM_ONLY } else { crate::rounded_corners::RoundedCorners::ALL };
- crate::rounded_corners::rounded_content_element(renderer, program, &surface, pos, w.opacity, crate::decoration::CORNER_RADIUS as f32, corners)
- });
- match rounded {
- Some(elem) => custom_elements.push(crate::rounded_corners::WinitElement::Rounded(elem)),
- None => custom_elements.extend(crate::elements::surface_content_elements(renderer, &surface, pos, w.opacity).into_iter().map(crate::rounded_corners::WinitElement::Base)),
- }
- }
- }
- occluders.push(geom);
- }
- // Background/bottom layer-shell (wallpaper engines) last --
- // bottommost, matching smithay's own `space_render_elements`
- // ordering, which this whole custom loop now replaces.
- custom_elements.extend(
- crate::elements::output_layer_elements(renderer, &self.output, (0, 0), |layer| matches!(layer, Layer::Background | Layer::Bottom))
- .into_iter()
- .map(crate::rounded_corners::WinitElement::Base),
- );
-
- // Not `smithay::desktop::space::render_output`: see `udev.rs`'s
- // matching call site for why (per-window opacity, fullscreen-aware
- // layer-shell inclusion - `custom_elements` above already carries
- // everything that wrapper would have built).
- let result = self
- .damage_tracker
- .render_output(renderer, &mut framebuffer, age, &custom_elements, [0.05, 0.05, 0.08, 1.0])
- .map_err(err)?;
- let damage_rects: Vec<Rectangle<i32, Physical>> = result.damage.cloned().unwrap_or_default();
- let has_damage = !damage_rects.is_empty();
- drop(framebuffer);
- // Both the buffer swap and the frame-callback notification are
- // conditional on real damage now - this used to run
- // unconditionally on every call to this function (every ~16ms
- // regardless of activity), which told every window it could render
- // its next frame whether or not the screen had actually changed.
- // Any client using the standard wait-for-frame-callback render
- // pattern (most of them) had no reason not to redraw at whatever
- // rate this loop cycled, forever - confirmed live on the udev
- // backend: wezterm-gui pinned at 140%+ CPU sitting on a fully idle,
- // unchanged terminal, from the identical bug there.
- //
- // That output-wide gate wasn't enough on its own: cursor motion
- // alone damages the small region around the pointer, which still
- // marked the *whole output* damaged and sent every mapped window a
- // callback regardless of whether the cursor was anywhere near it.
- // `windows_touched_by_damage` narrows this to windows the actual
- // damage rectangles overlap - see its doc comment in elements.rs.
- if has_damage {
- self.backend.submit(None).map_err(err)?;
- let scale = Scale::from(self.output.current_scale().fractional_scale());
- let now = self.state.start_time.elapsed();
- for w in crate::elements::windows_touched_by_damage(&self.state.space, &damage_rects, scale) {
- w.send_frame(&self.output, now, None, |_, _| Some(self.output.clone()));
- }
- }
- // Deliberately outside `if has_damage`: the whole point of
- // `always_notify` is covering the case where the output has *no*
- // damage at all (a fully idle desktop, cursor not moving) but the
- // focused/hovered window still has a pending callback it needs
- // answered to unblock an input-driven redraw. Nesting this inside
- // `if has_damage` (the first version of this fix) meant it only
- // ever ran on a tick that already had damage from something else
- // happening - i.e. never in the exact scenario it exists for.
- // Reported live as clicks in Firefox still doing nothing at all,
- // not just intermittently, after the first version of this fix.
- {
- let pointer_pos = last_pointer_pos(&self.state);
- let now = self.state.start_time.elapsed();
- let wm = self.wm.borrow();
- let always_notify = [wm.focused_id(), wm.window_at(pointer_pos.x as i32, pointer_pos.y as i32)];
- drop(wm);
- for w in always_notify.into_iter().flatten().filter_map(|id| self.state.id_to_window.get(&id)) {
- w.send_frame(&self.output, now, None, |_, _| Some(self.output.clone()));
- }
- }
- // Layer-shell surfaces get their callback every pass, unconditionally
- // - NOT folded into the `has_damage` gate above. See the matching
- // (much longer) comment in udev.rs's `render_udev_frame`: many
- // layer-shell clients (GTK4/AGS among them) drive their entire
- // repaint loop off frame callbacks with no independent timer
- // fallback, so withholding the callback until *something* on the
- // desktop happens to produce damage deadlocks them permanently
- // after their first frame - confirmed live, AGS and waybar both
- // froze exactly this way. Toplevel windows keep the damage gate
- // (that's what fixed the wezterm-gui CPU-burn bug); layer surfaces
- // are few, cheap to redraw, and are exactly the periodic-UI-chrome
- // case frame callbacks exist to pace.
- for layer in layer_map_for_output(&self.output).layers() {
- layer.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)?;
-
- // Not full parity with the on-screen render loop above (no border/
- // shadow strips here, same as before this function's content/opacity
- // fix) - a real, pre-existing gap in what a screenshot shows on
- // this backend, flagged rather than grown further in this pass.
- // Content (with each window's own `opacity`, unlike the
- // `self.state.space`-based single-alpha call this replaced) and the
- // bar/dock now render into the capture, at least: a screenshot used
- // to only ever show titlebars for windows that had one, and never
- // any layer-shell surface at all.
- let hide_top_layers = self.wm.borrow().visible_windows_front_to_back().any(|w| w.fullscreen);
- let mut custom_elements: Vec<crate::elements::OverlayElement<GlesRenderer>> = Vec::new();
- if !hide_top_layers {
- custom_elements.extend(crate::elements::output_layer_elements(renderer, &self.output, (0, 0), |layer| matches!(layer, Layer::Top | Layer::Overlay)));
- }
- for id in self.wm.borrow().visible_windows_front_to_back().map(|w| w.id).collect::<Vec<_>>() {
- let Some(w) = self.wm.borrow().window(id).cloned() else { continue };
- if let Some(deco) = self.state.decorations.get(&id) {
- if let Ok(elem) = MemoryRenderBufferRenderElement::from_buffer(renderer, (w.geometry.x as f64, w.geometry.y as f64), deco, None, None, None, Kind::Unspecified) {
- custom_elements.push(crate::elements::OverlayElement::Memory(elem));
- }
- }
- if let Some(dwindow) = self.state.id_to_window.get(&id) {
- if let Some(surface) = crate::elements::window_wl_surface(dwindow) {
- let band = if w.decorated { srdwm_core::TITLEBAR_HEIGHT as i32 } else { 0 };
- custom_elements.extend(crate::elements::surface_content_elements(renderer, &surface, (w.geometry.x, w.geometry.y + band), w.opacity));
- }
- }
- }
- custom_elements.extend(crate::elements::output_layer_elements(renderer, &self.output, (0, 0), |layer| matches!(layer, Layer::Background | Layer::Bottom)));
-
- // 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);
- tracker
- .render_output(renderer, &mut framebuffer, 0, &custom_elements, [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());
- }
- // This backend had no scroll handling at all - `InputEvent::
- // PointerAxis` fell into the catch-all below and was silently
- // dropped, unconditionally, on every device. Same forwarding as
- // `udev.rs`'s equivalent (see its own comment for the `stop()`/
- // `v120()` reasoning); duplicated rather than shared since the two
- // backends' `InputEvent` generic parameters differ and there's no
- // shared event type to write one function against.
- WinitEvent::Input(InputEvent::PointerAxis { event }) => {
- if crate::input::handle_workspace_scroll(state, &event) {
- return;
- }
- let Some(pointer) = state.seat.get_pointer() else { return };
- let source = event.source();
- let mut frame = smithay::input::pointer::AxisFrame::new(event.time_msec()).source(source);
- for axis in [Axis::Horizontal, Axis::Vertical] {
- match event.amount(axis) {
- Some(value) => frame = frame.value(axis, value),
- None if source == AxisSource::Finger => frame = frame.stop(axis),
- None => {}
- }
- if let Some(v120) = event.amount_v120(axis) {
- frame = frame.v120(axis, v120 as i32);
- }
- }
- pointer.axis(state, frame);
- pointer.frame(state);
- }
- WinitEvent::Resized { .. } => {}
- _ => {}
- }
-}
-
-impl Platform for WaylandPlatform {
- fn kind(&self) -> PlatformKind {
- PlatformKind::Wayland
- }
-
- /// **Self-paced, deliberately**: nothing else in this backend ever
- /// blocks. `pump_winit`'s underlying `dispatch_new_events` polls
- /// (returns immediately either way), and smithay's winit backend
- /// hardcodes `vsync: false` on the EGL surface it creates
- /// (`init_from_attributes_with_gl_attr` in smithay 0.7.0's own
- /// `backend/winit/mod.rs` - true of *every* entry point into that
- /// module, including the one this backend used before it needed custom
- /// `WindowAttributes`, so this was never introduced by that switch).
- /// `swap_buffers` therefore returns as soon as the GPU accepts the
- /// frame, with no wait for the next display refresh at all. Before this
- /// fix, that meant `poll_events` -> `render_frame` -> full render +
- /// `swap_buffers` ran back-to-back with nothing pacing the `while
- /// running.get()` loop in `main.rs` between iterations - confirmed
- /// live: an idle nested instance, zero windows, sat at a sustained
- /// ~52% of one core (`ps -o %cpu`), because it was rendering and
- /// presenting a full frame as fast as the CPU/GPU could physically
- /// cycle, forever, whether or not anything on screen had changed.
- /// Fixed by giving `idle_event_loop.dispatch` (already called every
- /// tick to service `ext_idle_notify_v1`'s timers, see its field doc
- /// comment) a real timeout instead of always `Duration::ZERO`: the
- /// remaining budget until `TARGET_FRAME_TIME` has elapsed since the
- /// last frame, clamped to zero once that budget is already spent. This
- /// reuses the one blocking wait this backend already has rather than
- /// adding a second, separate `thread::sleep`, and still services any
- /// idle-notify timer that comes due sooner than a full frame away.
- 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()));
- }
- // Held bindings that repeat - see `CompState::tick_repeat`.
- self.state.tick_repeat();
- self.display.dispatch_clients(&mut self.state).map_err(err)?;
- self.display.flush_clients().map_err(err)?;
- if let Some(ipc) = self.ipc.as_mut() {
- if ipc.poll(&self.wm) {
- self.pending.borrow_mut().push(CoreEvent::WorkspaceChanged);
- }
- }
- let wait = TARGET_FRAME_TIME.saturating_sub(self.last_frame.elapsed());
- let _ = self.idle_event_loop.dispatch(Some(wait), &mut self.state);
- self.last_frame = Instant::now();
- self.render_frame()?;
- Ok(self.pending.borrow_mut().drain(..).collect())
- }
-
- fn monitors(&mut self) -> PlatformResult<Vec<srdwm_core::Monitor>> {
- // Shrunk by any layer-shell exclusive zone - see the matching
- // comment in `udev.rs`'s `monitors()`. This backend is always a
- // single output at the global origin, so the output-local zone
- // rectangle already is the usable global-space rect.
- let zone = layer_map_for_output(&self.output).non_exclusive_zone();
- Ok(vec![{
- let rect = srdwm_core::Rect::new(zone.loc.x, zone.loc.y, zone.size.w as u32, zone.size.h as u32);
- let mut m = srdwm_core::Monitor::new(0, "winit", rect);
- // Same fix as `udev.rs`'s matching function: `Monitor::new`
- // defaults `full_geometry` to `geometry`, which is already
- // zone-shrunk here - without this, `toggle_fullscreen` had no
- // way to actually cover a bar/dock's reserved strip, since the
- // "true full rect" it targets was silently identical to the
- // "usable, shrunk rect" `toggle_maximize` targets.
- let full = self.backend.window_size();
- m.full_geometry = srdwm_core::Rect::new(0, 0, full.w as u32, full.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(())
- }
-
- /// See `udev.rs`'s matching impl for why this has to go through
- /// `crate::input::focus_window` (the same path a real mouse click
- /// already uses) rather than only touching core state.
- fn focus(&mut self, window: WindowId) -> PlatformResult<()> {
- crate::input::focus_window(&mut self.state, 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(())
- }
-}
diff --git a/crates/wayland/src/winit/capture.rs b/crates/wayland/src/winit/capture.rs
new file mode 100644
index 0000000..a96cff9
--- /dev/null
+++ b/crates/wayland/src/winit/capture.rs
@@ -0,0 +1,60 @@
+use super::*;
+
+impl WaylandPlatform {
+
+ /// 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.
+ pub(super) 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)?;
+
+ // Not full parity with the on-screen render loop above (no border/
+ // shadow strips here, same as before this function's content/opacity
+ // fix) - a real, pre-existing gap in what a screenshot shows on
+ // this backend, flagged rather than grown further in this pass.
+ // Content (with each window's own `opacity`, unlike the
+ // `self.state.space`-based single-alpha call this replaced) and the
+ // bar/dock now render into the capture, at least: a screenshot used
+ // to only ever show titlebars for windows that had one, and never
+ // any layer-shell surface at all.
+ let hide_top_layers = self.wm.borrow().visible_windows_front_to_back().any(|w| w.fullscreen);
+ let mut custom_elements: Vec<crate::elements::OverlayElement<GlesRenderer>> = Vec::new();
+ if !hide_top_layers {
+ custom_elements.extend(crate::elements::output_layer_elements(renderer, &self.output, (0, 0), |layer| matches!(layer, Layer::Top | Layer::Overlay)));
+ }
+ for id in self.wm.borrow().visible_windows_front_to_back().map(|w| w.id).collect::<Vec<_>>() {
+ let Some(w) = self.wm.borrow().window(id).cloned() else { continue };
+ if let Some(deco) = self.state.decorations.get(&id) {
+ if let Ok(elem) = MemoryRenderBufferRenderElement::from_buffer(renderer, (w.geometry.x as f64, w.geometry.y as f64), deco, None, None, None, Kind::Unspecified) {
+ custom_elements.push(crate::elements::OverlayElement::Memory(elem));
+ }
+ }
+ if let Some(dwindow) = self.state.id_to_window.get(&id) {
+ if let Some(surface) = crate::elements::window_wl_surface(dwindow) {
+ let band = if w.decorated { srdwm_core::TITLEBAR_HEIGHT as i32 } else { 0 };
+ custom_elements.extend(crate::elements::surface_content_elements(renderer, &surface, (w.geometry.x, w.geometry.y + band), w.opacity));
+ }
+ }
+ }
+ custom_elements.extend(crate::elements::output_layer_elements(renderer, &self.output, (0, 0), |layer| matches!(layer, Layer::Background | Layer::Bottom)));
+
+ // 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);
+ tracker
+ .render_output(renderer, &mut framebuffer, 0, &custom_elements, [0.05, 0.05, 0.08, 1.0])
+ .map_err(err)?;
+
+ screencopy::service_pending(captures, renderer, &framebuffer);
+ Ok(())
+ }
+}
diff --git a/crates/wayland/src/winit/connect.rs b/crates/wayland/src/winit/connect.rs
new file mode 100644
index 0000000..f76d4ff
--- /dev/null
+++ b/crates/wayland/src/winit/connect.rs
@@ -0,0 +1,206 @@
+use super::*;
+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], repeat_keys: &[String]) -> PlatformResult<Self> {
+ let display: Display<CompState> = Display::new().map_err(err)?;
+ let dh = display.handle();
+ // See `idle_event_loop`'s own doc comment on `WaylandPlatform`.
+ let idle_event_loop: CalloopEventLoop<'static, CompState> = CalloopEventLoop::try_new().map_err(err)?;
+
+ let (mut backend, winit_events) = winit::init_from_attributes::<GlesRenderer>(
+ WinitWindow::default_attributes()
+ .with_inner_size(WinitLogicalSize::new(1280.0, 800.0))
+ .with_title("srdwm")
+ .with_visible(true),
+ )
+ .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");
+ let system_xkb = crate::xkb_config::read();
+ let xkb_config = smithay::input::keyboard::XkbConfig {
+ rules: "",
+ model: system_xkb.model.as_deref().unwrap_or(""),
+ layout: system_xkb.layout.as_deref().unwrap_or(""),
+ variant: system_xkb.variant.as_deref().unwrap_or(""),
+ options: system_xkb.options.clone(),
+ };
+ // 600ms delay, not 200 - see `state.rs`'s `REPEAT_DELAY` doc
+ // comment for why.
+ seat.add_keyboard(xkb_config, 600, 25).map_err(err)?;
+ seat.add_pointer();
+
+ let mut space = Space::default();
+ space.map_output(&output, (0, 0));
+
+ // `zwp_linux_dmabuf_v1` - see `protocols.rs`'s `DmabufHandler` for
+ // the udev-vs-winit split on eager import validation, and udev.rs's
+ // matching global for why v3 (`create_global`) rather than v4's
+ // feedback variant.
+ let mut dmabuf_state = DmabufState::new();
+ dmabuf_state.create_global::<CompState>(&dh, backend.renderer().dmabuf_formats());
+
+ // See `state.rs`'s `rounded_corners_program` doc comment: `None` on
+ // any failure (an old/software GL driver missing something the
+ // shader needs) rather than refusing to start over a cosmetic
+ // feature - content just renders unrounded in that case.
+ let rounded_corners_program = match crate::rounded_corners::compile(backend.renderer()) {
+ Ok(program) => Some(program),
+ Err(e) => {
+ log::warn!("winit: rounded-corner shader failed to compile, content will render unrounded: {e}");
+ None
+ }
+ };
+
+ let pending = Rc::new(RefCell::new(Vec::new()));
+ let state = CompState {
+ compositor_state,
+ xdg_shell_state,
+ _xdg_decoration_state: xdg_decoration_state,
+ shm_state,
+ dmabuf_state,
+ xdg_activation_state: XdgActivationState::new::<CompState>(&dh),
+ _text_input_manager_state: smithay::wayland::text_input::TextInputManagerState::new::<CompState>(&dh),
+ _input_method_manager_state: smithay::wayland::input_method::InputMethodManagerState::new::<CompState, _>(&dh, |_client| true),
+ _gtk_shell_state: crate::gtk_shell::GtkShellState::new::<CompState>(&dh),
+ seat_state,
+ seat,
+ space,
+ popups: PopupManager::default(),
+ // 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(),
+ _foreign_toplevel_state: crate::foreign_toplevel::ForeignToplevelState::new::<CompState>(&dh),
+ foreign_toplevel_managers: Vec::new(),
+ foreign_toplevel_handles: HashMap::new(),
+ _workspace_state: crate::workspace::WorkspaceManagerState::new::<CompState>(&dh),
+ _output_power_state: None,
+ _gamma_control_state: None,
+ _output_management_state: crate::output_management::OutputManagementState::new::<CompState>(&dh),
+ output_managers: Vec::new(),
+ output_heads: HashMap::new(),
+ output_modes: HashMap::new(),
+ output_serial: 0,
+ last_broadcast_outputs: Vec::new(),
+ workspace_managers: Vec::new(),
+ workspace_groups: Vec::new(),
+ workspace_handles: HashMap::new(),
+ _viewporter_state: smithay::wayland::viewporter::ViewporterState::new::<CompState>(&dh),
+ _fractional_scale_state: smithay::wayland::fractional_scale::FractionalScaleManagerState::new::<CompState>(&dh),
+ _cursor_shape_state: smithay::wayland::cursor_shape::CursorShapeManagerState::new::<CompState>(&dh),
+ idle_notifier_state: smithay::wayland::idle_notify::IdleNotifierState::new(&dh, idle_event_loop.handle()),
+ _idle_inhibit_manager_state: smithay::wayland::idle_inhibit::IdleInhibitManagerState::new::<CompState>(&dh),
+ idle_inhibiting_surfaces: Vec::new(),
+ last_idle_notify: None,
+ window_anims: HashMap::new(),
+ last_broadcast_flags: HashMap::new(),
+ last_broadcast_workspace: None,
+ lock: SessionLock::default(),
+ cursor_status: smithay::input::pointer::CursorImageStatus::default_named(),
+ cursor_buffers: crate::cursor::make_buffers(),
+ last_titlebar_click: None,
+ context_menu: None,
+ context_menu_buffer: None,
+ wm: wm.clone(),
+ surface_to_id: HashMap::new(),
+ id_to_window: HashMap::new(),
+ dead_layer_surfaces: HashSet::new(),
+ decorations: HashMap::new(),
+ border_top_decorations: HashMap::new(),
+ shadow_buffers: HashMap::new(),
+ rounded_corners_program,
+ content_epoch: HashMap::new(),
+ rounded_content_buffers: HashMap::new(),
+ border_side_buffers: HashMap::new(),
+ last_synced_size: HashMap::new(),
+ pending: pending.clone(),
+ bound_keys: Rc::new(bound_keys.iter().cloned().collect()),
+ repeat_keys: Rc::new(repeat_keys.iter().cloned().collect()),
+ repeat: None,
+ 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(),
+ ewmh: None,
+ };
+
+ 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);
+
+ let ipc = match listener.socket_name().map(|n| n.to_string_lossy().into_owned()) {
+ Some(name) => match IpcServer::bind(&name) {
+ Ok(ipc) => Some(ipc),
+ Err(e) => {
+ log::warn!("control socket unavailable ({e}); srd and scripts that use it won't work");
+ None
+ }
+ },
+ None => None,
+ };
+
+ Ok(Self {
+ display,
+ state,
+ backend,
+ winit_events,
+ damage_tracker,
+ output,
+ listener,
+ clients: Vec::new(),
+ pending,
+ wm,
+ ipc,
+ idle_event_loop,
+ last_frame: Instant::now(),
+ })
+ }
+}
diff --git a/crates/wayland/src/winit/events.rs b/crates/wayland/src/winit/events.rs
new file mode 100644
index 0000000..017b7d0
--- /dev/null
+++ b/crates/wayland/src/winit/events.rs
@@ -0,0 +1,48 @@
+use super::*;
+
+pub(super) 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());
+ }
+ // This backend had no scroll handling at all - `InputEvent::
+ // PointerAxis` fell into the catch-all below and was silently
+ // dropped, unconditionally, on every device. Same forwarding as
+ // `udev.rs`'s equivalent (see its own comment for the `stop()`/
+ // `v120()` reasoning); duplicated rather than shared since the two
+ // backends' `InputEvent` generic parameters differ and there's no
+ // shared event type to write one function against.
+ WinitEvent::Input(InputEvent::PointerAxis { event }) => {
+ if crate::input::handle_workspace_scroll(state, &event) {
+ return;
+ }
+ let Some(pointer) = state.seat.get_pointer() else { return };
+ let source = event.source();
+ let mut frame = smithay::input::pointer::AxisFrame::new(event.time_msec()).source(source);
+ for axis in [Axis::Horizontal, Axis::Vertical] {
+ match event.amount(axis) {
+ Some(value) => frame = frame.value(axis, value),
+ None if source == AxisSource::Finger => frame = frame.stop(axis),
+ None => {}
+ }
+ if let Some(v120) = event.amount_v120(axis) {
+ frame = frame.v120(axis, v120 as i32);
+ }
+ }
+ pointer.axis(state, frame);
+ pointer.frame(state);
+ }
+ WinitEvent::Resized { .. } => {}
+ _ => {}
+ }
+}
diff --git a/crates/wayland/src/winit/mod.rs b/crates/wayland/src/winit/mod.rs
new file mode 100644
index 0000000..707055d
--- /dev/null
+++ b/crates/wayland/src/winit/mod.rs
@@ -0,0 +1,99 @@
+//! 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, HashSet};
+use std::rc::Rc;
+use std::time::{Duration, Instant};
+
+use smithay::backend::allocator::Fourcc;
+use smithay::backend::input::{
+ AbsolutePositionEvent, Axis, AxisSource, ButtonState as BackendButtonState, Event as InputEventTrait, InputEvent, PointerAxisEvent, 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::renderer::ImportDma;
+use smithay::backend::winit::{self, WinitEvent, WinitEventLoop, WinitGraphicsBackend};
+use smithay::reexports::winit::dpi::LogicalSize as WinitLogicalSize;
+use smithay::reexports::winit::window::Window as WinitWindow;
+use smithay::desktop::{layer_map_for_output, PopupManager, Space};
+use smithay::wayland::shell::wlr_layer::Layer;
+use smithay::input::SeatState;
+use smithay::output::{Mode as OutputMode, Output, PhysicalProperties, Subpixel};
+use smithay::reexports::calloop::EventLoop as CalloopEventLoop;
+use smithay::reexports::wayland_server::{Client, Display, ListeningSocket};
+use smithay::reexports::winit::platform::pump_events::PumpStatus;
+use smithay::utils::{Physical, Point, Rectangle, Scale, Size, Transform};
+use smithay::wayland::compositor::CompositorState;
+use smithay::wayland::dmabuf::DmabufState;
+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 smithay::wayland::xdg_activation::XdgActivationState;
+
+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 srdwm_platform::IpcServer;
+use crate::state::{ClientState, CompState, OutputEntry};
+use crate::{decoration, 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>>,
+ ipc: Option<IpcServer>,
+ /// Exists solely to host `IdleNotifierState`'s internal per-notification
+ /// timers - this backend otherwise has no `calloop` loop of its own at
+ /// all (see `ipc.rs`'s module doc comment), drawing everything instead
+ /// from `winit_events`'s manual poll and this struct's own per-tick
+ /// work. `ext_idle_notify_v1` needs a real `LoopHandle` to construct
+ /// (`smithay::wayland::idle_notify::IdleNotifierState::new`), and the
+ /// alternative - constructing the global without ever dispatching the
+ /// loop backing it - would advertise a protocol whose `idled`/`resumed`
+ /// events then simply never fire, a worse trap than the small addition
+ /// of a second, narrowly-scoped loop dispatched non-blocking once per
+ /// tick in `poll_events`.
+ idle_event_loop: CalloopEventLoop<'static, CompState>,
+ /// When the last frame was rendered - see `poll_events`' doc comment
+ /// on why this backend has to pace itself.
+ last_frame: Instant,
+}
+
+/// Target frame budget for the winit (nested) backend's self-imposed pacing
+/// - see `poll_events`' doc comment. 60fps to match `OutputMode`'s own
+/// `refresh: 60_000` a few lines below, not because either number is
+/// special.
+const TARGET_FRAME_TIME: Duration = Duration::from_micros(1_000_000 / 60);
+
+
+mod capture;
+mod connect;
+mod events;
+mod platform;
+mod render;
+mod run;
diff --git a/crates/wayland/src/winit/platform.rs b/crates/wayland/src/winit/platform.rs
new file mode 100644
index 0000000..eee42a9
--- /dev/null
+++ b/crates/wayland/src/winit/platform.rs
@@ -0,0 +1,142 @@
+use super::*;
+
+impl Platform for WaylandPlatform {
+ fn kind(&self) -> PlatformKind {
+ PlatformKind::Wayland
+ }
+
+ /// **Self-paced, deliberately**: nothing else in this backend ever
+ /// blocks. `pump_winit`'s underlying `dispatch_new_events` polls
+ /// (returns immediately either way), and smithay's winit backend
+ /// hardcodes `vsync: false` on the EGL surface it creates
+ /// (`init_from_attributes_with_gl_attr` in smithay 0.7.0's own
+ /// `backend/winit/mod.rs` - true of *every* entry point into that
+ /// module, including the one this backend used before it needed custom
+ /// `WindowAttributes`, so this was never introduced by that switch).
+ /// `swap_buffers` therefore returns as soon as the GPU accepts the
+ /// frame, with no wait for the next display refresh at all. Before this
+ /// fix, that meant `poll_events` -> `render_frame` -> full render +
+ /// `swap_buffers` ran back-to-back with nothing pacing the `while
+ /// running.get()` loop in `main.rs` between iterations - confirmed
+ /// live: an idle nested instance, zero windows, sat at a sustained
+ /// ~52% of one core (`ps -o %cpu`), because it was rendering and
+ /// presenting a full frame as fast as the CPU/GPU could physically
+ /// cycle, forever, whether or not anything on screen had changed.
+ /// Fixed by giving `idle_event_loop.dispatch` (already called every
+ /// tick to service `ext_idle_notify_v1`'s timers, see its field doc
+ /// comment) a real timeout instead of always `Duration::ZERO`: the
+ /// remaining budget until `TARGET_FRAME_TIME` has elapsed since the
+ /// last frame, clamped to zero once that budget is already spent. This
+ /// reuses the one blocking wait this backend already has rather than
+ /// adding a second, separate `thread::sleep`, and still services any
+ /// idle-notify timer that comes due sooner than a full frame away.
+ 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()));
+ }
+ // Held bindings that repeat - see `CompState::tick_repeat`.
+ self.state.tick_repeat();
+ self.display.dispatch_clients(&mut self.state).map_err(err)?;
+ self.display.flush_clients().map_err(err)?;
+ if let Some(ipc) = self.ipc.as_mut() {
+ if ipc.poll(&self.wm) {
+ self.pending.borrow_mut().push(CoreEvent::WorkspaceChanged);
+ }
+ }
+ let wait = TARGET_FRAME_TIME.saturating_sub(self.last_frame.elapsed());
+ let _ = self.idle_event_loop.dispatch(Some(wait), &mut self.state);
+ self.last_frame = Instant::now();
+ self.render_frame()?;
+ Ok(self.pending.borrow_mut().drain(..).collect())
+ }
+
+ fn monitors(&mut self) -> PlatformResult<Vec<srdwm_core::Monitor>> {
+ // Shrunk by any layer-shell exclusive zone - see the matching
+ // comment in `udev.rs`'s `monitors()`. This backend is always a
+ // single output at the global origin, so the output-local zone
+ // rectangle already is the usable global-space rect.
+ let zone = layer_map_for_output(&self.output).non_exclusive_zone();
+ Ok(vec![{
+ let rect = srdwm_core::Rect::new(zone.loc.x, zone.loc.y, zone.size.w as u32, zone.size.h as u32);
+ let mut m = srdwm_core::Monitor::new(0, "winit", rect);
+ // Same fix as `udev.rs`'s matching function: `Monitor::new`
+ // defaults `full_geometry` to `geometry`, which is already
+ // zone-shrunk here - without this, `toggle_fullscreen` had no
+ // way to actually cover a bar/dock's reserved strip, since the
+ // "true full rect" it targets was silently identical to the
+ // "usable, shrunk rect" `toggle_maximize` targets.
+ let full = self.backend.window_size();
+ m.full_geometry = srdwm_core::Rect::new(0, 0, full.w as u32, full.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(())
+ }
+
+ /// See `udev.rs`'s matching impl for why this has to go through
+ /// `crate::input::focus_window` (the same path a real mouse click
+ /// already uses) rather than only touching core state.
+ fn focus(&mut self, window: WindowId) -> PlatformResult<()> {
+ crate::input::focus_window(&mut self.state, 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(())
+ }
+}
diff --git a/crates/wayland/src/winit/render.rs b/crates/wayland/src/winit/render.rs
new file mode 100644
index 0000000..3dda278
--- /dev/null
+++ b/crates/wayland/src/winit/render.rs
@@ -0,0 +1,331 @@
+use super::*;
+
+impl WaylandPlatform {
+
+ pub(super) fn render_frame(&mut self) -> PlatformResult<()> {
+ self.state.tick_animations();
+ self.state.tick_dirty_broadcasts();
+ 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(());
+ }
+
+ // `WinitElement`, not `OverlayElement<GlesRenderer>` directly - see
+ // `rounded_corners.rs`'s own doc comment on why content that gets
+ // rounded needs a wider element type than everything else here,
+ // and why that couldn't just be added as a new `OverlayElement`
+ // variant instead. Every existing push below wraps its
+ // `OverlayElement` in `WinitElement::Base`; only the new
+ // rounded-content push (further down) uses `WinitElement::Rounded`
+ // directly.
+ let mut custom_elements: Vec<crate::rounded_corners::WinitElement> = Vec::new();
+ // The right-click titlebar menu, if open - pushed first so it's
+ // topmost over every window (this backend draws no cursor of its
+ // own, see this module's doc comment, so there's no "stay under
+ // the pointer" ordering concern like udev.rs's matching push has).
+ if let (Some(menu), Some(buffer)) = (self.state.context_menu.as_ref(), self.state.context_menu_buffer.as_ref()) {
+ let pos = (menu.pos.0 as f64, menu.pos.1 as f64);
+ match MemoryRenderBufferRenderElement::from_buffer(renderer, pos, buffer, None, None, None, Kind::Unspecified) {
+ Ok(elem) => custom_elements.push(crate::rounded_corners::WinitElement::Base(crate::elements::OverlayElement::Memory(elem))),
+ Err(e) => log::warn!("failed to import context menu buffer: {e}"),
+ }
+ }
+ // Content now renders here too, one window at a time, not through
+ // `render_output`'s own `spaces` argument - see this function's
+ // own call to `damage_tracker.render_output` further down for why,
+ // and `elements.rs`'s `surface_content_elements` doc comment for
+ // what it's actually for (per-window opacity, impossible through
+ // `spaces`, which takes one `alpha` for the whole frame).
+ //
+ // An earlier version of this exact change was reverted: with two or
+ // more native Wayland toplevels on screen, whichever was created
+ // *first* always painted in front of later ones regardless of real
+ // focus/stacking order. That bug's real root cause (found by
+ // instrumenting a locally vendored smithay copy directly) turned
+ // out to be `sync_geometry`'s `Space::map_element` call silently
+ // re-stacking windows to the top of `Space`'s *own* internal
+ // order as a side effect of updating position - see `state.rs`'s
+ // `resync_stacking_order` doc comment for the full story and the
+ // fix that landed for it (called after every `map_element` since).
+ // This loop never reads `Space`'s order at all: `ids` below comes
+ // from `WindowManager.order` (`visible_windows_front_to_back`),
+ // srdwm's own stacking model, the same source `hit_test` already
+ // trusts - so the specific bug that sank the earlier attempt
+ // can't recur here regardless of whether `resync_stacking_order`
+ // ever drifts again. `self.state.space` stays mapped and
+ // `resync_stacking_order`-maintained exactly as before; only the
+ // render step stopped reading from it.
+ let ids: Vec<WindowId> = self.wm.borrow().visible_windows_front_to_back().map(|w| w.id).collect();
+ let focused = self.wm.borrow().focused_id();
+ // Popups next: always above every window's own content - see the
+ // matching comment in `udev.rs`'s render loop for why this has to
+ // be pushed ahead of both the bar/dock and every window now that
+ // content shares this same list.
+ let popup_targets = crate::elements::popup_targets(&self.state);
+ custom_elements.extend(crate::elements::popup_render_elements(&popup_targets, renderer, (0, 0)).into_iter().map(crate::rounded_corners::WinitElement::Base));
+ // The bar/dock/launcher, skipped entirely for a fullscreen window --
+ // see `udev.rs`'s matching push for the full reasoning.
+ let hide_top_layers = self.wm.borrow().visible_windows_front_to_back().any(|w| w.fullscreen);
+ // `None` (the user's config never touched `general.rounded_corners`)
+ // defaults to *on* here - this backend has an actual GPU shader
+ // behind the feature (see `rounded_corners.rs`), no untested
+ // per-frame CPU cost to weigh the way the udev backend's own
+ // default has to.
+ let rounded_corners_enabled = self.wm.borrow().rounded_corners_enabled.unwrap_or(true);
+ if !hide_top_layers {
+ custom_elements.extend(
+ crate::elements::output_layer_elements(renderer, &self.output, (0, 0), |layer| matches!(layer, Layer::Top | Layer::Overlay))
+ .into_iter()
+ .map(crate::rounded_corners::WinitElement::Base),
+ );
+ }
+ // Windows stacked in front of whichever one border/decoration is
+ // being built right now - `ids` is already front-to-back, so this
+ // only ever needs appending to, not recomputing. A window's own
+ // *content*, pushed inside this same loop below, needs no separate
+ // occlusion test - see the matching comment in `udev.rs`'s render
+ // loop for why ordinary front-to-back push order already occludes
+ // it correctly. The border strips and titlebar bitmap are
+ // different: outside `geometry`, so they still need `occluders`'
+ // explicit clip against whichever window is stacked in front.
+ let mut occluders: Vec<srdwm_core::Rect> = Vec::with_capacity(ids.len());
+ for id in ids {
+ let Some(w) = self.wm.borrow().window(id).cloned() else { continue };
+ // `w.geometry` is the animation's target, not necessarily where
+ // the window is actually drawn this frame - see the matching
+ // comment in `udev.rs`'s render loop for the full story
+ // (reported live as the border "not flush" with the window
+ // during an animated maximize/fullscreen/open-slide transition).
+ let geom = self.state.window_anims.get(&id).map(crate::state::WindowAnim::current_rect).unwrap_or(w.geometry);
+ // Same reasoning as udev.rs's matching push: positioned from
+ // `geom`, not `w.geometry`, and not fragment-clipped against
+ // `occluders` - see that comment.
+ if let Some(shadow) = self.state.shadow_buffers.get(&id) {
+ let rect = decoration::shadow_rect(geom);
+ let pos = (rect.x as f64, rect.y as f64);
+ match MemoryRenderBufferRenderElement::from_buffer(renderer, pos, shadow, None, None, None, Kind::Unspecified) {
+ Ok(elem) => custom_elements.push(crate::rounded_corners::WinitElement::Base(crate::elements::OverlayElement::Memory(elem))),
+ Err(e) => log::warn!("failed to import shadow buffer for window {id}: {e}"),
+ }
+ }
+ if let Some(deco) = self.state.decorations.get(&id) {
+ // Fragment-clipped, same as udev.rs's matching titlebar
+ // push - see that comment for why all-or-nothing (skip
+ // only once *fully* covered) wasn't enough: a titlebar
+ // only partially covered, the common case for cascaded
+ // windows, still bled through the covered part.
+ let titlebar_rect = srdwm_core::Rect::new(geom.x, geom.y, geom.width, srdwm_core::TITLEBAR_HEIGHT);
+ for fragment in crate::elements::visible_border_fragments(titlebar_rect, &occluders) {
+ let pos = (fragment.x as f64, fragment.y as f64);
+ let src = Rectangle::new(
+ Point::from(((fragment.x - titlebar_rect.x) as f64, (fragment.y - titlebar_rect.y) as f64)),
+ Size::from((fragment.width as f64, fragment.height as f64)),
+ );
+ match MemoryRenderBufferRenderElement::from_buffer(renderer, pos, deco, None, Some(src), None, Kind::Unspecified) {
+ Ok(elem) => custom_elements.push(crate::rounded_corners::WinitElement::Base(crate::elements::OverlayElement::Memory(elem))),
+ Err(e) => log::warn!("failed to import titlebar buffer for window {id}: {e}"),
+ }
+ }
+ }
+ // Border strips sit entirely outside `geometry` (see
+ // `decoration::border_strips`), so they never overlap this same
+ // window's own decoration/content pixels - draw order relative
+ // to those doesn't matter, only relative to other windows'.
+ if w.border_width > 0 {
+ let color = crate::state::effective_border_color(w.border_color, focused == Some(id));
+ let strips = decoration::border_strips(geom, w.border_width);
+ // Strip 0 (top) is rounded to match the titlebar underneath
+ // it - see `render_border_top`'s doc comment - so it's a
+ // cached bitmap (rebuilt only in `redraw_decoration_buffer`,
+ // same as the titlebar itself), not rasterized fresh here
+ // every frame; the other three don't touch a rounded corner
+ // and stay persistent solid-colour buffers instead - see
+ // `elements::border_side_render_element`'s doc comment for
+ // why a per-frame rebuild of either was a real, continuous
+ // cost, not a cosmetic one. Not fragment-clipped like the
+ // other three below - see the matching comment in
+ // `udev.rs` for why the top strip only gets the cheaper
+ // all-or-nothing occlusion check.
+ if strips[0].width > 0 && strips[0].height > 0 && !strips[0].subtract_all(&occluders).is_empty() {
+ if let Some(buffer) = self.state.border_top_decorations.get(&id) {
+ match MemoryRenderBufferRenderElement::from_buffer(renderer, (strips[0].x as f64, strips[0].y as f64), buffer, None, None, None, Kind::Unspecified) {
+ Ok(elem) => custom_elements.push(crate::rounded_corners::WinitElement::Base(crate::elements::OverlayElement::Memory(elem))),
+ Err(e) => log::warn!("failed to import top border buffer for window {id}: {e}"),
+ }
+ }
+ }
+ let pool = self.state.border_side_buffers.entry(id).or_default();
+ let mut buf_index = 0;
+ for strip in &strips[1..] {
+ if strip.width == 0 || strip.height == 0 {
+ continue;
+ }
+ for fragment in crate::elements::visible_border_fragments(*strip, &occluders) {
+ let buf = crate::elements::border_fragment_buffer(pool, buf_index);
+ buf_index += 1;
+ custom_elements.push(crate::rounded_corners::WinitElement::Base(crate::elements::OverlayElement::Solid(crate::elements::border_side_render_element(buf, fragment, color, (0, 0)))));
+ }
+ }
+ }
+ // The window's own content, at its own `opacity` - see the
+ // matching push in `udev.rs`'s render loop for why. Single
+ // output at the global origin, so no offset to subtract (see
+ // `elements.rs`'s doc comment on why `udev.rs`'s per-head call
+ // does). Rounded via `rounded_corners::rounded_content_element`
+ // when the feature's on and the shader compiled - a decorated
+ // window only rounds its bottom two corners (the top two are
+ // already rounded, on the titlebar's own bitmap, by
+ // `decoration.rs`), an undecorated/CSD one rounds all four,
+ // since its content *is* the window's whole visible extent.
+ // Falls back to the plain, unrounded `surface_content_elements`
+ // on any failure - no committed buffer yet, a single-pixel
+ // solid-colour buffer, or the feature simply being off - same
+ // "always show something over a prettier maybe-nothing"
+ // reasoning `cursor.rs`'s built-in-arrow fallback already uses.
+ if let Some(dwindow) = self.state.id_to_window.get(&id) {
+ if let Some(surface) = crate::elements::window_wl_surface(dwindow) {
+ let band = if w.decorated { srdwm_core::TITLEBAR_HEIGHT as i32 } else { 0 };
+ let pos = (geom.x, geom.y + band);
+ let rounded = rounded_corners_enabled.then_some(self.state.rounded_corners_program.as_ref()).flatten().and_then(|program| {
+ let corners = if w.decorated { crate::rounded_corners::RoundedCorners::BOTTOM_ONLY } else { crate::rounded_corners::RoundedCorners::ALL };
+ crate::rounded_corners::rounded_content_element(renderer, program, &surface, pos, w.opacity, crate::decoration::CORNER_RADIUS as f32, corners)
+ });
+ match rounded {
+ Some(elem) => custom_elements.push(crate::rounded_corners::WinitElement::Rounded(elem)),
+ None => custom_elements.extend(crate::elements::surface_content_elements(renderer, &surface, pos, w.opacity).into_iter().map(crate::rounded_corners::WinitElement::Base)),
+ }
+ }
+ }
+ occluders.push(geom);
+ }
+ // Background/bottom layer-shell (wallpaper engines) last --
+ // bottommost, matching smithay's own `space_render_elements`
+ // ordering, which this whole custom loop now replaces.
+ custom_elements.extend(
+ crate::elements::output_layer_elements(renderer, &self.output, (0, 0), |layer| matches!(layer, Layer::Background | Layer::Bottom))
+ .into_iter()
+ .map(crate::rounded_corners::WinitElement::Base),
+ );
+
+ // Not `smithay::desktop::space::render_output`: see `udev.rs`'s
+ // matching call site for why (per-window opacity, fullscreen-aware
+ // layer-shell inclusion - `custom_elements` above already carries
+ // everything that wrapper would have built).
+ let result = self
+ .damage_tracker
+ .render_output(renderer, &mut framebuffer, age, &custom_elements, [0.05, 0.05, 0.08, 1.0])
+ .map_err(err)?;
+ let damage_rects: Vec<Rectangle<i32, Physical>> = result.damage.cloned().unwrap_or_default();
+ let has_damage = !damage_rects.is_empty();
+ drop(framebuffer);
+ // Both the buffer swap and the frame-callback notification are
+ // conditional on real damage now - this used to run
+ // unconditionally on every call to this function (every ~16ms
+ // regardless of activity), which told every window it could render
+ // its next frame whether or not the screen had actually changed.
+ // Any client using the standard wait-for-frame-callback render
+ // pattern (most of them) had no reason not to redraw at whatever
+ // rate this loop cycled, forever - confirmed live on the udev
+ // backend: wezterm-gui pinned at 140%+ CPU sitting on a fully idle,
+ // unchanged terminal, from the identical bug there.
+ //
+ // That output-wide gate wasn't enough on its own: cursor motion
+ // alone damages the small region around the pointer, which still
+ // marked the *whole output* damaged and sent every mapped window a
+ // callback regardless of whether the cursor was anywhere near it.
+ // `windows_touched_by_damage` narrows this to windows the actual
+ // damage rectangles overlap - see its doc comment in elements.rs.
+ if has_damage {
+ self.backend.submit(None).map_err(err)?;
+ let scale = Scale::from(self.output.current_scale().fractional_scale());
+ let now = self.state.start_time.elapsed();
+ for w in crate::elements::windows_touched_by_damage(&self.state.space, &damage_rects, scale) {
+ w.send_frame(&self.output, now, None, |_, _| Some(self.output.clone()));
+ }
+ }
+ // Deliberately outside `if has_damage`: the whole point of
+ // `always_notify` is covering the case where the output has *no*
+ // damage at all (a fully idle desktop, cursor not moving) but the
+ // focused/hovered window still has a pending callback it needs
+ // answered to unblock an input-driven redraw. Nesting this inside
+ // `if has_damage` (the first version of this fix) meant it only
+ // ever ran on a tick that already had damage from something else
+ // happening - i.e. never in the exact scenario it exists for.
+ // Reported live as clicks in Firefox still doing nothing at all,
+ // not just intermittently, after the first version of this fix.
+ {
+ let pointer_pos = last_pointer_pos(&self.state);
+ let now = self.state.start_time.elapsed();
+ let wm = self.wm.borrow();
+ let always_notify = [wm.focused_id(), wm.window_at(pointer_pos.x as i32, pointer_pos.y as i32)];
+ drop(wm);
+ for w in always_notify.into_iter().flatten().filter_map(|id| self.state.id_to_window.get(&id)) {
+ w.send_frame(&self.output, now, None, |_, _| Some(self.output.clone()));
+ }
+ }
+ // Layer-shell surfaces get their callback every pass, unconditionally
+ // - NOT folded into the `has_damage` gate above. See the matching
+ // (much longer) comment in udev.rs's `render_udev_frame`: many
+ // layer-shell clients (GTK4/AGS among them) drive their entire
+ // repaint loop off frame callbacks with no independent timer
+ // fallback, so withholding the callback until *something* on the
+ // desktop happens to produce damage deadlocks them permanently
+ // after their first frame - confirmed live, AGS and waybar both
+ // froze exactly this way. Toplevel windows keep the damage gate
+ // (that's what fixed the wezterm-gui CPU-burn bug); layer surfaces
+ // are few, cheap to redraw, and are exactly the periodic-UI-chrome
+ // case frame callbacks exist to pace.
+ for layer in layer_map_for_output(&self.output).layers() {
+ layer.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(())
+ }
+}
diff --git a/crates/wayland/src/winit/run.rs b/crates/wayland/src/winit/run.rs
new file mode 100644
index 0000000..ed7c4c3
--- /dev/null
+++ b/crates/wayland/src/winit/run.rs
@@ -0,0 +1,26 @@
+use super::*;
+use super::events::handle_winit_event;
+
+impl WaylandPlatform {
+
+ pub(super) 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(())
+ }
+
+ pub(super) 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)
+ }
+}