diff options
Diffstat (limited to 'crates/wayland/src')
| -rw-r--r-- | crates/wayland/src/elements.rs | 68 | ||||
| -rw-r--r-- | crates/wayland/src/udev.rs | 117 | ||||
| -rw-r--r-- | crates/wayland/src/winit.rs | 164 |
3 files changed, 254 insertions, 95 deletions
diff --git a/crates/wayland/src/elements.rs b/crates/wayland/src/elements.rs index f9a569e..c9f1d6c 100644 --- a/crates/wayland/src/elements.rs +++ b/crates/wayland/src/elements.rs @@ -13,8 +13,10 @@ use smithay::backend::renderer::element::surface::{render_elements_from_surface_ use smithay::backend::renderer::element::Kind; use smithay::backend::renderer::{Color32F, ImportAll, ImportMem, Renderer}; use smithay::desktop::{layer_map_for_output, PopupManager, Space, Window as DWindow}; +use smithay::output::Output; use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; use smithay::utils::{Physical, Point, Rectangle, Scale}; +use smithay::wayland::shell::wlr_layer::Layer; use srdwm_core::TITLEBAR_HEIGHT; @@ -107,6 +109,72 @@ pub(crate) fn border_fragment_buffer(pool: &mut Vec<SolidColorBuffer>, index: us &mut pool[index] } +/// A mapped window's own `WlSurface`, regardless of which protocol backs +/// it - a native Wayland `xdg_toplevel` or an XWayland client. `None` for +/// an X11 window between `MapRequest` and its first commit, which really +/// has no surface yet. +pub(crate) fn window_wl_surface(w: &DWindow) -> Option<WlSurface> { + if let Some(top) = w.toplevel() { + return Some(top.wl_surface().clone()); + } + w.x11_surface().and_then(|x| x.wl_surface()) +} + +/// Renders one window's (or one layer-shell surface's) own content -- +/// nothing else, not decoration, not popups - at `location` (output-local +/// physical space, the same convention every other `custom_elements` entry +/// already uses) and `alpha`. +/// +/// Deliberately not `Window::render_elements` (`AsRenderElements`): that +/// bundles a window's own popups into the same call, which would double +/// them with [`popup_render_elements`]'s own separate pass over +/// [`popup_targets`]. `render_elements_from_surface_tree` walks only the +/// given surface's own (sub)surface tree - the same low-level call this +/// file's popup rendering and `cursor.rs`'s client-cursor-image path +/// already use safely, so reusing it here for a window's *main* content is +/// the same proven primitive, not a new one. +/// +/// This is what lets content have its own `alpha`, at all: content used to +/// only ever render via `self.space` passed whole to `render_output`, +/// which takes one `alpha` for the entire frame's worth of space content, +/// not one per window - there was no way to make one window translucent +/// without also dimming everything else in `self.space`. +pub(crate) fn surface_content_elements<R>(renderer: &mut R, surface: &WlSurface, location: (i32, i32), alpha: f32) -> Vec<OverlayElement<R>> +where + R: Renderer + ImportAll + ImportMem, + R::TextureId: Clone + Send + 'static, +{ + render_elements_from_surface_tree(renderer, surface, location, 1.0, alpha, Kind::Unspecified) +} + +/// Every mapped layer-shell surface on `output` whose [`Layer`] `include` +/// accepts, each rendered via [`surface_content_elements`] at full opacity +/// - layer-shell surfaces (bars, docks, wallpaper engines) don't have a +/// per-surface opacity concept the way `srd.rule`'s `opacity` gives +/// windows. +/// +/// Order matches smithay's own `space_render_elements` (0.7.0): `.rev()` +/// on `map.layers()` before rendering, so surfaces sharing one `Layer` +/// keep the same relative stacking smithay's convenience wrapper gave +/// them, now that this function replaces it. +pub(crate) fn output_layer_elements<R>(renderer: &mut R, output: &Output, origin: (i32, i32), include: impl Fn(Layer) -> bool) -> Vec<OverlayElement<R>> +where + R: Renderer + ImportAll + ImportMem, + R::TextureId: Clone + Send + 'static, +{ + let map = layer_map_for_output(output); + let mut elements = Vec::new(); + for layer in map.layers().rev() { + if !include(layer.layer()) { + continue; + } + let Some(geo) = map.layer_geometry(layer) else { continue }; + let location = (origin.0 + geo.loc.x, origin.1 + geo.loc.y); + elements.extend(surface_content_elements(renderer, layer.wl_surface(), location, 1.0)); + } + elements +} + /// A mapped toplevel's surface and on-screen (global-space, band-adjusted) /// position - everything [`popup_render_elements`] needs to find and place /// that window's popups, pre-extracted from `CompState` by diff --git a/crates/wayland/src/udev.rs b/crates/wayland/src/udev.rs index 7e7a69f..75abcba 100644 --- a/crates/wayland/src/udev.rs +++ b/crates/wayland/src/udev.rs @@ -44,9 +44,9 @@ use smithay::backend::renderer::pixman::PixmanRenderer; use smithay::backend::renderer::{Bind, ImportDma}; use smithay::backend::session::{libseat::LibSeatSession, libseat::LibSeatSessionNotifier, Event as SessionEvent, Session}; use smithay::backend::udev::{self, UdevBackend, UdevEvent}; -use smithay::desktop::space::render_output; use smithay::desktop::{layer_map_for_output, PopupManager, Space}; use smithay::backend::input::AxisSource; +use smithay::wayland::shell::wlr_layer::Layer; use smithay::input::pointer::AxisFrame; use smithay::input::SeatState; use smithay::output::{Mode as OutputMode, Output, PhysicalProperties, Subpixel}; @@ -277,22 +277,54 @@ impl CompState { Err(e) => log::warn!("udev: failed to import context menu buffer: {e}"), } } - // Content still comes from `render_output`'s own `spaces` - // argument (`self.space`, passed below), not from a - // per-window `render_elements` call here - see the long - // comment on the matching loop in `winit.rs`, and `state.rs`'s - // `resync_stacking_order` doc comment, for the real - // stacking-order bug this was chasing and its actual fix. + // Popups next: always above every window's own content, + // matching this codebase's long-standing behavior from + // before content moved into this same `custom_elements` + // list (see below) - pushing them here, ahead of every + // window and every layer-shell surface, is what keeps that + // true now that "above everything in `self.space`" is no + // longer a free property of a separate tier. + custom_elements.extend(crate::elements::popup_render_elements(&popup_targets, &mut udev.renderer, (origin.x, origin.y))); + + // The bar/dock/launcher (`Layer::Top`/`Overlay`): rendered + // ourselves via `output_layer_elements`, not through + // `render_output`'s automatic inclusion of `self.space` + + // `layer_map_for_output` - see this function's own call + // site further down for why content had to stop flowing + // through that convenience wrapper at all (per-window + // opacity), which took layer-shell inclusion down with it as + // a side effect. Skipped entirely - not just covered - for + // a fullscreen window: `we should not see the bar at all`, + // and unmapping it (`gtk_shell`) or covering it are two + // different guarantees. `ids` is already front-to-back, so + // checking every id for `fullscreen` here (rather than just + // the frontmost) covers a fullscreen window stacked behind + // an always-on-top one too. + let hide_top_layers = ids.iter().any(|&id| self.wm.borrow().window(id).is_some_and(|w| w.fullscreen)); + if !hide_top_layers { + custom_elements.extend(crate::elements::output_layer_elements( + &mut udev.renderer, + &output, + (origin.x, origin.y), + |layer| matches!(layer, Layer::Top | Layer::Overlay), + )); + } + // 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. Only a window's own *content* occludes - // correctly on its own path (via `space`, real stacking - // order respected); everything drawn here goes through - // `custom_elements`, which composites above *all* content - // unconditionally, so both the border strips and the - // titlebar bitmap below need this explicit occlusion test - // against it. + // recomputing. A window's own *content*, pushed inside this + // same loop below, needs no separate occlusion test: it + // draws in the same front-to-back push order as everything + // else here, so ordinary painter's-algorithm draw order + // already occludes it correctly (this is exactly why content + // used to occlude correctly via `self.space`'s own order, + // before it had to move into this list for per-window + // opacity to be possible at all). The border strips and + // titlebar bitmap are different: outside `geometry`, drawn + // via a bitmap that isn't itself window-shaped, 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 }; @@ -413,9 +445,33 @@ impl CompState { } } } + // The window's own content, at its own `opacity` -- + // this, not decoration, is the entire reason content + // moved into this loop at all (see the doc comment on + // the popup push above). Positioned the same way + // `sync_geometry` maps it into `self.space` (band added + // for a decorated window's titlebar reservation), so + // switching rendering paths doesn't also shift content + // relative to where clicks still land (hit-testing is + // untouched, still `w.geometry`/`self.space`-based). + if let Some(dwindow) = self.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 - origin.x, geom.y + band - origin.y); + custom_elements.extend(crate::elements::surface_content_elements(&mut udev.renderer, &surface, pos, w.opacity)); + } + } occluders.push(geom); } - custom_elements.extend(crate::elements::popup_render_elements(&popup_targets, &mut udev.renderer, (origin.x, origin.y))); + // 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( + &mut udev.renderer, + &output, + (origin.x, origin.y), + |layer| matches!(layer, Layer::Background | Layer::Bottom), + )); } let lock_elements = if locked { crate::lock::lock_render_elements(lock_surface.as_ref(), &mut udev.renderer) @@ -440,18 +496,25 @@ impl CompState { .map(|r| (r.damage.is_some(), Vec::new())) .map_err(|e| e.to_string()) } else { - render_output( - &head.output, - &mut udev.renderer, - &mut framebuffer, - 1.0, - head.ages[back], - [&self.space], - &custom_elements, - &mut head.damage_tracker, - [0.05, 0.05, 0.08, 1.0], - ) - .map(|r| (r.damage.is_some(), r.damage.cloned().unwrap_or_default())) + // Not `smithay::desktop::space::render_output`: that + // convenience wrapper draws `self.space`'s window content at + // one `alpha` for the whole frame and pulls every + // layer-shell surface in unconditionally, neither of which + // leaves room for per-window opacity or hiding the bar/dock + // during fullscreen. `custom_elements` above already carries + // everything that wrapper would have built - window + // content (`surface_content_elements`, one call per window, + // each with its own `w.opacity`) and layer-shell surfaces + // (`output_layer_elements`, split Top/Overlay above content + // and Background/Bottom below it) - assembled by hand in + // the correct front-to-back order instead. `self.space` + // itself is untouched and still authoritative for + // hit-testing/stacking bookkeeping (`sync_geometry`'s + // `map_element` calls); only the *render* path stopped + // reading from it. + head.damage_tracker + .render_output(&mut udev.renderer, &mut framebuffer, head.ages[back], &custom_elements, [0.05, 0.05, 0.08, 1.0]) + .map(|r| (r.damage.is_some(), r.damage.cloned().unwrap_or_default())) // Both arms reduce to "was there damage" plus the damage // rects themselves; the two error types differ, so they are // flattened to a message here. diff --git a/crates/wayland/src/winit.rs b/crates/wayland/src/winit.rs index 11c2d1e..2c55dda 100644 --- a/crates/wayland/src/winit.rs +++ b/crates/wayland/src/winit.rs @@ -25,8 +25,8 @@ 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::space::render_output; 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; @@ -349,46 +349,54 @@ impl WaylandPlatform { Err(e) => log::warn!("failed to import context menu buffer: {e}"), } } - // Decoration/border built per window, front-to-back (topmost - // first), so a window's own titlebar/border at least stay ordered - // consistently relative to *other* windows' decoration/border. + // 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). // - // Content deliberately still goes through `render_output`'s own - // `spaces` argument below (`self.state.space`), not through this - // loop: an earlier version of this fix pushed each window's own - // `Window::render_elements` output into `custom_elements` here too, - // in per-window stacking order, specifically to let decoration/ - // border interleave correctly with *other* windows' content - // (custom_elements otherwise draws entirely above every window's - // content, `spaces` or not - seeing `render_output`'s source is - // what motivated that attempt). It 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, reproduced consistently across three - // separate test windows, independent of push order, forced full - // redraws (`age = 0`), and buffer age - i.e. a real ordering bug - // in mixing multiple windows' own `render_elements` output this - // way, not a damage-tracking artifact. The real root cause (found - // later, by instrumenting a locally vendored smithay copy - // directly) turned out to be unrelated to content-vs-decoration - // mixing at all: `sync_geometry`'s `Space::map_element` call - // silently re-stacked windows to the top of `Space`'s own order - // any time position/size synced for *any* reason, independent of - // which rendering path was used - see `state.rs`'s + // 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 - // actual fix. `self.state.space` stayed the content path here - // since reverting it was never itself wrong, just insufficient on - // its own. + // 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))); + // 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); + if !hide_top_layers { + custom_elements.extend(crate::elements::output_layer_elements(renderer, &self.output, (0, 0), |layer| matches!(layer, Layer::Top | Layer::Overlay))); + } // 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. Only a window's - // own *content* occludes correctly on its own path (via `space`, - // real stacking order respected); everything drawn here goes - // through `custom_elements`, which composites above *all* content - // unconditionally, so both the border strips and the titlebar - // bitmap below need this explicit occlusion test against it. + // 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 }; @@ -468,25 +476,32 @@ impl WaylandPlatform { } } } + // 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). + 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, (geom.x, geom.y + band), w.opacity)); + } + } occluders.push(geom); } - // Single output at the global origin - no offset to subtract, see - // `elements.rs`'s doc comment on why udev.rs's per-head call does. - let popup_targets = crate::elements::popup_targets(&self.state); - custom_elements.extend(crate::elements::popup_render_elements(&popup_targets, renderer, (0, 0))); - - let result = 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)?; + // 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))); + + // 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); @@ -588,29 +603,42 @@ impl WaylandPlatform { 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(); - for (&id, deco) in self.state.decorations.iter() { - let Some(geom) = self.wm.borrow().visible_windows().find(|w| w.id == 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(crate::elements::OverlayElement::Memory(elem)); + 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); - 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)?; + 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(()) |