253 lines
16 KiB
Markdown
253 lines
16 KiB
Markdown
# 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](../ARCHITECTURE.md), [UX](../UX.md), [limitations](../KNOWN_LIMITATIONS.md)
|
||
and [roadmap](../ROADMAP.md) 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](../../tooling/probes/step20c-valid-grab/README.md).
|
||
`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](../../tooling/probes/step20c-nested-grab/README.md)
|
||
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.
|