diff options
| author | srdusr <[email protected]> | 2026-07-23 22:56:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2026-07-23 22:56:00 +0200 |
| commit | 8ed79c21a53e10c29ea35b9a605250ea18f191cf (patch) | |
| tree | 22fbfc5a9348c7449a0b831e8066020a5faa68a4 /crates/wayland/src/udev | |
| parent | c3a34348b39c9518ad2e5bb28569fb9d78ce5dc1 (diff) | |
| download | srdwm-8ed79c21a53e10c29ea35b9a605250ea18f191cf.tar.gz srdwm-8ed79c21a53e10c29ea35b9a605250ea18f191cf.zip | |
Do not draw a window before its client has painted anything
Reported as "before a window spawns, the border corners look funny".
A toplevel is placed, sized and decorated the moment its role is created,
which is well before the client draws. srdwm was rendering it from that
moment, so what appeared first was an empty frame: border, titlebar and
shadow standing around bare desktop, at the guessed 800x600 placeholder
size, with nothing inside. When the real buffer arrived the frame snapped
to the real size.
Measured in a nested session, capturing a cold terminal's spawn with grim:
four consecutive captured frames spanning 540ms showed a complete red
border with zero client content inside it, at 642px outer height, which
then settled at 610 - a jump of exactly one TITLEBAR_HEIGHT. After this
change the same capture has no such frame at all: every frame that shows a
border shows content in it, and the height does not change afterward.
Two parts:
- Nothing is drawn for a window that has never committed a buffer. All
five paths that draw a frame agree on this - both udev render loops
(Pixman and GPU), the winit render loop, and both screencopy paths, so a
screenshot cannot show a frame the screen does not.
- The open-slide starts at the first commit that carries a buffer rather
than at role creation. A cold terminal took ~800ms to paint, long enough
for the whole tween to finish against the empty frame, so the window
simply appeared, already at rest, with no animation at all. It now
animates where it can actually be seen.
The answer latches once true (windows_shown_once), so a window that has
legitimately shown something is never hidden again by this however its
buffer state changes. A window that cannot be resolved to a surface 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 - can be hidden by a lookup that did
not apply to it.
Same shape, and the same reason, as sync_layer_visibility's own has_buffer
branch, which layer surfaces have had all along.
533 tests pass, clippy clean.
Diffstat (limited to 'crates/wayland/src/udev')
| -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 |
3 files changed, 19 insertions, 0 deletions
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 |