summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-15 12:56:19 +0900
committerAdam Malczewski <[email protected]>2026-06-15 12:56:19 +0900
commite051b9b22f8e237c7f52c119c57d71f02be24b3d (patch)
tree7ba412f7dcc6d8743abf4ad88099b856ac5ba049
parentc38f328c8b38546a19fc4cd955e01e70b42d624d (diff)
downloadunbox-e051b9b22f8e237c7f52c119c57d71f02be24b3d.tar.gz
unbox-e051b9b22f8e237c7f52c119c57d71f02be24b3d.zip
ext-xdg-shell(rml-compositing W2): add Toplevel::wl_surface() (additive)
Wave 2, additive — existing wlr_scene compositing of toplevels held intact (the switch to surface elements is a later wave behind a flag). - New contract: Toplevel::wl_surface() -> wlr_surface* (the root surface, == xdg_toplevel->base->surface, the same surface scene_tree() hosts). A borrow with the same lifetime as the Toplevel (valid until on_toplevel_unmapped). This is the surface ext-window-field (Wave 3) passes to the kernel's UiSubstrate::create_surface_element(); the kernel then manages the subsurface/popup tree itself, so no popup/subsurface handles are exposed. ABI-additive: ext-keybindings/ext-stage-dock/host-bin rebuild + link unchanged. - Fixed the ext-xdg-shell-client teardown SIGABRT. Diagnosed (not assumed): the abort is NOT pre-existing on baseline (seed 12e5016 passes) — it is the kernel Wave-1 surface-capture TEST SEAM attaching a commit Listener to every client wl_surface and only detaching at Server teardown, so a client destroying a surface mid-session trips wlroots' wl_list_empty(commit.listener_list) assertion. Fixed in-unit via teardown order (unmap via null-buffer commit; let the Server destructor reap the surface after detaching its seam). Filed a kernel change-request for the real fix (RAII per-surface destroy listener), which will also cure ext-stage-dock-glue. ext-xdg-shell suite 2/2 green in build + build-asan, no SIGABRT, no unbox:: leak/UB. wl_surface() covered (non-null, == root surface, round-trips through scene_tree_for()).
-rw-r--r--packages/ext-xdg-shell/include/unbox/ext-xdg-shell/ext_xdg_shell.hpp13
-rw-r--r--packages/ext-xdg-shell/src/extension.cpp6
-rw-r--r--packages/ext-xdg-shell/tests/test_minimize.cpp67
3 files changed, 73 insertions, 13 deletions
diff --git a/packages/ext-xdg-shell/include/unbox/ext-xdg-shell/ext_xdg_shell.hpp b/packages/ext-xdg-shell/include/unbox/ext-xdg-shell/ext_xdg_shell.hpp
index 2c499d8..6063525 100644
--- a/packages/ext-xdg-shell/include/unbox/ext-xdg-shell/ext_xdg_shell.hpp
+++ b/packages/ext-xdg-shell/include/unbox/ext-xdg-shell/ext_xdg_shell.hpp
@@ -85,6 +85,19 @@ public:
// hide()/show() enable/disable.
[[nodiscard]] virtual auto scene_tree() -> wlr_scene_tree* = 0;
+ // The toplevel's ROOT wl_surface — the surface whose tree scene_tree()
+ // hosts (i.e. xdg_toplevel->base->surface). This is the handle a
+ // compositor / window-field passes to UiSubstrate::create_surface_element()
+ // to composite the window as a live RCSS surface element (RML compositing,
+ // Phase 2): the kernel then manages the toplevel's whole subsurface/popup
+ // tree itself, so this extension does NOT expose popup/subsurface handles.
+ // A non-owning BORROW with the SAME lifetime as the rest of Toplevel: valid
+ // only while the Toplevel borrow is live (i.e. until the matching
+ // on_toplevel_unmapped fires for it). Never store it; never destroy it.
+ // Returns nullptr only in the degenerate case of an already-destroyed
+ // underlying xdg_toplevel (never for a live, mapped toplevel).
+ [[nodiscard]] virtual auto wl_surface() -> wlr_surface* = 0;
+
// Compositor-side HIDE / SHOW: disable / enable the toplevel's scene node
// so it is not composited and the client stops receiving frame callbacks
// (wlr_scene withholds them from a non-visible node), WITHOUT unmapping it
diff --git a/packages/ext-xdg-shell/src/extension.cpp b/packages/ext-xdg-shell/src/extension.cpp
index 68c517f..7d0f5a2 100644
--- a/packages/ext-xdg-shell/src/extension.cpp
+++ b/packages/ext-xdg-shell/src/extension.cpp
@@ -87,6 +87,12 @@ struct ToplevelEntry final : Toplevel {
return box;
}
[[nodiscard]] auto scene_tree() -> wlr_scene_tree* override { return scene; }
+ [[nodiscard]] auto wl_surface() -> wlr_surface* override {
+ // The toplevel's ROOT surface: the SAME surface whose tree `scene`
+ // hosts (it is what host_surface() registered and scene_tree_for()
+ // resolves back to this tree). A borrow with the Toplevel's lifetime.
+ return xdg_toplevel != nullptr ? xdg_toplevel->base->surface : nullptr;
+ }
void hide() override {
if (scene != nullptr) {
wlr_scene_node_set_enabled(&scene->node, false);
diff --git a/packages/ext-xdg-shell/tests/test_minimize.cpp b/packages/ext-xdg-shell/tests/test_minimize.cpp
index 2e60799..a333f52 100644
--- a/packages/ext-xdg-shell/tests/test_minimize.cpp
+++ b/packages/ext-xdg-shell/tests/test_minimize.cpp
@@ -244,10 +244,10 @@ TEST_CASE("slice 10/b1: hide/show/geometry/scene_tree on a real mapped toplevel"
// scene_tree(): non-null, a borrow, and equal to what the kernel registry
// resolves the toplevel's surface to (the typed surface->tree contract the
- // dock relies on). We recover the toplevel's wl_surface by walking the
- // buffer nodes under the returned tree (the public Toplevel contract does
- // not expose the surface; the kernel registry keys on it), then confirm the
- // round-trip scene_tree_for(surface) == scene_tree().
+ // dock relies on). We independently recover the toplevel's root surface by
+ // walking the buffer nodes under the returned tree (the kernel registry keys
+ // on it), then confirm the round-trip scene_tree_for(surface) ==
+ // scene_tree().
wlr_scene_tree* tree = tl->scene_tree();
REQUIRE(tree != nullptr);
struct SurfaceCatch {
@@ -267,6 +267,18 @@ TEST_CASE("slice 10/b1: hide/show/geometry/scene_tree on a real mapped toplevel"
REQUIRE(caught.surface != nullptr);
CHECK(observer->host()->scene_tree_for(caught.surface) == tree);
+ // wl_surface() (RML compositing Wave 2): the contract now exposes the
+ // toplevel's ROOT wl_surface — the handle ext-window-field passes to
+ // UiSubstrate::create_surface_element(). For a mapped toplevel it is
+ // non-null and IS the root surface: it must equal both the surface the scene
+ // tree hosts (walked above) and the surface the kernel registry resolves
+ // this very tree from. (Additive — scene_tree()/hide()/show()/geometry()
+ // are unchanged; the existing wlr_scene compositing path is untouched.)
+ wlr_surface* root = tl->wl_surface();
+ REQUIRE(root != nullptr);
+ CHECK(root == caught.surface);
+ CHECK(observer->host()->scene_tree_for(root) == tree);
+
// geometry(): a non-empty box for a mapped toplevel.
wlr_box box = tl->geometry();
CHECK(box.width > 0);
@@ -292,20 +304,51 @@ TEST_CASE("slice 10/b1: hide/show/geometry/scene_tree on a real mapped toplevel"
tl->show();
CHECK(tree->node.enabled == true);
- // A hidden toplevel must still unmap normally when the client closes it:
- // hide it again, then destroy client-side; on_toplevel_unmapped MUST fire.
+ // A hidden toplevel must still unmap normally when the client withdraws it:
+ // hide it again, then UNMAP from the client side by committing a NULL buffer
+ // (the xdg_toplevel withdraw — the surface unmaps without the role object or
+ // wl_surface resource being destroyed); on_toplevel_unmapped MUST fire.
+ //
+ // Teardown-order note (listener-lifetime): we deliberately unmap via a
+ // null-buffer commit rather than tearing the wl_surface resource down
+ // mid-session. Destroying the wl_surface while the server is still running
+ // would run wlroots' surface_handle_resource_destroy, which asserts
+ // wl_list_empty(&surface->events.commit.listener_list). Any commit listener
+ // still bound to that surface at that instant aborts the whole test process.
+ // Our own ToplevelEntry::commit listener IS released in time (it dies with
+ // the entry on the xdg_toplevel destroy), but the kernel's headless
+ // surface-capture test seam keeps a commit listener on every client surface
+ // for the Server's lifetime and only detaches it during Server teardown
+ // (before it destroys the clients) — so a client-driven surface-resource
+ // destroy mid-test trips the assertion. We therefore let the surface (and
+ // the rest of the client's objects) be reaped by the Server destructor's
+ // ordered teardown (seam detached first, clients destroyed after), which is
+ // the same ordering a real session uses. See reports/ext-xdg-shell.md and
+ // the kernel change-request for the seam's missing per-surface unsubscribe.
tl->hide();
+ wl_surface_attach(c.surface, nullptr, 0, 0);
+ wl_surface_commit(c.surface);
+ for (int i = 0; i < 200 && observer->unmapped_count() == 0; ++i) {
+ pump(*server, c.display);
+ }
+ CHECK(observer->unmapped_count() == 1);
+
+ // Client shutdown. We destroy every client proxy (so libwayland-client frees
+ // them — keeps the asan/lsan suite clean, mirroring the kernel's client
+ // test) and then disconnect, but we do NOT pump the server afterwards. The
+ // destroy requests sit unflushed-to-dispatch on the server until the Server
+ // destructor reaps the whole connection via wl_display_destroy_clients —
+ // which the kernel runs AFTER detaching its surface-capture commit seam, so
+ // the wl_surface resource is destroyed with no commit listener still bound
+ // (no surface_handle_resource_destroy assertion). Pumping here instead would
+ // dispatch the surface-destroy while the server (and seam) is alive and trip
+ // that wlroots assertion — the teardown-order bug this test now avoids.
xdg_toplevel_destroy(c.toplevel);
xdg_surface_destroy(c.xsurface);
wl_surface_destroy(c.surface);
c.toplevel = nullptr;
c.xsurface = nullptr;
c.surface = nullptr;
- for (int i = 0; i < 200 && observer->unmapped_count() == 0; ++i) {
- pump(*server, c.display);
- }
- CHECK(observer->unmapped_count() == 1);
-
wl_buffer_destroy(buffer);
if (c.wm_base != nullptr) {
xdg_wm_base_destroy(c.wm_base);
@@ -319,7 +362,5 @@ TEST_CASE("slice 10/b1: hide/show/geometry/scene_tree on a real mapped toplevel"
if (c.registry != nullptr) {
wl_registry_destroy(c.registry);
}
- wl_display_flush(c.display);
- pump(*server, c.display);
wl_display_disconnect(c.display);
}