diff options
| author | Adam Malczewski <[email protected]> | 2026-06-15 12:56:19 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-15 12:56:19 +0900 |
| commit | e051b9b22f8e237c7f52c119c57d71f02be24b3d (patch) | |
| tree | 7ba412f7dcc6d8743abf4ad88099b856ac5ba049 | |
| parent | c38f328c8b38546a19fc4cd955e01e70b42d624d (diff) | |
| download | unbox-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.hpp | 13 | ||||
| -rw-r--r-- | packages/ext-xdg-shell/src/extension.cpp | 6 | ||||
| -rw-r--r-- | packages/ext-xdg-shell/tests/test_minimize.cpp | 67 |
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); } |
