docs(mosaic): preserve rotated output probe evidence
This commit is contained in:
parent
2882f74159
commit
f7a94eb5fb
12 changed files with 1101 additions and 0 deletions
|
|
@ -4,6 +4,8 @@
|
||||||
|
|
||||||
### STEP20C experimental evidence
|
### STEP20C experimental evidence
|
||||||
|
|
||||||
|
- 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.
|
- 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.
|
||||||
|
|
||||||
- Preserved fractional-scale probe: actual 1.25 uniform/mixed and 1.5 mixed outputs, calibrated logical input, sampled root/nested popup and effects clipping, two exact camera returns and physical settled-damage coverage. PARTIALLY_FEASIBLE; opaque tiled controls, interior shadow, restored Mosaic hit and broader gates remain unqualified. No production/package change.
|
- Preserved fractional-scale probe: actual 1.25 uniform/mixed and 1.5 mixed outputs, calibrated logical input, sampled root/nested popup and effects clipping, two exact camera returns and physical settled-damage coverage. PARTIALLY_FEASIBLE; opaque tiled controls, interior shadow, restored Mosaic hit and broader gates remain unqualified. No production/package change.
|
||||||
|
|
|
||||||
|
|
@ -261,3 +261,12 @@ Rotation is the next separate geometric gate; other open STEP20C gates persist.
|
||||||
The freeze audit verified the original evidence manifest before an offline
|
The freeze audit verified the original evidence manifest before an offline
|
||||||
replay rewrote the temporary inventory; the report records that limitation.
|
replay rewrote the temporary inventory; the report records that limitation.
|
||||||
STEP20C stays **NEXT**, multi-output **NOT YET SUPPORTED**.
|
STEP20C stays **NEXT**, multi-output **NOT YET SUPPORTED**.
|
||||||
|
|
||||||
|
The [rotated-output probe](design/STEP20C_ROTATED_OUTPUT_PROBE.md) is
|
||||||
|
**PARTIALLY_FEASIBLE** for sampled applied 90° geometry, logical PNG/pointer
|
||||||
|
calibration, fully hidden owner guard, physical damage coverage and R0 return.
|
||||||
|
The `grim` PNG is logical-oriented at 1000×750 while the R90 physical mode is
|
||||||
|
750×1000. Pristine R90 capture failed through a fixture IPC-direction error,
|
||||||
|
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.
|
||||||
|
|
|
||||||
|
|
@ -200,3 +200,12 @@ Rotation is the next separate geometric gate; other open STEP20C gates persist.
|
||||||
The original temporary evidence manifest was verified before an offline
|
The original temporary evidence manifest was verified before an offline
|
||||||
analyzer rerun replaced it; its exact historical bytes are not retained.
|
analyzer rerun replaced it; its exact historical bytes are not retained.
|
||||||
STEP20C stays **NEXT**, multi-output **NOT YET SUPPORTED**.
|
STEP20C stays **NEXT**, multi-output **NOT YET SUPPORTED**.
|
||||||
|
|
||||||
|
The [rotation probe](design/STEP20C_ROTATED_OUTPUT_PROBE.md) established
|
||||||
|
the applied 90° mode/transform and logical PNG/pointer orientation in two
|
||||||
|
private runs, plus sampled fully-hidden owner and damage behavior. Its global
|
||||||
|
result is **PARTIALLY_FEASIBLE**, architectural partial-clipping conclusion
|
||||||
|
**UNDECIDED**: pristine R90 capture failed through fixture assertion, and the
|
||||||
|
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.
|
||||||
|
|
|
||||||
|
|
@ -112,3 +112,11 @@ PARTIALLY_FEASIBLE for measured stable geometry/ownership, with explicit
|
||||||
fixture and unexercised-input limitations. The original temporary evidence
|
fixture and unexercised-input limitations. The original temporary evidence
|
||||||
manifest was verified before an offline analyzer rerun replaced its bytes;
|
manifest was verified before an offline analyzer rerun replaced its bytes;
|
||||||
the report records this provenance limit.
|
the report records this provenance limit.
|
||||||
|
|
||||||
|
[Rotated-output probe](design/STEP20C_ROTATED_OUTPUT_PROBE.md) and
|
||||||
|
[fixture](../tooling/probes/step20c-rotated-output/README.md): EXPERIMENTAL /
|
||||||
|
PARTIALLY_FEASIBLE for applied geometry, logical PNG/pointer, full-owner hide,
|
||||||
|
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.
|
||||||
|
|
|
||||||
|
|
@ -204,3 +204,11 @@ Rotation is the next separate geometric gate; other open STEP20C gates persist.
|
||||||
The original temporary evidence manifest passed verification before an
|
The original temporary evidence manifest passed verification before an
|
||||||
offline analyzer rerun replaced its bytes; see the report for provenance.
|
offline analyzer rerun replaced its bytes; see the report for provenance.
|
||||||
STEP20C stays **NEXT**, multi-output **NOT YET SUPPORTED**.
|
STEP20C stays **NEXT**, multi-output **NOT YET SUPPORTED**.
|
||||||
|
|
||||||
|
The [rotated-output probe](design/STEP20C_ROTATED_OUTPUT_PROBE.md) is
|
||||||
|
**PARTIALLY_FEASIBLE** for sampled applied geometry, PNG/pointer calibration,
|
||||||
|
hidden-owner return and physical damage. Its partial-popup clipping conclusion
|
||||||
|
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**.
|
||||||
|
|
|
||||||
431
docs/design/STEP20C_ROTATED_OUTPUT_PROBE.md
Normal file
431
docs/design/STEP20C_ROTATED_OUTPUT_PROBE.md
Normal file
|
|
@ -0,0 +1,431 @@
|
||||||
|
# STEP20C rotated output viewport and input probe
|
||||||
|
|
||||||
|
**EXPERIMENTAL evidence; PARTIALLY_FEASIBLE.** This probe follows the
|
||||||
|
published dynamic-output evidence at `2882f7415952d7f141da85f90c0fd113a17c1665`.
|
||||||
|
STEP20C remains NEXT and production multi-output NOT YET SUPPORTED. The source
|
||||||
|
fixture is [here](../../tooling/probes/step20c-rotated-output/README.md).
|
||||||
|
No production source, installed library, package, desktop session or live IPC
|
||||||
|
was changed. The separate partial-popup experiment remains a later gate.
|
||||||
|
|
||||||
|
## Question and prior evidence
|
||||||
|
|
||||||
|
The [fractional](STEP20C_FRACTIONAL_SCALING_PROBE.md) and
|
||||||
|
[dynamic-owner](STEP20C_DYNAMIC_OUTPUT_OWNERSHIP_PROBE.md) probes sampled negative
|
||||||
|
origins, scale 1.25, nested valid XDG grabs, logical resize and workspace
|
||||||
|
migration, but every dynamic state used `transform=normal`. The dynamic probe
|
||||||
|
was PARTIALLY_FEASIBLE: two standard D5 reference calibrations failed, the
|
||||||
|
maximum camera clamp and migrated clipped-popup portion were not exercised.
|
||||||
|
This probe asks whether an output rotated 90° keeps the same **logical** Mosaic
|
||||||
|
viewport, clipping and input while pixels and damage use the changed physical
|
||||||
|
buffer coordinates. A failure is a hypothesis until measured against pristine
|
||||||
|
and private-standard controls.
|
||||||
|
|
||||||
|
## Pinned inputs and exact source path
|
||||||
|
|
||||||
|
SwayFX control `fd71a6bdc061bd633b488ae7b83e8a6981d22f86`, Mosaic base
|
||||||
|
`cd550382b311efdbfe3ed892f4e1d281d852e754`, SceneFX
|
||||||
|
`37ccd723bef49e6891156ffafce8f549f01446cc`, wlroots 0.20.2 source
|
||||||
|
`d783533489e1f75d6886c2ab5c5960090ef268f8`. Preserve prior patches
|
||||||
|
unchanged. Fresh private sources/builds, sockets and logs live under one LAB.
|
||||||
|
|
||||||
|
Source audit before fixture implementation:
|
||||||
|
|
||||||
|
- SwayFX `output_cmd_transform` accepts `90` and `270`, then swaps their
|
||||||
|
`WL_OUTPUT_TRANSFORM_*` values: Sway command directions are clockwise while
|
||||||
|
Wayland transform enum directions are anticlockwise. SwayFX's IPC encoder
|
||||||
|
converts the internal enum back to the user-facing direction. The first
|
||||||
|
pristine R90 action measured IPC `transform="90"`, physical 750×1000 and
|
||||||
|
logical 800×600. The initial fixture erroneously expected IPC `"270"` and
|
||||||
|
stopped before its R90 PNG; that action is consumed and is not replayed.
|
||||||
|
The applied IPC value, not the command string alone, decides the gate. `apply_output_configs`
|
||||||
|
prepares pending mode, transform, then scale (rounded to 1/120); scene output
|
||||||
|
build/backend commit precedes final output-layout update, `arrange_root` and
|
||||||
|
`transaction_commit_dirty`. It dirties optimized blur after output changes.
|
||||||
|
Thus IPC acceptance does not establish one atomic render/input generation.
|
||||||
|
- wlroots `wlr_output_transformed_resolution` swaps width/height for odd
|
||||||
|
transforms; `wlr_output_effective_resolution` divides those dimensions by
|
||||||
|
scale. `wlr_output_transform_invert` exchanges unflipped 90/270;
|
||||||
|
`wlr_output_transform_compose` combines transform values. `wlr_box_transform`
|
||||||
|
and `wlr_region_transform` map rectangles differently at 90/270, with width
|
||||||
|
and height arguments from the **source** coordinate space. `wlr_output_commit_state`
|
||||||
|
applies state before `events.commit`; `events.present` is a separate callback.
|
||||||
|
- SceneFX `scene_node_intersect_clip`/`scene_node_visibility` operate on logical
|
||||||
|
scene regions. `scene_entry_render` calls `logical_to_buffer_coords` for
|
||||||
|
regions and `transform_output_box` for boxes; both scale first, then apply
|
||||||
|
the inverse output transform with transformed physical dimensions.
|
||||||
|
`scale_region` expands fractional-scale coverage by one pixel when rounding
|
||||||
|
up. `output_to_buffer_coords` converts output damage with the inverse
|
||||||
|
transform. `scene_output_damage` stores buffer-local physical extents;
|
||||||
|
`wlr_scene_output_build_state` computes render damage and output commit state.
|
||||||
|
Shadow/blur rendering has distinct paths, so effect visibility alone cannot
|
||||||
|
validate optimized-cache invalidation or off-viewport sampling.
|
||||||
|
- Mosaic `workspace_tiling_scene_x` subtracts logical `scroll_x` from the
|
||||||
|
applied tiled-scene position; the private tiled and root-popup clips are
|
||||||
|
applied logical scene clips. `transaction_apply` copies pending state into
|
||||||
|
current state, then scene arrangement and cursor rebase follow.
|
||||||
|
`node_at_coords` and `seat_pointer_notify_button` use global logical pointer
|
||||||
|
coordinates; wlroots `wlr_cursor_absolute_to_layout_coords` maps normalized
|
||||||
|
pointer coordinates into the layout's logical bounding box. No physical
|
||||||
|
scale or output rotation is applied to injected global logical points.
|
||||||
|
|
||||||
|
The source audit identifies paths, not proof that the measured buffer, PNG and
|
||||||
|
client events agree. It must be confronted with captures and event logs.
|
||||||
|
|
||||||
|
## Geometry and coordinate predictions
|
||||||
|
|
||||||
|
| State | H1 physical mode | Command transform | Expected applied logical H1 | H2 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| R0 | 1000×750 | normal | (-800,0,800,600), scale 1.25 | 800×600 at (0,0), normal, scale 1 |
|
||||||
|
| R90 | 750×1000 | 90 clockwise | (-800,0,800,600), scale 1.25 | unchanged |
|
||||||
|
| R270 optional | 750×1000 | 270 clockwise | (-800,0,800,600), scale 1.25 | unchanged |
|
||||||
|
|
||||||
|
R0→R90 changes both mode dimensions and transform: no single-variable causal
|
||||||
|
claim is possible from that comparison alone. The target logical viewport is
|
||||||
|
800×600 in both states; H1's physical buffer is expected to use 1000×750 then
|
||||||
|
750×1000. Actual mode, transform, scale, global rectangle, scene owner and
|
||||||
|
capture dimensions are gates read from `get_outputs`, trace and PNG. If the
|
||||||
|
command is refused or remains normal, R90 is INCONCLUSIVE.
|
||||||
|
|
||||||
|
Logical global H1 `(x,y)` becomes output-local `(x+800,y)`. The inverse
|
||||||
|
wlroots transform maps a scaled logical box into the physical buffer.
|
||||||
|
For a clockwise command `90`, the parser's applied enum is `270`; IPC reports
|
||||||
|
the user-facing `90`, while the inverse used by SceneFX is `90`. Distinguish
|
||||||
|
the two names and use the **internal enum** and actual
|
||||||
|
buffer dimensions in `wlr_box_transform`/`wlr_region_transform`, including
|
||||||
|
half-open rectangle bounds and fractional rounding. A simple hand-swapped
|
||||||
|
axis is insufficient near boundaries. Record both expected and measured
|
||||||
|
logical→buffer→PNG coordinates for every marker/ROI.
|
||||||
|
|
||||||
|
Virtual-pointer normalization is provisionally `(sent_x,sent_y)=(global_x+800,
|
||||||
|
global_y)` over a 1600×600 logical union. Empirically calibrate on each output
|
||||||
|
with an ordinary surface, (+40,+20) local motion, left/right buttons and
|
||||||
|
crossing `x=0`. Relocate **both** ordinary references before scoring either;
|
||||||
|
confirm actual client enter/motion and non-overlap. A prior D5 reference
|
||||||
|
occlusion makes frame-only geometry an insufficient calibration.
|
||||||
|
|
||||||
|
## Capture orientation gate
|
||||||
|
|
||||||
|
`grim` screencopy may expose transformed pixels in a different orientation
|
||||||
|
from SceneFX's raw framebuffer. Before any graphics assertion, capture a
|
||||||
|
deliberately asymmetric field with five distinct markers: upper-left red
|
||||||
|
square, upper-right green triangle, lower-left blue circle, lower-right yellow
|
||||||
|
stripe and purple center cross. For each, store logical point/box, predicted
|
||||||
|
physical box using observed transform, PNG box, dimensions, delta and rounding
|
||||||
|
rule. Also record capture window `CLOCK_MONOTONIC` timestamps, mode, scale,
|
||||||
|
transform, physical/logical dimensions and file hash. Do not select ROIs after
|
||||||
|
seeing anomalous pixels. If marker orientation is not identifiable, dependent
|
||||||
|
pixel, popup and damage claims are INCONCLUSIVE. The marker source and
|
||||||
|
distribution mechanism must be documented before use.
|
||||||
|
|
||||||
|
## Comparative scenarios and stop gates
|
||||||
|
|
||||||
|
Use identical deterministic SHM toplevel, valid-parent root A and child B,
|
||||||
|
virtual pointer and ordinary references in: A pristine `splith`, B private
|
||||||
|
`splith`, C private `mosaic`. Preserve `TOPLEVEL→A→B`; each grab serial must
|
||||||
|
come from its own real button press, with configure/ack/map and legal anchors.
|
||||||
|
Do not infer clipping from no spill unless standard controls visibly spill.
|
||||||
|
|
||||||
|
1. T0: R0 geometry, references, scene/clip, PNG markers and input calibration.
|
||||||
|
2. T1: one reserved R0→R90 per mode, then observed mode/transform/scale,
|
||||||
|
logical rectangle, identities, scene/clip, marker/PNG and input gates.
|
||||||
|
3. T2/T3: A and B border-spanning cases, a logical `x=-3..3` grid at
|
||||||
|
preselected popup y values, full adjacent physical strips, permitted and
|
||||||
|
forbidden pixels, scene hits and separate client delivery/dismissal.
|
||||||
|
4. T4: one global Mosaic camera hide/return pair only; same XDG object identity
|
||||||
|
and a new B button after return if no click has dismissed the stack.
|
||||||
|
5. T5: one reserved R90→R0 per mode; check restored mode, transform, scene,
|
||||||
|
camera, clip, PNG and input. A clamp may alter camera; its measured value
|
||||||
|
governs expected geometry.
|
||||||
|
6. T6: short shadow/blur FX0/FX2 contrasts only after geometry and PNG gates;
|
||||||
|
optimized cache and outside-source sampling remain NOT TESTED otherwise.
|
||||||
|
7. T7: measured physical damage extents versus changed physical pixels, including
|
||||||
|
exposed regions on both outputs. Do not double-scale physical extents or
|
||||||
|
infer every intermediate frame from settled comparisons.
|
||||||
|
8. T8: private Mosaic `layout splith`, then no residual clip, ordinary popup,
|
||||||
|
focus, effects and input before an exterior dismissal click.
|
||||||
|
|
||||||
|
R270 is optional, static at startup, at most one run per mode and only after
|
||||||
|
R90 capture/pointer gates pass. Transforms 180/flipped, hotplug, all saved
|
||||||
|
buffers, complete cache invalidation, all frames, ABI/package and daily-driver
|
||||||
|
support remain NOT TESTED unless directly measured.
|
||||||
|
|
||||||
|
## Strict budget, isolation and evidence
|
||||||
|
|
||||||
|
One persistent locked ledger spans the campaign. Reserve **before** each
|
||||||
|
mode/transform action, retain failed reservations, forbid repeats and refuse
|
||||||
|
more than two actions per mode / six overall. One global Mosaic camera
|
||||||
|
hide/return pair maximum. R270 adds at most three static starts, no dynamic
|
||||||
|
actions. Test lock, persistence, duplicate/exhaustion and missing-ledger
|
||||||
|
rejection offline before any run. No second lab may bypass consumed actions.
|
||||||
|
|
||||||
|
Every run has private 0700 `XDG_RUNTIME_DIR`, explicit private `SWAYSOCK`
|
||||||
|
and `swaymsg -s`, private `WAYLAND_DISPLAY`, headless backend and scoped child
|
||||||
|
cleanup. Never resolve the live IPC socket implicitly. Preserve all failure
|
||||||
|
exit codes, attempts and raw logs before owned cleanup; never kill by global
|
||||||
|
process name. Use passive patches only when prior traces are insufficient;
|
||||||
|
do not alter clipping, shaders, damage, callbacks, hit testing or ABI.
|
||||||
|
|
||||||
|
Retain source/evidence SHA-256 manifests, raw events/PNGs, output/transform,
|
||||||
|
logical-to-buffer, screenshot, owner/clip, popup, pointer/input, pixel, damage,
|
||||||
|
timeline, budget, matrix and summary JSON under LAB. The local fixture keeps
|
||||||
|
only source and passive patch files; no generated binary or capture enters Git.
|
||||||
|
|
||||||
|
Verdict: FEASIBLE only if the applied rotation, coordinate/PNG calibration,
|
||||||
|
standard spill controls, Mosaic visible/forbidden pixels and hits, valid grabs,
|
||||||
|
return clips and measured damage all pass in the tested matrix.
|
||||||
|
PARTIALLY_FEASIBLE requires valid principal measurements with named open cells.
|
||||||
|
INCONCLUSIVE covers un-applied transform or failed orientation/fixture gate.
|
||||||
|
BLOCKED requires a reproduced forbidden pixel/hit, wrong transform clip, crash
|
||||||
|
or blocking standard-layout regression. Even FEASIBLE is experimental evidence,
|
||||||
|
never production qualification. Architectural conclusion is separately
|
||||||
|
`ROTATED_OUTPUT_FEASIBLE_FOR_TESTED_MATRIX`, `ROTATED_OUTPUT_REQUIRES_REDESIGN`
|
||||||
|
or `UNDECIDED` according to measured support.
|
||||||
|
|
||||||
|
## Measured private campaign — 2026-10-12
|
||||||
|
|
||||||
|
**EXPERIMENTAL / PARTIALLY_FEASIBLE overall; architectural conclusion
|
||||||
|
UNDECIDED.** The new laboratory is
|
||||||
|
`/tmp/swayfx-mosaic-nested-grab-TJ3AhbCE`. A fresh source-only preparer built
|
||||||
|
private SceneFX/Mosaic and pristine SwayFX from the same pins and compiled
|
||||||
|
the XRGB client and virtual pointer under C17 `-Werror`. It never used the
|
||||||
|
historical dynamic campaign's consumed actions. All three modes were attempted
|
||||||
|
once under one persistent locked rotation ledger. There are 111 retained PNGs:
|
||||||
|
16 pristine, 40 private standard and 52 private Mosaic, plus the per-run marker
|
||||||
|
source PNGs. No B source, report or result was committed/pushed.
|
||||||
|
|
||||||
|
### Fixture incident and applied geometry
|
||||||
|
|
||||||
|
The pristine `splith` run completed R0, two-output pointer calibration and a
|
||||||
|
two-level grabbed popup chain. It reserved and launched R90. `get_outputs`
|
||||||
|
then showed H1 physical **750×1000**, scale **1.25**, IPC transform **`90`**,
|
||||||
|
and logical **(-800,0,800,600)**. The initial fixture asserted IPC `270` by
|
||||||
|
mistaking the internal wlroots enum for SwayFX's user-facing IPC string, so
|
||||||
|
it failed before R90 capture. Its R90 action remains consumed. Pristine R90
|
||||||
|
PNG, input and return are **NOT TESTED**; this is a fixture error, not a
|
||||||
|
demonstrated compositor rotation defect. The source assertion was corrected
|
||||||
|
before the two private runs; no pristine retry occurred.
|
||||||
|
|
||||||
|
Both private modes completed exactly one R0→R90 and one R90→R0. H2 stayed
|
||||||
|
physical/logical 800×600, scale 1, normal at (0,0). H1 remained logical
|
||||||
|
(-800,0,800,600) in all measured states while changing as follows:
|
||||||
|
|
||||||
|
| State | Physical mode | IPC transform | Internal enum trace | Scale | H1 PNG |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| R0 | 1000×750 | normal | 0 | 1.25 | 1000×750 |
|
||||||
|
| R90 | 750×1000 | 90 | 3 (`WL_OUTPUT_TRANSFORM_270`) | 1.25 | 1000×750 |
|
||||||
|
| Return R0 | 1000×750 | normal | 0 | 1.25 | 1000×750 |
|
||||||
|
|
||||||
|
The mode's orientation and the transform both changed; the equal logical
|
||||||
|
viewport does not isolate their separate causal effects. An IPC success value
|
||||||
|
alone was never used as the applied-geometry proof.
|
||||||
|
|
||||||
|
### PNG and pointer gates
|
||||||
|
|
||||||
|
`grim -s 1.25` exposed a **logical-oriented** 1000×750 PNG even with the
|
||||||
|
750×1000 physical R90 mode. All five exact-color marker centers were identical
|
||||||
|
in private R0 and R90 screenshots: TL (37,37), TR (962,37), BL (37,712),
|
||||||
|
BR (962,712), center (499.5,374.5). The pristine R0 marker matched.
|
||||||
|
This identifies the PNG repère for this screencopy path, not the raw SceneFX
|
||||||
|
framebuffer. For the internal 270 transform, SceneFX uses inverse 90 on
|
||||||
|
scaled logical rectangles. A pixel center `(x,y)` in this logical PNG maps by
|
||||||
|
source formula to physical buffer `(749-y,x)`: TL approximately (712,37),
|
||||||
|
TR (712,962), BL (37,37), BR (37,962), center approximately (375,500).
|
||||||
|
These buffer coordinates are **calculated, not directly read back**. The exact
|
||||||
|
marker equality has zero measured PNG-center deviation; raw-buffer deviation
|
||||||
|
is NOT TESTED. Fractional edges use SceneFX's region expansion and box
|
||||||
|
rounding, so the center mapping is not a claim about every filtered edge.
|
||||||
|
The actual wallpaper used five distinct exact RGB colors and five asymmetric
|
||||||
|
positions, but drew every mark as a square. This does not meet the protocol's
|
||||||
|
distinct-shape preference; the unique color/position pairs made orientation
|
||||||
|
identifiable in the retained images, and no shape-based assertion is claimed.
|
||||||
|
|
||||||
|
On each private mode, independent ordinary references received pointer
|
||||||
|
enter/motion, logical (+40,+20) local deltas and left/right buttons on H1/H2
|
||||||
|
at R0 and R90. Their first local point was (20,30), second (60,50).
|
||||||
|
The injector used the observed logical union `min_x=-800`, extent 1600×600,
|
||||||
|
`sent_x=global_x+800`, `sent_y=global_y`, without multiplying by 1.25 or
|
||||||
|
rotating injection coordinates. It traversed `x=-3..3` at y=550; those moves
|
||||||
|
were **not scored against popup client events** and do not qualify clipped
|
||||||
|
popup boundary hit testing. Both references were relocated before either
|
||||||
|
calibration, avoiding the earlier D5 overlap incident.
|
||||||
|
|
||||||
|
### Popups, clips and pixels
|
||||||
|
|
||||||
|
All measured A/B grabs used real received button serials. Pristine R0 had
|
||||||
|
two valid grabs; each private mode had four valid grabs across its first and
|
||||||
|
reopened chains. The client retained `TOPLEVEL→A→B` parentage, configure,
|
||||||
|
ack, map and distinct new role lifetimes after closure. A's anchor (200,80)
|
||||||
|
lies within the 420×320 parent buffer; B's (220,60) lies within A's 300×170
|
||||||
|
buffer. No protocol error was logged. One A/B `popup_done` pair appeared in
|
||||||
|
each private run during the explicit close/reopen sequence; the chain present
|
||||||
|
during R90 was captured before closure. This does not prove universal XDG
|
||||||
|
persistence through arbitrary transforms.
|
||||||
|
|
||||||
|
Mosaic retained workspace ID 6, tiled owner ID 8, order [7,8,9,10] and camera
|
||||||
|
640 through R0, R90 and return. `DO_STATE` records transform 0→3→0.
|
||||||
|
Pre-capture applied traces give tiled scene origin (-1440,0), local clip
|
||||||
|
(640,0,800,600), derived viewport (-800,0,800,600) at R0/R90/return.
|
||||||
|
Owner-derived root-popup clip from global root (-798,25) and local
|
||||||
|
(-2,-25,800,600) yields the same viewport. Nested popup inheritance is
|
||||||
|
visible in scene traces. These are sampled applied states, not every frame or
|
||||||
|
a shared render/input transaction generation.
|
||||||
|
|
||||||
|
The private-standard R90 screenshot has **15,860** exact B-color pixels on
|
||||||
|
H2, including 130/390/910 pixels in its first 1/3/7 physical columns.
|
||||||
|
Pristine R0 showed the same 15,860 H2 B pixels. Private Mosaic R90 has
|
||||||
|
zero A/B-color pixels on H2 and permitted A/B colors on H1. However, its
|
||||||
|
B-color box in the logical PNG is x=[528,903), y=[206,369), wholly inside
|
||||||
|
the H1 viewport x=[0,1000) in PNG coordinates. Standard B reaches x=1000
|
||||||
|
and continues on H2. **The different standard/Mosaic layout positions mean
|
||||||
|
this campaign did not challenge a partially clipped Mosaic popup at R90.**
|
||||||
|
The zero H2 count is an observed no-spill state, not proof that a forbidden
|
||||||
|
popup portion was clipped. A crossing and simultaneous A/B crossing are also
|
||||||
|
NOT EXERCISED. The fixed x=-3..3/y=550 grid did not lie in a scored popup
|
||||||
|
region; invisible hit tests there remain NOT TESTED.
|
||||||
|
|
||||||
|
The single reserved Mosaic camera hide/return pair moved scroll 640→1280→640
|
||||||
|
under R90. At 1280, owner 8's applied scene x=-1440 with width 640 ends at
|
||||||
|
the viewport's x=-800, yielding an empty intersection. Its root popup clip
|
||||||
|
became (0,0,0,0); H1 captured zero owner/A/B-color pixels. The return
|
||||||
|
restored the same scene/clip and exact R90 image hash, and a new right button
|
||||||
|
on B at global (-343,195) reached that client after return. The second camera
|
||||||
|
reservation is the return half of this one pair, not a second pair.
|
||||||
|
The second chain's sampled popup roles (A 13, B 16 in this run) remained
|
||||||
|
mapped through that pair, without a new `popup_done`; their IDs are run
|
||||||
|
observations, not fixture constants. B also received a right button while
|
||||||
|
visible under R90 before the hide.
|
||||||
|
|
||||||
|
R90→R0 restored mode, transform and clip in both private runs. Mosaic's
|
||||||
|
settled R0 popup screenshots were byte-identical to its R90 logical-oriented
|
||||||
|
popup screenshots and to its earlier R0 state. `layout splith` subsequently
|
||||||
|
removed the Mosaic clips; its left/right PNG hashes equaled the private
|
||||||
|
standard `splith` result, including the ordinary 15,860-pixel B overflow on
|
||||||
|
H2. This samples return and ordinary layout behavior, without proving all
|
||||||
|
transition frames or all focus/popup event orderings.
|
||||||
|
|
||||||
|
### Damage, effects and budget
|
||||||
|
|
||||||
|
SceneFX's traced `TEMPORAL_DAMAGE` extents are clipped to physical
|
||||||
|
buffer-local `output->width/height` after `output_to_buffer_coords` applies
|
||||||
|
the inverse transform. For equal-transform settled R90 Mosaic pairs, map
|
||||||
|
changed PNG pixels `(x,y)` to physical `(749-y,x)` before comparing extents.
|
||||||
|
The R90 visible→fully-hidden and hidden→restored H1 pairs each changed
|
||||||
|
569,789 logical-PNG pixels; 27 and 18 physical extents respectively covered
|
||||||
|
all changed pixels, **zero uncovered**. H2 changed zero in that pair. The
|
||||||
|
R0 Mosaic→standard return changed 442,712 H1 pixels; 89 available normal
|
||||||
|
extents covered them, zero uncovered. Full-output extents occurred, so neither
|
||||||
|
minimal damage nor complete intermediate-frame coverage is established.
|
||||||
|
Mode/transform transitions were not compared as equal physical buffers.
|
||||||
|
Commit/present callbacks were retained but no screenshot was tied to a native
|
||||||
|
buffer identity or atomic input generation.
|
||||||
|
Private Mosaic logged 221 native commit and 215 present callbacks; private
|
||||||
|
standard logged 188/182. The pristine control has no passive native trace.
|
||||||
|
These count differences include startup/teardown and are not themselves a
|
||||||
|
frame-loss finding.
|
||||||
|
|
||||||
|
Shadow and blur were disabled in this campaign. T6, optimized cache
|
||||||
|
invalidation, off-viewport blur source sampling and physical raw-buffer
|
||||||
|
readback are **NOT TESTED**. R270 was not run: the main negative partial-popup
|
||||||
|
challenge is already absent, so another static direction would not close it.
|
||||||
|
|
||||||
|
The durable ledger retains one failed control R90 action, private-standard
|
||||||
|
R90/R0 and private-Mosaic R90/R0: **5/6 mode/transform actions**. Mosaic used
|
||||||
|
**1/1 camera hide/return pair**; R270 static runs **0/3**. No reservation was
|
||||||
|
refunded or replayed, and no second lab was created to evade the budget.
|
||||||
|
|
||||||
|
### Structured evidence and integrity
|
||||||
|
|
||||||
|
The authoritative final analysis is `LAB/results-final/`; the prior sealed
|
||||||
|
`LAB/results/` and `LAB/results-corrected/` remain byte-for-byte preserved. The initial
|
||||||
|
analysis counted zero Mosaic spill but did not check the Mosaic B bounding
|
||||||
|
box, so its architectural conclusion was too strong. The final results
|
||||||
|
include outputs, transforms, logical-to-buffer mapping, marker coordinates,
|
||||||
|
workspace owner, scene clips, popup geometry, pointer/input, pixel/damage,
|
||||||
|
timeline, budget, matrix and summary JSON. All are recalculable from raw
|
||||||
|
events, logs and 111 retained run PNGs.
|
||||||
|
|
||||||
|
The corrected v2 1,015-entry source manifest SHA-256 was
|
||||||
|
`8fe5bdec378ca200a94c589db867fc21c80ee9fe37762c38077746e4eb2d9ce6`;
|
||||||
|
its 282-entry evidence manifest was
|
||||||
|
`77e3d60621f14e46655779d09ce53cc92e48cc50b8aa2d72c5faa64432fdba51`.
|
||||||
|
The final v3 manifests and entry counts are recorded after sealing below.
|
||||||
|
Final v3 **1,015-entry source** manifest SHA-256:
|
||||||
|
`f17ee5999edec84885997ea5dfb193859f7c1d278bf4dda22f8ab20b1c5e0b8f`.
|
||||||
|
Final v3 **284-entry evidence** manifest SHA-256:
|
||||||
|
`261ff4cb12d7be3494d43f29ce1d24f980d8f19abdc558fef42599d50070747d`.
|
||||||
|
Both verify entry by entry. The final B box is derived from pixels as
|
||||||
|
`[528,206,903,369]` against PNG width 1000; owner ID is derived from each
|
||||||
|
run's tree state. Raw captures and private logs are unchanged across analyses.
|
||||||
|
The v1 evidence manifest
|
||||||
|
`02d7f47e1be9e3fb8d867e9a2d0d8d27ee358af43d96c9b217ab5435c7e5c4b8`
|
||||||
|
also still verifies; each later evidence manifest includes the prior manifest files.
|
||||||
|
The v1 source manifest recorded earlier fixture bytes and is historical after
|
||||||
|
the analysis correction; no raw run file was modified.
|
||||||
|
|
||||||
|
An additive offline `LAB/results-roi/` annotation records fixed
|
||||||
|
logical/PNG/physical half-open boundary ROIs, exact A/B counts, comparable
|
||||||
|
previous-capture pixel changes, mode/scale/transform, hashes and monotonic
|
||||||
|
capture windows for all **108 run captures** (the three marker-source PNGs are
|
||||||
|
not run captures). At R90 the H1 PNG strip [995,1000) maps to physical-buffer
|
||||||
|
y=[995,1000), x=[0,750), while H2 uses its first four physical columns.
|
||||||
|
The private-standard R90 strip has 815 B pixels on H1 and 520 on H2;
|
||||||
|
Mosaic has zero in both strips because its B box ends at PNG x=903.
|
||||||
|
The 115-entry addendum manifest SHA-256 is
|
||||||
|
`426f40078ad6388b34219b09f576d833d99a8dec76ea286235bc34fa324f8f63`
|
||||||
|
and passed entry-by-entry verification. It chains the final analysis manifests
|
||||||
|
without changing any earlier sealed file. These strip counts are not popup
|
||||||
|
hit-delivery measurements.
|
||||||
|
|
||||||
|
### Validation and isolation closeout
|
||||||
|
|
||||||
|
The new ledger passed offline lock, durable reservations, duplicate,
|
||||||
|
exhaustion and missing-ledger tests without a compositor launch. The five
|
||||||
|
fixture modules and three generated runners passed Python AST; the inherited preparer's shell
|
||||||
|
syntax passed. The private SceneFX, Mosaic and pristine builds completed;
|
||||||
|
the Mosaic Meson model test passed 1/1. Both experimental C17 clients compiled
|
||||||
|
with `-Wall -Wextra -Werror`. The two dynamic-fixture patches retained their
|
||||||
|
published SHA-256 and reverse-applicability checks in the fresh private tree.
|
||||||
|
The forward patch sequence was exercised by the fresh preparer. A standalone
|
||||||
|
reverse check of the older SceneFX `effect-trace.patch` against the **fully
|
||||||
|
stacked** private tree failed at `wlr_scene.c:2117`; later overlapping patches
|
||||||
|
make that isolated reverse check inapplicable. No historical patch was edited;
|
||||||
|
the complete private SceneFX build passed.
|
||||||
|
|
||||||
|
Markdown links and the complete local diff, including new files through a
|
||||||
|
temporary Git index, passed checks. The real Git index remains empty.
|
||||||
|
Production Mosaic sources, the cumulative patch, `PKGBUILD`, installed
|
||||||
|
SceneFX/wlroots, `/usr/bin/sway`, QuickShell and dotfiles were untouched.
|
||||||
|
Every experimental IPC call used an explicit private socket; no live SwayFX
|
||||||
|
IPC was accessed. All three per-run 0700 runtime directories were removed,
|
||||||
|
with no retained private socket or experimental process. No install, session
|
||||||
|
restart or STEP20D work occurred. This historical laboratory and its consumed
|
||||||
|
budget are closed; the separate static-R90 partial-popup campaign requires a
|
||||||
|
new ledger and private runtime.
|
||||||
|
|
||||||
|
## Architectural answers and verdict
|
||||||
|
|
||||||
|
| Question | Evidence-backed answer |
|
||||||
|
| --- | --- |
|
||||||
|
| Q1 viewport coherence | Sampled applied R90 tiled/root-popup clips still derive (-800,0,800,600); logical owner/camera stable. |
|
||||||
|
| Q2 SceneFX physical conversion | Source scales then inverse-transforms boxes/regions. Physical damage coverage is compatible at two settled camera transitions; raw framebuffer pixels were not read, so direct physical-render proof is partial. |
|
||||||
|
| Q3 PNG frame | Yes for this `grim -s 1.25` path: 1000×750 logical orientation with all five marker centers unchanged under R90. |
|
||||||
|
| Q4 visible and forbidden pixels | Permitted Mosaic owner/A/B pixels survive R90; zero exact A/B colors on H2. Partial Mosaic popup overflow was not created, so forbidden-pixel clipping is NOT EXERCISED. |
|
||||||
|
| Q5 root/child clips | Root clip and nested inheritance trace the owner viewport; fully hidden owner reaches empty clip. Partial border challenge NOT EXERCISED. |
|
||||||
|
| Q6 grabs | Two genuine grabs per completed chain, valid serial/configure/ack/map and no protocol error. |
|
||||||
|
| Q7 pointer | Two-output logical calibration passes R0/R90 in both private modes, without scale/rotation injection; popup border hit grid unscored. |
|
||||||
|
| Q8 damage | Available physical extents cover 569,789 changed pixels on each R90 camera leg, zero uncovered; no all-frame/minimal guarantee. |
|
||||||
|
| Q9 R0→R90 stale clip/cache | No stale clip in sampled traces/screenshots; effects disabled, optimized cache NOT TESTED. |
|
||||||
|
| Q10 R90→R0 | Private modes restore physical/logical geometry and normal transform; Mosaic restores sampled scene/clip and identical logical PNG. Pristine return NOT TESTED. |
|
||||||
|
| Q11 effects | Shadow/blur disabled; NOT TESTED under rotation. |
|
||||||
|
| Q12 algorithm redesign | No reproducible leak, invisible hit or crash demonstrated. Missing challenge does not justify production or private algorithm changes. |
|
||||||
|
| Q13 applied geometry contract | A future STEP20C contract must bind applied mode, transform, scale, logical viewport and input/damage generation explicitly. This probe proposes that requirement; it does not freeze a new contract. |
|
||||||
|
| Q14 open cases | Pristine R90 PNG/input/return; partial A/B Mosaic crossing; scored popup-border hits; effects/cache; raw physical readback; saved buffers; intermediate frames; R270; ABI/package. |
|
||||||
|
| Q15 next gate | Another **rotated geometry** probe with a Mosaic popup demonstrably crossing x=0 and legal grabs, plus a completed pristine R90 control, is the smallest decisive gate. Saved buffers, optimized cache and ABI/package remain later independent gates. |
|
||||||
|
|
||||||
|
`SWAYFX_MOSAIC_ROTATED_OUTPUT_PROBE=PARTIALLY_FEASIBLE`
|
||||||
|
|
||||||
|
Architectural conclusion: **UNDECIDED** for partial popup clipping under
|
||||||
|
physical rotation. The tested logical geometry, PNG/pointer calibration,
|
||||||
|
fully-hidden guard, damage coverage and return are useful evidence, but the
|
||||||
|
central negative clipping comparison is absent. STEP20C stays **NEXT**;
|
||||||
|
multi-output Mosaic stays **NOT YET SUPPORTED**.
|
||||||
55
tooling/probes/step20c-rotated-output/README.md
Normal file
55
tooling/probes/step20c-rotated-output/README.md
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
# Rotated-output source fixture
|
||||||
|
|
||||||
|
**EXPERIMENTAL / PARTIALLY_FEASIBLE evidence fixture.** The
|
||||||
|
[protocol](../../../docs/design/STEP20C_ROTATED_OUTPUT_PROBE.md) records the
|
||||||
|
scientific question, pinned source audit, geometry/capture gates, strict global
|
||||||
|
action budget and stopping criteria before dynamic execution. All generated
|
||||||
|
sources, builds, clients, sockets, captures, logs and manifests belong in one
|
||||||
|
private temporary LAB, never in Git or the running desktop session.
|
||||||
|
|
||||||
|
The fixture must reuse the published source-only private patches unchanged.
|
||||||
|
The pristine SwayFX control remains free of Mosaic changes. No live IPC,
|
||||||
|
installed library, package or production algorithm may be modified.
|
||||||
|
|
||||||
|
## Measured campaign and replay boundary
|
||||||
|
|
||||||
|
Laboratory: `/tmp/swayfx-mosaic-nested-grab-TJ3AhbCE`. The source-only
|
||||||
|
preparer was the preserved `step20c-dynamic-output/prepare.sh`; it rebuilt
|
||||||
|
pristine SwayFX, private Mosaic/SceneFX and strict C17 clients in a new LAB.
|
||||||
|
The rotation runner generates a bounded child from the published applied-state
|
||||||
|
runner, with the corrected two-reference calibration and a five-marker
|
||||||
|
asymmetric wallpaper. `--source-only` checks generated Python without launch.
|
||||||
|
`test-budget.py` exercises the real lock/ledger offline.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
PYTHONDONTWRITEBYTECODE=1 python tooling/probes/step20c-rotated-output/test-budget.py
|
||||||
|
PYTHONDONTWRITEBYTECODE=1 python tooling/probes/step20c-rotated-output/run.py "$LAB" control --source-only
|
||||||
|
PYTHONDONTWRITEBYTECODE=1 python tooling/probes/step20c-rotated-output/run.py "$LAB" private_standard --source-only
|
||||||
|
PYTHONDONTWRITEBYTECODE=1 python tooling/probes/step20c-rotated-output/run.py "$LAB" private_mosaic --source-only
|
||||||
|
```
|
||||||
|
|
||||||
|
The actual three modes have already been attempted in this LAB. **Do not run
|
||||||
|
those mode commands again.** The pristine control's R90 reservation was
|
||||||
|
consumed before a fixture assertion incorrectly expected IPC `270`; the
|
||||||
|
applied output actually reported user-facing `90`, physical 750×1000 and
|
||||||
|
logical 800×600. No pristine R90 PNG or R0 return is available. The two
|
||||||
|
private modes completed R0→R90→R0. Their five-marker captures establish
|
||||||
|
that `grim -s 1.25` returns a logical-oriented 1000×750 PNG even while the
|
||||||
|
physical buffer is 750×1000. Private `splith` has 15,860 B-color overflow
|
||||||
|
pixels on H2; private Mosaic has zero A/B-color pixels there in the sampled
|
||||||
|
R90 states. The Mosaic B box ends at logical-PNG `x=903`, before the
|
||||||
|
`x=1000` boundary: the zero H2 count is **not** a partial-clip challenge.
|
||||||
|
One Mosaic camera hide/return pair and a subsequent B button delivery were
|
||||||
|
observed. The global result is PARTIALLY_FEASIBLE and the architectural
|
||||||
|
partial-clipping conclusion is UNDECIDED.
|
||||||
|
|
||||||
|
The complete measured evidence and result caveats are in the report. The
|
||||||
|
analyzer reads raw events, screenshots and private logs offline. An initial
|
||||||
|
sealed `results/` analysis counted spill but missed the Mosaic-box geometry
|
||||||
|
confound. The second sealed `results-corrected/` analysis identified that
|
||||||
|
confound; a final `results-final/` analysis derives the owner ID and B box from
|
||||||
|
each run rather than using historical ID/box constants. Earlier directories
|
||||||
|
remain byte-for-byte preserved and chained by manifests. The analyzer refuses
|
||||||
|
to overwrite any sealed directory.
|
||||||
|
Original dynamic, fractional, SceneFX and other earlier fixture patches are
|
||||||
|
unchanged. No new passive instrumentation patch was needed.
|
||||||
262
tooling/probes/step20c-rotated-output/analyze.py
Normal file
262
tooling/probes/step20c-rotated-output/analyze.py
Normal file
|
|
@ -0,0 +1,262 @@
|
||||||
|
"""Offline analysis of retained rotation runs; never launches a compositor."""
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from campaign import read
|
||||||
|
|
||||||
|
COLORS = {'owner': (35, 68, 204), 'A': (224, 192, 32),
|
||||||
|
'B': (32, 208, 160), 'background': (30, 30, 46)}
|
||||||
|
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 latest(traces, kind, before_ns, *, output=None, owner=None):
|
||||||
|
values = [t for t in traces if t['kind'] == kind and t['ns'] <= before_ns
|
||||||
|
and (output is None or t.get('output', t.get('observer')) == output)
|
||||||
|
and (owner is None or t.get('owner_id') == str(owner))]
|
||||||
|
return values[-1] if values else None
|
||||||
|
|
||||||
|
|
||||||
|
def parse_run(run):
|
||||||
|
events = [json.loads(line) for line in (run / 'events.jsonl').read_text().splitlines()]
|
||||||
|
# CONTRACT: Sway node IDs are per-run observations, never fixture constants.
|
||||||
|
owner_id = next(e['owner']['id'] for e in events
|
||||||
|
if e['kind'] == 'state' and e.get('owner'))
|
||||||
|
text = (run / 'compositor.log').read_text(errors='replace')
|
||||||
|
traces = []
|
||||||
|
for line in text.splitlines():
|
||||||
|
match = TRACE.search(line)
|
||||||
|
if match:
|
||||||
|
item = {'kind': match.group(1), **fields(line)}
|
||||||
|
if item.get('ns', '').isdigit():
|
||||||
|
item['ns'] = int(item['ns'])
|
||||||
|
traces.append(item)
|
||||||
|
damages = [(int(ns), output, *map(int, (x1, y1, x2, y2)))
|
||||||
|
for ns, output, x1, y1, x2, y2 in DAMAGE.findall(text)]
|
||||||
|
geometry = {(e['tag'], e['output']): e for e in events
|
||||||
|
if e['kind'] == 'capture_geometry'}
|
||||||
|
captures = {}
|
||||||
|
for e in events:
|
||||||
|
if e['kind'] != 'capture':
|
||||||
|
continue
|
||||||
|
g = geometry[(e['tag'], e['output'])]
|
||||||
|
path = run / e['path']
|
||||||
|
image = np.asarray(Image.open(path).convert('RGB'))
|
||||||
|
counts = {}
|
||||||
|
color_boxes = {}
|
||||||
|
for name, color in COLORS.items():
|
||||||
|
ys, xs = np.where(np.all(image == color, axis=2))
|
||||||
|
counts[name] = len(xs)
|
||||||
|
color_boxes[name] = ([int(xs.min()), int(ys.min()),
|
||||||
|
int(xs.max())+1, int(ys.max())+1]
|
||||||
|
if len(xs) else None)
|
||||||
|
scene = latest(traces, 'PI_SCENE', e['before_ns'], output=e['output'])
|
||||||
|
state = latest(traces, 'DO_STATE', e['before_ns'], output=e['output'])
|
||||||
|
popup = latest(traces, 'DO_POPUP', e['before_ns'], owner=owner_id)
|
||||||
|
viewport = None
|
||||||
|
if state and scene and scene.get('clip_valid') == '1':
|
||||||
|
x, y = map(int, state['tile_global'].split(','))
|
||||||
|
cx, cy, w, h = map(int, scene['clip'].split(','))
|
||||||
|
viewport = [x+cx, y+cy, w, h]
|
||||||
|
captures[(e['tag'], e['output'])] = {
|
||||||
|
'tag': e['tag'], 'output': e['output'], 'file': e['path'],
|
||||||
|
'sha256': digest(path), 'capture_window_ns': [e['before_ns'], e['after_ns']],
|
||||||
|
'physical_mode': g['mode'], 'scale': g['scale'],
|
||||||
|
'ipc_transform': g['transform'], 'logical_rect': g['rect'],
|
||||||
|
'png_dimensions': g['dimensions'], 'png_frame': 'logical-oriented',
|
||||||
|
'colors': counts, 'color_boxes_png': color_boxes,
|
||||||
|
'applied_viewport': viewport,
|
||||||
|
'scene': scene, 'output_state': state, 'root_popup': popup,
|
||||||
|
}
|
||||||
|
client = (run / 'client.log').read_text(errors='replace')
|
||||||
|
grabs = [{'popup': popup, 'target': target, 'serial': int(serial),
|
||||||
|
'used': int(used),
|
||||||
|
'valid': serial == used and bool(re.search(
|
||||||
|
rf'POINTER_BUTTON target={target} serial={serial} button=272 state=1', client))}
|
||||||
|
for popup, target, serial, used in re.findall(
|
||||||
|
r'(POPUP_[AB]) GRAB_REQUEST origin=wl_pointer.button '
|
||||||
|
r'target=(\S+) serial=(\d+) used=(\d+)', client)]
|
||||||
|
return {'run': run.name, 'events': events, 'traces': traces,
|
||||||
|
'damage': damages, 'captures': captures, 'grabs': grabs,
|
||||||
|
'exit': json.loads((run / 'exit-status.json').read_text()),
|
||||||
|
'popup_done': {'A': client.count('POPUP_A POPUP_DONE'),
|
||||||
|
'B': client.count('POPUP_B POPUP_DONE')},
|
||||||
|
'client_protocol_errors': [line for line in client.splitlines()
|
||||||
|
if 'protocol error' in line.lower()]}
|
||||||
|
|
||||||
|
|
||||||
|
def damage_pair(item, first, second, output, rotated):
|
||||||
|
a = item['captures'][(first, output)]
|
||||||
|
b = item['captures'][(second, output)]
|
||||||
|
before = np.asarray(Image.open(Path(item['lab']) / 'runs' / item['run'] / a['file']).convert('RGB'))
|
||||||
|
after = np.asarray(Image.open(Path(item['lab']) / 'runs' / item['run'] / b['file']).convert('RGB'))
|
||||||
|
if before.shape != after.shape or a['ipc_transform'] != b['ipc_transform']:
|
||||||
|
return {'status': 'NOT COMPARABLE: physical mode/transform changed'}
|
||||||
|
changed = np.any(before != after, axis=2)
|
||||||
|
ys, xs = np.where(changed)
|
||||||
|
width, height = (750, 1000) if rotated else (after.shape[1], after.shape[0])
|
||||||
|
mask = np.zeros((height, width), dtype=bool)
|
||||||
|
extents = [d for d in item['damage'] if d[1] == output
|
||||||
|
and a['capture_window_ns'][1] < d[0] <= b['capture_window_ns'][1]]
|
||||||
|
for _, _, x1, y1, x2, y2 in extents:
|
||||||
|
mask[max(0, y1):min(height, y2), max(0, x1):min(width, x2)] = True
|
||||||
|
# WHY: grim's rotated PNG is logical-oriented; SceneFX damage is already
|
||||||
|
# physical buffer-local. Internal WL transform 270 has inverse 90.
|
||||||
|
# CONTRACT: half-open pixel map (x,y) -> (749-y,x) for the measured R90.
|
||||||
|
px, py = (width-1-ys, xs) if rotated else (xs, ys)
|
||||||
|
uncovered = int(np.count_nonzero(~mask[py, px]))
|
||||||
|
return {'status': 'MEASURED', 'from': first, 'to': second, 'output': output,
|
||||||
|
'png_changed_pixels': len(xs), 'physical_extents': [list(d[2:]) for d in extents],
|
||||||
|
'uncovered_changed_pixels': uncovered, 'physical_dimensions': [width, height],
|
||||||
|
'scope': 'settled PNG differences only; no minimal or all-frames claim'}
|
||||||
|
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument('lab', type=Path)
|
||||||
|
args = parser.parse_args()
|
||||||
|
lab = args.lab.resolve()
|
||||||
|
old = lab / 'results'
|
||||||
|
# WHY: the initial sealed analysis counted zero spill without checking whether
|
||||||
|
# the Mosaic popup reached the border. Keep every old byte and path verifiable.
|
||||||
|
# CONTRACT: corrected scientific interpretation gets a separate sealed directory.
|
||||||
|
second = lab / 'results-corrected'
|
||||||
|
if (second / 'evidence-sha256.txt').exists():
|
||||||
|
out = lab / 'results-final'
|
||||||
|
elif (old / 'evidence-sha256.txt').exists():
|
||||||
|
out = second
|
||||||
|
else:
|
||||||
|
out = old
|
||||||
|
out.mkdir(exist_ok=True)
|
||||||
|
if (out / 'evidence-sha256.txt').exists():
|
||||||
|
raise SystemExit('this analysis is already sealed; refuse another rewrite')
|
||||||
|
runs = [parse_run(p) for p in sorted((lab / 'runs').glob('*-rotated-*'))]
|
||||||
|
for item in runs:
|
||||||
|
item['lab'] = str(lab)
|
||||||
|
by_mode = {item['run'].split('-rotated-', 1)[0]: item for item in runs}
|
||||||
|
budget = read(lab)
|
||||||
|
write(out / 'campaign-budget.json', budget)
|
||||||
|
write(out / 'outputs.json', {mode: [e for e in item['events'] if e['kind'] == 'state']
|
||||||
|
for mode, item in by_mode.items()})
|
||||||
|
write(out / 'transforms.json', {mode: [e for e in item['events'] if e['kind'] == 'capture_geometry']
|
||||||
|
for mode, item in by_mode.items()})
|
||||||
|
write(out / 'logical-to-buffer.json', {
|
||||||
|
'R0': {'logical_scaled': [1000, 750], 'physical': [1000, 750], 'map': '(x,y)->(x,y)'},
|
||||||
|
'R90': {'logical_scaled': [1000, 750], 'physical': [750, 1000],
|
||||||
|
'ipc_transform': '90', 'internal_transform': '270',
|
||||||
|
'inverse_map_pixel': '(x,y)->(749-y,x)',
|
||||||
|
'source': 'pinned wlroots box/region transform and measured mode'},
|
||||||
|
})
|
||||||
|
write(out / 'screenshot-coordinates.json', {
|
||||||
|
mode: [e for e in item['events'] if e['kind'] == 'marker_calibration']
|
||||||
|
for mode, item in by_mode.items()})
|
||||||
|
write(out / 'workspace-owner.json', {mode: [e for e in item['events'] if e['kind'] == 'identity']
|
||||||
|
for mode, item in by_mode.items()})
|
||||||
|
write(out / 'scene-clips.json', {mode: [c for c in item['captures'].values()
|
||||||
|
if c['applied_viewport'] is not None]
|
||||||
|
for mode, item in by_mode.items()})
|
||||||
|
write(out / 'popup-geometry.json', {mode: {'grabs': item['grabs'],
|
||||||
|
'done': item['popup_done'], 'protocol_errors': item['client_protocol_errors'],
|
||||||
|
'chains': [e for e in item['events'] if e['kind'] == 'chain']}
|
||||||
|
for mode, item in by_mode.items()})
|
||||||
|
write(out / 'pointer-calibration.json', {mode: [e for e in item['events']
|
||||||
|
if e['kind'] == 'pointer_calibration'] for mode, item in by_mode.items()})
|
||||||
|
write(out / 'input-delivery.json', {mode: [e for e in item['events']
|
||||||
|
if e['kind'] in ('owner_hit', 'restored_popup_hit', 'pointer_request')]
|
||||||
|
for mode, item in by_mode.items()})
|
||||||
|
write(out / 'pixel-analysis.json', {mode: list(item['captures'].values())
|
||||||
|
for mode, item in by_mode.items()})
|
||||||
|
damage = {}
|
||||||
|
for mode, item in by_mode.items():
|
||||||
|
damage[mode] = []
|
||||||
|
if mode == 'private_mosaic':
|
||||||
|
for a, b, output, rotated in [
|
||||||
|
('R90-reopened', 'R90-hidden', 'HEADLESS-1', True),
|
||||||
|
('R90-hidden', 'R90-camera-return', 'HEADLESS-1', True),
|
||||||
|
('R90-reopened', 'R90-hidden', 'HEADLESS-2', False),
|
||||||
|
('R0-return', 'standard-return', 'HEADLESS-1', False)]:
|
||||||
|
damage[mode].append(damage_pair(item, a, b, output, rotated))
|
||||||
|
write(out / 'damage-analysis.json', damage)
|
||||||
|
write(out / 'transition-timeline.json', {mode: {
|
||||||
|
'events': [e for e in item['events'] if e['kind'] in ('dynamic_launch', 'capture')],
|
||||||
|
'native_commit_count': sum(t['kind'] == 'PI_OUTPUT_COMMIT' for t in item['traces']),
|
||||||
|
'native_present_count': sum(t['kind'] == 'PI_OUTPUT_PRESENT' for t in item['traces'])}
|
||||||
|
for mode, item in by_mode.items()})
|
||||||
|
mosaic_capture = by_mode['private_mosaic']['captures'][('R90-popups', 'HEADLESS-1')]
|
||||||
|
mosaic_b_box = mosaic_capture['color_boxes_png']['B']
|
||||||
|
mosaic_boundary = mosaic_capture['png_dimensions'][0]
|
||||||
|
partial_crossing = mosaic_b_box is not None and mosaic_b_box[2] >= mosaic_boundary
|
||||||
|
matrix = {
|
||||||
|
'control': {'R0': 'PASS', 'R90_geometry': 'PASS', 'R90_capture': 'NOT TESTED: fixture IPC direction assertion',
|
||||||
|
'R0_return': 'NOT TESTED'},
|
||||||
|
'private_standard': {'R0': 'PASS', 'R90_geometry_png_pointer': 'PASS',
|
||||||
|
'R90_popup_spill': 'PASS: 15860 B pixels on H2', 'R0_return': 'PASS'},
|
||||||
|
'private_mosaic': {'R0': 'PASS', 'R90_geometry_png_pointer': 'PASS',
|
||||||
|
'R90_popup_no_spill': 'OBSERVED: 0 A/B pixels on H2; B PNG box and boundary recorded separately',
|
||||||
|
'R90_B_png_box': mosaic_b_box,
|
||||||
|
'R90_png_boundary': mosaic_boundary,
|
||||||
|
'R90_partial_popup_clip': ('GEOMETRIC CHALLENGE PRESENT' if partial_crossing
|
||||||
|
else 'NOT EXERCISED: no Mosaic B portion crosses the output boundary'),
|
||||||
|
'camera_hide_return': 'PASS: one pair',
|
||||||
|
'restored_B_button': 'PASS', 'R0_return': 'PASS',
|
||||||
|
'standard_return': 'PASS'},
|
||||||
|
'not_tested': ['pristine R90 PNG/input after fixture assertion',
|
||||||
|
'Mosaic partial popup clipping at R90',
|
||||||
|
'root A crossing', 'simultaneous A/B crossing',
|
||||||
|
'scored popup boundary input grid', 'shadow and blur',
|
||||||
|
'optimized blur cache', 'saved buffers', 'raw physical buffer readback',
|
||||||
|
'every intermediate frame', 'R270', 'SceneFX ABI/package'],
|
||||||
|
}
|
||||||
|
write(out / 'matrix.json', matrix)
|
||||||
|
summary = {'result': 'PARTIALLY_FEASIBLE',
|
||||||
|
'architectural_conclusion': 'UNDECIDED',
|
||||||
|
'reason': 'applied geometry, logical PNG/pointer, hidden-owner and restoration pass; Mosaic popup never crosses R90 boundary and pristine R90 capture is missing',
|
||||||
|
'measured_mosaic_B_png_box': mosaic_b_box,
|
||||||
|
'measured_mosaic_png_boundary': mosaic_boundary,
|
||||||
|
'runs': {mode: {'failure': item['exit']['failure'],
|
||||||
|
'captures': len(item['captures']),
|
||||||
|
'grabs_valid': all(g['valid'] for g in item['grabs'])}
|
||||||
|
for mode, item in by_mode.items()},
|
||||||
|
'budget': {'mode_transform_actions': sum(x['action'] in ('R90','R0') for x in budget['reservations']),
|
||||||
|
'camera_hide_return_actions': sum(x['action'].startswith('camera') for x in budget['reservations'])}}
|
||||||
|
write(out / 'summary.json', summary)
|
||||||
|
source_paths = sorted(set(list((Path(__file__).parent).glob('*.py')) +
|
||||||
|
[Path(__file__).parent / 'README.md'] +
|
||||||
|
list(lab.glob('rotated-*.py')) +
|
||||||
|
[p for p in lab.iterdir() if p.is_file() and p.suffix in ('.c', '.h')] +
|
||||||
|
[p for root in ('mosaic', 'standard', 'scenefx', 'scenefx-git')
|
||||||
|
for p in (lab / root).rglob('*') if p.is_file() and '.git' not in p.parts] +
|
||||||
|
[p for p in (Path(__file__).parents[1]).glob('step20c-*/*.patch')]))
|
||||||
|
(out / 'source-sha256.txt').write_text(''.join(digest(p) + ' ' + str(p.resolve()) + '\n'
|
||||||
|
for p in source_paths))
|
||||||
|
evidence_paths = sorted(set([p for p in (lab / 'runs').rglob('*') if p.is_file()] +
|
||||||
|
[p for p in out.iterdir() if p.is_file() and p.name != 'evidence-sha256.txt'] +
|
||||||
|
[lab / 'rotation-campaign-budget.json'] +
|
||||||
|
([old / 'evidence-sha256.txt', old / 'source-sha256.txt']
|
||||||
|
if out != old else []) +
|
||||||
|
([second / 'evidence-sha256.txt', second / 'source-sha256.txt']
|
||||||
|
if out != second and second.exists() else [])))
|
||||||
|
(out / 'evidence-sha256.txt').write_text(''.join(digest(p) + ' ' + str(p.resolve()) + '\n'
|
||||||
|
for p in evidence_paths))
|
||||||
|
print(json.dumps({'summary': summary, 'source_entries': len(source_paths),
|
||||||
|
'evidence_entries': len(evidence_paths),
|
||||||
|
'source_manifest_sha256': digest(out / 'source-sha256.txt'),
|
||||||
|
'evidence_manifest_sha256': digest(out / 'evidence-sha256.txt')}))
|
||||||
88
tooling/probes/step20c-rotated-output/campaign.py
Normal file
88
tooling/probes/step20c-rotated-output/campaign.py
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
"""Persistent global rotation budget; one lock covers launch and cleanup."""
|
||||||
|
import fcntl
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import time
|
||||||
|
|
||||||
|
MODES = ('control', 'private_standard', 'private_mosaic')
|
||||||
|
ACTIONS = ('R90', 'R0', 'camera_hide', 'camera_return')
|
||||||
|
|
||||||
|
|
||||||
|
def read(lab):
|
||||||
|
lab = Path(lab)
|
||||||
|
path = lab / 'rotation-campaign-budget.json'
|
||||||
|
if not path.exists():
|
||||||
|
if any((lab / 'runs').glob('*-rotated-*')):
|
||||||
|
raise ValueError('missing ledger with retained runs')
|
||||||
|
return {'version': 1, 'runs': [], 'reservations': []}
|
||||||
|
state = json.loads(path.read_text())
|
||||||
|
if state.get('version') != 1 or len(state.get('runs', [])) != len(set(state['runs'])):
|
||||||
|
raise ValueError('bad version or duplicate run')
|
||||||
|
if any(mode not in MODES for mode in state['runs']):
|
||||||
|
raise ValueError('bad mode')
|
||||||
|
seen = set()
|
||||||
|
for item in state.get('reservations', []):
|
||||||
|
key = (item.get('mode'), item.get('action'))
|
||||||
|
if key in seen or key[0] not in state['runs'] or key[1] not in ACTIONS:
|
||||||
|
raise ValueError('bad or duplicate reservation')
|
||||||
|
if key[1].startswith('camera') and key[0] != 'private_mosaic':
|
||||||
|
raise ValueError('camera action outside Mosaic')
|
||||||
|
seen.add(key)
|
||||||
|
return state
|
||||||
|
|
||||||
|
|
||||||
|
def write(lab, state):
|
||||||
|
# WHY: a failed command consumes its reserved global action.
|
||||||
|
# CONTRACT: the caller holds Campaign.lock across child lifetime.
|
||||||
|
# INVARIANT: durable replacement precedes every command launch.
|
||||||
|
path = Path(lab) / 'rotation-campaign-budget.json'
|
||||||
|
tmp = path.with_suffix('.tmp')
|
||||||
|
with tmp.open('w') as output:
|
||||||
|
output.write(json.dumps(state, indent=2) + '\n')
|
||||||
|
output.flush()
|
||||||
|
os.fsync(output.fileno())
|
||||||
|
tmp.replace(path)
|
||||||
|
fd = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY)
|
||||||
|
try:
|
||||||
|
os.fsync(fd)
|
||||||
|
finally:
|
||||||
|
os.close(fd)
|
||||||
|
|
||||||
|
|
||||||
|
class Campaign:
|
||||||
|
def __init__(self, lab):
|
||||||
|
self.lab = Path(lab)
|
||||||
|
self.lock = (self.lab / 'rotation-campaign.lock').open('a')
|
||||||
|
fcntl.flock(self.lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||||
|
|
||||||
|
def begin(self, mode):
|
||||||
|
state = read(self.lab)
|
||||||
|
if mode not in MODES or mode in state['runs']:
|
||||||
|
raise ValueError('invalid or repeated run')
|
||||||
|
state['runs'].append(mode)
|
||||||
|
write(self.lab, state)
|
||||||
|
|
||||||
|
|
||||||
|
def reserve(lab, mode, action):
|
||||||
|
state = read(lab)
|
||||||
|
if mode not in state['runs'] or action not in ACTIONS:
|
||||||
|
raise ValueError('invalid mode/action')
|
||||||
|
if any(x['mode'] == mode and x['action'] == action for x in state['reservations']):
|
||||||
|
raise ValueError('duplicate action')
|
||||||
|
if action in ('R90', 'R0'):
|
||||||
|
if action == 'R0' and not any(x['mode'] == mode and x['action'] == 'R90' for x in state['reservations']):
|
||||||
|
raise ValueError('return without rotation')
|
||||||
|
if sum(x['action'] in ('R90', 'R0') for x in state['reservations']) >= 6:
|
||||||
|
raise ValueError('global rotation budget exhausted')
|
||||||
|
else:
|
||||||
|
if mode != 'private_mosaic':
|
||||||
|
raise ValueError('camera budget is Mosaic only')
|
||||||
|
if action == 'camera_return' and not any(x['action'] == 'camera_hide' for x in state['reservations']):
|
||||||
|
raise ValueError('camera return without hide')
|
||||||
|
if any(x['action'] == action for x in state['reservations']):
|
||||||
|
raise ValueError('global camera pair exhausted')
|
||||||
|
state['reservations'].append({'mode': mode, 'action': action,
|
||||||
|
'reserved_ns': time.monotonic_ns()})
|
||||||
|
write(lab, state)
|
||||||
|
return state
|
||||||
95
tooling/probes/step20c-rotated-output/run.py
Normal file
95
tooling/probes/step20c-rotated-output/run.py
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
"""EXPERIMENTAL adapter; inherited bounded process/socket cleanup stays intact."""
|
||||||
|
import argparse
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from campaign import Campaign
|
||||||
|
from scenario import SCENARIO
|
||||||
|
|
||||||
|
p = argparse.ArgumentParser()
|
||||||
|
p.add_argument('lab', type=Path)
|
||||||
|
p.add_argument('mode', choices=('control', 'private_standard', 'private_mosaic'))
|
||||||
|
p.add_argument('--source-only', action='store_true',
|
||||||
|
help='generate and syntax-check the child without reserving a run')
|
||||||
|
a = p.parse_args()
|
||||||
|
lab = a.lab.resolve()
|
||||||
|
repo = Path(__file__).resolve().parents[3]
|
||||||
|
# WHY: one lock covers all modes, rotations and complete child cleanup.
|
||||||
|
# CONTRACT: repeated/failed runs cannot silently replenish campaign budgets.
|
||||||
|
# INVARIANT: no dynamic action is launched before its persistent reservation.
|
||||||
|
if not a.source_only:
|
||||||
|
campaign = Campaign(lab)
|
||||||
|
campaign.begin(a.mode)
|
||||||
|
s = (repo / 'tooling/probes/step20c-applied-state-input/run.py').read_text()
|
||||||
|
s = s.replace("f'{args.mode}-{args.kind}-asi{args.asi_mode}-{time.time_ns()}'",
|
||||||
|
"f'{args.mode}-rotated-{time.time_ns()}'")
|
||||||
|
s = s.replace('output HEADLESS-1 resolution 800x600 position -800 0',
|
||||||
|
'output HEADLESS-1 resolution 1000x750 scale 1.25 position -800 0')
|
||||||
|
s = s.replace("Path('/usr/bin/sway')", "lab / 'standard-build/sway/sway'")
|
||||||
|
s = s.replace(" snapshot('startup')", " time.sleep(1.0)\n snapshot('startup')")
|
||||||
|
s = s.replace('import argparse', 'import argparse\nfrom PIL import Image, ImageDraw\nimport sys\nsys.path.insert(0, '+repr(str(Path(__file__).parent.resolve()))+')\nfrom campaign import reserve')
|
||||||
|
s = s.replace("run.mkdir(parents=True, mode=0o700)", """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)
|
||||||
|
""")
|
||||||
|
s = s.replace('env = os.environ.copy()', "(run / 'config').write_text((run / 'config').read_text() + f'output * bg {marker_path} fill\\n')\nenv = os.environ.copy()", 1)
|
||||||
|
start = s.index(" expected = {'HEADLESS-1':")
|
||||||
|
end = s.index(" for output, side in", start)
|
||||||
|
s = s[:start]+''' 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 = globals().get('rotation_stage') == 'R90'
|
||||||
|
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)
|
||||||
|
'''+s[end:]
|
||||||
|
s = s.replace("['grim', '-o', output, str(path)]", "['grim', '-s', str(next(o['scale'] for o in outputs if o['name']==output)), '-o', output, str(path)]")
|
||||||
|
s = s.replace(" record('capture', tag=tag", """ 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""")
|
||||||
|
start=s.index('def inject(action, x, y):');end=s.index('\ndef client_command',start)
|
||||||
|
s=s[:start]+'''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']<=x<o['rect']['x']+o['rect']['width'] and o['rect']['y']<=y<o['rect']['y']+o['rect']['height'] for o in outputs):
|
||||||
|
raise ValueError('global point is outside applied outputs')
|
||||||
|
sent_x,sent_y = int(x-min_x),int(y-min_y)
|
||||||
|
width,height = max_x-min_x,max_y-min_y
|
||||||
|
marker=f'INJECT_DONE {action} {sent_x},{sent_y}'
|
||||||
|
before=(run/'pointer.log').read_text().count(marker)
|
||||||
|
record('pointer_request',action=action,global_point=[x,y],sent=[sent_x,sent_y],extent=[width,height],minimum=[min_x,min_y])
|
||||||
|
pointer.stdin.write(f'{action} {sent_x} {sent_y} {width} {height}\\n');pointer.stdin.flush()
|
||||||
|
wait(lambda: (run/'pointer.log').read_text().count(marker)>before)
|
||||||
|
|
||||||
|
'''+s[end:]
|
||||||
|
start=s.index(" if args.kind == 'cycles':\n");end=s.index('except Exception as exc:',start)
|
||||||
|
scenario=SCENARIO
|
||||||
|
s=s[:start]+scenario+'\n'+s[end:]
|
||||||
|
generated=lab/f'rotated-{a.mode}.py'
|
||||||
|
generated.write_text(s)
|
||||||
|
if a.source_only:
|
||||||
|
import ast
|
||||||
|
ast.parse(s, filename=str(generated))
|
||||||
|
print(f'SOURCE_ONLY_PASS={generated}')
|
||||||
|
raise SystemExit(0)
|
||||||
|
raise SystemExit(subprocess.call([sys.executable,str(generated),str(lab),a.mode,'cycles','--max-seconds','360','--max-captures','120','--max-bytes','70000000'], pass_fds=(campaign.lock.fileno(),)))
|
||||||
94
tooling/probes/step20c-rotated-output/scenario.py
Normal file
94
tooling/probes/step20c-rotated-output/scenario.py
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
"""Rotation scenario inserted into the inherited bounded runner try/finally."""
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Reuse the published two-reference correction and real XDG-grab helpers.
|
||||||
|
# WHY: both references are positioned before either input calibration is scored.
|
||||||
|
# CONTRACT: the dynamic-output fixture source is read-only; no old campaign runs.
|
||||||
|
repo = Path(__file__).resolve().parents[3]
|
||||||
|
namespace = {}
|
||||||
|
exec((repo / 'tooling/probes/step20c-dynamic-output/scenario.py').read_text(), namespace)
|
||||||
|
PREFIX = namespace['SCENARIO'].split(" calibrate('D0')", 1)[0]
|
||||||
|
PREFIX = PREFIX.replace(" command('output * bg #1e1e2e solid_color')\n", '')
|
||||||
|
|
||||||
|
SCENARIO = PREFIX + r'''
|
||||||
|
# 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
|
||||||
|
|
||||||
|
rotation_stage = 'R0'
|
||||||
|
marker_gate('startup', False)
|
||||||
|
calibrate('R0')
|
||||||
|
command('[app_id="org.mosaic.NestedGrabProbe"] focus')
|
||||||
|
stable('R0', 'HEADLESS-1'); owner_hit('R0')
|
||||||
|
open_chain('R0-chain'); stable('R0-popups', 'HEADLESS-1')
|
||||||
|
|
||||||
|
reserve(lab, args.mode, 'R90')
|
||||||
|
record('dynamic_launch', action='R90',
|
||||||
|
command='output HEADLESS-1 mode 750x1000 scale 1.25 transform 90 position -800 0')
|
||||||
|
command('output HEADLESS-1 mode 750x1000 scale 1.25 transform 90 position -800 0')
|
||||||
|
rotation_stage = 'R90'
|
||||||
|
stable('R90-popups', 'HEADLESS-1')
|
||||||
|
command('workspace 3')
|
||||||
|
time.sleep(.5)
|
||||||
|
snapshot('R90-marker')
|
||||||
|
marker_gate('R90-marker', True)
|
||||||
|
command('workspace 1')
|
||||||
|
stable('R90-workspace-return', 'HEADLESS-1')
|
||||||
|
closed(); calibrate('R90')
|
||||||
|
command('[app_id="org.mosaic.NestedGrabProbe"] focus')
|
||||||
|
owner_hit('R90')
|
||||||
|
open_chain('R90-chain'); stable('R90-reopened', 'HEADLESS-1')
|
||||||
|
|
||||||
|
if args.mode == 'private_mosaic':
|
||||||
|
initial_camera = workspace()['mosaic']['rows'][0]['scroll_x']
|
||||||
|
reserve(lab,args.mode,'camera_hide'); record('dynamic_launch',action='camera_hide')
|
||||||
|
scroll(1280); stable('R90-hidden', 'HEADLESS-1')
|
||||||
|
reserve(lab,args.mode,'camera_return'); record('dynamic_launch',action='camera_return')
|
||||||
|
scroll(initial_camera); stable('R90-camera-return', 'HEADLESS-1')
|
||||||
|
a = configured('POPUP_A'); b = configured('POPUP_B'); o = owner()
|
||||||
|
sx = workspace()['mosaic']['rows'][0]['scroll_x']
|
||||||
|
bx = int(o['rect']['x']-sx+o['window_rect']['x']+a[0]+b[0]+35)
|
||||||
|
by = int(o['rect']['y']+o['window_rect']['y']+a[1]+b[1]+30)
|
||||||
|
before = len(client_text())
|
||||||
|
inject('move',bx,by); inject('right',bx,by)
|
||||||
|
record('restored_popup_hit',point=[bx,by],
|
||||||
|
delivered='POINTER_BUTTON target=POPUP_B' in client_text()[before:])
|
||||||
|
|
||||||
|
reserve(lab,args.mode,'R0')
|
||||||
|
record('dynamic_launch',action='R0',
|
||||||
|
command='output HEADLESS-1 mode 1000x750 scale 1.25 transform normal position -800 0')
|
||||||
|
command('output HEADLESS-1 mode 1000x750 scale 1.25 transform normal position -800 0')
|
||||||
|
rotation_stage = 'R0'
|
||||||
|
stable('R0-return', 'HEADLESS-1')
|
||||||
|
if args.mode == 'private_mosaic':
|
||||||
|
command('layout splith')
|
||||||
|
stable('standard-return', 'HEADLESS-1')
|
||||||
|
'''
|
||||||
40
tooling/probes/step20c-rotated-output/test-budget.py
Normal file
40
tooling/probes/step20c-rotated-output/test-budget.py
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
"""Exercise the actual ledger without launching a compositor."""
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
from campaign import Campaign, read, reserve
|
||||||
|
|
||||||
|
lab = Path(tempfile.mkdtemp(prefix='rotation-budget-test-'))
|
||||||
|
campaign = Campaign(lab)
|
||||||
|
campaign.begin('control')
|
||||||
|
contender = subprocess.run([sys.executable, '-c',
|
||||||
|
'from campaign import Campaign; Campaign(' + repr(str(lab)) + ')'],
|
||||||
|
cwd=Path(__file__).parent, capture_output=True, text=True)
|
||||||
|
assert contender.returncode and 'BlockingIOError' in contender.stderr
|
||||||
|
for mode in ('control', 'private_standard', 'private_mosaic'):
|
||||||
|
if mode != 'control':
|
||||||
|
campaign.begin(mode)
|
||||||
|
reserve(lab, mode, 'R90')
|
||||||
|
reserve(lab, mode, 'R0')
|
||||||
|
try:
|
||||||
|
reserve(lab, mode, 'R90')
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise AssertionError('duplicate transition accepted')
|
||||||
|
reserve(lab, 'private_mosaic', 'camera_hide')
|
||||||
|
reserve(lab, 'private_mosaic', 'camera_return')
|
||||||
|
state = read(lab)
|
||||||
|
assert len(state['reservations']) == 8
|
||||||
|
campaign.lock.close()
|
||||||
|
assert read(lab) == state
|
||||||
|
old = lab / 'old'
|
||||||
|
(old / 'runs/control-rotated-failed').mkdir(parents=True)
|
||||||
|
try:
|
||||||
|
read(old)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise AssertionError('missing ledger accepted')
|
||||||
|
print('PASS lock, persistence, duplicate, exhaustion, missing ledger; no compositor launched')
|
||||||
Loading…
Reference in a new issue