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
|
||||
|
||||
- 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 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
|
||||
replay rewrote the temporary inventory; the report records that limitation.
|
||||
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
|
||||
analyzer rerun replaced it; its exact historical bytes are not retained.
|
||||
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
|
||||
manifest was verified before an offline analyzer rerun replaced its bytes;
|
||||
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
|
||||
offline analyzer rerun replaced its bytes; see the report for provenance.
|
||||
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