From 89567ba2463ebd2aeae58986a58640495568aaa0 Mon Sep 17 00:00:00 2001 From: fy59 Date: Mon, 12 Oct 2026 17:20:06 +0200 Subject: [PATCH] docs(mosaic): preserve rotated partial popup clipping evidence --- CHANGELOG.md | 2 + docs/ARCHITECTURE.md | 9 + docs/KNOWN_LIMITATIONS.md | 10 + docs/README.md | 8 + docs/ROADMAP.md | 9 + ...TEP20C_ROTATED_PARTIAL_POPUP_CLIP_PROBE.md | 207 +++++++ .../step20c-rotated-partial-popup/README.md | 45 ++ .../__pycache__/analyze.cpython-314.pyc | Bin 0 -> 20502 bytes .../__pycache__/budget.cpython-314.pyc | Bin 0 -> 4278 bytes .../__pycache__/run.cpython-314.pyc | Bin 0 -> 49396 bytes .../__pycache__/test-budget.cpython-314.pyc | Bin 0 -> 2170 bytes .../step20c-rotated-partial-popup/analyze.py | 213 +++++++ .../step20c-rotated-partial-popup/budget.py | 45 ++ .../step20c-rotated-partial-popup/prepare.sh | 23 + .../rot-cross-client.patch | 13 + .../step20c-rotated-partial-popup/run.py | 538 ++++++++++++++++++ .../test-budget.py | 40 ++ 17 files changed, 1162 insertions(+) create mode 100644 docs/design/STEP20C_ROTATED_PARTIAL_POPUP_CLIP_PROBE.md create mode 100644 tooling/probes/step20c-rotated-partial-popup/README.md create mode 100644 tooling/probes/step20c-rotated-partial-popup/__pycache__/analyze.cpython-314.pyc create mode 100644 tooling/probes/step20c-rotated-partial-popup/__pycache__/budget.cpython-314.pyc create mode 100644 tooling/probes/step20c-rotated-partial-popup/__pycache__/run.cpython-314.pyc create mode 100644 tooling/probes/step20c-rotated-partial-popup/__pycache__/test-budget.cpython-314.pyc create mode 100644 tooling/probes/step20c-rotated-partial-popup/analyze.py create mode 100644 tooling/probes/step20c-rotated-partial-popup/budget.py create mode 100644 tooling/probes/step20c-rotated-partial-popup/prepare.sh create mode 100644 tooling/probes/step20c-rotated-partial-popup/rot-cross-client.patch create mode 100644 tooling/probes/step20c-rotated-partial-popup/run.py create mode 100644 tooling/probes/step20c-rotated-partial-popup/test-budget.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 91cd54d..abdbb1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### STEP20C experimental evidence +- Local static-R90 partial-popup probe: a legal nested B popup crossed the Mosaic viewport by 72 logical pixels; 46,455 B pixels and a real button survived on H1, while H2 was pixel-identical to its reference and delivered no B button. Both standard controls showed 35,360 B pixels and a B button on H2. A crossed visually, but its forbidden-side input was not independently clicked. PARTIALLY_FEASIBLE for the tested matrix; no production change or phase-B commit/push. + - Preserved rotated-output probe: actual 90° output keeps an 800×600 logical viewport while the physical mode becomes 750×1000 and `grim` remains logical-oriented 1000×750. Private standard shows 15,860 B-popup pixels on H2; private Mosaic shows zero, but its B popup ends before the boundary, so partial clipping is unchallenged. Fully hidden owner, pointer, measured physical damage and R0/standard returns pass sampled checks. Pristine R90 capture stopped on a fixture IPC-direction assertion; no replay. PARTIALLY_FEASIBLE, architectural conclusion UNDECIDED; no production change. - Preserved dynamic-output ownership probe: real 800→960→800 logical resize and H1→H2→H1 workspace migration preserve IDs and sampled applied clips. Exact vacated-output backgrounds and measured physical damage are compatible; two standard-control final calibrations fail through reference occlusion. PARTIALLY_FEASIBLE; maximum clamp, migrated clipped-popup input, opaque spill and effects remain open. Six reconfigurations/six migrations, zero extra camera pairs; no production change. The original temporary evidence manifest was verified before an offline analyzer rerun replaced it; the report records the provenance limit. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 75bed48..d9b54d6 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -270,3 +270,12 @@ The `grim` PNG is logical-oriented at 1000×750 while the R90 physical mode is and the Mosaic B popup never reached the boundary, so partial popup clipping under rotation remains **UNDECIDED**. No applied-geometry generation contract or production behavior is accepted by this evidence. + +The separate [static R90 partial-popup probe](design/STEP20C_ROTATED_PARTIAL_POPUP_CLIP_PROBE.md) +measured a real Mosaic B box crossing x=0 by 72 logical pixels, with B +visible/clickable inside H1 and absent visually and from client delivery on +H2. Its applied root clip and nested inheritance matched the H1 owner +viewport in the sampled state; pristine and private-standard controls both +drew and handled the crossed portion. Result **PARTIALLY_FEASIBLE** because +the clipped-away part of A was not independently clicked and no atomic +render/input generation was proved. The architecture remains experimental. diff --git a/docs/KNOWN_LIMITATIONS.md b/docs/KNOWN_LIMITATIONS.md index f4f0e50..232ae64 100644 --- a/docs/KNOWN_LIMITATIONS.md +++ b/docs/KNOWN_LIMITATIONS.md @@ -209,3 +209,13 @@ result is **PARTIALLY_FEASIBLE**, architectural partial-clipping conclusion Mosaic popup stayed wholly inside the viewport despite standard control spill. Scored border hits, rotated shadow/blur/cache and raw buffer readback are **NOT TESTED**. No production support claim follows. + +The subsequent [static R90 partial-popup probe](design/STEP20C_ROTATED_PARTIAL_POPUP_CLIP_PROBE.md) +closed the missing B geometry challenge for one legal XDG placement: +Mosaic B crossed by 72 logical pixels; sampled H1 visual/button input +survived and H2 matched its prior reference with no B delivery. Its +overall status is **PARTIALLY_FEASIBLE**. The A root popup also crossed, +but independent input on its forbidden portion remains **NOT TESTED**. +Saved buffers, optimized blur caches, inner shadows, intermediate frames, +atomic generation and SceneFX/wlroots ABI/package remain unqualified. +Multi-output stays **NOT YET SUPPORTED**. diff --git a/docs/README.md b/docs/README.md index 151a137..9ade9cc 100644 --- a/docs/README.md +++ b/docs/README.md @@ -120,3 +120,11 @@ sampled damage and return. Pristine R90 capture and a Mosaic popup portion actually crossing the boundary were not exercised; architectural clipping conclusion UNDECIDED. STEP20C remains NEXT; production multi-output remains NOT YET SUPPORTED. + +[Static R90 partial-popup probe](design/STEP20C_ROTATED_PARTIAL_POPUP_CLIP_PROBE.md) +and [fixture](../tooling/probes/step20c-rotated-partial-popup/README.md): +EXPERIMENTAL / PARTIALLY_FEASIBLE. Mosaic B crossed x=0 by 72 logical pixels; +its H1 pixels and real button remained, while H2 matched its reference and +delivered no B button. Both standard controls drew and handled the overflow. +Independent clipped-away A input and saved buffers remain open. STEP20C is +NEXT; production multi-output is NOT YET SUPPORTED. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index b26bad8..f233496 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -212,3 +212,12 @@ is **UNDECIDED** because the Mosaic popup never crossed the R90 boundary and the pristine R90 capture was lost to a fixture assertion. The next distinct geometric gate should force a legal grabbed Mosaic popup portion across that boundary with a completed pristine control. STEP20C remains **NEXT**. + +The [static R90 partial-popup gate](design/STEP20C_ROTATED_PARTIAL_POPUP_CLIP_PROBE.md) +then measured exactly that B crossing and completed the pristine control. +The bounded result is **PARTIALLY_FEASIBLE**: B's sampled pixels/input obeyed +the owner clip, while the independent clipped-away A input point and broader +temporal/effect cases remain open. Next prioritize saved transaction buffers +against the same owner-derived viewport before optimized blur cache, +interior shadow and ABI/package maintenance. STEP20C remains **NEXT**; +production multi-output remains **NOT YET SUPPORTED**. diff --git a/docs/design/STEP20C_ROTATED_PARTIAL_POPUP_CLIP_PROBE.md b/docs/design/STEP20C_ROTATED_PARTIAL_POPUP_CLIP_PROBE.md new file mode 100644 index 0000000..9f621e5 --- /dev/null +++ b/docs/design/STEP20C_ROTATED_PARTIAL_POPUP_CLIP_PROBE.md @@ -0,0 +1,207 @@ +# STEP20C static R90 partial popup clipping probe + +**EXPERIMENTAL protocol, before execution.** STEP20C is NEXT; production +multi-output Mosaic is NOT YET SUPPORTED. The preceding +[rotation probe](STEP20C_ROTATED_OUTPUT_PROBE.md) was PARTIALLY_FEASIBLE: +private standard displayed 15,860 B pixels on H2, but Mosaic B ended at +PNG x=903, short of the x=1000 boundary. Its pristine R90 fixture stopped +after applying transform `90` because it incorrectly expected IPC `270`. +This independent campaign tests only a static R90 startup, with no dynamic +transform, resize, migration, camera hide/return, effects or algorithm change. + +## Fixed geometry and model + +H1 starts at global (-800,0), physical 750×1000, IPC transform `90`, scale +1.25, logical 800×600. H2 starts at (0,0), physical/logical 800×600, normal +at scale 1. Read `get_outputs` before scoring. The expected `grim -s 1.25` +H1 PNG is logically oriented 1000×750 according to the earlier probe; its +actual dimensions and five distinct markers must be measured anew. The +internal Wayland transform enum `270`, physical buffer and logical PNG are +different coordinate systems. Pointer injection uses the measured logical +union (-800..800, 0..600), with sent x = global x + 800. + +Use one fixed XDG placement `rot_cross`: TOPLEVEL 420×320; A 480×170 with +anchor rect (360,80,1,1); B 300×130 with anchor rect (210,60,1,1). +Both anchors fit their parent dimensions. Positioners use TOP_LEFT anchor, +BOTTOM_RIGHT gravity and no constraint adjustment requested by the client. +Actual `xdg_popup.configure` x/y/w/h, ack, map and scene placement must be +recorded; compositor unconstrain may change them. No alternate placement is +authorized in this campaign. + +The prior applied Mosaic owner plus camera 640 placed B's right edge near +global -78 (PNG x=903). Moving A's anchor +160 and B's -10 predicts B's +right edge near +72, enough for a discriminating H2 portion. A's width +adds 180, predicting an A right edge near +42. These are **predictions**, not +observations. The original `(390,80)` candidate would shift B by +180: the +earlier standard B left edge near -178 would move to +2, removing its H1 +portion. This single pre-run variant shifts B by +150 and predicts standard +B left near -28, retaining 28 logical pixels on H1. The owner, workspace, +applied camera and popup root scene +coordinates must be measured separately per mode. Compute each global popup +box from the applied owner/scene origin and configured XDG offsets, then +intersect with H1 [-800,0)×[0,600) and H2 [0,800)×[0,600). +Partial clipping is exercised only when left < 0 < right and both intersections +are nonempty. A crossing less than 16 logical pixels is reported with its +actual width before qualification. A box wholly within H1 is NOT EXERCISED, +even when H2 contains zero popup pixels. + +## Runs, observations and decisions + +One run each: C0 pristine pinned SwayFX `splith`, C1 private `splith`, C2 +same private compositor `mosaic`. Static output config is applied at startup. +Each run uses one 0700 runtime and explicit private IPC socket, at most two +distinct A/B grab chains, 16 captures and 120 seconds. The entire campaign +allows at most 3 runs and 50 captures; failed reservations count. A persistent +exclusive ledger is tested offline and reserved before each launch. A single +initial Mosaic scroll to 640 is permitted before popup creation, logged once. + +Capture a five-marker H1 background and stable H2 reference before popup +creation. Record capture monotonic windows, SHA-256, mode/scale/transform, +logical rect, PNG dimensions, popup boxes and clips. Compare H2's entire +forbidden intersection to its predeclared reference as well as exact A/B +colors, without post-hoc ROI exclusions. At least one standard control must +show B pixels on both outputs and a real B button on H2. Mosaic must show +its own geometric B crossing, B pixels and a delivered button inside H1, +zero B contribution and no B delivery on H2. Derive boundary y from the +applied popup box and inspect x=-3..3. Keep hover probes in one chain; use +a second chain for an exterior click that may dismiss the grab. A is evaluated +separately at points outside B when its own box crosses. + +The client must log a real button serial on TOPLEVEL before A's grab and on +A before B's grab, each configure/ack/map and protocol error. The runner +records applied tiled/root/child clips, owner output, damage in buffer-local +physical coordinates, commits/presents and actual client delivery. Damage +from the inherited `TEMPORAL_DAMAGE` path is already physical: do not apply +scale twice. Settled PNG differences do not prove every intermediate frame +or an atomic render/input generation. + +FEASIBLE requires valid crossing B in Mosaic, standard overflow controls, +retained H1 pixels/input, excluded H2 pixels/input, correct owner clips and +completed pristine R90. PARTIALLY_FEASIBLE preserves a crossing with missing +secondary proof. INCONCLUSIVE covers absent crossing, invalid fixture or +incomparable controls. BLOCKED requires a reproducible forbidden pixel/hit, +wrong owner clip, crash or standard regression. A confirmed leak stops the +affected campaign; no clipping algorithm is changed to obtain a pass. +R270, saved buffers, optimized blur cache, inner shadow, raw framebuffer, +all intermediate frames and ABI/package maintenance remain NOT TESTED. + +The [source fixture](../../tooling/probes/step20c-rotated-partial-popup/README.md) +retains only reviewed scripts and the client placement patch. Generated +builds, binaries, captures, sockets, logs and structured results stay in a +private LAB. + +## Measured static-R90 campaign — 2026-10-12 + +**EXPERIMENTAL / PARTIALLY_FEASIBLE.** LAB: +`/tmp/swayfx-mosaic-nested-grab-PioJ5X43`. The new preparer rebuilt the pinned +pristine SwayFX, private Mosaic/SceneFX and strict C17 clients from the prior +published source chain, then applied only the dedicated `rot_cross` client +patch. The clipping algorithm and installed libraries were untouched. One +main run each of pristine `splith`, private `splith` and private `mosaic` +completed normally, with 14 captures and about 14.6 seconds per run. The +new locked ledger consumed **3/3** runs; dynamic transitions, migrations, +resize, camera hide/return and R270 each consumed **0**. Mosaic made one +pre-popup static camera placement to scroll 640, recorded in the raw events. + +All three `get_outputs` states reported H1 physical 750×1000, IPC transform +`90`, scale 1.25 and logical (-800,0,800,600). H2 remained 800×600, +normal/scale 1 at (0,0). Every H1 `grim -s 1.25` PNG was 1000×750 and the +five marker centers established the logical PNG orientation. The control +pristine therefore completed the R90 capture and input matrix missed by the +historical fixture; no dynamic R90→R0 action occurred. Its two grabs used +real button serials 121/126, configured, acked, mapped, and exited without +protocol error. Private standard used 123/128; Mosaic used 105/110. The +configured XDG offsets were A (360,80,480,170) and B (210,60,300,130) +in all modes, matching the fixed legal 1×1 anchors. The actual source scene +and owner positions produced these global half-open boxes: + +| Mode | Applied owner x / camera | A global | A∩H1 / A∩H2 widths | B global | B∩H1 / B∩H2 widths | +| --- | --- | --- | --- | --- | --- | +| Pristine `splith` | -600 / 0 | [-238,242)×[105,275) | 238 / 242 | [-28,272)×[165,295) | 28 / 272 | +| Private `splith` | -600 / 0 | [-238,242)×[105,275) | 238 / 242 | [-28,272)×[165,295) | 28 / 272 | +| Private Mosaic | -160 / 640 | [-438,42)×[105,275) | 438 / 42 | [-228,72)×[165,295) | 228 / 72 | + +The Mosaic owner current rect was x=-160 with width 640, while its applied +scene origin was x=-800 after scroll 640. Its content/window x offset was 2, +giving root-popup global (-798,25). A starts there plus configured (360,80), +B starts at A plus configured (210,60). The sampled `DO_STATE` reported +internal transform 3 (`WL_OUTPUT_TRANSFORM_270`), physical 750×1000 and +logical 800×600. `PI_SCENE` held tile global -1440 and local clip +(640,0,800,600), hence viewport [-800,0)×[0,600). `DO_POPUP` identified +owner 8 on H1 and local root clip (-2,-25,800,600), which yields the same +global viewport. B remained a scene descendant of A and inherited that +root clip. The scene and configured XDG offsets agree at the sampled applied +state; this does not establish an atomic generation over all frames. + +The predefined reference was each mode's `right-reference.png`, captured +before A/B creation. At the mapped-B capture, pristine and private standard +each had **5,705 exact B pixels on H1** and **35,360 on H2**; their H2 +full-image difference from reference contained **49,880 pixels**, with box +[0,105,272,295). Mosaic retained **46,455 B pixels on H1**, including +163/489/815 pixels in its last 1/3/5 PNG columns, and **zero B pixels on +H2**. Its H2 whole PNG was **pixel-identical** to the +predeclared reference: zero differing pixels, not merely zero exact SHM +colors. A itself also crossed: Mosaic showed 77,181 A pixels on H1 and +zero on H2; the standards showed 58,431 on H1 and 14,520 on H2. The A +count includes occlusion by B where their rectangles overlap; it is not a +full theoretical A-area count. The measured B geometry, unlike the old +probe, genuinely challenged the clip by 72 logical pixels on H2. + +For B's y=230 boundary grid, the pristine and private-standard clients +reported B local motions x=25..31 for global x=-3..3 and delivered right +buttons on B at (-14,230) and (30,230). The Mosaic client reported B local +motions x=225,226,227 at global x=-3,-2,-1, then a B leave at x=0; no +B enter/motion followed for x=0..3. A right button at (-30,230) was delivered +to B with serial 119. At forbidden (30,230), B received **no button**; +the valid grab produced `popup_done` for B and A. The button on visible A +that created B was genuinely delivered (serial 110). An independent click +on the clipped-away portion of **A** was not made; P2 root-popup input is +therefore PARTIAL even though A's geometry and pixels cross. No universal +click-through contract is inferred from the dismissal. + +Available `TEMPORAL_DAMAGE` extents for the settled reference→mapped-B +Mosaic H1 pair covered all **123,636 changed logical-PNG pixels** after +mapping `(x,y)` to physical `(749-y,x)`: 10 physical extents, zero uncovered. +H2 changed zero pixels. The private-standard pair likewise had zero +uncovered changed pixels on both outputs (10 H1 and 6 H2 extents). The +pristine control had no passive damage trace; its nonzero screenshot diff +cannot be scored against absent extents. These broad damage observations +cover only the settled pair, not minimal invalidation, all intermediate +frames, saved buffers or render/input generation identity. Private Mosaic +logged 126 native commits and 124 presents during the whole run; private +standard logged 119/117. Startup, pointer and teardown activity are included. + +`LAB/results/` retains geometry, positions/intersections, output transform, +grab, applied clip, input, pixels, damage, matrix and summary JSON. Raw +client/protocol, compositor, pointer, `get_outputs`/tree, PNG and capture +windows remain under each `LAB/runs/` directory. The final source and +evidence manifest entry counts and SHA-256 values are recorded below after +documentation synchronization: **910 source entries**, +`3e6930c46dd9abf50aa2720357ccbdd47ac3fdd0f1106a47734383cb281b41a5`; +**146 evidence entries**, +`e3c13e4fc02f6b66b59b87407ef7b91f5b7cd67430cc1142252098ef3885a949`. +The dedicated placement patch SHA-256 is +`0f45d48625c062ca05f36849068aedecd02e4480a98f3e0bafe98c1f1c529089`, +based on nested client SHA-256 +`2e40c98af72a0df8b316810589a0be8f4425a763db1de448b49a7813956b6f7d`. +Both manifest files passed entry-by-entry verification after the fixture +README was synchronized and the CLI limits were made non-overridable above +120 seconds / 16 captures / 30 MB. Each run retains its executed +`fixture-source.py`; that exact source precedes this post-run limit hardening. +The hardening was tested offline with an over-limit argument and caused no +fourth run or ledger change. The ledger is separate +from the historical 5/6 rotation and 1/1 camera budget, which were not +reopened. Every run used a private 0700 runtime and explicit private IPC; +exit status and cleanup were retained. No production, package or UX change +was made. + +**Verdict:** `SWAYFX_MOSAIC_ROTATED_PARTIAL_POPUP_CLIP_PROBE=PARTIALLY_FEASIBLE`. +The central B crossing/visible/clipped/input distinction passed this one +static-R90 matrix, including a complete pristine control. The scored +clipped-away A input point, all-frame damage and applied generation are +still open. Architectural conclusion: +`ROTATED_PARTIAL_POPUP_CLIP_FEASIBLE_FOR_TESTED_MATRIX`, without production +STEP20C qualification. Saved transaction buffers are the next priority: +they can reintroduce pixels from an earlier scene at the same viewport even +when the live popup tree clips correctly. Optimized blur cache, interior +shadow and SceneFX/wlroots ABI/package maintenance remain separate gates. diff --git a/tooling/probes/step20c-rotated-partial-popup/README.md b/tooling/probes/step20c-rotated-partial-popup/README.md new file mode 100644 index 0000000..bb8fef3 --- /dev/null +++ b/tooling/probes/step20c-rotated-partial-popup/README.md @@ -0,0 +1,45 @@ +# Static R90 partial popup fixture + +**EXPERIMENTAL / PARTIALLY_FEASIBLE.** See the +[scientific report](../../../docs/design/STEP20C_ROTATED_PARTIAL_POPUP_CLIP_PROBE.md). +The fixed placement is `rot_cross`: A (360,80), 480×170; B (210,60), +300×130; toplevel 420×320. It was fixed before the three runs. + +Use a new private LAB and a locked ledger; the previous rotation laboratory +and its consumed budget are read-only. The runner must use startup R90 config, +one run per mode, explicit private sockets, at most 16 captures per run and +120 seconds. No production source, clipping algorithm, installed library or +daily session changes are permitted. Generated files remain outside Git. + +The completed LAB is `/tmp/swayfx-mosaic-nested-grab-PioJ5X43`; its ledger +has consumed all three runs. **Do not run those modes again or reset the +ledger.** Source-only preparation in another authorized campaign uses: + +```sh +sh tooling/probes/step20c-rotated-partial-popup/prepare.sh +``` + +This rebuilds pinned private sources and applies `rot-cross-client.patch` +to the [nested client](../step20c-nested-grab/nested-client.c), whose base +SHA-256 is `2e40c98af72a0df8b316810589a0be8f4425a763db1de448b49a7813956b6f7d`. +The patch preserves earlier placements and adds only `rot_cross`. +`prepare.sh` does not launch a compositor. + +The completed campaign used these one-run commands, in order: + +```sh +PYTHONDONTWRITEBYTECODE=1 python3 tooling/probes/step20c-rotated-partial-popup/test-budget.py +PYTHONDONTWRITEBYTECODE=1 python3 tooling/probes/step20c-rotated-partial-popup/run.py "$LAB" control +PYTHONDONTWRITEBYTECODE=1 python3 tooling/probes/step20c-rotated-partial-popup/run.py "$LAB" private_standard +PYTHONDONTWRITEBYTECODE=1 python3 tooling/probes/step20c-rotated-partial-popup/run.py "$LAB" private_mosaic +PYTHONDONTWRITEBYTECODE=1 python3 tooling/probes/step20c-rotated-partial-popup/analyze.py "$LAB" +``` + +`LAB/results/` contains structured analysis; `LAB/runs/` contains raw +captures, client/protocol events, applied traces and per-run exit codes. +The source manifest covers 910 files and the evidence manifest 146 files; +their final SHA-256 values are recorded in the scientific report. +Mosaic B crossed by 72 logical pixels, remained visible/clickable on H1 +and was absent from H2 pixels/client delivery. Both standards visibly +crossed. Independent forbidden-side A input was not tested, so the verdict +stays PARTIALLY_FEASIBLE. diff --git a/tooling/probes/step20c-rotated-partial-popup/__pycache__/analyze.cpython-314.pyc b/tooling/probes/step20c-rotated-partial-popup/__pycache__/analyze.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..88796c542f71e9b1e8fc189c076b8092a0dd0bfe GIT binary patch literal 20502 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 XE8O{#f9}Y0@0I4Zv^+Lyl3P$|8<^gm-C+aW~V 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 JtVRY3e*p*^9HIaK literal 0 HcmV?d00001 diff --git a/tooling/probes/step20c-rotated-partial-popup/__pycache__/test-budget.cpython-314.pyc b/tooling/probes/step20c-rotated-partial-popup/__pycache__/test-budget.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..79393fac1088a921638d1b298b7681d7aa9069be GIT binary patch literal 2170 zcmb_cPfXiZ7=MoK_>mW!2BwbkXECs81p*AGAB^W0o z!3Nu%T>uiy5H;};0Lxf&C-?xYP#xgk7_e?nH%Pc0#uZ8cN#q6@>_{gFfG)7eC?G<2 z0foANtFHfy8oogr*Qo3Qu6pnP$ORj@F5P9*WxB*{4X03Z$kXty_k4#s@LZ5<44(j5 z0kGIAfC2D!H3Z8p;;Mots0)8bu0^`$?f)@75W9wIrSK!(af%(}Oe5~J)JNG&Tr^kS$G|;ZzyzkvNowBI)vV3v zg}kxWk|}6eMG{R}5G6_0OgUp;(R2kf$SK1V^BGak*u1loQw>p(*8K7pD`LSQMTGRK zCK|>X%Bs@+T41}ELOz9TDSJ>L zv_YZ9Ggx)u*FW*3TeX`qbx3E$w9QrRlWl&=z&?@&@94U!+n#%3wjfurZ<>h0ZOzB38*J`A z=E082V#a2%S9$Ym@RCfD9WhlEpWkvw)77+WgmAao$gni1t0s0hGpLEUh?pJJRIQ+e z$mtGNH4ka4HoTcrGli@?sgoZ9`I9wD__IP&6m<-ypM$oafbTiD_A}dj$O=EO!XevJ zVtXDd#~yxTbu+X+bKKmzHM9Bgdh`hSA5K1)eAHJ&Z$7?PM!}tHJE(*r789{hq~c=P z(Idz|!`)fF!CBNiO_qrw62)!zWcHXlPTF3&cKP|RXt7K+uXTJ8NsG_V~lqtTt|G8(rc3njE*F$)%2Af-pnDLq`| z!xjvmUz%lu$B^GJzCtHLA0>w5 zxbfvDhfLc6(^g{IYw^}xne9AedJmZ167v?3JaJNg6CJ{~1K3uA?KQ`46!~!rj-Mx+ zDDgEYdGQ*c+?M;!k05GnJVp~d0cRnz2T0Byg&zYx)aYxZOkm literal 0 HcmV?d00001 diff --git a/tooling/probes/step20c-rotated-partial-popup/analyze.py b/tooling/probes/step20c-rotated-partial-popup/analyze.py new file mode 100644 index 0000000..71c30c4 --- /dev/null +++ b/tooling/probes/step20c-rotated-partial-popup/analyze.py @@ -0,0 +1,213 @@ +"""Offline, single-seal analysis of the static R90 runs and their raw evidence.""" +import argparse +import hashlib +import json +from pathlib import Path +import re + +import numpy as np +from PIL import Image + +COLORS = {'A': (224, 192, 32), 'B': (32, 208, 160)} +TRACE = re.compile(r'\b(DO_STATE|DO_POPUP|PI_SCENE|PI_OWNER|PI_OUTPUT_COMMIT|PI_OUTPUT_PRESENT)\b') +DAMAGE = re.compile(r'TEMPORAL_DAMAGE ordinal=\d+ ns=(\d+) output=(\S+) ' + r'extent=(-?\d+),(-?\d+)\.\.(-?\d+),(-?\d+)') + + +def digest(path): + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def write(path, obj): + path.write_text(json.dumps(obj, indent=2, sort_keys=True) + '\n') + + +def fields(line): + return dict(re.findall(r'(\w+)=([^\s]+)', line)) + + +def bbox(mask): + yy, xx = np.where(mask) + return [int(xx.min()), int(yy.min()), int(xx.max()) + 1, int(yy.max()) + 1] if len(xx) else None + + +def latest(traces, kind, before, **constraints): + found = [x for x in traces if x['kind'] == kind and x['ns'] <= before and + all(x.get(k) == v for k, v in constraints.items())] + return found[-1] if found else None + + +def image(path): + return np.asarray(Image.open(path).convert('RGB')) + + +def colors(path): + im = image(path) + return {name: {'pixels': int(mask.sum()), 'box': bbox(mask)} + for name, color in COLORS.items() + for mask in [np.all(im == color, axis=2)]} + + +def damage_coverage(run, a, b, output, damages): + before, after = image(run / a['path']), image(run / b['path']) + changed = np.any(before != after, axis=2) + yy, xx = np.where(changed) + width, height = (750, 1000) if output == 'HEADLESS-1' else (800, 600) + mask = np.zeros((height, width), dtype=bool) + extents = [d for d in damages if d[1] == output and + a['after_ns'] < d[0] <= b['after_ns']] + for _, _, x1, y1, x2, y2 in extents: + mask[max(0, y1):min(height, y2), max(0, x1):min(width, x2)] = True + # WHY: grim H1 is logical-oriented; inherited damage extents already use + # physical buffer-local coordinates after SceneFX inverse rotation. + # CONTRACT: measured static R90 maps PNG (x,y) to buffer (749-y,x). + px, py = (width - 1 - yy, xx) if output == 'HEADLESS-1' else (xx, yy) + return {'from': a['tag'], 'to': b['tag'], 'output': output, + 'changed_png_pixels': int(len(xx)), 'changed_box_png': bbox(changed), + 'physical_extents_count': len(extents), + 'uncovered_changed_pixels': int(np.count_nonzero(~mask[py, px])), + 'physical_extents': [list(d[2:]) for d in extents], + 'scope': 'settled PNG pair only; no intermediate-frame claim'} + + +parser = argparse.ArgumentParser() +parser.add_argument('lab', type=Path) +args = parser.parse_args() +lab = args.lab.resolve() +out = lab / 'results' +if out.exists(): + raise SystemExit('sealed results exist; refusing overwrite') +runs = sorted((lab / 'runs').glob('*-static-r90-*')) +if len(runs) != 3: + raise SystemExit('exactly three retained runs required') +budget = json.loads((lab / 'partial-campaign-budget.json').read_text()) +if sorted(budget['runs']) != ['control', 'private_mosaic', 'private_standard']: + raise SystemExit('campaign ledger incomplete') + +outputs, geometry, intersections, grabs, clips = {}, {}, {}, {}, {} +hits, delivery, pixels, damages_out, matrix, summary_runs = {}, {}, {}, {}, {}, {} +source_paths = [] +for tree in (lab / 'mosaic', lab / 'scenefx', lab / 'standard'): + source_paths.extend(p for p in tree.rglob('*') if p.is_file()) +source_paths.extend(p for p in lab.glob('*.c')) +source_paths.extend(p for p in lab.glob('*.h')) +source_paths.extend(p for p in Path(__file__).parent.glob('*') if p.is_file()) + +for run in runs: + mode = run.name.split('-static-r90-', 1)[0] + events = [json.loads(line) for line in (run / 'events.jsonl').read_text().splitlines()] + by_kind = lambda kind: [e for e in events if e['kind'] == kind] + geom = by_kind('applied_popup_geometry')[-1] + gate = by_kind('geometry_gate')[-1] + client = (run / 'client.log').read_text(errors='replace') + log = (run / 'compositor.log').read_text(errors='replace') + traces = [] + for line in log.splitlines(): + m = TRACE.search(line) + if m: + item = {'kind': m[1], **fields(line)} + if item.get('ns', '').isdigit(): + item['ns'] = int(item['ns']) + traces.append(item) + damage = [(int(ns), output, *map(int, (x1, y1, x2, y2))) + for ns, output, x1, y1, x2, y2 in DAMAGE.findall(log)] + captures = {(e['tag'], e['output']): e for e in by_kind('capture')} + capture_geometry = {(e['tag'], e['output']): e for e in by_kind('capture_geometry')} + outputs[mode] = {'states': by_kind('state'), 'capture_geometry': by_kind('capture_geometry'), + 'markers': by_kind('marker_calibration'), + 'pointer_calibration': by_kind('pointer_calibration')} + geometry[mode] = geom + intersections[mode] = {k: geom[k] for k in ('a_h1', 'a_h2', 'b_h1', 'b_h2')} + parsed_grabs = [{'popup': p, 'target': t, 'serial': int(s), 'used': int(u), + 'real_button': bool(re.search(rf'POINTER_BUTTON target={t} serial={s} button=272 state=1', client))} + for p, t, s, u in re.findall( + r'(POPUP_[AB]) GRAB_REQUEST origin=wl_pointer.button target=(\S+) serial=(\d+) used=(\d+)', client)] + grabs[mode] = {'requests': parsed_grabs, + 'role_configures': re.findall(r'(POPUP_[AB]) ROLE_CONFIGURE x=(-?\d+) y=(-?\d+) w=(\d+) h=(\d+)', client), + 'xdg_configure_acks': re.findall(r'(POPUP_[AB]) XDG_CONFIGURE serial=(\d+) ack', client), + 'maps': re.findall(r'(POPUP_[AB]) MAP wl=(\d+) xdg=(\d+)', client), + 'protocol_errors': [line for line in client.splitlines() if 'protocol error' in line.lower()]} + capture = captures[('rot-cross-S2-B', 'HEADLESS-1')] + before = capture['before_ns'] + clips[mode] = {'output_state': latest(traces, 'DO_STATE', before, observer='HEADLESS-1'), + 'tiled': latest(traces, 'PI_SCENE', before, output='HEADLESS-1'), + 'root_popup': latest(traces, 'DO_POPUP', before, owner_id=str(geom['owner_id'])), + 'owner': latest(traces, 'PI_OWNER', before, id=str(geom['owner_id'])), + 'native_commits': sum(t['kind'] == 'PI_OUTPUT_COMMIT' for t in traces), + 'native_presents': sum(t['kind'] == 'PI_OUTPUT_PRESENT' for t in traces)} + hits[mode] = {'border_hovers': by_kind('border_hover'), + 'visible_b_button': by_kind('visible_b_button'), + 'forbidden_b_button': by_kind('forbidden_b_button')} + delivery[mode] = {'client_grabs': grabs[mode], + 'visible_b_events': by_kind('visible_b_button')[-1]['client_events'], + 'forbidden_b_events': by_kind('forbidden_b_button')[-1]['client_events'], + 'popup_done_A': client.count('POPUP_A POPUP_DONE'), + 'popup_done_B': client.count('POPUP_B POPUP_DONE'), + 'client_log': str(run / 'client.log')} + pair = {} + for output, side in (('HEADLESS-1', 'left'), ('HEADLESS-2', 'right')): + a, b = captures[('reference', output)], captures[('rot-cross-S2-B', output)] + before_im, popup_im = image(run / a['path']), image(run / b['path']) + diff = np.any(before_im != popup_im, axis=2) + pair[output] = {'reference': {'path': a['path'], 'sha256': digest(run / a['path']), + 'window_ns': [a['before_ns'], a['after_ns']]}, + 'popup': {'path': b['path'], 'sha256': digest(run / b['path']), + 'window_ns': [b['before_ns'], b['after_ns']]}, + 'png_dimensions': list(popup_im.shape[1::-1]), + 'output': capture_geometry[('rot-cross-S2-B', output)], + 'reference_diff_pixels': int(diff.sum()), 'reference_diff_box': bbox(diff), + 'colors': colors(run / b['path'])} + pixels[mode] = pair + damages_out[mode] = [damage_coverage(run, captures[('reference', output)], + captures[('rot-cross-S2-B', output)], output, damage) + for output in ('HEADLESS-1', 'HEADLESS-2')] + status = json.loads((run / 'exit-status.json').read_text()) + matrix[mode] = {'run_failure': status['failure'], 'captures': status['captures'], + 'duration_seconds': status['duration_seconds'], + 'geometry_gate': gate['result'], 'B_box': geom['b_box'], + 'A_box': geom['a_box'], 'grab_count': len(parsed_grabs), + 'H1_B_pixels': pair['HEADLESS-1']['colors']['B']['pixels'], + 'H2_B_pixels': pair['HEADLESS-2']['colors']['B']['pixels'], + 'H2_full_reference_diff': pair['HEADLESS-2']['reference_diff_pixels']} + summary_runs[mode] = {'run': str(run), 'exit': status} + +if not all(matrix[m]['geometry_gate'] == 'CROSSING' for m in matrix): + verdict = 'INCONCLUSIVE' +elif matrix['private_mosaic']['H2_full_reference_diff'] or matrix['private_mosaic']['H2_B_pixels']: + verdict = 'BLOCKED' +elif any(matrix[m]['run_failure'] for m in matrix): + verdict = 'INCONCLUSIVE' +else: + # P2 root-popup input was not independently clicked in the one-chain run. + verdict = 'PARTIALLY_FEASIBLE' + +out.mkdir() +write(out / 'geometry.json', geometry) +write(out / 'popup-positions.json', {m: {'A': x['a_box'], 'B': x['b_box']} for m, x in geometry.items()}) +write(out / 'popup-intersections.json', intersections) +write(out / 'output-transforms.json', outputs) +write(out / 'client-grabs.json', grabs) +write(out / 'scene-clips.json', clips) +write(out / 'pointer-hits.json', hits) +write(out / 'client-delivery.json', delivery) +write(out / 'pixel-differences.json', pixels) +write(out / 'damage.json', damages_out) +write(out / 'budget.json', budget) +write(out / 'matrix.json', matrix) +write(out / 'summary.json', {'result': verdict, 'architectural_conclusion': + 'ROTATED_PARTIAL_POPUP_CLIP_FEASIBLE_FOR_TESTED_MATRIX' + if verdict in ('FEASIBLE', 'PARTIALLY_FEASIBLE') else 'UNDECIDED', + 'runs': summary_runs, 'unexercised': ['separate A input and exterior hit', + 'raw framebuffer', 'intermediate frames', 'saved buffers', + 'optimized blur cache', 'inner shadow', 'ABI/package']}) +write(out / 'source-provenance.json', {'pinned_swayfx': 'fd71a6bdc061bd633b488ae7b83e8a6981d22f86', + 'mosaic_private_base': 'cd550382b311efdbfe3ed892f4e1d281d852e754', + 'scenefx': '37ccd723bef49e6891156ffafce8f549f01446cc', + 'client_base_sha256': '2e40c98af72a0df8b316810589a0be8f4425a763db1de448b49a7813956b6f7d', + 'client_patch_sha256': digest(Path(__file__).parent / 'rot-cross-client.patch')}) +(out / 'source-sha256.txt').write_text(''.join(f'{digest(p)} {p.resolve()}\n' for p in sorted(set(source_paths)))) +evidence = [p for p in sorted(run_file for run in runs for run_file in run.rglob('*') if run_file.is_file())] +evidence += [lab / 'partial-campaign-budget.json', lab / 'partial-build-log.txt'] +evidence += [p for p in out.iterdir() if p.is_file() and p.name != 'evidence-sha256.txt'] +(out / 'evidence-sha256.txt').write_text(''.join(f'{digest(p)} {p.resolve()}\n' for p in sorted(set(evidence)))) +print(f'RESULT={verdict} LAB={lab} SOURCES={len(set(source_paths))} EVIDENCE={len(set(evidence))}') diff --git a/tooling/probes/step20c-rotated-partial-popup/budget.py b/tooling/probes/step20c-rotated-partial-popup/budget.py new file mode 100644 index 0000000..f9f3531 --- /dev/null +++ b/tooling/probes/step20c-rotated-partial-popup/budget.py @@ -0,0 +1,45 @@ +"""Persistent one-run-per-mode budget for the independent static-R90 campaign.""" +import fcntl +import json +import os +from pathlib import Path + +MODES = ('control', 'private_standard', 'private_mosaic') + + +class Budget: + def __init__(self, lab): + self.lab = Path(lab) + self.lock = (self.lab / 'partial-campaign.lock').open('a') + fcntl.flock(self.lock, fcntl.LOCK_EX | fcntl.LOCK_NB) + self.path = self.lab / 'partial-campaign-budget.json' + + def read(self): + if not self.path.exists(): + if (self.lab / 'runs').exists() and any((self.lab / 'runs').iterdir()): + raise ValueError('missing ledger with retained run') + return {'version': 1, 'runs': []} + state = json.loads(self.path.read_text()) + runs = state.get('runs') + if state.get('version') != 1 or not isinstance(runs, list) or len(runs) != len(set(runs)) or any(r not in MODES for r in runs): + raise ValueError('invalid ledger') + return state + + def reserve(self, mode): + state = self.read() + if mode not in MODES or mode in state['runs'] or len(state['runs']) >= 3: + raise ValueError('run budget exhausted or duplicate mode') + state['runs'].append(mode) + tmp = self.path.with_suffix('.tmp') + with tmp.open('w') as output: + json.dump(state, output, indent=2) + output.write('\n') + output.flush() + os.fsync(output.fileno()) + tmp.replace(self.path) + fd = os.open(self.lab, os.O_RDONLY | os.O_DIRECTORY) + try: + os.fsync(fd) + finally: + os.close(fd) + return state diff --git a/tooling/probes/step20c-rotated-partial-popup/prepare.sh b/tooling/probes/step20c-rotated-partial-popup/prepare.sh new file mode 100644 index 0000000..598429c --- /dev/null +++ b/tooling/probes/step20c-rotated-partial-popup/prepare.sh @@ -0,0 +1,23 @@ +#!/bin/sh +# EXPERIMENTAL source-only preparation; never launches a compositor. +set -eu +repo=$(CDPATH= cd -- "$(dirname -- "$0")/../../.." && pwd) +fixture="$repo/tooling/probes/step20c-rotated-partial-popup" +base="$repo/tooling/probes/step20c-nested-grab/nested-client.c" +test "$(sha256sum "$base" | cut -d ' ' -f 1)" = 2e40c98af72a0df8b316810589a0be8f4425a763db1de448b49a7813956b6f7d +log=$(mktemp /tmp/rot-partial-prepare-XXXXXXXX) +if ! sh "$repo/tooling/probes/step20c-dynamic-output/prepare.sh" > "$log" 2>&1; then + printf 'BUILD_FAILED log=%s\n' "$log" >&2 + tail -40 "$log" >&2 + exit 1 +fi +lab=$(sed -n 's/^LAB=//p' "$log") +test -n "$lab" && test -d "$lab" +cp "$base" "$lab/nested-client.c" +patch -d "$lab" -p1 --dry-run < "$fixture/rot-cross-client.patch" >> "$log" 2>&1 +patch -d "$lab" -p1 < "$fixture/rot-cross-client.patch" >> "$log" 2>&1 +cc -std=c17 -Wall -Wextra -Werror -Wno-unused-parameter -I"$lab" \ + "$lab/nested-client.c" "$lab/xdg-shell-protocol.c" -o "$lab/nested-client" \ + $(pkg-config --cflags --libs wayland-client) >> "$log" 2>&1 +cp "$log" "$lab/partial-build-log.txt" +printf 'LAB=%s\nBUILD_LOG=%s\n' "$lab" "$log" diff --git a/tooling/probes/step20c-rotated-partial-popup/rot-cross-client.patch b/tooling/probes/step20c-rotated-partial-popup/rot-cross-client.patch new file mode 100644 index 0000000..f31acb4 --- /dev/null +++ b/tooling/probes/step20c-rotated-partial-popup/rot-cross-client.patch @@ -0,0 +1,13 @@ +--- a/nested-client.c ++++ b/nested-client.c +@@ -211,6 +211,10 @@ + } else if (!strcmp(placement,"both_cross")) { + a.anchor_x=350; a.anchor_y=80; a.width=350; + b.anchor_x=160; b.anchor_y=60; b.width=300; ++ } else if (!strcmp(placement,"rot_cross")) { ++ /* CONTRACT: both 1x1 anchors stay inside their parent buffers. */ ++ a.anchor_x=360; a.anchor_y=80; a.width=480; ++ b.anchor_x=210; b.anchor_y=60; b.width=300; + } else if (!strcmp(placement,"b_cross")) { + a.anchor_x=200; a.anchor_y=80; a.width=300; + b.anchor_x=220; b.anchor_y=60; b.width=300; diff --git a/tooling/probes/step20c-rotated-partial-popup/run.py b/tooling/probes/step20c-rotated-partial-popup/run.py new file mode 100644 index 0000000..ea83845 --- /dev/null +++ b/tooling/probes/step20c-rotated-partial-popup/run.py @@ -0,0 +1,538 @@ +"""EXPERIMENTAL applied-state/input headless campaign. See README.""" +import argparse +from PIL import Image, ImageDraw +import sys +from budget import Budget +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('--cycles', type=int, default=3) +parser.add_argument('--asi-mode', type=int, choices=(0, 1, 2), default=0) +parser.add_argument('--test-hold', action='store_true') +parser.add_argument('--test-timeout', action='store_true') +parser.add_argument('--moves', type=int, default=18) +parser.add_argument('--max-seconds', type=int, default=120) +parser.add_argument('--max-captures', type=int, default=16) +parser.add_argument('--max-bytes', type=int, default=30_000_000) +args = parser.parse_args() +if not (1 <= args.max_seconds <= 120 and 1 <= args.max_captures <= 16 and + 1 <= args.max_bytes <= 30_000_000): + parser.error('static R90 run bounds exceeded') +if not 1 <= args.cycles <= 30 or not 1 <= args.moves <= 40: + parser.error('cycle/move bounds exceeded') + +lab = args.lab.resolve() +if not (lab / 'nested-client').is_file(): + parser.error('prepared LAB required') +budget = Budget(lab) +budget.reserve(args.mode) +run = lab / 'runs' / f'{args.mode}-static-r90-{time.time_ns()}' +run.mkdir(parents=True, mode=0o700) +(run / 'fixture-source.py').write_text(Path(__file__).read_text()) +# WHY: five asymmetric markers identify whether PNG uses logical or buffer axes. +# CONTRACT: predeclared logical centers and distinct exact RGB codes. +marker_path = run / 'marker.png' +marker = Image.new('RGB', (800, 600), (30, 30, 46)) +draw = ImageDraw.Draw(marker) +for name, box, color in ( + ('TL', (15, 15, 44, 44), (255, 32, 32)), + ('TR', (755, 15, 784, 44), (32, 255, 32)), + ('BL', (15, 555, 44, 584), (32, 32, 255)), + ('BR', (755, 555, 784, 584), (255, 255, 32)), + ('C', (385, 285, 414, 314), (192, 32, 255))): + draw.rectangle(box, fill=color) +marker.save(marker_path) + +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 750x1000 scale 1.25 transform 90 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') +(run / 'config').write_text((run / 'config').read_text() + f'output * bg {marker_path} fill\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() +compositor_env['ASI_MODE'] = str(args.asi_mode) +if args.test_hold or args.test_timeout: + compositor_env['ASI_TEST_HOLD_AFTER_PRESENT'] = '1' +readback_dir = None +if args.asi_mode: + readback_dir = lab / 'buffers' / run.name + readback_dir.mkdir(parents=True, mode=0o700) + compositor_env['BI_OUTPUT_DIR'] = str(readback_dir) +(run / 'asi-mode.json').write_text(json.dumps({'mode': args.asi_mode, + 'readback_dir': str(readback_dir) if readback_dir else None}) + '\n') +binary = lab / 'standard-build/sway/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 = {o['name']: tuple(o['rect'][k] for k in ('x','y','width','height')) for o in outputs if o['active']} + rotated = True + h1 = next(o for o in outputs if o['name'] == 'HEADLESS-1') + h2 = next(o for o in outputs if o['name'] == 'HEADLESS-2') + wanted_mode = (750, 1000) if rotated else (1000, 750) + if (actual != expected or h1['scale'] != 1.25 or h2['scale'] != 1 or + h2['transform'] != 'normal' or h1['transform'] != ('90' if rotated else 'normal') or + (h1['current_mode']['width'], h1['current_mode']['height']) != wanted_mode): + raise RuntimeError(f'applied rotation geometry gate failed: {actual} {h1}') + record('state', tag=tag, workspaces=ipc('-t','get_workspaces'), workspace=workspace(), owner=owner(), outputs=outputs) + 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', '-s', str(next(o['scale'] for o in outputs if o['name']==output)), '-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') + out = next(o for o in outputs if o['name']==output) + dimensions = list(Image.open(path).size) + record('capture_geometry',tag=tag,output=output,dimensions=dimensions,rect=out['rect'],scale=out['scale'],transform=out['transform'],mode=out['current_mode'], physical_expected=[out['current_mode']['width'],out['current_mode']['height']]) + 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): + outputs = [o for o in ipc('-t','get_outputs') if o['active']] + min_x = min(o['rect']['x'] for o in outputs) + min_y = min(o['rect']['y'] for o in outputs) + max_x = max(o['rect']['x']+o['rect']['width'] for o in outputs) + max_y = max(o['rect']['y']+o['rect']['height'] for o in outputs) + if not any(o['rect']['x']<=xbefore) + + +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) + if readback_dir: + (readback_dir / 'enable').touch() + 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) + time.sleep(1.0) + 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'), 'rot_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(640) + record('initial_camera', value=640) + time.sleep(.25) + # EXPERIMENTAL fragment inserted inside the inherited bounded runner's try. + # WHY: persistence observations precede reference clicks that can dismiss grabs. + # CONTRACT: four reserved dynamic actions per mode; no additional camera pair. + dynamic_stage = 'D0' + initial_ws_id = workspace()['id'] + initial_owner_id = owner()['id'] + ref_right = find(tree(), lambda n: n.get('app_id') == 'wev')['id'] + command('workspace 1') + left_proc = spawn(['stdbuf','-oL','wev'], 'wev-left.log') + wait(lambda: find(tree(), lambda n: n.get('app_id') == 'wev' and n['id'] != ref_right)) + ref_left = find(tree(), lambda n: n.get('app_id') == 'wev' and n['id'] != ref_right)['id'] + command('shadows disable'); command('blur disable') + + def closed(): + client_command('close_a') + time.sleep(.12) + + def calibrate(tag): + outputs = {o['name']: o for o in ipc('-t','get_outputs') if o['active']} + active = {w['output']:w for w in ipc('-t','get_workspaces') if w['visible']} + for out, con_id, logname in [('HEADLESS-1',ref_left,'wev-left.log'),('HEADLESS-2',ref_right,'wev.log')]: + name = active[out]['name'] + command(f'[con_id={con_id}] move container to workspace {name}') + command(f'[con_id={con_id}] floating enable') + command(f'[con_id={con_id}] border none') + command(f'[con_id={con_id}] resize set 200 150') + ox=outputs[out]['rect']['x']+50; oy=400 + command(f'[con_id={con_id}] move absolute position {ox} {oy}') + time.sleep(.25) + # WHY: after return both references can temporarily share workspace 1. + # CONTRACT: relocate BOTH before scoring input, avoiding mutual occlusion. + for out, con_id, logname in [('HEADLESS-1',ref_left,'wev-left.log'),('HEADLESS-2',ref_right,'wev.log')]: + ox=outputs[out]['rect']['x']+50; oy=400 + text=lambda: (run/logname).read_text() + # Force a different surface, then accept actual enter or motion. + inject('move', outputs['HEADLESS-2' if out=='HEADLESS-1' else 'HEADLESS-1']['rect']['x']+10,10) + before=len(text());inject('move',ox+20,oy+30) + pattern=r'(?:enter:[^\n]*|motion:[^\n]*)x, y: ([\d.]+), ([\d.]+)' + wait(lambda: re.search(pattern,text()[before:])) + ex,ey=map(float,re.findall(pattern,text()[before:])[-1]) + if ex != 20 or not 0 <= ey < 100: + raise RuntimeError(f'calibration interior mismatch {tag} {out}: {ex},{ey}') + before=len(text());inject('move',ox+60,oy+50) + wait(lambda: f'x, y: {ex+40:.6f}, {ey+20:.6f}' in text()[before:]) + before=len(text()) + inject('left',ox+60,oy+50);inject('right',ox+60,oy+50) + wait(lambda: 'button: 272' in text()[before:] and 'button: 273' in text()[before:]) + record('pointer_calibration',tag=tag,output=out,con_id=con_id,global_point=[ox+60,oy+50],local_expected=[ex+40,ey+20],surface_origin=[ox+20-ex,oy+30-ey],passed=True) + for x in (-2,-1,0,1,2):inject('move',x,550) + inject('move',outputs['HEADLESS-1']['rect']['x']+110,450) + command('workspace 1') + + def stable(tag, expected_owner): + # CONTRACT: repeated geometry and paired settled captures are evidence, + # not proof that intermediate configurations each had a presentation. + time.sleep(.35) + before=ipc('-t','get_outputs') + time.sleep(.12) + after=ipc('-t','get_outputs') + if before != after: + raise RuntimeError(f'unstable output IPC at {tag}') + ws=workspace();o=owner() + current=next(w for w in ipc('-t','get_workspaces') if w['id']==initial_ws_id) + if ws['id']!=initial_ws_id or o['id']!=initial_owner_id or current['output']!=expected_owner: + raise RuntimeError(f'workspace/owner identity mismatch at {tag}') + snapshot(tag) + time.sleep(.16);snapshot(tag+'-repeat') + record('identity',tag=tag,workspace_id=ws['id'],owner_id=o['id'],output=current['output'],layout=ws['layout'],order=[n['id'] for n in ws['nodes']],mosaic=ws.get('mosaic'),focused=current['focused']) + + def owner_hit(tag): + o=owner();ws=workspace() + scroll_x=ws['mosaic']['rows'][0]['scroll_x'] if args.mode=='private_mosaic' else 0 + point=[int(o['rect']['x']-scroll_x+o['window_rect']['x']+100),150] + before=len(client_text());inject('move',*point);inject('right',*point) + delivered='POINTER_BUTTON target=TOPLEVEL' in client_text()[before:] + record('owner_hit',tag=tag,point=point,delivered=delivered,events=client_text()[before:]) + if not delivered:raise RuntimeError(f'visible owner input failed {tag}') + + + # WHY: exact colors identify PNG orientation before interpreting output pixels. + # CONTRACT: marker positions/colors were fixed before the first transition. + colors = {'TL':(255,32,32),'TR':(32,255,32),'BL':(32,32,255), + 'BR':(255,255,32),'C':(192,32,255)} + def marker_gate(tag, rotated): + im = Image.open(run / f'left-{tag}.png').convert('RGB') + points = {name: [] for name in colors} + reverse = {v:k for k,v in colors.items()} + for y in range(im.height): + for x in range(im.width): + name = reverse.get(im.getpixel((x,y))) + if name: points[name].append((x,y)) + centers = {} + for name, positions in points.items(): + if len(positions) < 100: + raise RuntimeError(f'{tag}: marker {name} absent ({len(positions)})') + centers[name] = [sum(p[0] for p in positions)/len(positions), + sum(p[1] for p in positions)/len(positions)] + logical = (im.size == (1000,750) and + centers['TL'][0] < centers['TR'][0] and + centers['TL'][1] < centers['BL'][1]) + physical = (im.size == (750,1000) and + centers['TL'][0] > centers['BL'][0] and + centers['TL'][1] < centers['TR'][1]) + frame = 'logical-oriented PNG' if logical else 'physical-buffer-oriented PNG' if physical else 'unknown' + record('marker_calibration', tag=tag, dimensions=list(im.size), + centers=centers, counts={k:len(v) for k,v in points.items()}, + png_frame=frame) + if (rotated and not (logical or physical)) or (not rotated and not logical): + raise RuntimeError(f'{tag}: PNG frame not established') + return frame + + # CONTRACT: static R90 from compositor startup; no transform command. + marker_gate('startup', True) + calibrate('static-R90') + command('[app_id="org.mosaic.NestedGrabProbe"] focus') + snapshot('reference') + owner_hit('static-R90') + open_chain('rot-cross') + o=owner(); ws=workspace(); a_cfg=configured('POPUP_A'); b_cfg=configured('POPUP_B') + sx=ws['mosaic']['rows'][0]['scroll_x'] if args.mode=='private_mosaic' else 0 + base_x=o['rect']['x']-sx+o['window_rect']['x'] + base_y=o['rect']['y']+o['window_rect']['y'] + a_box=[base_x+a_cfg[0],base_y+a_cfg[1],base_x+a_cfg[0]+a_cfg[2],base_y+a_cfg[1]+a_cfg[3]] + b_box=[a_box[0]+b_cfg[0],a_box[1]+b_cfg[1],a_box[0]+b_cfg[0]+b_cfg[2],a_box[1]+b_cfg[1]+b_cfg[3]] + record('applied_popup_geometry',owner_id=o['id'], owner_rect=o['rect'],window_rect=o['window_rect'], + camera=sx,a_configure=a_cfg,b_configure=b_cfg,a_box=a_box,b_box=b_box, + a_h1=[max(-800,a_box[0]),a_box[1],min(0,a_box[2]),a_box[3]], + a_h2=[max(0,a_box[0]),a_box[1],min(800,a_box[2]),a_box[3]], + b_h1=[max(-800,b_box[0]),b_box[1],min(0,b_box[2]),b_box[3]], + b_h2=[max(0,b_box[0]),b_box[1],min(800,b_box[2]),b_box[3]]) + if not (b_box[0] < 0 < b_box[2] and b_box[2]>=16): + record('geometry_gate',result='NOT EXERCISED',box=b_box) + else: + record('geometry_gate',result='CROSSING',box=b_box) + # WHY: evaluate a shared y within B and scan both sides of the boundary. + y=int((max(0,b_box[1])+min(600,b_box[3]))/2) + for x in range(-3,4): + before=len(client_text()); inject('move',x,y) + record('border_hover',point=[x,y],client_events=client_text()[before:]) + inside_x=max(-30,int((max(-800,b_box[0])+min(0,b_box[2]))/2)) + before=len(client_text()); inject('move',inside_x,y); inject('right',inside_x,y) + record('visible_b_button',point=[inside_x,y],client_events=client_text()[before:]) + snapshot('after-visible-button') + # A left/right roles may dismiss on exterior click. Preserve it last. + if b_box[2]>0: + outside_x=min(30,int(b_box[2]/2)) + before=len(client_text()); inject('move',outside_x,y); inject('right',outside_x,y) + record('forbidden_b_button',point=[outside_x,y],client_events=client_text()[before:]) + time.sleep(.15) + snapshot('after-forbidden-button') + +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) diff --git a/tooling/probes/step20c-rotated-partial-popup/test-budget.py b/tooling/probes/step20c-rotated-partial-popup/test-budget.py new file mode 100644 index 0000000..6f87aa9 --- /dev/null +++ b/tooling/probes/step20c-rotated-partial-popup/test-budget.py @@ -0,0 +1,40 @@ +"""Offline ledger tests; no compositor or campaign reservation.""" +from pathlib import Path +import tempfile +from budget import Budget + +with tempfile.TemporaryDirectory() as directory: + lab = Path(directory) + budget = Budget(lab) + assert budget.reserve('control')['runs'] == ['control'] + try: + budget.reserve('control') + except ValueError: + pass + else: + raise AssertionError('duplicate accepted') + assert budget.reserve('private_standard')['runs'][-1] == 'private_standard' + assert budget.reserve('private_mosaic')['runs'][-1] == 'private_mosaic' + try: + budget.reserve('control') + except ValueError: + pass + else: + raise AssertionError('exhaustion bypass') + try: + Budget(lab) + except BlockingIOError: + pass + else: + raise AssertionError('lock bypass') +with tempfile.TemporaryDirectory() as directory: + lab = Path(directory) + (lab / 'runs').mkdir() + (lab / 'runs' / 'failed').mkdir() + try: + Budget(lab).read() + except ValueError: + pass + else: + raise AssertionError('missing ledger accepted') +print('PASS persistence, duplicate, exhaustion, lock, missing-ledger')