swayfx-mosaic/docs/design/STEP20C_NESTED_VALID_GRAB_PROBE.md

16 KiB
Raw Permalink Blame History

STEP20C nested valid-grab probe

Status: EXPERIMENTAL / FEASIBLE for the sampled nested-grab matrix. This document records a bounded headless feasibility experiment, not a production STEP20C implementation or UX decision. The architecture, UX, limitations and roadmap retain authority.

Hypothesis and protocol contract (before fixture implementation)

A real xdg_toplevel → xdg_popup A → xdg_popup B chain can keep both popups mapped under two valid explicit grabs. The disposable SceneFX/Mosaic clip should constrain both popup levels to the applied viewport, including when their tiled owner is fully hidden by camera translation. The unmodified SwayFX control establishes ordinary grab dismissal and pointer routing.

The installed xdg-shell.xml declares version 7; the prior fixture binds xdg_wm_base at version 3. Its xdg_popup.grab text requires a user action serial, a grabbing parent for nested grabbing popups, a mapped parent, grab before mapping, and reverse destruction order. It permits a compositor to dismiss the whole nested stack, topmost first. The initial buffer for each popup follows xdg_popup.configure, xdg_surface.configure, and xdg_surface.ack_configure. A denied grab immediately dismisses the popup. The client must log the originating wl_pointer.button target, serial and the matching xdg_popup.grab request for A and B separately. Nonzero serial alone is insufficient proof.

Fixture architecture and geometry

The toplevel has its own wl_surface, xdg_surface and xdg_toplevel. Popup A has its own wl_surface, xdg_surface and xdg_popup with the toplevel xdg_surface as parent. Popup B has distinct objects and uses A's xdg_surface as its actual XDG parent. Toplevel, A and B use distinct solid colors and explicit TOPLEVEL, POPUP_A, POPUP_B log labels. Surface object identity and configure traces, rather than color alone, establish hierarchy.

A left button press on the mapped toplevel opens A with exactly that event's serial. After A is configured, acknowledged and mapped, a new left press on A opens B with that second event's serial. Each popup uses an independent positioner anchored inside its parent window geometry. Positioner offsets will provide a fully visible placement and one or two viewport-crossing placements. The runner records actual configure geometry, owner IPC rectangle, Mosaic scroll_x, scene clip log, output positions and injection coordinates; it must not infer rendered position from logical IPC rect.x alone.

Normal lifecycle: B role → B xdg_surface → B wl_surface, then A role → A xdg_surface → A wl_surface, then toplevel role/surface. On compositor popup_done, log event order separately from client destruction order and destroy topmost first. Explicit B closure must leave A mapped and interactive. Owner destruction is tested separately from normal popup closure; compositor xdg_toplevel.close and process teardown are distinct observations.

The probe uses two private 800×600 headless outputs at (0,0) and (800,0). Configuration A is unmodified /usr/bin/sway in splith; B is private SceneFX/Mosaic in splith; C is the private guarded binary in mosaic. Every run has private XDG_RUNTIME_DIR, Wayland/IPC sockets, persistent virtual pointer, compositor log, WAYLAND_DEBUG=1 client log, pointer log, IPC JSON and screenshots of both outputs. Private SceneFX is loaded only by the private compositor. No command addresses the desktop session.

Scenario acceptance matrix

PASS requires matching protocol, pixels and pointer traces at named sample points. A popup closing before a hidden-owner sample makes that sample INCONCLUSIVE; a client merely surviving is never proof. FAIL records an observed violated expectation. PARTIAL means only part of the stated evidence exists. Unexecuted cells remain NOT TESTED.

Scenario A control B private splith C private mosaic Required evidence
1. Both popups visible and interactive, two valid grabs PASS PASS PASS Distinct button serials; ordered XDG configure/ack/map; A/B pixels and B right-button events
2. B crosses viewport PASS PASS PASS B pixels on each output in standard layouts; Mosaic B pixels left only, no B hit right; normal right-output target receives click
3. A and B cross viewport PASS PASS PASS Both colors right in standard layouts, neither right in Mosaic; distinct A and B button targets left
4. Explicit B close, A remains PASS PASS PASS B disappears; A receives right press/release; no B re-creation or ghost at sampled point
5. Outside click PASS PASS PASS popup_done B then A; client destroys B then A; normal right-output client gets left press/release
6. Tiled owner fully hidden, then camera return N/A N/A PASS Same owner and popup IDs, both mapped; no A/B pixels or hits while hidden; both return without recreation
6b. Hidden owner with separate click N/A N/A PASS No hidden popup button; grab dismisses B then A; no return after dismissal
7. Toplevel destruction with A/B open PASS PASS PASS xdg_toplevel.close, then client destroys B, A and toplevel; no popup_done required on this path
8. Mosaic → splith with A/B open N/A N/A PASS Same B responds at global (900,190) after layout change; no prior popup_done

