diff options
Diffstat (limited to 'crates/wayland/src/protocols/compositor.rs')
| -rw-r--r-- | crates/wayland/src/protocols/compositor.rs | 182 |
1 files changed, 182 insertions, 0 deletions
diff --git a/crates/wayland/src/protocols/compositor.rs b/crates/wayland/src/protocols/compositor.rs new file mode 100644 index 0000000..f7e7b74 --- /dev/null +++ b/crates/wayland/src/protocols/compositor.rs @@ -0,0 +1,182 @@ +//! `wl_compositor`/`wl_surface`: surface creation and the per-commit +//! bookkeeping every other protocol handler in this module tree depends on +//! (window mapping, layer-surface visibility, popup lifecycle). + +use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; +use smithay::reexports::wayland_server::Client; +use smithay::wayland::compositor::{add_pre_commit_hook, with_states, CompositorClientState, CompositorHandler, CompositorState}; +use smithay::wayland::shell::wlr_layer::LayerSurfaceCachedState; + +use crate::state::{ClientState, CompState}; + +impl CompositorHandler for CompState { + fn compositor_state(&mut self) -> &mut CompositorState { + &mut self.compositor_state + } + + fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState { + // Two possible client kinds now: our own `ClientState` for regular + // Wayland clients, or smithay's `XWaylandClientData` for the single + // XWayland client (see `xwayland.rs`) - both carry a + // `CompositorClientState`, just under different wrapper types. + if let Some(state) = client.get_data::<ClientState>() { + return &state.compositor_state; + } + &client.get_data::<smithay::xwayland::XWaylandClientData>().expect("client is neither ours nor XWayland's").compositor_state + } + + /// Workaround for a real smithay bug (see docs/PANEL_SUPPORT_TODO.md and + /// `layer_destroyed` below): destroying a `zwlr_layer_surface_v1` role + /// resets the surface's `LayerSurfaceCachedState` to + /// `Default::default()` (size 0x0, no anchor) rather than removing it, + /// but the pre-commit hook smithay itself registers at + /// `get_layer_surface` time keeps validating that state against every + /// future commit regardless of whether the role still exists -- + /// tripping its own `width/height 0 requested without ... anchors` + /// check and posting `invalid_size`, which kills the client's whole + /// connection over what is protocol-legal (committing a now-roleless + /// surface). + /// + /// Fixed by registering our own pre-commit hook here, in `new_surface` + /// - called at `wl_compositor.create_surface`, strictly before any + /// later `get_layer_surface` on the same surface could register + /// smithay's own hook. Hooks run in registration order (`tree.rs`: + /// `pre_commit_hooks` is a plain `Vec`, pushed and iterated in order), + /// so ours always runs first and can neutralize the stale reset state + /// before smithay's hook ever inspects it. This depends on that + /// ordering guarantee holding in future smithay versions - it isn't + /// documented as an API contract, just an implementation detail + /// confirmed against 0.7.0's source - so re-check this file against + /// whatever smithay version replaces it. + /// + /// Cost: one closure registered per `wl_surface` (not just layer + /// surfaces, since we don't know in advance which ones will become + /// one), each a no-op unless that exact surface is in + /// `dead_layer_surfaces`. + fn new_surface(&mut self, surface: &WlSurface) { + add_pre_commit_hook::<CompState, _>(surface, |state, _dh, surface| { + if !state.dead_layer_surfaces.contains(surface) { + return; + } + with_states(surface, |states| { + let mut cached = states.cached_state.get::<LayerSurfaceCachedState>(); + let pending = cached.pending(); + if pending.size.w == 0 && !pending.anchor.anchored_horizontally() { + pending.size.w = 1; + } + if pending.size.h == 0 && !pending.anchor.anchored_vertically() { + pending.size.h = 1; + } + }); + }); + } + + fn commit(&mut self, surface: &WlSurface) { + smithay::backend::renderer::utils::on_commit_buffer_handler::<CompState>(surface); + // XWayland's association of an X11 window with this wl_surface can + // arrive at any point relative to the map request (see + // `xwayland.rs`'s module docs); `surface_associated` handles the + // common ordering, this retries the surfaces still waiting on a + // commit to actually make that association queryable. + self.retry_pending_x11_windows(); + if let Some(&id) = self.surface_to_id.get(surface) { + if let Some(w) = self.id_to_window.get(&id) { + w.on_commit(); + } + // See `content_epoch`'s doc comment: this is the only per-commit + // signal the udev backend's rounded-corner mask cache has to + // invalidate itself, since content can change every frame, + // independent of the geometry-driven points `redraw_decoration_ + // buffer` already runs at. + *self.content_epoch.entry(id).or_insert(0) += 1; + crate::state::sync_toplevel_metadata(self, id, surface); + // `redraw_decoration_buffer` reads `dwindow.geometry()` (via + // `effective_frame`) to size the border/titlebar/shadow against + // what the client's surface *really* committed - but nothing + // updates that value except this very commit + // (`on_commit()` above). Without a call here, a client whose + // first real commit settles at a different size than what was + // requested (a terminal snapping to a whole character-cell + // grid) wouldn't get corrected decoration until some unrelated + // trigger (a resize, a focus change) happened to call this + // again - cheap regardless, since the signature check inside + // makes every commit that didn't actually change the *visible* + // size an early return, not a real rebuild. + self.redraw_decoration_buffer(id); + // `sync_geometry` is what actually maps this window into + // `self.space` at `geom.x - content_offset.x, ...` - the same + // `content_offset` (`dwindow.geometry().loc`) the render loop + // (`udev/render.rs`'s per-frame `pos` computation) reads fresh + // on every single frame, straight off the live surface, not + // from any cache. Before this call existed here, `self.space` + // only got a fresh position from whichever *other* trigger last + // called `sync_geometry` (a resize, `maximize_request`, a + // decoration-mode change) - so a client that recommits a + // *different* `xdg_surface::set_window_geometry` on its own, + // with no accompanying resize (a GTK4/Firefox CSD window + // shrinking its declared shadow margin once real content + // replaces its first, provisional paint, concretely), left + // `self.space`'s cached position silently stale while the + // render loop kept self-correcting every frame - confirmed + // live via temporary diagnostic logging: a window's real render + // position and `self.space`'s own `element_under`-reported + // position for it disagreed by exactly one `content_offset`, + // 10 physical pixels on both axes for the Firefox window that + // exposed it. `refresh_pointer_focus`'s content-click path + // (`input.rs`) computes `win_relative` from *that* stale + // position, not the render loop's fresh one - every click on + // such a window was silently off by the same 10px the whole + // time it stayed unmapped-and-remapped-by-nothing-else, which + // reads as "clicks land near, but not on, whatever's visibly + // there" - worst for a window's own small CSD buttons, + // exactly what was reported live. Same idempotent-when-nothing- + // moved shape as `redraw_decoration_buffer` above: `map_element` + // itself is unconditional and cheap (a hashmap insert), and the + // one potentially-expensive part - sending a fresh + // `xdg_toplevel::configure` - stays gated on `size_changed` + // and the existing throttle, both untouched, so a commit that + // didn't change size never sends one just because this call is + // now here too. + self.sync_geometry(id); + } else { + // `surface` itself isn't a tracked window's root, but may be a + // descendant (subsurface) of one - a real commit still + // happened, just not on the surface `surface_to_id` keys off. + // `masked_content_buffer`'s own resolver + // (`rounded_corners_pixman::resolve_content_surface`) reads a + // *child* subsurface's buffer directly for the common GTK4/ + // WebRender pattern (confirmed live: Firefox), so a repaint + // that only ever commits that child - which is the normal + // case, that's where the real content lives - must still + // bump this window's own `content_epoch`, or the masked- + // corner cache never sees a reason to invalidate and freezes + // on whatever the first frame happened to show. Bounded to a + // handful of hops purely as a safety net against a malformed + // subsurface tree looping back on itself - a real one is + // never more than one or two levels deep. + let mut ancestor = smithay::wayland::compositor::get_parent(surface); + for _ in 0..8 { + let Some(parent) = ancestor else { break }; + if let Some(&id) = self.surface_to_id.get(&parent) { + *self.content_epoch.entry(id).or_insert(0) += 1; + break; + } + ancestor = smithay::wayland::compositor::get_parent(&parent); + } + } + // Before `ensure_layer_initial_configure`: if this commit just + // hid or re-showed a layer surface, `sync_layer_visibility` needs + // to unmap/re-map it first, so the lookup that function does via + // `layer_for_surface` sees the corrected state rather than acting + // on stale membership in `LayerMap`'s own list. + self.sync_layer_visibility(surface); + self.ensure_layer_initial_configure(surface); + // Advances a just-created popup from unmapped to mapped (needed for + // `PopupManager::popups_for_surface`, which `popup_render_elements` + // reads at render time) and prunes dead ones. Cheap and only does + // real work on a popup-role surface, so doing it on every commit + // rather than throttling is not worth the extra bookkeeping. + self.popups.commit(surface); + self.popups.cleanup(); + } +} |