shell: dismiss folder overlays when the launcher closes - #18
Conversation
Every layer-shell window in the shell can be killed by the compositor after a
variable number of open/close cycles:
Gdk-Message: Error 71 (Protocol error) dispatching to Wayland display.
wl_display error 2: zwlr_layer_surface_v1 has never been configured
Root cause, from WAYLAND_DEBUG=1 traces. GTK keeps ONE wl_surface alive across
hide/show. gtk4-layer-shell destroys the zwlr_layer_surface_v1 at unmap and
creates a NEW one over that same wl_surface on the next open. Comparing a
working cycle with a crashing one in a single session:
working (#77 -> #64) crashing (#64 -> #82)
destroy() destroy()
attach(nil); commit() attach(nil); commit()
<nothing> attach(wl_buffer#79); commit() <-- stray
get_layer_surface(#64) get_layer_surface(#82)
commit() -> configure OK commit() -> ERROR 2
The only difference is a frame queued by the closing animation that lands
~0.7ms AFTER the unmap and re-attaches a live buffer to the now-roleless
surface. wl_surface state is persistent, so that buffer is still current when
the next layer surface is created; its first commit therefore carries a buffer
before any configure, which wlroots rejects. Whether that frame lands is a race
on wl_buffer.release, which is why the shell died after a VARIABLE number of
opens rather than deterministically.
unrealize() after hide() drops the GdkSurface, so the next open allocates a
brand-new wl_surface that cannot carry a stale buffer. This is the same call
gtk4-layer-shell itself makes in gtk_layer_surface_remap(), which does
gtk_widget_unrealize() then gtk_widget_map() -- a supported, exercised path.
unrealize() on a never-realized widget is a documented no-op, so the
constructor-time hide() in workspace_overview is harmless.
The cast is required: GtkWindow implements GtkNative, so a bare unrealize()
binds to gtk_native_unrealize, an internal vfunc, not gtk_widget_unrealize.
Verified by inspecting the C that valac emits for each spelling.
This is not launcher-specific. An audit found 19 windows calling
init_for_window(), of which six hide and then re-show -- each one a distinct
instance of the same fault. Applied to all 24 close paths in those six files:
dock.vala 7
sidebar.vala 5
app_menu.vala 4
workspace_overview.vala 4
hot_corner_manager.vala 2
overview.vala 2
Note that a close path is spelled either `hide();` or
`((Gtk.Widget) this).hide();`, and both need the same treatment -- dock.vala
uses the cast form for five of its seven, including the production autohide
reveal, which does that then present(): a full remap.
Verified on CIX Sky1 (Radxa Orion O6N, labwc/wlroots, GTK4, GLES on libmali)
with a Wayland trace analyzer that flags a layer surface being re-created over
a wl_surface that still holds a buffer: 1 dangerous re-creation before the
change, 0 across 36 layer surfaces after it. Confirmed by hand over ~30 open
cycles and across a clean reboot, with zero shell restarts.
Upstream gtk4-layer-shell has no fix. Issue #94 is this exact bug (same error,
same "random after N opens", reproduces only on the gl/vulkan renderers) and
was closed because Hyprland changed; PR #119 reordered teardown and was
rejected as a smithay bug. A library-side fix is proposed separately in
wmww/gtk4-layer-shell#130, which would cover every close path in every client
without each one having to remember; this change is the client-side fix for
shells running against the library as it stands.
A shared close helper would be cleaner than 24 call sites; happy to respin that
way if preferred.
Replaces 24 open-coded hide()+unrealize() pairs, and the explanatory comment repeated at each of them, with a single close_layer_window() in src/core/layer_window.vala. The rationale now lives once, next to the code that implements it. No behaviour change: the helper performs the same hide() then unrealize() in the same order, including the cast that makes unrealize() bind to gtk_widget_unrealize rather than GtkNative's internal vfunc. Verified every one of the 24 sites resolves to a Gtk.Window: two of them are in hot_corner_manager.vala, whose first class is HotCornerManager : Object, but both sit inside HotCornerHintWindow : Gtk.Window further down the file. overview.vala likewise contains a WorkspaceCard : Gtk.Box, and no call site falls inside it. Net -132/+25 lines across six files.
Opening an app folder creates an AppFolderOverlay, which is its own toplevel layer-shell window on the OVERLAY layer. Hiding the launcher therefore does not take it down. Nothing closed it at close time. The only thing that dismisses folder overlays is launcher_grid.depopulate(), and that is deferred to an idle timer of IDLE_DEPOPULATE_MS -- 45 seconds -- whose purpose is reclaiming grid widgets, icon textures and preview buffers. So the overlay stayed on screen for 45s after the launcher closed, and if the launcher was reopened inside that window the timer was cancelled (overview.vala, reopen branch), leaving the overlay up indefinitely. Split the cheap part out: close_folder_overlays() dismisses the overlays and nothing else, and both close paths -- the animated close and the gesture dismiss -- call it immediately. depopulate() still does the expensive teardown on its idle timer and now delegates the overlay half to the same helper, so there is no behaviour change for the memory-reclaim path. The overlay feature itself is untouched; only the point at which it goes away changes.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f011b70517
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // transform is presented reliably without moving the surface. | ||
| set_body_class("dock-reveal-offset", true); | ||
| ((Gtk.Widget) this).hide(); | ||
| close_layer_window (this); |
There was a problem hiding this comment.
Keep frame-clock pulses paired across dock remaps
When an autohiding dock starts hiding and is revealed again within the 350 ms pulse window, this newly added unrealize creates a different surface/frame clock. pulse_frame_clock() begins updating the old clock at line 977, but its timeout fetches the current clock at lines 979-980; after this remap, the old timeout therefore ends the new clock early, and the new timeout later calls end_updating() on it again. This can truncate the reveal transition or trigger an unmatched frame-clock update warning during ordinary rapid pointer leave/re-enter, so the timeout should retain and end the same fc instance it began rather than looking it up again.
Useful? React with 👍 / 👎.
Opening an app folder creates an
AppFolderOverlay, which is its own toplevel layer-shell window on the OVERLAY layer. Hiding the launcher does not take it down, and nothing dismisses it at close time.The only thing that closes folder overlays is
launcher_grid.depopulate(), and both close paths defer that to an idle timer:That timer exists to reclaim grid widgets, icon textures and preview buffers; overlay teardown is bundled into it incidentally.
So: open a folder, close the launcher, and the overlay stays on screen for 45 seconds. Reopen the launcher inside that window and the reopen branch cancels the timer —
— so
depopulate()never runs and the overlay stays up until some later close plus a full idle.This is a sibling of #51. The comment on
force_close()notes that the animated close "depends on frame-clock ticks that stop once the launched window takes focus, leaving the overlay stuck on screen" — same defect via the launch path; this is the close-the-launcher path.The fix
close_folder_overlays()onAppLauncherGriddismisses the overlays and nothing else.Overview— the animated close andfinish_gesture_hide()— call it immediately.depopulate()keeps its idle timer and delegates the overlay half to the same helper, so the memory-reclaim behaviour is unchanged.The overlay feature is untouched; only the moment it goes away changes.
One question for you
If folder overlays are meant to outlive the launcher and persist across a close/reopen, this is working as designed and you should close it. I do not think so, since the dismissal lives in a memory-reclaim path rather than anywhere intentional — but that is your call.
Reproduced on
main, labwc 0.9.5 / wlroots 0.20.2, GTK 4.22.4.