The previously retained source fixtures and three patches must remain byte-identical. A new private correction, if needed, is a separately named delta with an explicit reason. Builds, sockets, generated protocol sources, captures and logs remain outside Git. Output resize/migration, vertical, negative-origin, scaled/rotated outputs, transaction generations, complete damage/effects and package ABI are outside this experiment.

Reproduction and retained artifacts

The lab is /tmp/swayfx-mosaic-nested-grab-If8ofeUu/ (about 54 MB after all runs). Its artifacts-sha256.txt lists 658 measured log, IPC and capture files; the manifest SHA-256 is 869f97f2ed8a7babc9db2bae49531a1c45e3aef84e6e799af87f2d8fcedb8a70. It was reconstructed from Mosaic cd550382b311efdbfe3ed892f4e1d281d852e754 and SceneFX 37ccd723bef49e6891156ffafce8f549f01446cc, applying the unchanged private SceneFX patch, Mosaic patch, then hidden-owner guard in that order. All three patch SHA-256 values match the previous fixture. prepare.sh rebuilt both private libraries/compositor and generated XDG and virtual-pointer code; nested-client.c and the reused pointer-daemon.c were compiled with -std=c17. /usr/bin/sway provided the unmodified control. The new fixture README gives clean-checkout commands. Each run-* directory holds client protocol, pointer, wev, compositor, IPC and two-output screenshot evidence. The runner uses short separate /tmp/mng-* socket directories recorded in runtime-path.

The client fixture SHA-256 at the measured build is 2e40c98af72a0df8b316810589a0be8f4425a763db1de448b49a7813956b6f7d. The runner was corrected during the probe: its first, long runtime socket path caused one private splith startup to exit before any client appeared, with Socket path won't fit into ipc_sockaddr->sun_path at sway/ipc-server.c:142 in the compositor log. ipc_user_sockaddr() checks the snprintf result against sun_path; the retained aborted directory has only config, compositor.log and an empty wayland-1.lock. A SIGSEGV process result was reported during the probe, but no exit-status record, core dump or stack trace is retained to corroborate the signal or locate its origin. Source inspection identifies a plausible error-path defect: sway_abort invokes the registered sway_terminate(EXIT_FAILURE) callback; with a Wayland display already present, sway_terminate calls ipc_event_shutdown, whose listener check dereferences ipc_client_list. The length check runs before ipc_init creates that list. This ordering could explain a null dereference, but no retained backtrace proves that this was the observed process exit. The overlong path is a confirmed fixture setup error; the reported SIGSEGV and its precise origin remain a separate unverified startup incident. A future isolated diagnostic run could preserve the exit status and a backtrace; this probe did not do so. Short /tmp/mng-* paths avoided the length failure in subsequent control, private standard and Mosaic runs. This is a fixture workaround, not a proven compositor bug fix. The first close_b runs used a left click that legitimately reopened B; they are retained with -initial suffix. The revised right click measures A's continued interaction without reopening B. No private source patch was changed to obtain these results.

Independent post-audit replay

A fresh disposable laboratory was rebuilt from the pinned commits and the same three private patches. The reconstructed SceneFX and Mosaic builds, generated protocol code and both C17 clients succeeded; the Mosaic Meson model test passed. Twelve representative isolated runs plus one final open smoke run returned successfully. Direct log, pointer and screenshot checks confirmed both genuine grabs in all three visible_click configurations. With b_cross, unmodified SwayFX and private splith each showed 15,860 B-green pixels on HEADLESS-2; private Mosaic showed 0 there and 17,940 on HEADLESS-1. Mosaic's (900,190) click went to wev, not B, while standard controls delivered it to B. With both_cross, Mosaic showed 0 A-yellow and B-green pixels on HEADLESS-2 and delivered separate left-output right clicks to A and B. In hidden, both colors fell to 0 and returned to their prior counts with one A/B map each; hidden_click dismissed B then A without a hidden popup button. close_b left A receiving right press/release, outside logged B then A popup_done and child-first destruction, and toggle restored B-green pixels and B's button delivery on HEADLESS-2 in splith. These are fresh confirmations of named samples, not a broader qualification. The original 658-artifact manifest was also rechecked with 658 OK, 0 failures. Its historical measurements remain described here even if the temporary laboratory is later removed. The post-audit runner changes affect socket isolation and cleanup, not popup actions or the private compositor patches.

Protocol and hierarchy evidence

