diff options
| -rw-r--r-- | crates/wayland/src/protocols/compositor.rs | 18 | ||||
| -rw-r--r-- | crates/wayland/src/state/geometry.rs | 35 | ||||
| -rw-r--r-- | crates/wayland/src/state/lifecycle.rs | 20 | ||||
| -rw-r--r-- | crates/wayland/src/state/mod.rs | 23 | ||||
| -rw-r--r-- | crates/wayland/src/udev/capture.rs | 5 | ||||
| -rw-r--r-- | crates/wayland/src/udev/platform.rs | 1 | ||||
| -rw-r--r-- | crates/wayland/src/udev/render.rs | 13 | ||||
| -rw-r--r-- | crates/wayland/src/winit/capture.rs | 5 | ||||
| -rw-r--r-- | crates/wayland/src/winit/connect.rs | 1 | ||||
| -rw-r--r-- | crates/wayland/src/winit/render.rs | 4 |
10 files changed, 112 insertions, 13 deletions
diff --git a/crates/wayland/src/protocols/compositor.rs b/crates/wayland/src/protocols/compositor.rs index e2868e4..7be5d7e 100644 --- a/crates/wayland/src/protocols/compositor.rs +++ b/crates/wayland/src/protocols/compositor.rs @@ -89,6 +89,24 @@ impl CompositorHandler for CompState { // own first real size a chance to adopt it from what `on_commit` // just recomputed, rather than keep rendering/configuring // against the guessed placeholder for one more round-trip. + // The first commit that actually carries a buffer is when this + // window becomes visible, so it is also when the open-slide + // should start - see `windows_shown_once`. Registered here + // rather than in `new_managed_window` because a role is created + // well before a client paints (measured at ~800ms for a cold + // terminal), which is long enough for the whole tween to finish + // against an empty frame and for the window to simply appear, + // already at rest, with no animation at all. + if !self.windows_shown_once.contains(&id) && self.window_has_content(id) { + self.windows_shown_once.insert(id); + let mut wm = self.wm.borrow_mut(); + if wm.animations_enabled { + if let Some(win) = wm.window_mut(id) { + let g = win.geometry; + win.anim_from = Some(srdwm_core::Rect { y: g.y + crate::state::OPEN_SLIDE_OFFSET, ..g }); + } + } + } self.adopt_provisional_size(id); // See `content_epoch`'s doc comment: this is the only per-commit // signal the udev backend's rounded-corner mask cache has to diff --git a/crates/wayland/src/state/geometry.rs b/crates/wayland/src/state/geometry.rs index 5cbece9..d3c7ab8 100644 --- a/crates/wayland/src/state/geometry.rs +++ b/crates/wayland/src/state/geometry.rs @@ -314,6 +314,41 @@ impl CompState { (x, y) } + /// True when this window has something on screen to draw a frame + /// around - it has committed a buffer at least once. + /// + /// A toplevel is placed and decorated the moment its role is created, + /// which is well before the client paints. Drawing it then puts a + /// border, a titlebar and a shadow around bare desktop, at the guessed + /// placeholder size (`Window::size_is_provisional`), and that empty + /// frame then jumps when the real buffer arrives at the real size. + /// + /// A window that cannot be resolved to a surface at all counts as + /// drawable, deliberately: this hides a window only on positive + /// evidence that it has never drawn, so nothing whose surface plumbing + /// works differently (an XWayland window, say) can be hidden by a + /// lookup that simply did not apply to it. + /// + /// See `windows_shown_once` for why the answer latches once true. + /// + /// Takes the two maps rather than `&self` so a render loop can call it + /// while it already holds `self.udev` mutably borrowed. + pub(crate) fn has_content(shown_once: &HashSet<WindowId>, id_to_window: &HashMap<WindowId, DWindow>, id: WindowId) -> bool { + if shown_once.contains(&id) { + return true; + } + let Some(surface) = id_to_window.get(&id).and_then(crate::elements::window_wl_surface) else { return true }; + smithay::backend::renderer::utils::with_renderer_surface_state( + &surface, + |state: &mut smithay::backend::renderer::utils::RendererSurfaceState| state.buffer().is_some(), + ) + .unwrap_or(false) + } + + pub(crate) fn window_has_content(&self, id: WindowId) -> bool { + Self::has_content(&self.windows_shown_once, &self.id_to_window, id) + } + pub(crate) fn effective_frame(&self, id: WindowId, geom: srdwm_core::Rect) -> srdwm_core::Rect { Self::effective_frame_of(&self.wm, &self.id_to_window, &self.pending_size_configure, id, geom) } diff --git a/crates/wayland/src/state/lifecycle.rs b/crates/wayland/src/state/lifecycle.rs index 163064e..b6c7f09 100644 --- a/crates/wayland/src/state/lifecycle.rs +++ b/crates/wayland/src/state/lifecycle.rs @@ -19,18 +19,13 @@ impl CompState { if wm.window(id).is_some_and(|w| w.size_is_provisional) { self.provisional_size.insert(id); } - // Starts the open-slide tween (see `WindowAnim`'s doc comment): - // the window's first `sync_geometry` call below will see this, - // register the tween, and place it here - a few pixels below - // its resting position - rather than jumping straight to - // `geometry`. Same size throughout, so no extra client configure - // is needed for the tween itself. - if wm.animations_enabled { - if let Some(win) = wm.window_mut(id) { - let g = win.geometry; - win.anim_from = Some(srdwm_core::Rect { y: g.y + OPEN_SLIDE_OFFSET, ..g }); - } - } + // The open-slide tween is NOT started here, deliberately. + // A toplevel role exists well before its client paints + // anything, so starting it here ran the animation against an + // empty frame and left the window simply appearing, already at + // rest. `CompositorHandler::commit` starts it at the first + // commit that carries a buffer instead - see + // `windows_shown_once`. id }; @@ -383,6 +378,7 @@ impl CompState { self.shadow_buffers.remove(&id); self.border_side_buffers.remove(&id); self.decoration_signatures.remove(&id); + self.windows_shown_once.remove(&id); self.last_synced_size.remove(&id); self.content_epoch.remove(&id); self.rounded_content_buffers.remove(&id); diff --git a/crates/wayland/src/state/mod.rs b/crates/wayland/src/state/mod.rs index 5c1b02e..44ef772 100644 --- a/crates/wayland/src/state/mod.rs +++ b/crates/wayland/src/state/mod.rs @@ -461,6 +461,27 @@ pub(crate) struct CompState { /// this set is ever consulted, so a surface only reaches the unmap path /// once it has legitimately shown something. pub(crate) layer_surfaces_shown_once: HashSet<WlSurface>, + /// Windows whose surface has committed a buffer at least once. + /// + /// A toplevel exists, and is placed and decorated, from the moment its + /// role is created - which is well before the client has drawn + /// anything. Rendering it at that point paints a border and a titlebar + /// around empty desktop: an empty frame stands there on its own, then + /// snaps to a different size once the real buffer arrives and the + /// guessed `800x600` placeholder (`Window::size_is_provisional`) is + /// replaced. Measured in a nested session: the frame was drawn ~800ms + /// before any content, one full `TITLEBAR_HEIGHT` too tall, which is + /// what "the border corners look funny before a window spawns" is. + /// + /// So this gates two things: nothing is drawn for a window that has + /// never had a buffer, and the open-slide starts at the first buffer + /// rather than at role creation, so the animation plays where it can + /// actually be seen instead of finishing against an empty frame. + /// + /// Same shape, and the same reason, as `layer_surfaces_shown_once` + /// above: a window that has legitimately shown something once is never + /// hidden again by this, however its buffer state changes afterward. + pub(crate) windows_shown_once: HashSet<WindowId>, pub(crate) decorations: HashMap<WindowId, MemoryRenderBuffer>, /// The top border strip's rounded-corner bitmap, cached the same way /// and at the same trigger points as `decorations` (built in @@ -787,7 +808,7 @@ impl WindowAnim { /// resize tween is reserved for maximize/fullscreen, where the client is /// already live and redrawing, not for a window whose first paint may not /// have arrived yet). -const OPEN_SLIDE_OFFSET: i32 = 24; +pub(crate) const OPEN_SLIDE_OFFSET: i32 = 24; /// A held keybinding that is firing repeatedly. /// diff --git a/crates/wayland/src/udev/capture.rs b/crates/wayland/src/udev/capture.rs index a64a73c..287c003 100644 --- a/crates/wayland/src/udev/capture.rs +++ b/crates/wayland/src/udev/capture.rs @@ -69,6 +69,11 @@ impl CompState { let Some(udev) = self.udev.as_mut() else { return Err("no udev backend".to_string()) }; let mut elements: Vec<crate::elements::OverlayElement<PixmanRenderer>> = Vec::new(); for id in ids { + // Matches the render loops: a capture must not show a frame the + // screen does not (see `window_has_content`). + if !Self::has_content(&self.windows_shown_once, &self.id_to_window, id) { + continue; + } let Some(w) = self.id_to_window.get(&id) else { continue }; let Some(surface) = crate::input::dwindow_wl_surface(w) else { continue }; let Some(geom) = self.wm.borrow().window(id).map(|w| w.geometry) else { continue }; diff --git a/crates/wayland/src/udev/platform.rs b/crates/wayland/src/udev/platform.rs index 8dad7c3..86307dc 100644 --- a/crates/wayland/src/udev/platform.rs +++ b/crates/wayland/src/udev/platform.rs @@ -256,6 +256,7 @@ impl UdevPlatform { dead_layer_surfaces: HashSet::new(), hidden_layer_surfaces: HashMap::new(), layer_surfaces_shown_once: HashSet::new(), + windows_shown_once: HashSet::new(), decorations: HashMap::new(), border_top_decorations: HashMap::new(), border_bottom_decorations: HashMap::new(), diff --git a/crates/wayland/src/udev/render.rs b/crates/wayland/src/udev/render.rs index 9992b2c..c2899ae 100644 --- a/crates/wayland/src/udev/render.rs +++ b/crates/wayland/src/udev/render.rs @@ -303,6 +303,14 @@ impl CompState { let mut elements: Vec<crate::elements::OverlayElement<smithay::backend::renderer::gles::GlesRenderer>> = crate::cursor::render_elements(&cursor_status, &cursor_buffers, &mut gpu.renderer, udev.pointer_pos, origin, udev.heads[index].size); for &id in &ids { + // Nothing is drawn for a window that has never committed a + // buffer - otherwise its border, titlebar and shadow + // stand around empty desktop until the client paints, + // then jump when the placeholder size is replaced. See + // `window_has_content`. + if !Self::has_content(&self.windows_shown_once, &self.id_to_window, id) { + continue; + } let Some(w) = self.wm.borrow().window(id).cloned() else { continue }; let Some(dwindow) = self.id_to_window.get(&id) else { continue }; let Some(surface) = crate::elements::window_wl_surface(dwindow) else { continue }; @@ -541,6 +549,11 @@ impl CompState { // window is stacked in front. let mut occluders: Vec<srdwm_core::Rect> = Vec::with_capacity(ids.len()); for &id in &ids { + // See the GPU loop above: a window with no buffer yet has + // nothing for a frame to go around. + if !Self::has_content(&self.windows_shown_once, &self.id_to_window, id) { + continue; + } 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 diff --git a/crates/wayland/src/winit/capture.rs b/crates/wayland/src/winit/capture.rs index 3813559..6e03dd8 100644 --- a/crates/wayland/src/winit/capture.rs +++ b/crates/wayland/src/winit/capture.rs @@ -98,6 +98,11 @@ impl WaylandPlatform { let mut occluders: Vec<srdwm_core::Rect> = Vec::new(); let focused = self.wm.borrow().focused_id(); for id in self.wm.borrow().visible_windows_front_to_back().map(|w| w.id).collect::<Vec<_>>() { + // Matches the render loops: a capture must not show a frame the + // screen does not (see `window_has_content`). + if !crate::state::CompState::has_content(&self.state.windows_shown_once, &self.state.id_to_window, id) { + continue; + } 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) { diff --git a/crates/wayland/src/winit/connect.rs b/crates/wayland/src/winit/connect.rs index aa59d9d..cca991c 100644 --- a/crates/wayland/src/winit/connect.rs +++ b/crates/wayland/src/winit/connect.rs @@ -168,6 +168,7 @@ impl WaylandPlatform { dead_layer_surfaces: HashSet::new(), hidden_layer_surfaces: HashMap::new(), layer_surfaces_shown_once: HashSet::new(), + windows_shown_once: HashSet::new(), decorations: HashMap::new(), border_top_decorations: HashMap::new(), border_bottom_decorations: HashMap::new(), diff --git a/crates/wayland/src/winit/render.rs b/crates/wayland/src/winit/render.rs index 432ebba..d49351f 100644 --- a/crates/wayland/src/winit/render.rs +++ b/crates/wayland/src/winit/render.rs @@ -236,6 +236,10 @@ impl WaylandPlatform { let monitor_bounds: Vec<srdwm_core::Rect> = self.wm.borrow().monitors().iter().map(|m| m.full_geometry).collect(); let mut occluders: Vec<srdwm_core::Rect> = Vec::with_capacity(ids.len()); for id in ids { + // See `window_has_content`: no buffer yet means no frame yet. + if !crate::state::CompState::has_content(&self.state.windows_shown_once, &self.state.id_to_window, id) { + continue; + } 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 |