16 KiB
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.