In run-control-open-visible/client.log, A used button serial 61 from TOPLEVEL and B used 66 from POPUP_A. Both respective GRAB_REQUEST entries show used=61 and used=66. POPUP_A CREATE records parent_xdg=9 (TOPLEVEL); POPUP_B CREATE records parent_xdg=12, exactly A's XDG object. The two role configure events were (50,80,250,170) and (20,60,170,130); their xdg_surface.configure serials 62 and 67 were acknowledged before each MAP. Private standard repeated 61→66 in the first visible run; private Mosaic used distinct real serials 55 and 60. WAYLAND_DEBUG=1 logs corroborate client request order. No protocol error, immediate grab denial or pre-configure dismissal was observed in the successful runs. A later right click on B was delivered in all three configurations (control/private standard button serial 71; Mosaic 65, with releases).

These are real nested XDG grabs, not merely two transient surfaces: B's parent is A's xdg_surface, A was mapped before B was created, both were mapped at the open capture, and each received pointer interaction. Source colors are blue TOPLEVEL (35,68,204), yellow A (224,192,32) and green B (32,208,160) in the captured RGB images. Surface IDs and event targets are the primary identity evidence.

Visual and pointer observations

All screenshots are 800×600 for HEADLESS-1 and HEADLESS-2. For b_cross, the compositor configured A at (200,80,300,170) relative to TOPLEVEL and B at (220,60,300,130) relative to A. At open, both standard configurations had 15,860 exact B-green pixels on HEADLESS-2; Mosaic had 0 while retaining 17,940 B-green pixels on HEADLESS-1. At global (700,190), B received right press/release in all three cases. In the control, moving to global (900,190) yielded B-local (278,25) and a further right press/release. The private standard case matched it. In Mosaic, B received a leave, no right-output B enter/button, and the ordinary wev on HEADLESS-2 received the right press/release. The Mosaic grab then dismissed B and A. Thus a legitimate grab's dismissal is separated from a hit on an invisible B.

For both_cross, A was configured (350,80,350,170) and B (160,60,300,130). At open, both standard configurations had 6,120 A-yellow and 27,560 B-green pixels on HEADLESS-2. Mosaic had 0 of both colors there, while retaining 30,080 A-yellow and 6,240 B-green pixels on HEADLESS-1. A right click at global (700,190) targeted A and one at (780,190) targeted B in Mosaic, confirmed by separate surface logs; the control and private standard also targeted A then B. At (900,190), only the standard layouts delivered the button to B; Mosaic's ordinary right-output client received it and B/A were dismissed. These points test two hierarchy levels rather than treating the whole menu as one colored area.

On explicit close_b, the POPUP_B DESTROY entry preceded a right click on A in all three configurations. A received press and release and B did not reappear. The sampled pixel at B's former overlap changed from green to A's yellow. On the outside click (900,400), every configuration logged POPUP_B POPUP_DONE, then POPUP_A POPUP_DONE, then client destruction B, then A. wev got the left press/release. On compositor IPC kill of the toplevel, the client logged TOPLEVEL CLOSE_REQUEST, destroyed B and A, then destroyed TOPLEVEL; no popup_done preceded this normal owner teardown.

Hidden owner and standard-layout return

The Mosaic owner remained IPC container ID 8, logical rectangle (640,25,640,575), with scroll_x 400→1280→400. Private compositor log PROBE_POPUP_HIDDEN_OWNER recorded applied owner (-640,0,640,600) against viewport (0,0,800,600), an empty intersection. During hidden and hidden-hover, both A-yellow and B-green exact pixel counts on HEADLESS-1 were 0, down from 42,200 and 17,940 at open. No popup enter, button, popup_done, reconfigure, object re-creation or client exit occurred before the hidden measurement. After camera return, both counts returned to 42,200 and 17,940 with the same XDG object IDs. In the separate hidden_click case, right click at (100,190) produced no A/B button but did produce popup_done B then A; the returned frame correctly did not restore them after dismissal. This is sampled guard and grab behavior, not a universal offscreen-popup policy.

The toggle run left Mosaic with B still mapped. HEADLESS-2 local (100,190) changed from ordinary (102,102,102) to B-green (32,208,160) in splith, and B then received right press/release at global (900,190) without a prior popup_done. The private guard did not persist into the sampled standard-layout path.

Classification and remaining gates

The specified horizontal, two-output nested-grab experiment is FEASIBLE at its measured placements and event points. This does not establish continuous clipping between sample points or a general popup dismissal policy. The startup incident above does not affect the later runs, which started their clients, mapped both popups and produced protocol, pointer and pixel evidence. No architectural guarantee is established for universal grab behavior, complete damage/stale-pixel coverage, decorations, shadows/blur/saved buffers, vertical/negative/scaled/rotated outputs, output resize/migration, transaction-generation agreement, SceneFX ABI or package maintenance. Each of those remains NOT TESTED in this probe. The private SceneFX symbols and owner guard remain disposable; no source or patch enters production. STEP20C remains NEXT and cross-output Mosaic remains NOT YET SUPPORTED. The smallest next bounded experiment should exercise a changed output geometry with these same nested grabbed surfaces and compare the control, private standard and private Mosaic paths before any production integration decision.