docs(mosaic): preserve presentation identity probe evidence
This commit is contained in:
parent
d19b15bfde
commit
006adf69d7
11 changed files with 1149 additions and 4 deletions
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
### STEP20C experimental evidence
|
||||
|
||||
- Added a source-only private presentation-identity probe at fixed negative-origin headless geometry. Native wlroots output present events paired with 470 accepted commits by output and `commit_seq`; applied scene/clip and sampled input agreed at the target states, but exact commit-buffer pixels remain unproven. STEP20C stays NEXT and production multi-output Mosaic unsupported.
|
||||
- Added a source-only temporal damage fixture at the fixed negative-origin geometry. Ten XDG open/close cycles in each of three modes and sampled camera transitions restored popup regions without a detected ghost pixel; private damage/commit traces remain insufficient for a presented-frame guarantee. STEP20C remains NEXT and multi-output Mosaic unsupported.
|
||||
- Added a source-only negative-origin nested-grab probe. Two outputs at `x=-800/0` verified signed pointer conversion, real standard popup crossing, sampled private Mosaic clipping, hidden-owner return, standard-layout return and a positive horizontal regression. Production STEP20C support remains open.
|
||||
- Added a source-only stacked-output nested valid-grab probe. Both standard controls showed real popup overflow and pointer delivery below `y=600`; the private guarded Mosaic build clipped A/B at sampled lower points, preserved hidden-owner and standard-layout return, and reproduced the horizontal control. STEP20C production support remains open.
|
||||
|
|
|
|||
|
|
@ -109,6 +109,22 @@ native transaction-generation contract binding applied scene, clip and hit
|
|||
testing. The temporal evidence is **PARTIALLY_FEASIBLE**, not a production
|
||||
rendering or ABI guarantee.
|
||||
|
||||
The [presentation identity probe](design/STEP20C_PRESENTATION_IDENTITY_PROBE.md)
|
||||
then observed native wlroots 0.20.2 headless output commit and deferred
|
||||
present callbacks in a separate private compositor. Each observed present
|
||||
paired with its accepted native `(output, commit_seq)`, and a scene snapshot
|
||||
inside each commit callback recorded the applied tiled position and clip.
|
||||
Ten settled target transitions showed matching applied scene/clip values at
|
||||
400 and 1280, while some faster command intervals had no commit. During
|
||||
initial layout setup, a presented commit exposed logical camera 1760 with
|
||||
applied scene and clip still at 1120; this reinforces the distinction between
|
||||
requested and applied state. The applied scene and clip themselves agreed.
|
||||
Headless present is an idle-delivered simulated event, and neither it nor
|
||||
sampled `grim` images identify the pixels of that exact output-state buffer.
|
||||
Pointer events remain a separate time-ordered stream. The result is
|
||||
**PARTIALLY_FEASIBLE** and does not define a production transaction or
|
||||
SceneFX ABI contract.
|
||||
|
||||
## IPC
|
||||
|
||||
On a Mosaic workspace, `get_tree` reports `layout: "mosaic"` and adds a `mosaic` object with `active_row` and `rows`. Each row entry contains ordered child node IDs, `scroll_x`, and focused node ID or null. Existing Sway fields retain their shapes; normal workspace/window events and `get_workspaces` paths remain in use. This is an extension to the Sway layout vocabulary: strict IPC consumers with a closed layout enum must recognize `mosaic`. Additional rows in the model are not a promise of usable multi-row IPC behavior.
|
||||
|
|
|
|||
|
|
@ -70,6 +70,20 @@ cannot prove per-frame rendering, input/scene synchronization or damage
|
|||
minimality. Popup effects, saved buffers and the SceneFX ABI/package gate
|
||||
remain unqualified. Production multi-output Mosaic is **NOT YET SUPPORTED**.
|
||||
|
||||
The [presentation identity probe](design/STEP20C_PRESENTATION_IDENTITY_PROBE.md)
|
||||
is **PARTIALLY_FEASIBLE** in the same fixed headless geometry. A private
|
||||
listener paired 470 headless present callbacks with accepted native output
|
||||
commits; 12 other accepted commits had no observed present and are not
|
||||
classified as presented. Applied scene and clip snapshots at the commit
|
||||
callback covered the measured target transitions, but no exact buffer
|
||||
readback tied those values to the pixels of a particular commit. `grim`
|
||||
captures, damage extents and pointer delivery cannot fill that missing
|
||||
identity. Headless events are simulated, not evidence for physical DRM
|
||||
presentation. A transient logical/applied camera lag was observed during
|
||||
initial layout setup; no wrong applied clip or invisible hit was reproduced
|
||||
in the targeted matrix. Production multi-output Mosaic remains
|
||||
**NOT YET SUPPORTED**.
|
||||
|
||||
The [isolated feasibility probe](design/STEP20C_FEASIBILITY_PROBE.md) is **PARTIALLY FEASIBLE**, not a release qualification. On two horizontal headless outputs, real Mosaic content leaked visually into an empty adjacent output; that earlier disposable candidate also delivered pointer enter from a partly visible client outside its viewport. Negative-origin, scaled and rotated arrangements showed visual leakage in that candidate. Popups, effects, saved buffers and complete damage correctness remain unqualified across the full matrix. The later probes above narrow individual cases without qualifying multi-output Mosaic, which remains **NOT YET SUPPORTED**.
|
||||
|
||||
The later [SceneFX subtree clipping spike](design/STEP20C_SCENEFX_CLIPPING_SPIKE.md) is also **PARTIALLY FEASIBLE**. Its private inherited tree clip stopped tested tiled pixels and pointer hits from crossing the output boundary, including a fully hidden cell, but a real GTK XDG popup rendered and received input on the neighboring output because its scene tree lives in the global popup layer. Dedicated subsurface, saved-buffer, nested-popup and complete effect/damage validation remain open. The spike changed no production library or compositor source; multi-output Mosaic remains **NOT YET SUPPORTED**.
|
||||
|
|
|
|||
|
|
@ -27,7 +27,9 @@ The [root README](../README.md) is the public summary. For a specific topic, use
|
|||
[Negative-origin source fixture](../tooling/probes/step20c-negative-origin/README.md) replays it in a disposable laboratory.
|
||||
16. [STEP20C temporal damage probe](design/STEP20C_TEMPORAL_DAMAGE_PROBE.md): repeated XDG cycles, camera transitions, sampled pixel restoration and timed private damage/commit traces at the same negative-origin geometry; frame presentation remains unproven.
|
||||
[Temporal source fixture](../tooling/probes/step20c-temporal-damage/README.md) rebuilds the private campaign.
|
||||
17. [STEP20A](STEP20A.md) and [STEP20B](STEP20B.md): historical evidence.
|
||||
17. [STEP20C presentation identity probe](design/STEP20C_PRESENTATION_IDENTITY_PROBE.md): native headless commit/present correlation and sampled applied camera/clip/input evidence; exact committed buffer content remains unproven.
|
||||
[Presentation source fixture](../tooling/probes/step20c-presentation-identity/README.md) rebuilds the private instrumentation.
|
||||
18. [STEP20A](STEP20A.md) and [STEP20B](STEP20B.md): historical evidence.
|
||||
|
||||
## Project
|
||||
|
||||
|
|
@ -67,6 +69,7 @@ The [root README](../README.md) is the public summary. For a specific topic, use
|
|||
| [STEP20C vertical-output nested-grab probe](design/STEP20C_VERTICAL_OUTPUT_NESTED_GRAB_PROBE.md) | Experimental evidence | FEASIBLE for sampled two-level grabbed XDG popup cases across the horizontal output boundary; no production qualification |
|
||||
| [STEP20C negative-origin nested-grab probe](design/STEP20C_NEGATIVE_ORIGIN_NESTED_GRAB_PROBE.md) | Experimental evidence | FEASIBLE for sampled two-level grabbed XDG popup cases with HEADLESS-1 at `x=-800`; no production qualification |
|
||||
| [STEP20C temporal damage probe](design/STEP20C_TEMPORAL_DAMAGE_PROBE.md) | Experimental evidence | PARTIALLY_FEASIBLE for sampled repeated lifecycle/camera states; damage-to-presented-frame and transaction/input synchronization remain open |
|
||||
| [STEP20C presentation identity probe](design/STEP20C_PRESENTATION_IDENTITY_PROBE.md) | Experimental evidence | PARTIALLY_FEASIBLE; native headless events pair with commits, but buffer pixels and per-frame input identity remain unproven |
|
||||
|
||||
A proposal does not supersede accepted canonical contracts or assert implementation status. Its open feasibility items must be resolved before implementation.
|
||||
|
||||
|
|
|
|||
|
|
@ -63,11 +63,24 @@ sampled private Mosaic pixels and standard controls, and time-ordered private
|
|||
damage extents covered changed pixels between sampled captures. It did not
|
||||
identify every presented frame or bind camera/clip/input to a native applied
|
||||
transaction generation. Its [source fixture](../tooling/probes/step20c-temporal-damage/README.md)
|
||||
remains experimental. The next bounded gate is a headless presentation-event
|
||||
trace for one hide/return transition, with applied camera/clip and pointer
|
||||
target. Scaling, rotation, resize/migration, complete effects and dependency
|
||||
remains experimental. Its proposed headless presentation-event gate was
|
||||
examined by the subsequent probe below. Scaling, rotation,
|
||||
resize/migration, complete effects and dependency
|
||||
maintenance remain open. STEP20C stays **NEXT**.
|
||||
|
||||
The [presentation identity probe](design/STEP20C_PRESENTATION_IDENTITY_PROBE.md)
|
||||
is **PARTIALLY_FEASIBLE**: native wlroots 0.20.2 headless present events
|
||||
were paired to commits by `(output, commit_seq)` and applied scene/clip
|
||||
snapshots were taken at the commit callback. Settled target states passed
|
||||
sampled clipping and input checks; faster commands could coalesce or defer.
|
||||
The actual buffer content of each commit and frame-specific pointer relation
|
||||
remain unproven. Its [private source fixture](../tooling/probes/step20c-presentation-identity/README.md)
|
||||
does not enter the production build. The smallest remaining identity gate is
|
||||
a bounded fingerprint/readback of the same retained output-state buffer at
|
||||
commit and headless present, with safe buffer lifetime. Scaling, rotation,
|
||||
migration/resize, complete effects/damage and SceneFX maintenance remain
|
||||
open. STEP20C stays **NEXT**.
|
||||
|
||||
## STEP20D — multi-row Spatial Canvas · PLANNED
|
||||
|
||||
**Goal:** connect rows to runtime tree, transactions, scene, input, and IPC. **Planned deliverables:** active-row navigation, row-local scroll/focus, deterministic insertion/reflow, in-row reorder, and between-row movement. **Exit criteria:** real tiled containers preserve identity and order across row changes; focus and camera restore correctly; headless multi-row tests pass. **Docs first:** settle exact interactions and row invariants in UX and Architecture. Unset key combinations remain TBD.
|
||||
|
|
|
|||
221
docs/design/STEP20C_PRESENTATION_IDENTITY_PROBE.md
Normal file
221
docs/design/STEP20C_PRESENTATION_IDENTITY_PROBE.md
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
# STEP20C presentation identity probe
|
||||
|
||||
**Status: EXPERIMENTAL / PARTIALLY_FEASIBLE.** STEP20C
|
||||
remains **NEXT**; production multi-output Mosaic remains **NOT YET SUPPORTED**.
|
||||
No production compositor, public ABI, package, patch or UX contract changes.
|
||||
|
||||
## Question and fixed scope
|
||||
|
||||
Can a private headless trace correlate an accepted output commit with its
|
||||
backend `present` event, the applied Mosaic camera/clip state observed while
|
||||
building that output state, and later pointer delivery? Repeat only
|
||||
`scroll_x 400 → 1280 → 400` with the same mapped, genuinely grabbed nested
|
||||
XDG popups A/B. HEADLESS-1 is `(-800,0,800,600)` and HEADLESS-2 is
|
||||
`(0,0,800,600)`, scale 1, normal transform. Controls are unmodified SwayFX
|
||||
`splith`, private SwayFX `splith`, and private Mosaic `mosaic`. Standard
|
||||
controls have no synthetic camera action.
|
||||
|
||||
## Audited signal path
|
||||
|
||||
The installed `wlroots-0.20` package reports 0.20.2. Its installed
|
||||
`wlr_output.h` exposes a per-output `uint32_t commit_seq`, `events.commit`,
|
||||
`events.present`, and a present event with `uint32_t commit_seq`, `bool
|
||||
presented`, `timespec when`, `unsigned seq`, `int refresh` in nanoseconds and
|
||||
`uint32_t flags`. The sequence may wrap. The pinned upstream wlroots 0.20.2
|
||||
source is tag commit `d783533489e1f75d6886c2ab5c5960090ef268f8`.
|
||||
`wlr_output_commit_state` increments the native sequence only after a
|
||||
successful backend commit, then emits `events.commit`. Its commit event has
|
||||
a monotonic `when` and a borrowed state pointer valid during the callback.
|
||||
|
||||
The 0.20.2 headless backend creates a `presented=true` event with
|
||||
`commit_seq = wlr_output->commit_seq + 1` for a commit leaving the output
|
||||
enabled, then defers it to an event-loop idle callback. The core fills a
|
||||
zero `when` with `CLOCK_MONOTONIC` at delivery. Headless initializes neither
|
||||
hardware refresh sequence nor hardware presentation flags, and it has no
|
||||
physical display. Allocation failure can suppress a deferred event. An
|
||||
accepted commit, an idle `present`, a rendered buffer, a `grim` screenshot
|
||||
and photons are distinct observations. The header alone cannot establish
|
||||
buffer content or physical timing. Source references:
|
||||
[`headless/output.c`](https://gitlab.freedesktop.org/wlroots/wlroots/-/blob/0.20.2/backend/headless/output.c),
|
||||
[`types/output/output.c`](https://gitlab.freedesktop.org/wlroots/wlroots/-/blob/0.20.2/types/output/output.c).
|
||||
|
||||
SwayFX `sway/desktop/output.c` calls `output_configure_scene`, tests
|
||||
`wlr_scene_output_needs_frame`, builds the output state, and calls
|
||||
`wlr_output_commit_state` in `output_repaint_timer_handler`. Its existing
|
||||
`present` listener updates refresh prediction but ignores unpresented
|
||||
events. The private SceneFX subtree clip and owner visibility guard govern
|
||||
render and hit testing; prior `TEMPORAL_DAMAGE` lines are output-local
|
||||
bounding extents, not pixman region decompositions. Sway's logical
|
||||
`scroll_x`, current container geometry and applied scene nodes are separate.
|
||||
|
||||
## Instrumentation contract
|
||||
|
||||
Apply a distinct, hash-checked patch only in a pinned private compositor
|
||||
copy. Log native output identity and `commit_seq` from `events.commit` and
|
||||
`events.present`, the monotonic callback time, present `when`, `presented`,
|
||||
`seq`, `refresh`, `flags`, and the result of the repaint commit call. A
|
||||
per-output listener must be removed before its output is destroyed. The
|
||||
trace must not change commit, clip, damage, hit-test or transaction decisions.
|
||||
Logging itself may affect timing and requires a control comparison.
|
||||
|
||||
At the last safely observable point before output-state build, record the
|
||||
active workspace, Mosaic logical camera, owner identity and current geometry,
|
||||
scene node position, and any accessible applied clip/owner visibility. Label
|
||||
unavailable fields `UNOBSERVED`; never replace them with pending values.
|
||||
Record a native sequence only when the callback or accepted commit provides
|
||||
it. A scene snapshot taken before build is an ordered observation, not proof
|
||||
of the pixels within the committed buffer. Event data and object pointers
|
||||
must not be retained beyond callback/object lifetime.
|
||||
|
||||
Keep `TEMPORAL_DAMAGE`, `SPIKE_DAMAGE`, client XDG logs, real virtual-pointer
|
||||
requests, IPC snapshots, and `grim` PNGs. The runner records capture start
|
||||
and finish monotonic times, output, SHA-256, ROI and A/B color counts. Only a
|
||||
native `(output, commit_seq)` match may pair commit and present; timestamp
|
||||
order alone cannot do so. An accepted commit lacking present stays
|
||||
unpresented in the observed trace. Multiple commands can coalesce before a
|
||||
commit. Pointer enter/button and `popup_done` belong to their own timed
|
||||
event stream and are never assigned a fictional frame number.
|
||||
|
||||
## Bounded matrix
|
||||
|
||||
1. Validate listener registration, native sequences, timestamps, output
|
||||
separation and removal in one private headless run before repetition.
|
||||
2. Open A and B with fresh real button serials; capture settled scene, IPC,
|
||||
clips, XDG mapping, last commit/present and both output PNGs.
|
||||
3. From 400, scroll +880; observe full hiding without a click. Scroll -880;
|
||||
check same owner and popup identities, applied clips and restored pixels.
|
||||
4. Repeat the settled pair five times, with per-pair event and capture IDs.
|
||||
5. Send twelve controlled faster alternating requests; preserve command
|
||||
times, coalesced or missing presentations and unobserved intervals.
|
||||
6. In separate runs test B visible, a clipped portion, fully hidden owner
|
||||
and return without prior popup closure. An outside click may dismiss the
|
||||
grab; child-first `popup_done` is not an invisible-popup hit.
|
||||
7. Compare the private standard layout with unmodified SwayFX, then compare
|
||||
private builds with and without new presentation listeners at settled
|
||||
positions. Preserve all failures, timeouts and protocol errors.
|
||||
|
||||
## Verdict and permanent limits
|
||||
|
||||
`FEASIBLE` requires real headless present events paired by native sequence
|
||||
and demonstrated association of necessary applied state without a repeatable
|
||||
regression. `PARTIALLY_FEASIBLE` covers partial event/state correlation with
|
||||
unproven buffer identity, input relation or transitions. `BLOCKED` requires
|
||||
a reproducible leak, invisible hit, lifecycle fault, crash or synchronization
|
||||
violation. `INCONCLUSIVE` applies when the tested backend/instrumentation
|
||||
cannot supply the needed signal or measurements cannot decide. Untested
|
||||
cells remain **NOT TESTED**. Even `FEASIBLE` would not prove every presented
|
||||
pixel, DRM behavior, production readiness, effects, saved buffers, scaling,
|
||||
rotation, migration or SceneFX ABI maintenance.
|
||||
|
||||
## Measured private headless result
|
||||
|
||||
The private laboratory `/tmp/swayfx-mosaic-nested-grab-FQLULvot/` contains
|
||||
two identity runs (the second adds a B click after camera return), one
|
||||
complete A/B cycle per configuration, separate Mosaic hidden and outside
|
||||
clicks, and four fixed negative-origin input controls. An earlier listener
|
||||
availability pilot remains at `/tmp/swayfx-mosaic-nested-grab-TSp2JWyA/`.
|
||||
The structured seven-run presentation analysis reports **482 native accepted commits**, **470 headless present
|
||||
events**, **470 pairs by `(output, commit_seq)`**, **12 commits without an
|
||||
observed present**, no orphan present and no rejected repaint call. All
|
||||
present events have `presented=true`, `seq=0`, `refresh=0`, `flags=0`.
|
||||
Every native commit callback captured a scene snapshot. The unmodified SwayFX
|
||||
control has no private event listeners and is correctly **NOT TESTED** for
|
||||
native event correlation. None of the 12 unpaired commits is labelled
|
||||
presented. Both HEADLESS-1 and HEADLESS-2 emitted paired events; no paired
|
||||
present callback or monotonic event timestamp preceded its own commit.
|
||||
The 12 unpaired accepted commits are exactly native sequence 2 on both
|
||||
outputs in six instrumented runs (Mosaic cycles, hidden click, outside click,
|
||||
two identity runs, and private standard cycles). Each reports a new buffer and
|
||||
`workspace=none` during initial output setup. Their cause is not established:
|
||||
**UNEXPLAINED / NO PRESENT OBSERVED**. They are neither counted as presented
|
||||
nor declared lost. The unmodified control has no native listener.
|
||||
|
||||
In the final identity run, ten settled transitions form five complete
|
||||
400 → 1280 → 400 pairs. At the target-valued HEADLESS-1 commits, the
|
||||
applied tiled scene translation is `-scroll_x` and its local clip starts at
|
||||
`scroll_x` (56/56 target-valued commits, all with a paired headless present).
|
||||
With A/B mapped, the root popup clip is nonempty at 400 and
|
||||
empty at 1280. Nested B inherits that root clip; its own mapping identity
|
||||
comes from client logs, not a separate root scene tree. All ten stable left
|
||||
captures alternate zero A/B pixels when hidden and 42,200 A-yellow / 17,940
|
||||
B-green pixels on return; right output has no sampled popup color. The same
|
||||
owner IPC ID 8 and single A/B map each persist through the identity run.
|
||||
A fresh B button after the final return was delivered to B before explicit
|
||||
closure.
|
||||
|
||||
Each identity run issued twelve faster alternating commands. Four command
|
||||
intervals in the pilot and three in the final run contained no HEADLESS-1
|
||||
commit or present, while later intervals could contain more than one. This
|
||||
is observable coalescence or deferral, not evidence that a command's target
|
||||
was displayed. The final run had 125 commits and 123 associated presents;
|
||||
the pilot had 121 and 119. Of 220 sampled captures across the seven-run
|
||||
analysis, 200 overlapped one present callback during the capture call and
|
||||
20 overlapped none. Even a single overlapping event does not identify the
|
||||
buffer read by `grim`.
|
||||
|
||||
During initial Mosaic layout setup, five private Mosaic runs observed one
|
||||
presented commit with logical `scroll_x=1760` while the applied scene was
|
||||
translated by `-1120` and its clip began at 1120. This is a reproducible
|
||||
logical-to-applied lag before the target experiment. The applied scene and
|
||||
clip agreed with each other; the record does not establish a wrong rendered
|
||||
pixel or hit target. No mismatch of those applied values was found in the
|
||||
target 400/1280 commits. It illustrates why logical camera cannot be used
|
||||
as a substitute for an applied scene snapshot.
|
||||
|
||||
Separate fixed-geometry input controls used real XDG button serials. Both
|
||||
standard configurations delivered B buttons on the crossing portion of
|
||||
HEADLESS-2 and had 15,860 B-green pixels there. Private Mosaic delivered B
|
||||
inside the viewport; a click on the clipped crossing portion delivered no
|
||||
B button and dismissed B then A under the grab. The separate hidden-owner
|
||||
click likewise delivered no popup button and dismissed in child-first
|
||||
order. Client event lines lack a common monotonic timestamp, so only runner
|
||||
request times, callback times, event order and observed delivery are known;
|
||||
the compositor's internal hit-test result at repaint time is **NOT TESTED**.
|
||||
No pointer event is assigned to a specific frame. These clicks are not a
|
||||
universal click-through UX decision.
|
||||
|
||||
An instrumentation-impact check compared one no-new-listener temporal cycle
|
||||
with the new private cycle. Popup color counts at visible, hidden, returned
|
||||
and closed sampled states, right-output absence, owner IPC ID/geometry/focus,
|
||||
two valid grabs, popup mapping and absence of `popup_done` all agreed. This
|
||||
does not exclude timing perturbations between samples. No crash, protocol
|
||||
error, failed grab or reproducible invisible popup hit appeared in the
|
||||
tested matrix.
|
||||
|
||||
**Verdict: PARTIALLY_FEASIBLE.** Native headless `present` is available and
|
||||
can be paired with native output commits. Applied scene state can be read
|
||||
at the commit callback, after output-state construction. Neither callback
|
||||
proves which pixels occupy that commit's buffer; `grim` captures lack an
|
||||
exact commit identity. The smallest next experiment is a private, bounded
|
||||
buffer fingerprint tied to the actual output-state buffer at build/commit
|
||||
and retained safely until its matching headless present callback, then
|
||||
compared with a capture from that same buffer. It must establish buffer
|
||||
lifetime and readback semantics first, without changing the production
|
||||
renderer or SceneFX ABI.
|
||||
|
||||
The retained `presentation-results/presentation-artifacts-sha256.txt` lists
|
||||
653 raw and derived files and has SHA-256
|
||||
`d37dd26a95b895a115518c9b40b288822f71bac41dd08a2fda59cd30e0013e03`.
|
||||
All 653 entries passed `sha256sum -c`. The five-input
|
||||
`presentation-source-sha256.txt` has SHA-256
|
||||
`40477a991f294b85c2828a8124e24379fba985695a583435a11de91a9005e92f`
|
||||
and passed its five source checks. The distinct instrumentation patch has
|
||||
SHA-256 `dbf1f1d11c9c741b641916d25ac22bcb13c3455ee3002e1f534aa6794cd8a538`.
|
||||
The lab retains its private build log and Meson test log. Generated binaries,
|
||||
PNG, sockets and logs remain outside Git.
|
||||
|
||||
## Protocol history
|
||||
|
||||
- 2026-10-12: API/backend/repaint source audit and protocol recorded before
|
||||
a new private patch or experimental run. Measurements follow below.
|
||||
- 2026-10-12: private event listener and applied-scene trace passed the
|
||||
bounded headless matrix above. The result remains PARTIALLY_FEASIBLE.
|
||||
- 2026-10-12: independently rebuilt the pinned private stack in
|
||||
`/tmp/swayfx-mosaic-nested-grab-tMVZBojD/` and ran one 400 → 1280 → 400
|
||||
cycle with A/B in private Mosaic, plus unmodified and private `splith`
|
||||
controls. The three-run analysis found 127 accepted native commits, 123
|
||||
matching headless presents, zero orphan presents, four initial sequence-2
|
||||
commits without observed present (two per instrumented run), 70 captures,
|
||||
and zero runner or protocol failures. The model Meson test passed. This
|
||||
bounded replay confirms event correlation and settled sampled behavior;
|
||||
it does not identify committed buffer pixels.
|
||||
119
tooling/probes/step20c-presentation-identity/README.md
Normal file
119
tooling/probes/step20c-presentation-identity/README.md
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
# STEP20C presentation identity fixture — EXPERIMENTAL
|
||||
|
||||
The [protocol](../../../docs/design/STEP20C_PRESENTATION_IDENTITY_PROBE.md)
|
||||
precedes the private instrumentation. Only fixed two-output headless geometry
|
||||
`HEADLESS-1 (-800,0)` and `HEADLESS-2 (0,0)`, both 800×600 at scale 1 and
|
||||
normal transform, is in scope. STEP20C remains NEXT and production
|
||||
multi-output Mosaic is NOT YET SUPPORTED.
|
||||
|
||||
The base is the published temporal fixture at Mosaic commit
|
||||
`d19b15bfdeaa49ce81317c88a1baa250220a9998`: pinned SwayFX
|
||||
`fd71a6bdc061bd633b488ae7b83e8a6981d22f86`, private Mosaic
|
||||
`cd550382b311efdbfe3ed892f4e1d281d852e754`, and private SceneFX
|
||||
`37ccd723bef49e6891156ffafce8f549f01446cc`, plus the historical
|
||||
clip, popup guard and timing patches named in the
|
||||
[temporal README](../step20c-temporal-damage/README.md). Installed wlroots
|
||||
0.20.2 is audited against upstream tag commit
|
||||
`d783533489e1f75d6886c2ab5c5960090ef268f8`.
|
||||
|
||||
The new [`presentation-events.patch`](presentation-events.patch), SHA-256
|
||||
`dbf1f1d11c9c741b641916d25ac22bcb13c3455ee3002e1f534aa6794cd8a538`,
|
||||
is applied only after the temporal private Mosaic patch in a disposable lab.
|
||||
It adds a listener to the private compositor's internal output object,
|
||||
logs native `commit_seq` and headless present event fields, and reads scene
|
||||
position and clip in the accepted commit callback. The listener is removed
|
||||
before output destruction. No public SceneFX ABI, headless backend,
|
||||
production build, cumulative patch or package is changed. Neither a commit
|
||||
callback nor a `grim` screenshot proves the pixels in a specific commit
|
||||
buffer. Logging may perturb timing.
|
||||
|
||||
## Rebuild and replay
|
||||
|
||||
Requires the temporal fixture's Git/Meson/Ninja/C17, Wayland, Kitty, `grim`,
|
||||
`wev`, `swaymsg` and Pillow dependencies. Run from a full clone containing
|
||||
the pinned commits and this uncommitted phase-B fixture directory; when
|
||||
starting from published `main`, copy this directory into that clone first.
|
||||
The source manifest verifies the copied inputs. Preparation checks patch applicability with
|
||||
`git apply --check`, builds private SceneFX and Mosaic, compiles private C17
|
||||
clients and prints the laboratory and build log. All runtime sockets are
|
||||
private; only the private compositor loads the private SceneFX library.
|
||||
|
||||
```sh
|
||||
prep=$(sh tooling/probes/step20c-presentation-identity/prepare.sh)
|
||||
printf '%s\n' "$prep"
|
||||
LAB=$(printf '%s\n' "$prep" | sed -n 's/^LAB=//p')
|
||||
(cd "$LAB" && sha256sum -c temporal-source-sha256.txt && \
|
||||
sha256sum -c presentation-source-sha256.txt)
|
||||
PYTHONDONTWRITEBYTECODE=1 python tooling/probes/step20c-presentation-identity/run.py \
|
||||
"$LAB" private_mosaic identity
|
||||
for mode in control private_standard private_mosaic; do
|
||||
PYTHONDONTWRITEBYTECODE=1 python tooling/probes/step20c-presentation-identity/run.py \
|
||||
"$LAB" "$mode" cycles --cycles 1
|
||||
done
|
||||
PYTHONDONTWRITEBYTECODE=1 python tooling/probes/step20c-presentation-identity/run.py \
|
||||
"$LAB" private_mosaic hidden_click
|
||||
PYTHONDONTWRITEBYTECODE=1 python tooling/probes/step20c-presentation-identity/run.py \
|
||||
"$LAB" private_mosaic outside
|
||||
PYTHONDONTWRITEBYTECODE=1 python tooling/probes/step20c-presentation-identity/analyze.py "$LAB"
|
||||
```
|
||||
|
||||
The `identity` run creates A/B once with genuine button serials, executes
|
||||
five settled hide/return pairs and then twelve faster alternating requests.
|
||||
After the last return, it sends a fresh B button without closing or
|
||||
recreating either popup. `cycles` exercises explicit B then A closure;
|
||||
`hidden_click` and `outside` are separate grab-dismissal runs. The runner
|
||||
limits duration, captures and bytes, records exit codes, reaps only its own
|
||||
process groups, and removes only its private runtime. Do not use the daily
|
||||
Wayland or Sway IPC socket.
|
||||
|
||||
The [negative-origin input fixture](../step20c-negative-origin/README.md)
|
||||
can also run `b_cross` for all three configurations and `visible` for private
|
||||
Mosaic in the prepared lab. These runs show pointer delivery at the clipped
|
||||
boundary, with their own `analysis.json`; they are not silently merged into
|
||||
the seven-run presentation summary.
|
||||
|
||||
```sh
|
||||
for mode in control private_standard private_mosaic; do
|
||||
PYTHONDONTWRITEBYTECODE=1 python tooling/probes/step20c-negative-origin/run.py \
|
||||
"$LAB" "$mode" b_cross negative
|
||||
done
|
||||
PYTHONDONTWRITEBYTECODE=1 python tooling/probes/step20c-negative-origin/run.py \
|
||||
"$LAB" private_mosaic visible negative
|
||||
PYTHONDONTWRITEBYTECODE=1 python tooling/probes/step20c-negative-origin/analyze.py "$LAB"
|
||||
```
|
||||
|
||||
## Correlation and retained evidence
|
||||
|
||||
`LAB/presentation-results/` contains `events.jsonl`, `commits.json`,
|
||||
`presentations.json`, `camera-states.json`, `damage-analysis.json`,
|
||||
`input-analysis.json`, `pixel-analysis.json`, `transitions.json`,
|
||||
`matrix.json` and `summary.json`. The analyzer pairs only an actual native
|
||||
`(output, commit_seq)`. The scene, owner and root-popup clip lines logged
|
||||
inside that synchronous commit callback are associated with that callback.
|
||||
It never assigns a `grim` PNG, pointer event or preceding damage extent to
|
||||
a commit without proof. PNG records contain monotonic capture bounds,
|
||||
output, SHA-256, fixed ROI and exact A/B color counts. Client event lines
|
||||
lack a common monotonic timestamp; this is an explicit input limitation.
|
||||
|
||||
The measured laboratory is `/tmp/swayfx-mosaic-nested-grab-FQLULvot/`.
|
||||
It retains a pilot identity run, the final identity run, five further
|
||||
presentation runs, four separate crossing/visible input controls, private
|
||||
builds and raw logs. The [report](../../../docs/design/STEP20C_PRESENTATION_IDENTITY_PROBE.md)
|
||||
records the limited PARTIALLY_FEASIBLE verdict. The source manifest is
|
||||
`presentation-source-sha256.txt`; the retained artifact manifest is
|
||||
`presentation-artifacts-sha256.txt` under `presentation-results/`.
|
||||
|
||||
## Validation and cleanup
|
||||
|
||||
```sh
|
||||
sh -n tooling/probes/step20c-presentation-identity/prepare.sh
|
||||
PYTHONDONTWRITEBYTECODE=1 python -c 'import ast,pathlib; [ast.parse(p.read_text()) for p in pathlib.Path("tooling/probes/step20c-presentation-identity").glob("*.py")]'
|
||||
meson test -C "$LAB/mosaic-build" --print-errorlogs
|
||||
(cd "$LAB" && sha256sum -c presentation-source-sha256.txt)
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Inspect `exit-status.json`, native event logs, XDG client traces, IPC trees,
|
||||
PNG hashes and matrix assertions before a verdict. Keep failed runs. If
|
||||
cleaning later, remove only the printed `LAB` and `BUILD_LOG` paths after
|
||||
preserving their evidence; never clean all `/tmp` or alter the daily session.
|
||||
165
tooling/probes/step20c-presentation-identity/analyze.py
Normal file
165
tooling/probes/step20c-presentation-identity/analyze.py
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
"""Correlate native headless output events without inventing frame identity."""
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
|
||||
from PIL import Image
|
||||
|
||||
if len(sys.argv) != 2:
|
||||
raise SystemExit('usage: analyze.py LAB')
|
||||
lab = Path(sys.argv[1]).resolve()
|
||||
results = lab / 'presentation-results'
|
||||
results.mkdir(exist_ok=True)
|
||||
|
||||
|
||||
def fields(line):
|
||||
return {k: v for k, v in re.findall(r'([a-z_]+)=([^ ]+)', line)}
|
||||
|
||||
|
||||
def save(name, value):
|
||||
(results / name).write_text(json.dumps(value, indent=2) + '\n')
|
||||
|
||||
|
||||
events, commits, presents, scenes, inputs, pixels, damage, matrix, transitions = [], [], [], [], [], [], [], [], []
|
||||
repaint_results = []
|
||||
for run in sorted((lab / 'runs').glob('*')):
|
||||
status_path = run / 'exit-status.json'
|
||||
if not status_path.exists():
|
||||
continue
|
||||
run_id = run.name
|
||||
status = json.loads(status_path.read_text())
|
||||
output_by_seq = {}
|
||||
active_commit = None
|
||||
for line in (run / 'compositor.log').read_text().splitlines():
|
||||
match = re.search(r'\b(PI_OUTPUT_COMMIT|PI_OUTPUT_PRESENT|PI_SCENE|PI_OWNER|PI_POPUP|TEMPORAL_DAMAGE|SPIKE_DAMAGE|TEMPORAL_OUTPUT_COMMIT)\b(.*)', line)
|
||||
if not match:
|
||||
continue
|
||||
kind = match.group(1)
|
||||
data = {'run': run_id, 'kind': kind, **fields(match.group(2))}
|
||||
events.append(data)
|
||||
if kind == 'PI_OUTPUT_COMMIT':
|
||||
key = (data['output'], int(data['commit_seq']))
|
||||
if key in output_by_seq:
|
||||
raise RuntimeError(f'duplicate native commit sequence in {run_id}: {key}')
|
||||
data['commit_seq'] = key[1]
|
||||
data['present_count'] = 0
|
||||
data['scene'] = None
|
||||
data['owners'] = []
|
||||
data['popups'] = []
|
||||
commits.append(data)
|
||||
output_by_seq[key] = data
|
||||
active_commit = data
|
||||
elif kind == 'PI_OUTPUT_PRESENT':
|
||||
data['commit_seq'] = int(data['commit_seq'])
|
||||
data['matched'] = (data['output'], data['commit_seq']) in output_by_seq
|
||||
if data['matched']:
|
||||
output_by_seq[(data['output'], data['commit_seq'])]['present_count'] += 1
|
||||
presents.append(data)
|
||||
active_commit = None
|
||||
elif kind in ('PI_SCENE', 'PI_OWNER', 'PI_POPUP'):
|
||||
# CONTRACT: these synchronous callback lines belong to the
|
||||
# immediately preceding commit callback, never to a later frame.
|
||||
if active_commit is not None and data['output'] == active_commit['output']:
|
||||
if kind == 'PI_SCENE':
|
||||
active_commit['scene'] = data
|
||||
scenes.append({'run': run_id, 'output': data['output'],
|
||||
'commit_seq': active_commit['commit_seq'], **data})
|
||||
elif kind == 'PI_OWNER':
|
||||
active_commit['owners'].append(data)
|
||||
else:
|
||||
active_commit['popups'].append(data)
|
||||
else:
|
||||
if kind in ('TEMPORAL_DAMAGE', 'SPIKE_DAMAGE'):
|
||||
damage.append(data)
|
||||
if kind == 'TEMPORAL_OUTPUT_COMMIT':
|
||||
repaint_results.append(data)
|
||||
active_commit = None
|
||||
runner_events = [json.loads(line) for line in (run / 'events.jsonl').read_text().splitlines()]
|
||||
starts = [e for e in runner_events if e['kind'] == 'transition_start']
|
||||
for index, start in enumerate(starts):
|
||||
begin = start['monotonic_ns']
|
||||
end = starts[index + 1]['monotonic_ns'] if index + 1 < len(starts) else None
|
||||
own_commits = [c for c in commits if c['run'] == run_id and
|
||||
c['output'] == 'HEADLESS-1' and int(c['ns']) >= begin and
|
||||
(end is None or int(c['ns']) < end)]
|
||||
own_presents = [p for p in presents if p['run'] == run_id and
|
||||
p['output'] == 'HEADLESS-1' and int(p['ns']) >= begin and
|
||||
(end is None or int(p['ns']) < end)]
|
||||
transitions.append({'run': run_id, 'series': start['series'],
|
||||
'index': start['index'], 'requested_scroll': start['target'],
|
||||
'start_ns': begin, 'next_command_ns': end,
|
||||
'commit_count_in_interval': len(own_commits),
|
||||
'present_count_in_interval': len(own_presents),
|
||||
'observed_commit_scrolls': [c['scene'].get('scroll')
|
||||
if c['scene'] else None for c in own_commits],
|
||||
'command_to_frame_identity': 'UNPROVEN'})
|
||||
for event in runner_events:
|
||||
if event['kind'] in ('pointer_request', 'camera_observed', 'ipc', 'client_request'):
|
||||
inputs.append({'run': run_id, **event})
|
||||
if event['kind'] != 'capture':
|
||||
continue
|
||||
path = run / event['path']
|
||||
image = Image.open(path).convert('RGB')
|
||||
counts = {name: image.get_flattened_data().count(rgb)
|
||||
for name, rgb in {'A_yellow': (224, 192, 32),
|
||||
'B_green': (32, 208, 160)}.items()}
|
||||
before, after = event['before_ns'], event['after_ns']
|
||||
nearby = [p['commit_seq'] for p in presents if p['run'] == run_id and
|
||||
p['output'] == event['output'] and before <= int(p['ns']) <= after]
|
||||
pixels.append({'run': run_id, 'output': event['output'], 'tag': event['tag'],
|
||||
'path': event['path'], 'sha256': hashlib.sha256(path.read_bytes()).hexdigest(),
|
||||
'before_ns': before, 'after_ns': after, 'size': list(image.size),
|
||||
'roi': [400, 90, 800, 310] if event['output'] == 'HEADLESS-1'
|
||||
else [0, 90, 200, 310], 'exact_color_pixels': counts,
|
||||
'present_events_during_capture': nearby,
|
||||
'commit_seq_of_captured_buffer': None})
|
||||
client = (run / 'client.log').read_text() if (run / 'client.log').exists() else ''
|
||||
compositor_text = (run / 'compositor.log').read_text()
|
||||
protocol_error = bool(re.search(r'protocol error|invalid serial',
|
||||
compositor_text + client, re.I))
|
||||
grabs = re.findall(r'POPUP_[AB] GRAB_REQUEST origin=wl_pointer.button '
|
||||
r'target=\w+ serial=(\d+) used=(\d+)', client)
|
||||
button_serials = set(re.findall(r'POINTER_BUTTON target=\w+ serial=(\d+) '
|
||||
r'button=272 state=1', client))
|
||||
valid_grabs = len(grabs) >= 2 and all(
|
||||
serial == used and serial in button_serials for serial, used in grabs)
|
||||
for line in client.splitlines():
|
||||
if any(token in line for token in ('GRAB_REQUEST', 'POINTER_ENTER', 'POINTER_BUTTON',
|
||||
'POPUP_DONE', 'MAP', 'DESTROY')):
|
||||
inputs.append({'run': run_id, 'kind': 'client_line', 'line': line})
|
||||
own_commits = [c for c in commits if c['run'] == run_id]
|
||||
own_presents = [p for p in presents if p['run'] == run_id]
|
||||
matrix.append({'run': run_id, 'runner': 'FAIL' if status['failure'] else 'PASS',
|
||||
'accepted_commits': len(own_commits), 'present_events': len(own_presents),
|
||||
'matched_present_events': sum(p['matched'] for p in own_presents),
|
||||
'commits_without_present': sum(c['present_count'] == 0 for c in own_commits),
|
||||
'scene_snapshots': sum(c['scene'] is not None for c in own_commits),
|
||||
'valid_button_serials': 'PASS' if valid_grabs else 'INCONCLUSIVE',
|
||||
'protocol_error': protocol_error,
|
||||
'capture_count': sum(p['run'] == run_id for p in pixels),
|
||||
'failure': status['failure']})
|
||||
|
||||
for name, value in [('commits.json', commits), ('presentations.json', presents),
|
||||
('camera-states.json', scenes), ('damage-analysis.json', damage),
|
||||
('input-analysis.json', inputs), ('pixel-analysis.json', pixels),
|
||||
('matrix.json', matrix), ('transitions.json', transitions)]:
|
||||
save(name, value)
|
||||
(results / 'events.jsonl').write_text(''.join(json.dumps(e) + '\n' for e in events))
|
||||
summary = {'accepted_commits': len(commits), 'present_events': len(presents),
|
||||
'matched_present_events': sum(p['matched'] for p in presents),
|
||||
'unmatched_present_events': sum(not p['matched'] for p in presents),
|
||||
'commits_without_present': sum(c['present_count'] == 0 for c in commits),
|
||||
'headless_presented_true': sum(p['presented'] == '1' for p in presents),
|
||||
'presented_false': sum(p['presented'] == '0' for p in presents),
|
||||
'repaint_commit_calls_rejected': sum(r['accepted'] == '0' for r in repaint_results),
|
||||
'scene_snapshots': len(scenes), 'capture_count': len(pixels),
|
||||
'captures_with_present_during_capture': sum(bool(p['present_events_during_capture'])
|
||||
for p in pixels),
|
||||
'runs': len(matrix), 'runner_failures': sum(x['runner'] == 'FAIL' for x in matrix),
|
||||
'invalid_or_unproven_grabs': sum(x['valid_button_serials'] != 'PASS' for x in matrix),
|
||||
'protocol_error_runs': sum(x['protocol_error'] for x in matrix),
|
||||
'buffer_content_identity': 'UNPROVEN'}
|
||||
save('summary.json', summary)
|
||||
print(json.dumps(summary))
|
||||
24
tooling/probes/step20c-presentation-identity/prepare.sh
Normal file
24
tooling/probes/step20c-presentation-identity/prepare.sh
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
#!/bin/sh
|
||||
# EXPERIMENTAL: build a private compositor with native commit/present tracing.
|
||||
set -eu
|
||||
repo=$(CDPATH= cd -- "$(dirname -- "$0")/../../.." && pwd)
|
||||
fixture="$repo/tooling/probes/step20c-presentation-identity"
|
||||
prep_log=$(mktemp /tmp/mpi-prepare-XXXXXXXX)
|
||||
if ! sh "$repo/tooling/probes/step20c-temporal-damage/prepare.sh" > "$prep_log" 2>&1; then
|
||||
printf 'PRIVATE_BUILD_FAILED log=%s\n' "$prep_log" >&2
|
||||
tail -40 "$prep_log" >&2
|
||||
exit 1
|
||||
fi
|
||||
lab=$(sed -n 's/^LAB=//p' "$prep_log")
|
||||
test -n "$lab" && test -d "$lab"
|
||||
trap 'rm -rf -- "$lab"' 0
|
||||
# CONTRACT: apply only to the already-patched private Mosaic source;
|
||||
# never to this repository's production compositor or installed libraries.
|
||||
(cd "$lab/mosaic" && git apply --check "$fixture/presentation-events.patch" &&
|
||||
git apply "$fixture/presentation-events.patch")
|
||||
meson compile -C "$lab/mosaic-build" -j 8 >> "$prep_log" 2>&1
|
||||
sha256sum "$fixture/presentation-events.patch" "$fixture/prepare.sh" \
|
||||
"$fixture/run.py" "$fixture/analyze.py" "$fixture/README.md" \
|
||||
> "$lab/presentation-source-sha256.txt"
|
||||
trap - 0
|
||||
printf 'LAB=%s\nBUILD_LOG=%s\n' "$lab" "$prep_log"
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
diff --git a/include/sway/output.h b/include/sway/output.h
|
||||
index 3d1d1ad1..34aa33e1 100644
|
||||
--- a/include/sway/output.h
|
||||
+++ b/include/sway/output.h
|
||||
@@ -64,6 +64,8 @@ struct sway_output {
|
||||
struct wl_listener layout_destroy;
|
||||
struct wl_listener destroy;
|
||||
struct wl_listener present;
|
||||
+ // Private STEP20C trace: removed before output destruction.
|
||||
+ struct wl_listener presentation_probe_commit;
|
||||
struct wl_listener frame;
|
||||
struct wl_listener request_state;
|
||||
|
||||
diff --git a/sway/desktop/output.c b/sway/desktop/output.c
|
||||
index cd5eed21..fdf93421 100644
|
||||
--- a/sway/desktop/output.c
|
||||
+++ b/sway/desktop/output.c
|
||||
@@ -37,6 +37,7 @@
|
||||
#include "sway/tree/root.h"
|
||||
#include "sway/tree/view.h"
|
||||
#include "sway/tree/workspace.h"
|
||||
+#include "sway/tree/mosaic.h"
|
||||
|
||||
#if WLR_HAS_DRM_BACKEND
|
||||
#include <wlr/backend/drm.h>
|
||||
@@ -365,6 +366,83 @@ static bool output_can_tear(struct sway_output *output) {
|
||||
// INVARIANT: repaint and commit decisions are unchanged by the trace.
|
||||
static uint64_t temporal_output_commit_ordinal;
|
||||
|
||||
+// WHY: the private headless experiment needs native output event identity.
|
||||
+// CONTRACT: all scene values are read while their owning output is alive;
|
||||
+// this trace never changes render, damage, clipping or commit decisions.
|
||||
+// INVARIANT: event.commit sees the accepted native commit_seq, while a
|
||||
+// later present event can only be paired by (output, commit_seq).
|
||||
+static uint64_t presentation_probe_now_ns(void) {
|
||||
+ struct timespec now;
|
||||
+ clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
+ return (uint64_t)now.tv_sec * UINT64_C(1000000000) + now.tv_nsec;
|
||||
+}
|
||||
+
|
||||
+static void presentation_probe_scene(struct sway_output *output) {
|
||||
+ struct sway_workspace *ws = output->current.active_workspace;
|
||||
+ if (!ws) {
|
||||
+ sway_log(SWAY_INFO, "PI_SCENE output=%s workspace=none ns=%" PRIu64,
|
||||
+ output->wlr_output->name, presentation_probe_now_ns());
|
||||
+ return;
|
||||
+ }
|
||||
+ struct wlr_box clip = {0};
|
||||
+ bool has_clip = wlr_scene_tree_get_clip(ws->layers.tiling, &clip);
|
||||
+ double scroll = -1;
|
||||
+ if (ws->current.layout == L_MOSAIC && ws->mosaic && ws->mosaic->rows->length) {
|
||||
+ struct mosaic_row *row = ws->mosaic->rows->items[0];
|
||||
+ scroll = row->state.scroll_x;
|
||||
+ }
|
||||
+ sway_log(SWAY_INFO, "PI_SCENE output=%s ns=%" PRIu64
|
||||
+ " workspace=%zu layout=%d scroll=%.0f tile_scene=%d,%d"
|
||||
+ " clip_valid=%d clip=%d,%d,%d,%d",
|
||||
+ output->wlr_output->name, presentation_probe_now_ns(), ws->node.id,
|
||||
+ ws->current.layout, scroll, ws->layers.tiling->node.x,
|
||||
+ ws->layers.tiling->node.y, has_clip,
|
||||
+ clip.x, clip.y, clip.width, clip.height);
|
||||
+ if (ws->current.tiling) {
|
||||
+ for (int i = 0; i < ws->current.tiling->length; i++) {
|
||||
+ struct sway_container *con = ws->current.tiling->items[i];
|
||||
+ int x = 0, y = 0;
|
||||
+ bool coords = con->scene_tree &&
|
||||
+ wlr_scene_node_coords(&con->scene_tree->node, &x, &y);
|
||||
+ sway_log(SWAY_INFO, "PI_OWNER output=%s ns=%" PRIu64
|
||||
+ " id=%zu current=%.0f,%.0f,%.0f,%.0f"
|
||||
+ " scene_valid=%d scene=%d,%d applied_size=%d,%d",
|
||||
+ output->wlr_output->name, presentation_probe_now_ns(),
|
||||
+ con->node.id, con->current.x, con->current.y,
|
||||
+ con->current.width, con->current.height, coords, x, y,
|
||||
+ con->animation_state.current_width,
|
||||
+ con->animation_state.current_height);
|
||||
+ }
|
||||
+ }
|
||||
+ if (root->layers.popup) {
|
||||
+ struct wlr_scene_node *node;
|
||||
+ unsigned popup_index = 0;
|
||||
+ wl_list_for_each(node, &root->layers.popup->children, link) {
|
||||
+ if (!scene_descriptor_try_get(node, SWAY_SCENE_DESC_POPUP)) continue;
|
||||
+ struct wlr_scene_tree *tree = wlr_scene_tree_from_node(node);
|
||||
+ struct wlr_box box = {0};
|
||||
+ bool valid = wlr_scene_tree_get_clip(tree, &box);
|
||||
+ sway_log(SWAY_INFO, "PI_POPUP output=%s ns=%" PRIu64
|
||||
+ " local_index=%u tree=%p enabled=%d clip_valid=%d clip=%d,%d,%d,%d",
|
||||
+ output->wlr_output->name, presentation_probe_now_ns(),
|
||||
+ ++popup_index, (void *)tree, node->enabled, valid,
|
||||
+ box.x, box.y, box.width, box.height);
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+static void presentation_probe_handle_commit(struct wl_listener *listener, void *data) {
|
||||
+ struct sway_output *output = wl_container_of(listener, output, presentation_probe_commit);
|
||||
+ const struct wlr_output_event_commit *event = data;
|
||||
+ sway_log(SWAY_INFO, "PI_OUTPUT_COMMIT output=%s commit_seq=%" PRIu32
|
||||
+ " ns=%" PRIu64 " event_ns=%" PRIu64 " has_buffer=%d",
|
||||
+ output->wlr_output->name, output->wlr_output->commit_seq,
|
||||
+ presentation_probe_now_ns(),
|
||||
+ (uint64_t)event->when.tv_sec * UINT64_C(1000000000) + event->when.tv_nsec,
|
||||
+ !!(event->state->committed & WLR_OUTPUT_STATE_BUFFER));
|
||||
+ presentation_probe_scene(output);
|
||||
+}
|
||||
+
|
||||
static int output_repaint_timer_handler(void *data) {
|
||||
struct sway_output *output = data;
|
||||
|
||||
@@ -540,6 +618,7 @@ static void begin_destroy(struct sway_output *output) {
|
||||
wl_list_remove(&output->layout_destroy.link);
|
||||
wl_list_remove(&output->destroy.link);
|
||||
wl_list_remove(&output->present.link);
|
||||
+ wl_list_remove(&output->presentation_probe_commit.link);
|
||||
wl_list_remove(&output->frame.link);
|
||||
wl_list_remove(&output->request_state.link);
|
||||
|
||||
@@ -576,6 +655,15 @@ static void handle_layout_destroy(struct wl_listener *listener, void *data) {
|
||||
static void handle_present(struct wl_listener *listener, void *data) {
|
||||
struct sway_output *output = wl_container_of(listener, output, present);
|
||||
struct wlr_output_event_present *output_event = data;
|
||||
+ sway_log(SWAY_INFO, "PI_OUTPUT_PRESENT output=%s commit_seq=%" PRIu32
|
||||
+ " ns=%" PRIu64 " event_ns=%" PRIu64
|
||||
+ " presented=%d seq=%u refresh=%d flags=%" PRIu32,
|
||||
+ output->wlr_output->name, output_event->commit_seq,
|
||||
+ presentation_probe_now_ns(),
|
||||
+ (uint64_t)output_event->when.tv_sec * UINT64_C(1000000000) +
|
||||
+ output_event->when.tv_nsec,
|
||||
+ output_event->presented, output_event->seq,
|
||||
+ output_event->refresh, output_event->flags);
|
||||
|
||||
if (!output->enabled || !output_event->presented) {
|
||||
return;
|
||||
@@ -690,6 +778,8 @@ void handle_new_output(struct wl_listener *listener, void *data) {
|
||||
output->layout_destroy.notify = handle_layout_destroy;
|
||||
wl_signal_add(&wlr_output->events.destroy, &output->destroy);
|
||||
output->destroy.notify = handle_destroy;
|
||||
+ wl_signal_add(&wlr_output->events.commit, &output->presentation_probe_commit);
|
||||
+ output->presentation_probe_commit.notify = presentation_probe_handle_commit;
|
||||
wl_signal_add(&wlr_output->events.present, &output->present);
|
||||
output->present.notify = handle_present;
|
||||
wl_signal_add(&wlr_output->events.frame, &output->frame);
|
||||
427
tooling/probes/step20c-presentation-identity/run.py
Normal file
427
tooling/probes/step20c-presentation-identity/run.py
Normal file
|
|
@ -0,0 +1,427 @@
|
|||
"""EXPERIMENTAL presentation-identity headless campaign. See README."""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import signal
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('lab', type=Path)
|
||||
parser.add_argument('mode', choices=('control', 'private_standard', 'private_mosaic'))
|
||||
parser.add_argument('kind', choices=('cycles', 'identity', 'outside', 'hidden_click', 'toggle'))
|
||||
parser.add_argument('--cycles', type=int, default=10)
|
||||
parser.add_argument('--moves', type=int, default=18)
|
||||
parser.add_argument('--max-seconds', type=int, default=900)
|
||||
parser.add_argument('--max-captures', type=int, default=400)
|
||||
parser.add_argument('--max-bytes', type=int, default=250_000_000)
|
||||
args = parser.parse_args()
|
||||
if not 1 <= args.cycles <= 30 or not 1 <= args.moves <= 40:
|
||||
parser.error('cycle/move bounds exceeded')
|
||||
if args.kind in ('identity', 'hidden_click', 'toggle') and args.mode != 'private_mosaic':
|
||||
parser.error('camera/toggle requires private_mosaic')
|
||||
lab = args.lab.resolve()
|
||||
if not (lab / 'nested-client').is_file():
|
||||
parser.error('prepared LAB required')
|
||||
run = lab / 'runs' / f'{args.mode}-{args.kind}-{time.time_ns()}'
|
||||
run.mkdir(parents=True, mode=0o700)
|
||||
runtime = Path(tempfile.mkdtemp(prefix='mtd-', dir='/tmp'))
|
||||
if len(os.fsencode(str(runtime / 'sway-ipc.4294967295.2147483647.sock'))) >= 108:
|
||||
raise RuntimeError('IPC socket path exceeds sockaddr_un')
|
||||
(run / 'runtime-path').write_text(str(runtime) + '\n')
|
||||
(run / 'config').write_text(
|
||||
'output HEADLESS-1 resolution 800x600 position -800 0\n'
|
||||
'output HEADLESS-2 resolution 800x600 position 0 0\n'
|
||||
'workspace 1 output HEADLESS-1\nworkspace 2 output HEADLESS-2\n'
|
||||
'focus_follows_mouse no\n')
|
||||
env = os.environ.copy()
|
||||
env.update(XDG_RUNTIME_DIR=str(runtime), WLR_BACKENDS='headless',
|
||||
WLR_HEADLESS_OUTPUTS='2', WLR_RENDERER='pixman')
|
||||
for key in ('SWAYSOCK', 'I3SOCK', 'WAYLAND_DISPLAY', 'LD_LIBRARY_PATH'):
|
||||
env.pop(key, None)
|
||||
compositor_env = env.copy()
|
||||
binary = Path('/usr/bin/sway') if args.mode == 'control' else lab / 'mosaic-build/sway/swayfx-mosaic'
|
||||
if args.mode != 'control':
|
||||
compositor_env['LD_LIBRARY_PATH'] = str(lab / 'scenefx-build')
|
||||
files, children, events = [], [], []
|
||||
started = time.monotonic()
|
||||
capture_count = 0
|
||||
capture_bytes = 0
|
||||
|
||||
|
||||
def record(kind, **fields):
|
||||
entry = {'monotonic_ns': time.monotonic_ns(), 'kind': kind, **fields}
|
||||
events.append(entry)
|
||||
with (run / 'events.jsonl').open('a') as out:
|
||||
out.write(json.dumps(entry) + '\n')
|
||||
return entry
|
||||
|
||||
|
||||
def spawn(argv, log_name, *, stdin=False, debug=False):
|
||||
log = (run / log_name).open('w')
|
||||
files.append(log)
|
||||
child_env = env.copy()
|
||||
if debug:
|
||||
child_env['WAYLAND_DEBUG'] = '1'
|
||||
proc = subprocess.Popen(argv, env=child_env, stdin=subprocess.PIPE if stdin else None,
|
||||
stdout=log, stderr=subprocess.STDOUT,
|
||||
start_new_session=True, text=stdin)
|
||||
children.append(proc)
|
||||
return proc
|
||||
|
||||
|
||||
def wait(predicate, seconds=12):
|
||||
deadline = time.monotonic() + seconds
|
||||
while time.monotonic() < deadline:
|
||||
if time.monotonic() - started > args.max_seconds:
|
||||
raise RuntimeError('run duration bound exceeded')
|
||||
result = predicate()
|
||||
if result:
|
||||
return result
|
||||
if compositor.poll() is not None:
|
||||
raise RuntimeError(f'compositor exited {compositor.returncode}')
|
||||
if 'pointer' in globals() and pointer.poll() is not None:
|
||||
raise RuntimeError(f'pointer exited {pointer.returncode}')
|
||||
if 'client' in globals() and client.poll() is not None:
|
||||
raise RuntimeError(f'client exited {client.returncode}')
|
||||
time.sleep(.04)
|
||||
raise RuntimeError('timeout waiting for event')
|
||||
|
||||
|
||||
def ipc(*argv):
|
||||
completed = subprocess.run(['swaymsg', '-s', str(sock), *argv], env=env,
|
||||
text=True, capture_output=True, check=True, timeout=8)
|
||||
result = json.loads(completed.stdout)
|
||||
record('ipc', argv=list(argv), result=result)
|
||||
return result
|
||||
|
||||
|
||||
def command(value):
|
||||
result = ipc(value)
|
||||
if not isinstance(result, list) or not result or not all(x.get('success') for x in result):
|
||||
raise RuntimeError(f'IPC command rejected: {value}: {result}')
|
||||
|
||||
|
||||
def find(node, predicate):
|
||||
if isinstance(node, dict):
|
||||
if predicate(node):
|
||||
return node
|
||||
for child in node.get('nodes', []) + node.get('floating_nodes', []):
|
||||
found = find(child, predicate)
|
||||
if found:
|
||||
return found
|
||||
return None
|
||||
|
||||
|
||||
def tree():
|
||||
return ipc('-t', 'get_tree')
|
||||
|
||||
|
||||
def workspace():
|
||||
return find(tree(), lambda n: n.get('type') == 'workspace' and n.get('name') == '1')
|
||||
|
||||
|
||||
def owner():
|
||||
return find(tree(), lambda n: n.get('app_id') == 'org.mosaic.NestedGrabProbe')
|
||||
|
||||
|
||||
def client_text():
|
||||
return (run / 'client.log').read_text()
|
||||
|
||||
|
||||
def count(marker):
|
||||
return client_text().count(marker)
|
||||
|
||||
|
||||
def configured(name):
|
||||
matches = re.findall(rf'{name} ROLE_CONFIGURE x=(-?\d+) y=(-?\d+) w=(\d+) h=(\d+)',
|
||||
client_text())
|
||||
if not matches:
|
||||
raise RuntimeError(f'no {name} role configure')
|
||||
return tuple(map(int, matches[-1]))
|
||||
|
||||
|
||||
def snapshot(tag):
|
||||
global capture_count, capture_bytes
|
||||
t = tree()
|
||||
outputs = ipc('-t', 'get_outputs')
|
||||
(run / f'tree-{tag}.json').write_text(json.dumps(t, indent=2) + '\n')
|
||||
(run / f'outputs-{tag}.json').write_text(json.dumps(outputs, indent=2) + '\n')
|
||||
expected = {'HEADLESS-1': (-800, 0, 800, 600), 'HEADLESS-2': (0, 0, 800, 600)}
|
||||
actual = {}
|
||||
for output in outputs:
|
||||
if output.get('name') in expected and output.get('active'):
|
||||
rect = output['rect']
|
||||
if output.get('scale') != 1.0 or output.get('transform') != 'normal':
|
||||
raise RuntimeError('output scale/transform mismatch')
|
||||
actual[output['name']] = (rect['x'], rect['y'], rect['width'], rect['height'])
|
||||
if actual != expected:
|
||||
raise RuntimeError(f'output geometry mismatch: {actual}')
|
||||
for output, side in (('HEADLESS-1', 'left'), ('HEADLESS-2', 'right')):
|
||||
if capture_count >= args.max_captures:
|
||||
raise RuntimeError('capture count bound exceeded')
|
||||
path = run / f'{side}-{tag}.png'
|
||||
before = time.monotonic_ns()
|
||||
subprocess.run(['grim', '-o', output, str(path)], env=env, check=True,
|
||||
stdout=subprocess.DEVNULL, timeout=10)
|
||||
after = time.monotonic_ns()
|
||||
capture_count += 1
|
||||
capture_bytes += path.stat().st_size
|
||||
if capture_bytes > args.max_bytes:
|
||||
raise RuntimeError('capture byte bound exceeded')
|
||||
record('capture', tag=tag, output=output, path=path.name,
|
||||
before_ns=before, after_ns=after, size=path.stat().st_size)
|
||||
|
||||
|
||||
def inject(action, x, y):
|
||||
if not (-800 <= x < 800 and 0 <= y < 600):
|
||||
raise ValueError('global pointer out of bounds')
|
||||
sent = x + 800
|
||||
marker = f'INJECT_DONE {action} {sent},{y}'
|
||||
before = (run / 'pointer.log').read_text().count(marker)
|
||||
record('pointer_request', action=action, global_point=[x, y], sent=[sent, y],
|
||||
extent=[1600, 600], output='HEADLESS-1' if x < 0 else 'HEADLESS-2')
|
||||
pointer.stdin.write(f'{action} {sent} {y}\n')
|
||||
pointer.stdin.flush()
|
||||
wait(lambda: (run / 'pointer.log').read_text().count(marker) > before)
|
||||
|
||||
|
||||
def client_command(value):
|
||||
marker = f'COMMAND {value} DONE'
|
||||
before = count(marker)
|
||||
record('client_request', value=value)
|
||||
client.stdin.write(value + '\n')
|
||||
client.stdin.flush()
|
||||
wait(lambda: count(marker) > before)
|
||||
|
||||
|
||||
def scroll(target):
|
||||
current = workspace()['mosaic']['rows'][0]['scroll_x']
|
||||
command(f'layout mosaic scroll {target-current:g}')
|
||||
wait(lambda: workspace()['mosaic']['rows'][0]['scroll_x'] == target)
|
||||
record('camera_observed', target=target, owner_id=owner()['id'])
|
||||
|
||||
|
||||
def open_chain(cycle):
|
||||
a_before, b_before = count('POPUP_A MAP'), count('POPUP_B MAP')
|
||||
o = owner()
|
||||
scroll_x = workspace()['mosaic']['rows'][0]['scroll_x'] if args.mode == 'private_mosaic' else 0
|
||||
top_x = int(o['rect']['x'] - scroll_x + o['window_rect']['x'] + 100)
|
||||
inject('move', top_x, 150)
|
||||
inject('left', top_x, 150)
|
||||
wait(lambda: count('POPUP_A MAP') > a_before)
|
||||
snapshot(f'{cycle}-S1-A')
|
||||
a_cfg = configured('POPUP_A')
|
||||
a_x = int(o['rect']['x'] - scroll_x + o['window_rect']['x'] + a_cfg[0] + 40)
|
||||
a_y = int(o['rect']['y'] + o['window_rect']['y'] + a_cfg[1] + 35)
|
||||
inject('move', a_x, a_y)
|
||||
inject('left', a_x, a_y)
|
||||
wait(lambda: count('POPUP_B MAP') > b_before)
|
||||
snapshot(f'{cycle}-S2-B')
|
||||
b_cfg = configured('POPUP_B')
|
||||
b_x = int(o['rect']['x'] - scroll_x + o['window_rect']['x'] + a_cfg[0] + b_cfg[0] + 35)
|
||||
b_y = int(o['rect']['y'] + o['window_rect']['y'] + a_cfg[1] + b_cfg[1] + 30)
|
||||
inject('move', b_x, b_y)
|
||||
inject('right', b_x, b_y)
|
||||
time.sleep(.12)
|
||||
snapshot(f'{cycle}-S3-visible')
|
||||
record('chain', cycle=cycle, owner_id=o['id'], scroll_x=scroll_x,
|
||||
a_configure=a_cfg, b_configure=b_cfg,
|
||||
top_global=[top_x, 150], a_global=[a_x, a_y], b_global=[b_x, b_y])
|
||||
|
||||
|
||||
def cycle_once(number):
|
||||
label = f'cycle-{number:03d}'
|
||||
record('cycle_start', cycle=number)
|
||||
snapshot(f'{label}-S0-reference')
|
||||
open_chain(label)
|
||||
if args.mode == 'private_mosaic':
|
||||
scroll(1280)
|
||||
snapshot(f'{label}-S4-hidden-immediate')
|
||||
time.sleep(.18)
|
||||
snapshot(f'{label}-S4-hidden-stable')
|
||||
inject('move', -700, 190)
|
||||
time.sleep(.1)
|
||||
snapshot(f'{label}-S4-hidden-hover')
|
||||
scroll(400)
|
||||
snapshot(f'{label}-S5-return-immediate')
|
||||
time.sleep(.18)
|
||||
snapshot(f'{label}-S5-return-stable')
|
||||
client_command('close_b')
|
||||
snapshot(f'{label}-S6-close-B-immediate')
|
||||
time.sleep(.18)
|
||||
snapshot(f'{label}-S6-close-B-stable')
|
||||
a_cfg = configured('POPUP_A')
|
||||
o = owner()
|
||||
scroll_x = workspace()['mosaic']['rows'][0]['scroll_x'] if args.mode == 'private_mosaic' else 0
|
||||
a_x = int(o['rect']['x'] - scroll_x + o['window_rect']['x'] + a_cfg[0] + 20)
|
||||
a_y = int(o['rect']['y'] + o['window_rect']['y'] + a_cfg[1] + 20)
|
||||
before_a = len(re.findall(r'POINTER_BUTTON target=POPUP_A[^\n]*button=273 state=1', client_text()))
|
||||
inject('move', a_x, a_y)
|
||||
inject('right', a_x, a_y)
|
||||
wait(lambda: len(re.findall(r'POINTER_BUTTON target=POPUP_A[^\n]*button=273 state=1',
|
||||
client_text())) > before_a)
|
||||
snapshot(f'{label}-S6-A-interaction')
|
||||
client_command('close_a')
|
||||
snapshot(f'{label}-S7-close-A-immediate')
|
||||
time.sleep(.18)
|
||||
snapshot(f'{label}-S7-close-A-stable')
|
||||
record('cycle_end', cycle=number, owner_id=owner()['id'], client_alive=client.poll() is None)
|
||||
|
||||
|
||||
compositor_log = (run / 'compositor.log').open('w')
|
||||
files.append(compositor_log)
|
||||
compositor = None
|
||||
failure = None
|
||||
try:
|
||||
compositor = subprocess.Popen([str(binary), '-c', str(run / 'config'), '-d'],
|
||||
env=compositor_env, stdout=compositor_log,
|
||||
stderr=subprocess.STDOUT, start_new_session=True)
|
||||
def sockets():
|
||||
s = next(iter(runtime.glob('sway-ipc.*.sock')), None)
|
||||
w = next((p for p in runtime.glob('wayland-*') if p.suffix != '.lock'), None)
|
||||
return (s, w) if s and w else None
|
||||
sock, way = wait(sockets)
|
||||
env.update(SWAYSOCK=str(sock), WAYLAND_DISPLAY=way.name)
|
||||
snapshot('startup')
|
||||
pointer = spawn([str(lab / 'pointer-daemon'), '1600', '600'], 'pointer.log', stdin=True)
|
||||
wait(lambda: 'POINTER_READY' in (run / 'pointer.log').read_text())
|
||||
command('workspace 1')
|
||||
command('layout mosaic' if args.mode == 'private_mosaic' else 'layout splith')
|
||||
spawn(['kitty', '--class', 'A', '-o', 'cursor_blink_interval=0',
|
||||
'-o', 'background=#aa2222', '/usr/bin/sleep', '900'], 'A.log')
|
||||
wait(lambda: len(workspace()['nodes']) >= 1)
|
||||
client = spawn([str(lab / 'nested-client'), 'b_cross'], 'client.log', stdin=True, debug=True)
|
||||
wait(lambda: owner() and 'TOPLEVEL MAP' in client_text())
|
||||
for name in ('C', 'D'):
|
||||
spawn(['kitty', '--class', name, '-o', 'cursor_blink_interval=0',
|
||||
'/usr/bin/sleep', '900'], f'{name}.log')
|
||||
time.sleep(.25)
|
||||
command('workspace 2')
|
||||
spawn(['stdbuf', '-oL', 'wev'], 'wev.log')
|
||||
wait(lambda: find(tree(), lambda n: n.get('app_id') == 'wev'))
|
||||
command('workspace 1')
|
||||
command('[app_id="org.mosaic.NestedGrabProbe"] focus')
|
||||
if args.mode == 'private_mosaic':
|
||||
scroll(400)
|
||||
time.sleep(.25)
|
||||
if args.kind == 'cycles':
|
||||
for i in range(1, args.cycles + 1):
|
||||
cycle_once(i)
|
||||
elif args.kind == 'identity':
|
||||
# CONTRACT: ten settled changes are five complete 400/1280 pairs.
|
||||
# Twelve following requests omit intervening captures; acceptance is
|
||||
# recorded, but coalesced intermediate presentations remain possible.
|
||||
snapshot('identity-S0-reference')
|
||||
open_chain('identity')
|
||||
for i in range(10):
|
||||
target = 1280 if i % 2 == 0 else 400
|
||||
record('transition_start', series='settled', index=i, target=target)
|
||||
scroll(target)
|
||||
snapshot(f'identity-settled-{i:02d}-immediate')
|
||||
time.sleep(.18)
|
||||
snapshot(f'identity-settled-{i:02d}-stable')
|
||||
for i in range(12):
|
||||
target = 1280 if i % 2 == 0 else 400
|
||||
record('transition_start', series='rapid', index=i, target=target)
|
||||
scroll(target)
|
||||
if i % 4 == 3:
|
||||
snapshot(f'identity-rapid-{i:02d}')
|
||||
snapshot('identity-rapid-final')
|
||||
# CONTRACT: this input check follows camera return without closing or
|
||||
# recreating either XDG popup; a fresh button is delivered to B.
|
||||
o = owner()
|
||||
a_cfg, b_cfg = configured('POPUP_A'), configured('POPUP_B')
|
||||
current_scroll = workspace()['mosaic']['rows'][0]['scroll_x']
|
||||
b_x = int(o['rect']['x'] - current_scroll + o['window_rect']['x'] +
|
||||
a_cfg[0] + b_cfg[0] + 35)
|
||||
b_y = int(o['rect']['y'] + o['window_rect']['y'] +
|
||||
a_cfg[1] + b_cfg[1] + 30)
|
||||
before_b = len(re.findall(r'POINTER_BUTTON target=POPUP_B[^\n]*button=273 state=1',
|
||||
client_text()))
|
||||
inject('move', b_x, b_y)
|
||||
inject('right', b_x, b_y)
|
||||
wait(lambda: len(re.findall(r'POINTER_BUTTON target=POPUP_B[^\n]*button=273 state=1',
|
||||
client_text())) > before_b)
|
||||
record('post_return_B_button', global_point=[b_x, b_y], owner_id=o['id'])
|
||||
snapshot('identity-post-return-B-button')
|
||||
client_command('close_b')
|
||||
client_command('close_a')
|
||||
snapshot('identity-closed')
|
||||
elif args.kind == 'outside':
|
||||
snapshot('outside-S0-reference')
|
||||
open_chain('outside')
|
||||
inject('move', 100, 350)
|
||||
inject('left', 100, 350)
|
||||
time.sleep(.18)
|
||||
snapshot('outside-dismissed')
|
||||
elif args.kind == 'hidden_click':
|
||||
snapshot('hidden-click-S0-reference')
|
||||
open_chain('hidden-click')
|
||||
scroll(1280)
|
||||
snapshot('hidden-click-hidden')
|
||||
before_buttons = len(re.findall(r'POINTER_BUTTON target=POPUP_[AB][^\n]*state=1',
|
||||
client_text()))
|
||||
inject('move', -700, 190)
|
||||
inject('right', -700, 190)
|
||||
wait(lambda: 'POPUP_B POPUP_DONE' in client_text() and
|
||||
'POPUP_A POPUP_DONE' in client_text())
|
||||
after_buttons = len(re.findall(r'POINTER_BUTTON target=POPUP_[AB][^\n]*state=1',
|
||||
client_text()))
|
||||
record('hidden_click_target', popup_buttons_before=before_buttons,
|
||||
popup_buttons_after=after_buttons)
|
||||
snapshot('hidden-click-dismissed')
|
||||
scroll(400)
|
||||
snapshot('hidden-click-returned')
|
||||
else:
|
||||
snapshot('toggle-S0-reference')
|
||||
open_chain('toggle')
|
||||
command('layout splith')
|
||||
snapshot('toggle-standard-immediate')
|
||||
time.sleep(.4)
|
||||
snapshot('toggle-standard-stable')
|
||||
o = owner()
|
||||
a_cfg, b_cfg = configured('POPUP_A'), configured('POPUP_B')
|
||||
b_x = int(max(20, o['rect']['x'] + o['window_rect']['x'] +
|
||||
a_cfg[0] + b_cfg[0] + 20))
|
||||
b_y = int(o['rect']['y'] + o['window_rect']['y'] + a_cfg[1] + b_cfg[1] + 30)
|
||||
if b_x < 0:
|
||||
raise RuntimeError('standard B sample does not cross x=0')
|
||||
inject('move', b_x, b_y)
|
||||
inject('right', b_x, b_y)
|
||||
snapshot('toggle-standard-click')
|
||||
client_command('close_b')
|
||||
client_command('close_a')
|
||||
snapshot('toggle-closed')
|
||||
except Exception as exc:
|
||||
failure = repr(exc)
|
||||
record('failure', error=failure)
|
||||
finally:
|
||||
for proc in children + ([compositor] if compositor is not None else []):
|
||||
if proc.poll() is None:
|
||||
try:
|
||||
os.killpg(proc.pid, signal.SIGTERM)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
for proc in children + ([compositor] if compositor is not None else []):
|
||||
try:
|
||||
proc.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
os.killpg(proc.pid, signal.SIGKILL)
|
||||
proc.wait(timeout=5)
|
||||
(run / 'exit-status.json').write_text(json.dumps({
|
||||
'failure': failure, 'compositor': None if compositor is None else compositor.returncode,
|
||||
'children': [{'argv': p.args, 'returncode': p.returncode} for p in children],
|
||||
'captures': capture_count, 'capture_bytes': capture_bytes,
|
||||
'duration_seconds': time.monotonic() - started}, indent=2) + '\n')
|
||||
for file in files:
|
||||
file.close()
|
||||
shutil.rmtree(runtime)
|
||||
record('cleanup', runtime_removed=not runtime.exists())
|
||||
print(f'RUN={run} FAILURE={failure} CAPTURES={capture_count}')
|
||||
if failure:
|
||||
raise SystemExit(1)
|
||||
Loading…
Reference in a new issue