docs(mosaic): preserve saved target crossing evidence
This commit is contained in:
parent
642c61aaaf
commit
75f057d471
14 changed files with 1253 additions and 1 deletions
|
|
@ -4,6 +4,10 @@
|
||||||
|
|
||||||
### STEP20C experimental evidence
|
### STEP20C experimental evidence
|
||||||
|
|
||||||
|
- Local target-crossing saved-buffer probe: one measured signed camera delta placed the real target source and saved node across x=0. The target node rendered within H1's clip and retained its SHM pixels; H2 showed no target color but had a repeated 253-pixel reference difference. Mosaic E2 timeout stopped through stale fixture pointer correlation. PARTIALLY_FEASIBLE, architecture UNDECIDED; no production change, commit or push for this campaign.
|
||||||
|
|
||||||
|
- Freeze audit of that target crossing: the repeated H2 difference is one filled 11×23 rectangle with Kitty cursor/background colors in pristine and Mosaic runs, strongly compatible with cursor blinking but not causally proven. The E2 visible bound at x=-30 matches the applied tiled clip narrowed by gaps. Timeout/input remain untested.
|
||||||
|
|
||||||
- Preserved saved-transaction-buffer probe: real held XDG configures produced nonempty saved scene buffers; private saved nodes entered rendering and sampled ACK/timeout paths removed their trees. Mosaic's target was outside H1 after the applied camera reached 1760, so partial-boundary clipping is NOT EXERCISED and the verdict is INCONCLUSIVE. Three distinct executed runner copies and the private-standard fixture timeout are retained; no production change.
|
- Preserved saved-transaction-buffer probe: real held XDG configures produced nonempty saved scene buffers; private saved nodes entered rendering and sampled ACK/timeout paths removed their trees. Mosaic's target was outside H1 after the applied camera reached 1760, so partial-boundary clipping is NOT EXERCISED and the verdict is INCONCLUSIVE. Three distinct executed runner copies and the private-standard fixture timeout are retained; no production change.
|
||||||
|
|
||||||
- Local static-R90 partial-popup probe: a legal nested B popup crossed the Mosaic viewport by 72 logical pixels; 46,455 B pixels and a real button survived on H1, while H2 was pixel-identical to its reference and delivered no B button. Both standard controls showed 35,360 B pixels and a B button on H2. A crossed visually, but its forbidden-side input was not independently clicked. PARTIALLY_FEASIBLE for the tested matrix; no production change or phase-B commit/push.
|
- Local static-R90 partial-popup probe: a legal nested B popup crossed the Mosaic viewport by 72 logical pixels; 46,455 B pixels and a real button survived on H1, while H2 was pixel-identical to its reference and delivered no B button. Both standard controls showed 35,360 B pixels and a B button on H2. A crossed visually, but its forbidden-side input was not independently clicked. PARTIALLY_FEASIBLE for the tested matrix; no production change or phase-B commit/push.
|
||||||
|
|
|
||||||
|
|
@ -288,3 +288,15 @@ The experimental Mosaic tiled clip is an ancestor of the view tree, so a
|
||||||
saved tree has a plausible inherited path. The measured target was outside
|
saved tree has a plausible inherited path. The measured target was outside
|
||||||
H1 after the applied camera reached 1760, preventing a boundary-clipping
|
H1 after the applied camera reached 1760, preventing a boundary-clipping
|
||||||
conclusion. This remains INCONCLUSIVE, with no production architecture change.
|
conclusion. This remains INCONCLUSIVE, with no production architecture change.
|
||||||
|
|
||||||
|
The separate [target-crossing saved-buffer probe](design/STEP20C_SAVED_BUFFER_TARGET_CROSSING_PROBE.md)
|
||||||
|
then measured the actual source and saved scene buffer crossing x=0 after a
|
||||||
|
single calculated camera correction. Its target saved node rendered with a
|
||||||
|
visible region ending at x=0 and retained target pixels on H1; no target SHM
|
||||||
|
color appeared on H2. A 253-pixel whole-H2 reference difference and incomplete
|
||||||
|
Mosaic timeout/input/damage/lifetime evidence limit the result to
|
||||||
|
PARTIALLY_FEASIBLE and architecture UNDECIDED. No production contract changes.
|
||||||
|
Offline freeze analysis strongly supports a blinking Kitty block cursor for
|
||||||
|
the repeated H2 reference difference, without proving its cause. E2's
|
||||||
|
`x=-30` visible bound matches the applied tiled clip narrowed by gaps; it is
|
||||||
|
not evidence of a distinct content clip. Target timeout cleanup remains open.
|
||||||
|
|
|
||||||
|
|
@ -229,3 +229,18 @@ clipped input, transaction-scoped damage, destruction while pending and
|
||||||
reference lifetime after texture conversion remain unqualified. One
|
reference lifetime after texture conversion remain unqualified. One
|
||||||
private-standard episode failed through fixture timing. STEP20C remains
|
private-standard episode failed through fixture timing. STEP20C remains
|
||||||
**NEXT** and production multi-output **NOT YET SUPPORTED**.
|
**NEXT** and production multi-output **NOT YET SUPPORTED**.
|
||||||
|
|
||||||
|
The [new target-crossing probe](design/STEP20C_SAVED_BUFFER_TARGET_CROSSING_PROBE.md)
|
||||||
|
is **PARTIALLY_FEASIBLE** for one real saved target that crossed x=0 and
|
||||||
|
rendered only its permitted H1 portion in the sampled ACK episode. H2 had no
|
||||||
|
target-color pixels but differed from its whole-image reference by 253 pixels
|
||||||
|
of unproven cause, also seen in pristine controls. A stale fixture pointer
|
||||||
|
stopped Mosaic E2 before natural timeout; target input, complete damage and
|
||||||
|
GPU reference lifetime are unqualified. STEP20C remains **NEXT** and
|
||||||
|
production multi-output **NOT YET SUPPORTED**.
|
||||||
|
|
||||||
|
Offline RGB analysis found the same filled 11×23 `#F5E0DC`→`#303038`
|
||||||
|
rectangle in pristine and Mosaic H2 comparisons. Kitty's recorded local
|
||||||
|
block-cursor color and blink setting strongly support a dynamic reference,
|
||||||
|
but no effective cursor phase was logged. Whole-H2 equality is still
|
||||||
|
unproven; the new timeout probe must use a static reference.
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,9 @@ The [root README](../README.md) is the public summary. For a specific topic, use
|
||||||
[R90 source fixture](../tooling/probes/step20c-rotated-partial-popup/README.md) preserves the published experimental inputs.
|
[R90 source fixture](../tooling/probes/step20c-rotated-partial-popup/README.md) preserves the published experimental inputs.
|
||||||
25. [STEP20C saved transaction buffer probe](design/STEP20C_SAVED_TRANSACTION_BUFFER_PROBE.md): real saved nodes and lifetime evidence, but target partial-overflow geometry NOT EXERCISED; result INCONCLUSIVE.
|
25. [STEP20C saved transaction buffer probe](design/STEP20C_SAVED_TRANSACTION_BUFFER_PROBE.md): real saved nodes and lifetime evidence, but target partial-overflow geometry NOT EXERCISED; result INCONCLUSIVE.
|
||||||
[Saved-buffer source fixture](../tooling/probes/step20c-saved-transaction-buffer/README.md) preserves the experimental inputs.
|
[Saved-buffer source fixture](../tooling/probes/step20c-saved-transaction-buffer/README.md) preserves the experimental inputs.
|
||||||
26. [STEP20A](STEP20A.md) and [STEP20B](STEP20B.md): historical evidence.
|
26. [STEP20C saved target crossing probe](design/STEP20C_SAVED_BUFFER_TARGET_CROSSING_PROBE.md): measured saved target crossing and clipped H1 render; H2 reference difference and incomplete timeout/input gate keep it PARTIALLY_FEASIBLE.
|
||||||
|
[Target-crossing source fixture](../tooling/probes/step20c-saved-buffer-target-crossing/README.md) remains local and experimental.
|
||||||
|
27. [STEP20A](STEP20A.md) and [STEP20B](STEP20B.md): historical evidence.
|
||||||
|
|
||||||
## Project
|
## Project
|
||||||
|
|
||||||
|
|
@ -94,6 +96,7 @@ The [root README](../README.md) is the public summary. For a specific topic, use
|
||||||
| [STEP20C effects/damage probe](design/STEP20C_EFFECTS_DAMAGE_PROBE.md) | Experimental, independent private replay | PARTIALLY_FEASIBLE for visible blur, excluded sampled shadow overflow, settled camera restoration and sampled damage; shadow interior, optimized cache, popup effects and saved buffers remain unqualified |
|
| [STEP20C effects/damage probe](design/STEP20C_EFFECTS_DAMAGE_PROBE.md) | Experimental, independent private replay | PARTIALLY_FEASIBLE for visible blur, excluded sampled shadow overflow, settled camera restoration and sampled damage; shadow interior, optimized cache, popup effects and saved buffers remain unqualified |
|
||||||
| [STEP20C static R90 partial-popup probe](design/STEP20C_ROTATED_PARTIAL_POPUP_CLIP_PROBE.md) | Experimental evidence | PARTIALLY_FEASIBLE; sampled B crossing clips under R90, independent A input remains untested |
|
| [STEP20C static R90 partial-popup probe](design/STEP20C_ROTATED_PARTIAL_POPUP_CLIP_PROBE.md) | Experimental evidence | PARTIALLY_FEASIBLE; sampled B crossing clips under R90, independent A input remains untested |
|
||||||
| [STEP20C saved transaction buffer probe](design/STEP20C_SAVED_TRANSACTION_BUFFER_PROBE.md) | Experimental, local evidence | INCONCLUSIVE; real saved nodes rendered, target partial crossing not exercised |
|
| [STEP20C saved transaction buffer probe](design/STEP20C_SAVED_TRANSACTION_BUFFER_PROBE.md) | Experimental, local evidence | INCONCLUSIVE; real saved nodes rendered, target partial crossing not exercised |
|
||||||
|
| [STEP20C saved target crossing probe](design/STEP20C_SAVED_BUFFER_TARGET_CROSSING_PROBE.md) | Experimental, local evidence | PARTIALLY_FEASIBLE; target saved node crossed and rendered within H1, but whole-H2 reference and timeout/input evidence remain open |
|
||||||
|
|
||||||
A proposal does not supersede accepted canonical contracts or assert implementation status. Its open feasibility items must be resolved before implementation.
|
A proposal does not supersede accepted canonical contracts or assert implementation status. Its open feasibility items must be resolved before implementation.
|
||||||
|
|
||||||
|
|
@ -141,3 +144,14 @@ EXPERIMENTAL / INCONCLUSIVE. Real held XDG configures created nonempty saved
|
||||||
scene buffers; private render entries and ACK/timeout removal were observed.
|
scene buffers; private render entries and ACK/timeout removal were observed.
|
||||||
The Mosaic target was wholly off H1 after an applied relative camera scroll,
|
The Mosaic target was wholly off H1 after an applied relative camera scroll,
|
||||||
so the partial-boundary clipping gate was NOT EXERCISED. STEP20C stays NEXT.
|
so the partial-boundary clipping gate was NOT EXERCISED. STEP20C stays NEXT.
|
||||||
|
|
||||||
|
[Saved target crossing probe](design/STEP20C_SAVED_BUFFER_TARGET_CROSSING_PROBE.md)
|
||||||
|
and [local source fixture](../tooling/probes/step20c-saved-buffer-target-crossing/README.md):
|
||||||
|
EXPERIMENTAL / PARTIALLY_FEASIBLE. One calculated camera correction produced
|
||||||
|
a real saved target crossing x=0 with clipped render and retained H1 pixels.
|
||||||
|
H2's target color was absent, but its whole-image reference differed by 253
|
||||||
|
pixels; Mosaic target timeout and held input remain unqualified. STEP20C
|
||||||
|
stays NEXT and production multi-output remains NOT YET SUPPORTED.
|
||||||
|
The offline freeze audit found an identical filled 11×23 Kitty cursor-color
|
||||||
|
rectangle across pristine and Mosaic captures. Cursor blinking is strongly
|
||||||
|
supported, not causally proven; a static H2 reference is the next gate.
|
||||||
|
|
|
||||||
|
|
@ -231,3 +231,20 @@ authorized saved-buffer experiment must calibrate the applied camera and
|
||||||
target crossing before the first transaction. Optimized blur/cache and
|
target crossing before the first transaction. Optimized blur/cache and
|
||||||
SceneFX ABI/package remain separate unqualified gates. STEP20C stays
|
SceneFX ABI/package remain separate unqualified gates. STEP20C stays
|
||||||
**NEXT**, multi-output **NOT YET SUPPORTED**.
|
**NEXT**, multi-output **NOT YET SUPPORTED**.
|
||||||
|
|
||||||
|
The independent [saved target crossing campaign](design/STEP20C_SAVED_BUFFER_TARGET_CROSSING_PROBE.md)
|
||||||
|
used one measured signed camera correction and observed the actual target
|
||||||
|
saved node cross x=0, render inside H1 and retain permitted pixels. H2 showed
|
||||||
|
no target color, but whole-image equality failed by a repeated 253-pixel
|
||||||
|
reference difference. Mosaic timeout cleanup and held input remain untested
|
||||||
|
after an E2 fixture pointer error. Result **PARTIALLY_FEASIBLE**, architecture
|
||||||
|
**UNDECIDED**. The next narrow gate should first resolve the H2 reference
|
||||||
|
artifact and complete a fresh target timeout/input observation; optimized
|
||||||
|
blur/cache and SceneFX ABI/package remain later separate gates. STEP20C is
|
||||||
|
still **NEXT** and multi-output **NOT YET SUPPORTED**.
|
||||||
|
|
||||||
|
The saved-target freeze audit identifies the repeated 253-pixel H2 difference
|
||||||
|
as strongly compatible with Kitty's blinking block cursor, not causally
|
||||||
|
proven. A separate static-reference Mosaic timeout/input gate is authorized
|
||||||
|
before optimized blur/cache and SceneFX ABI/package work. No production
|
||||||
|
readiness or changed UX contract follows.
|
||||||
|
|
|
||||||
243
docs/design/STEP20C_SAVED_BUFFER_TARGET_CROSSING_PROBE.md
Normal file
243
docs/design/STEP20C_SAVED_BUFFER_TARGET_CROSSING_PROBE.md
Normal file
|
|
@ -0,0 +1,243 @@
|
||||||
|
# STEP20C saved target boundary crossing probe
|
||||||
|
|
||||||
|
**EXPERIMENTAL / PARTIALLY_FEASIBLE, measured 2026-10-12.** STEP20C remains NEXT and
|
||||||
|
production multi-output Mosaic is NOT YET SUPPORTED. This is a new, independent
|
||||||
|
campaign following the [saved transaction buffer freeze](STEP20C_SAVED_TRANSACTION_BUFFER_PROBE.md),
|
||||||
|
whose verdict remains INCONCLUSIVE. No Mosaic production, cumulative patch,
|
||||||
|
package, UX or SceneFX algorithm change is authorized here.
|
||||||
|
|
||||||
|
## Scientific identity and source path
|
||||||
|
|
||||||
|
The target is the XDG toplevel with `app_id=org.mosaic.SavedBufferProbe`.
|
||||||
|
Its Sway tiled container, borrowed workspace tiling row, view scene tree,
|
||||||
|
live content tree, temporary saved tree and saved scene buffer are distinct.
|
||||||
|
The source buffer of this view must be identified before the transaction.
|
||||||
|
An output buffer or optimized blur cache is never evidence for that source.
|
||||||
|
|
||||||
|
`transaction_commit` configures a changed-size visible view when
|
||||||
|
`should_configure` holds, then `view_save_buffer` creates a sibling saved tree
|
||||||
|
under `view->scene_tree`. `view_save_buffer_iterator` visits enabled live
|
||||||
|
buffers via `wlr_scene_node_for_each_buffer`, creates saved scene nodes via
|
||||||
|
`wlr_scene_buffer_create`, and calls `wlr_scene_buffer_set_buffer` with the
|
||||||
|
source buffer. Current live content is disabled and the saved tree enabled.
|
||||||
|
`transaction_progress` applies on readiness or `handle_timeout`; the normal
|
||||||
|
`apply_container_state` path calls `view_remove_saved_buffer`. SceneFX's
|
||||||
|
`scene_node_intersect_clip` intersects ancestral tree clips for visible/damage
|
||||||
|
regions and `scene_node_point_inside_clip` guards node-at; `scene_entry_render`
|
||||||
|
is the actual render-entry observation. This source path warrants a test, not
|
||||||
|
a clipping conclusion. The target node ID must be correlated from source to
|
||||||
|
saved creation to render entry and destruction. Parentage and enabled state
|
||||||
|
must be recorded. Pointer equality alone does not prove all GPU references
|
||||||
|
were released.
|
||||||
|
|
||||||
|
## Geometry gate before reservation
|
||||||
|
|
||||||
|
Use only two normal scale-1 headless outputs: H1 `(-800,0),800x600` and H2
|
||||||
|
`(0,0),800x600`. Map the deterministic C17 XDG client and ordinary reference
|
||||||
|
clients, settle focus, query actual outputs and workspace Mosaic state, and
|
||||||
|
measure the target **source scene buffer**: node pointer, source buffer
|
||||||
|
pointer, global node coordinates, destination width/height, source box,
|
||||||
|
transform, owner container/view, applied camera and tiled clip. A container
|
||||||
|
rect alone is insufficient. The previous runner issued relative
|
||||||
|
`layout mosaic scroll 400` and reached applied scroll 1760, leaving its target
|
||||||
|
outside H1. Here `desired_scroll` is computed from the measured source buffer
|
||||||
|
left/right and applied `current_scroll`; issue exactly one signed
|
||||||
|
`delta=desired_scroll-current_scroll`, then remeasure. Require
|
||||||
|
`left<0<right`, preferably at least 64 logical pixels on each side, a visible
|
||||||
|
H1 intersection and a theoretical H2 intersection. The source must be
|
||||||
|
nonempty, have a tiled Mosaic ancestor clip, and remain stable after focus.
|
||||||
|
If any gate fails, stop that run before reserving an episode. Do not scan
|
||||||
|
camera positions or create a second lab to evade the budget.
|
||||||
|
|
||||||
|
The transaction trigger may alter pending dimensions. During the hold,
|
||||||
|
remeasure the **saved** scene buffer and require all of:
|
||||||
|
`SAVED_TARGET_CREATED`, `SAVED_TARGET_ENABLED`,
|
||||||
|
`SAVED_TARGET_BUFFER_NONEMPTY`, `SAVED_TARGET_CROSSES_X0`, and
|
||||||
|
`SAVED_TARGET_RENDER_ENTRY_OBSERVED`. Correlate exact view, owner, source
|
||||||
|
buffer and saved node. If the actual saved node does not cross, classify the
|
||||||
|
episode NOT EXERCISED even if another saved node renders.
|
||||||
|
|
||||||
|
## Episodes, observations and bounds
|
||||||
|
|
||||||
|
Use pinned SwayFX 0.6 `fd71a6bdc061bd633b488ae7b83e8a6981d22f86`,
|
||||||
|
private Mosaic `cd550382b311efdbfe3ed892f4e1d281d852e754`, SceneFX 0.5
|
||||||
|
`37ccd723bef49e6891156ffafce8f549f01446cc` and wlroots 0.20.2.
|
||||||
|
Reapply the preserved clipping and trace patches without changing their bytes.
|
||||||
|
Any additional passive trace is a separate hashed private patch. Build and
|
||||||
|
run only in one disposable `/tmp` laboratory, with private 0700 runtime,
|
||||||
|
Wayland and Sway IPC sockets. `-Dtxn-timeout=2500` is a lab policy only; source
|
||||||
|
parsing and equal use in comparable modes must be confirmed. Do not use
|
||||||
|
`-Dtxn-wait`, `-Dnoatomic`, direct save calls, compositor sleeps or altered
|
||||||
|
pending transaction counts.
|
||||||
|
|
||||||
|
At most one run each: C2 private Mosaic (priority), C0 pristine splith and C1
|
||||||
|
private splith. Each has at most two reserved episodes: E1 real configure
|
||||||
|
hold then latest-valid-serial ACK and commit before timeout; E2 a second real
|
||||||
|
hold until natural timeout with the client alive and pings serviced. The
|
||||||
|
ordinary `gaps inner current set` trigger is acceptable only when a changed
|
||||||
|
target configure and waiting instruction are observed. During E1 collect the
|
||||||
|
target saved node, render entry and minimal capture, then release promptly;
|
||||||
|
no six pointer injections during the hold. Observe applied transaction,
|
||||||
|
saved-tree removal and live-content return in both episodes.
|
||||||
|
|
||||||
|
Define H1 permitted and H2 forbidden regions before pixel analysis. Require
|
||||||
|
target SHM color on H1 while the saved node renders, no target color on H2,
|
||||||
|
and whole-H2 RGB equality against its reference; retain every difference.
|
||||||
|
Sample allowed and forbidden pointer points only if the hold window permits.
|
||||||
|
Distinguish `wlr_scene_node_at`, descriptor/surface association, seat notice
|
||||||
|
and client delivery; a saved scene buffer need not deliver clicks to the
|
||||||
|
held client. Record available damage around creation, activation, removal and
|
||||||
|
new live image, native commits/presents when traced, and do not infer every
|
||||||
|
intermediate frame from stabilized PNGs. Do not compare unrelated clocks.
|
||||||
|
Mapped-view destruction is NOT TESTED unless separately shown safe and within
|
||||||
|
budget. No rotation, scale, migration, resize, popup, effects or camera
|
||||||
|
hide/return pair belongs to this campaign.
|
||||||
|
|
||||||
|
The locked persistent ledger permits three runs, two episodes per mode, one
|
||||||
|
calculated camera adjustment in the Mosaic run, at most 16 captures per run
|
||||||
|
and 48 overall, and 150 seconds per run. Reserve before an episode; a failed
|
||||||
|
reservation is consumed and never refunded. Preserve the exact runner copy,
|
||||||
|
raw logs, outputs, captures, exit codes, structured JSON and SHA-256 source
|
||||||
|
and evidence manifests. Offline ledger tests precede any run. Stop early on
|
||||||
|
uncalibrated geometry, a noncrossing saved target, or an incident; preserve
|
||||||
|
the facts and do not call placement error BLOCKED.
|
||||||
|
|
||||||
|
FEASIBLE requires correct target identity/crossing/render, permitted H1 pixels,
|
||||||
|
no forbidden H2 pixels/hits, ACK and timeout cleanup and no blocking standard
|
||||||
|
regression. PARTIALLY_FEASIBLE requires crossing and rendering with limited
|
||||||
|
input, lifetime or damage evidence. INCONCLUSIVE covers absent target
|
||||||
|
crossing/render or inseparable identities. BLOCKED requires a reproduced
|
||||||
|
forbidden pixel/hit, lifetime defect, crash or blocking regression. Any
|
||||||
|
unstudied matrix cell remains NOT TESTED. Neither an experimental success nor
|
||||||
|
a compile qualifies STEP20C production.
|
||||||
|
|
||||||
|
## Measured campaign and incidents
|
||||||
|
|
||||||
|
The independent LAB is `/tmp/swayfx-mosaic-nested-grab-eVtLtpYd`. Its private
|
||||||
|
passive geometry patch is `target-geometry-trace.patch`, SHA-256
|
||||||
|
`7bbb27bc8617645e1acfd0589a410d470426363bd8a108bd43fc1006747f012e`.
|
||||||
|
The old saved-buffer Sway and SceneFX trace patches retained their published
|
||||||
|
bytes. The private build used `-Dtxn-timeout=2500` in all three modes; source
|
||||||
|
parses `txn-timeout=` into `server.txn_timeout_ms`, whose default is 200 ms.
|
||||||
|
All outputs were queried as H1 `(-800,0),800x600` and H2 `(0,0),800x600`,
|
||||||
|
scale 1, normal. The target's workspace was ID 6 on H1, container ID 8,
|
||||||
|
view `0x55997bcb85b0`; pointers identify objects only within this process.
|
||||||
|
|
||||||
|
Before correction the applied camera read **1760** and the actual source
|
||||||
|
scene buffer was `[-1918,-1282)` with 636×573 destination/source size,
|
||||||
|
normal transform and source box `(0,0,636,573)`. The fixture computed
|
||||||
|
`desired_scroll=160` and issued exactly one relative delta **−1600**.
|
||||||
|
Afterward `get_tree` read scroll **160** and the same source node/buffer at
|
||||||
|
`[-318,318)`, y=25..598. H1's applied tiled scene was `(-160,0)` with clip
|
||||||
|
`(160,0,800,600)`, equivalent to global x `[-800,0)`. No subsequent focus
|
||||||
|
action moved the camera. The 318-pixel permitted and theoretical forbidden
|
||||||
|
horizontal portions exceed the 64-pixel preflight target. This run passed
|
||||||
|
source preflight before E1 reservation.
|
||||||
|
|
||||||
|
In E1, a genuine held configure (serial 50) followed a changed target size.
|
||||||
|
The target saved tree `0x55997bcecd00` was created under its view tree,
|
||||||
|
and target saved node `0x55997bd6a560` reused source buffer
|
||||||
|
`0x55997bd4ec00`. Its measured global box was `[-318,318)×[25,598)`;
|
||||||
|
`content_enabled=0`, `saved_enabled=1`. The **same node** entered `SB_RENDER`
|
||||||
|
with global visible extent `[-318,0)×[25,598)` and damage extent in output
|
||||||
|
coordinates `[(482,25),(800,598))`. The held H1 capture contained **182,214**
|
||||||
|
pixels of the target's prior SHM color `(32,208,160)`; the H2 capture
|
||||||
|
contained **zero** of that color. The client ACKed the real serial 50 and
|
||||||
|
committed a new buffer before 2500 ms; the target saved tree was destroyed,
|
||||||
|
and the applied H1 capture contained the new target color. This is direct
|
||||||
|
partial saved-target crossing and render evidence, rather than evidence from
|
||||||
|
the other three saved nodes in the transaction.
|
||||||
|
|
||||||
|
Whole-H2 RGB comparison did **not** equal its reference during E1: 253 pixels
|
||||||
|
changed in `[4,35,15,58)`. Each changed pixel went from `(245,224,220)` in
|
||||||
|
the reference to `(48,48,56)` in the held image. The same count, box and color
|
||||||
|
transition appeared in the new pristine control after timeout and both old
|
||||||
|
pristine/Mosaic final-timeout comparisons. It is consistent with a
|
||||||
|
reference-specific disappearing shape, but its cause is **UNDECIDED**. It is
|
||||||
|
not target SHM color, and this evidence does not establish a Mosaic leak.
|
||||||
|
The whole-H2 equality requirement nevertheless remains unmet.
|
||||||
|
|
||||||
|
E2 created a new saved buffer after E1's ACK, as the ordinary client commit
|
||||||
|
changed its source pointer. The new target snapshot was observed at
|
||||||
|
`[-258,378)×[55,568)`, and its own node `0x55997bcea350` entered render
|
||||||
|
with visible extent `[-258,-30)×[55,568)`. The **executed runner** still
|
||||||
|
compared that new pointer with the pre-E1 source pointer and stopped with
|
||||||
|
`actual target saved buffer does not cross x=0`. That error is misleading:
|
||||||
|
the trace proves crossing; the failed condition was stale buffer identity.
|
||||||
|
The runner was corrected locally to remeasure before each episode, but the
|
||||||
|
exact executed copy remains in `runs/*/fixture-source.py`. No Mosaic E2
|
||||||
|
capture or natural timeout was obtained; shutdown follows the fixture
|
||||||
|
exception. Do not treat this as a compositor timeout or clipping failure.
|
||||||
|
The run was not repeated and both episode reservations remain spent.
|
||||||
|
|
||||||
|
C0 (instrumented pristine SwayFX source, unmodified SceneFX behavior) and C1
|
||||||
|
(private standard `splith`) completed their held ACK and natural 2500 ms
|
||||||
|
timeout episodes, with saved-tree cleanup. Their standard tiling geometry is
|
||||||
|
not camera-equivalent to C2. Neither had target-color pixels on H2 during its
|
||||||
|
ACK hold. No blocking standard regression was demonstrated. The new C0 final
|
||||||
|
timeout image showed the same 253-pixel H2 difference; C1's H2 remained
|
||||||
|
identical throughout. C2's target input during hold, surface/client delivery,
|
||||||
|
complete damage and native buffer/texture release remain **NOT TESTED** or
|
||||||
|
unqualified. Mapped-view destruction, output changes, effects and intermediate
|
||||||
|
frames were outside the matrix.
|
||||||
|
|
||||||
|
The persistent ledger reads **3/3 runs**, **6/6 episode reservations**,
|
||||||
|
**26/48 captures** (C0 10, C1 10, C2 6), and **1/1** calculated Mosaic
|
||||||
|
camera adjustment. All run durations were below 150 seconds. No extra run or
|
||||||
|
camera retry is authorized. The raw runs, structured results, runner copies,
|
||||||
|
exit codes, build log and SHA-256 manifests are under the LAB. The result is
|
||||||
|
`SWAYFX_MOSAIC_SAVED_BUFFER_TARGET_CROSSING_PROBE=PARTIALLY_FEASIBLE`:
|
||||||
|
target crossing/render and permitted H1 pixels are established, while whole-H2
|
||||||
|
equality, input, full damage/lifetime and Mosaic target timeout cleanup are
|
||||||
|
unqualified. Architectural conclusion: `UNDECIDED`. No SceneFX algorithm or
|
||||||
|
production Mosaic correction is justified by a reproduced counterexample.
|
||||||
|
|
||||||
|
The final source manifest covers **913** entries (including the three private
|
||||||
|
source trees and fixture), SHA-256
|
||||||
|
`816b55d94936fa32c7272845cd2f0514a55ad73b3cb04d32d442d5910e3da09b`.
|
||||||
|
The evidence manifest covers **99** raw and derived entries, SHA-256
|
||||||
|
`0ea45da93396810cf63c451f64d47101c4d4b8c7fb27900043494a0ec591096e`.
|
||||||
|
Both passed entry-by-entry verification. They exclude the report itself, whose
|
||||||
|
wording was synchronized after the measurements.
|
||||||
|
|
||||||
|
## Freeze audit: H2 reference and E2 clip (offline, no replay)
|
||||||
|
|
||||||
|
The exact executed runner is the manifest-covered `fixture-source.py` in each
|
||||||
|
run (SHA-256 `51eb1274b757701e80f54004016944b7fa23f731d194193d1baabbe25a6cdc82`).
|
||||||
|
The preserved local `run.py` differs: it remeasures `TARGET_SOURCE` before
|
||||||
|
each episode, so E2 no longer compares a post-ACK source buffer against the
|
||||||
|
pre-E1 pointer. This fixes the identified correlation condition in source;
|
||||||
|
the corrected runner was **NOT EXECUTED** in this exhausted campaign.
|
||||||
|
|
||||||
|
An offline full-RGB comparison of the old and new LABs found five nonempty
|
||||||
|
H2-reference differences: old pristine timeout-applied, old Mosaic
|
||||||
|
timeout-applied, new pristine timeout-applied, and new Mosaic ACK-held and
|
||||||
|
ACK-applied. **Each** has exactly 253 changed pixels filling the complete
|
||||||
|
rectangle `[4,15)×[35,58)` (11×23), with every pixel changing from
|
||||||
|
`#F5E0DC` `(245,224,220)` to `#303038` `(48,48,56)`. No other H2 pixel
|
||||||
|
differs in those comparisons, and none of the changed pixels is a target SHM
|
||||||
|
color. The corresponding new-reference-to-image capture-start intervals are
|
||||||
|
about 2.725 s for pristine timeout, 0.083 s for Mosaic ACK-held and 0.180 s
|
||||||
|
for Mosaic ACK-applied; the old pristine/Mosaic timeout intervals are about
|
||||||
|
1.455/1.476 s. Other sampled H2 pairs remained equal. These timings can
|
||||||
|
sample either phase of a blinking cursor; they do not prove a blink event.
|
||||||
|
|
||||||
|
The H2 reference process was Kitty with the fixture argument
|
||||||
|
`background=#303038`. The local `~/.config/kitty/kitty.conf` and included
|
||||||
|
`generated-appearance.conf` predate these runs and contain `cursor_shape
|
||||||
|
block`, a later `cursor_blink_interval 0.5`, and cursor color `#f5e0dc`.
|
||||||
|
The matching filled rectangle, exact colors and repeated pristine/Mosaic
|
||||||
|
observations strongly support a blinking Kitty block cursor as the cause.
|
||||||
|
The recorded logs do not capture Kitty's effective parsed configuration or
|
||||||
|
its cursor phase, so **causal confirmation remains open**. Whole-H2 RGB
|
||||||
|
equality remains historically unsatisfied; no pixels were masked or images
|
||||||
|
rewritten. The next lab must use a static reference.
|
||||||
|
|
||||||
|
The E2 saved target itself crossed x=0. Its render visible extent ended at
|
||||||
|
`x=-30`, matching the **applied tiled Mosaic clip after E1's 30-pixel gaps**:
|
||||||
|
H1 logged `tile_scene=-130,30`, local clip `(160,0,740,540)` and global
|
||||||
|
viewport `[-770,-30)×[30,570)`. The source in `transaction.c` derives clip
|
||||||
|
width/height from output usable area minus current gaps. Thus the measured
|
||||||
|
right bound is explained by this outer tiled clip and gap geometry; it does
|
||||||
|
not establish an additional content/view clip. E2's timeout and cleanup
|
||||||
|
remain **NOT TESTED** because the fixture stopped before expiration.
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
# Saved target crossing source fixture
|
||||||
|
|
||||||
|
**EXPERIMENTAL / PARTIALLY_FEASIBLE.** See the [report](../../../docs/design/STEP20C_SAVED_BUFFER_TARGET_CROSSING_PROBE.md).
|
||||||
|
This new campaign has its own locked budget and one disposable `/tmp` LAB.
|
||||||
|
The prior saved-buffer ledger and captures are read-only historical evidence.
|
||||||
|
Production source, cumulative patch, package and installed libraries are not
|
||||||
|
changed. No commit or push is authorized for this fixture.
|
||||||
|
|
||||||
|
`prepare.sh` reconstructs the pinned private builds using the preserved
|
||||||
|
upstream clipping and trace patches, then applies any separately hashed
|
||||||
|
passive target-geometry instrumentation. It never starts a compositor.
|
||||||
|
`run.py LAB MODE` uses only private sockets and must pass the source-buffer
|
||||||
|
geometry preflight before reserving a transaction episode. A failed preflight
|
||||||
|
ends the run without an exploratory camera retry. The exact runner used is
|
||||||
|
copied into each run directory. All generated data remain under LAB, outside
|
||||||
|
Git. Run the offline ledger test before a graphical run.
|
||||||
|
|
||||||
|
Completed LAB: `/tmp/swayfx-mosaic-nested-grab-eVtLtpYd`. It used one
|
||||||
|
calculated Mosaic camera delta, three runs, six episode reservations and 26
|
||||||
|
captures. The exact executed `fixture-source.py` in each run preserves the
|
||||||
|
runner before the post-run E2 pointer-correlation fix. Do not replay this
|
||||||
|
exhausted campaign. The geometry trace patch has SHA-256
|
||||||
|
`7bbb27bc8617645e1acfd0589a410d470426363bd8a108bd43fc1006747f012e`;
|
||||||
|
the prior trace patches are referenced by the source manifest. `analyze.py`
|
||||||
|
regenerates the offline JSON summaries and both manifests without launching
|
||||||
|
another compositor. Target E1 crossing/render and H1 pixels are measured;
|
||||||
|
Mosaic E2 timeout cleanup and input remain untested.
|
||||||
125
tooling/probes/step20c-saved-buffer-target-crossing/analyze.py
Normal file
125
tooling/probes/step20c-saved-buffer-target-crossing/analyze.py
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
"""Offline, bounded analysis of the one target-crossing LAB; no compositor run."""
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
from PIL import Image, ImageChops
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument('lab', type=Path)
|
||||||
|
args = parser.parse_args()
|
||||||
|
lab = args.lab.resolve()
|
||||||
|
results = lab / 'results'
|
||||||
|
results.mkdir(exist_ok=True)
|
||||||
|
repo = Path(__file__).resolve().parents[3]
|
||||||
|
run_dirs = {mode: next((lab / 'runs').glob(f'saved-{mode}-*'))
|
||||||
|
for mode in ('control', 'private_standard', 'private_mosaic')}
|
||||||
|
|
||||||
|
|
||||||
|
def write(name, value):
|
||||||
|
(results / f'{name}.json').write_text(json.dumps(value, indent=2, sort_keys=True) + '\n')
|
||||||
|
|
||||||
|
|
||||||
|
def events(mode):
|
||||||
|
return [json.loads(line) for line in (run_dirs[mode] / 'events.jsonl').read_text().splitlines()]
|
||||||
|
|
||||||
|
|
||||||
|
def diff(a, b):
|
||||||
|
first = Image.open(a).convert('RGB')
|
||||||
|
second = Image.open(b).convert('RGB')
|
||||||
|
d = ImageChops.difference(first, second)
|
||||||
|
return {'box': d.getbbox(), 'changed_pixels': sum(1 for p in d.get_flattened_data() if p != (0, 0, 0))}
|
||||||
|
|
||||||
|
|
||||||
|
def count_color(path, color):
|
||||||
|
im = Image.open(path).convert('RGB')
|
||||||
|
return sum(1 for pixel in im.get_flattened_data() if pixel == color)
|
||||||
|
|
||||||
|
budget = json.loads((lab / 'target-budget.json').read_text())
|
||||||
|
write('budget', budget)
|
||||||
|
preflight = json.loads((results / 'target-preflight.json').read_text())
|
||||||
|
write('target-geometry', {'source_before': {k: preflight['before'][k] for k in ('left', 'right', 'width', 'source_size', 'dest', 'src_box', 'transform')},
|
||||||
|
'source_after': {k: preflight['after'][k] for k in ('left', 'right', 'width', 'source_size', 'dest', 'src_box', 'transform')}})
|
||||||
|
write('camera-calibration', {k: preflight[k] for k in ('current_scroll', 'desired_scroll', 'delta')})
|
||||||
|
mosaic = run_dirs['private_mosaic']
|
||||||
|
me = events('private_mosaic')
|
||||||
|
saved = [e for e in me if e['kind'] == 'saved_target']
|
||||||
|
log = (mosaic / 'compositor.log').read_text()
|
||||||
|
client = (mosaic / 'client.log').read_text()
|
||||||
|
ack_node = saved[0]['node']
|
||||||
|
ack_buffer = saved[0]['buffer']
|
||||||
|
render = [line for line in log.splitlines() if 'SB_RENDER ' in line and f'node={ack_node}' in line]
|
||||||
|
destroy = [line for line in log.splitlines() if 'SB_TREE_DESTROY ' in line and f'view={preflight["after"]["view"]}' in line]
|
||||||
|
held_color = (32, 208, 160)
|
||||||
|
pixels = {}
|
||||||
|
for mode, run in run_dirs.items():
|
||||||
|
ref = run / 'right-reference.png'
|
||||||
|
pixels[mode] = {p.stem: diff(ref, p) for p in sorted(run.glob('right-*.png'))}
|
||||||
|
pixels[mode]['target_color_on_h2_held'] = (count_color(run / 'right-ack-held.png', held_color)
|
||||||
|
if (run / 'right-ack-held.png').exists() else None)
|
||||||
|
pixels['private_mosaic']['target_color_on_h1_ack_held'] = count_color(mosaic / 'left-ack-held.png', held_color)
|
||||||
|
pixels['private_mosaic']['target_color_on_h1_reference'] = count_color(mosaic / 'left-reference.png', held_color)
|
||||||
|
write('pixel-analysis', pixels)
|
||||||
|
write('saved-tree-identity', {'target_view': preflight['after']['view'], 'target_container': preflight['after']['container'],
|
||||||
|
'ack_saved_node': ack_node, 'ack_destroy_observations': destroy,
|
||||||
|
'e2_saved_node': saved[1]['node'] if len(saved) > 1 else None})
|
||||||
|
write('saved-buffer-identity', {'source_node': preflight['after']['source_node'], 'source_buffer': preflight['after']['source_buffer'],
|
||||||
|
'ack_saved_buffer': ack_buffer, 'ack_same_source': ack_buffer == preflight['after']['source_buffer'],
|
||||||
|
'e2_saved_buffer': saved[1]['buffer'] if len(saved) > 1 else None,
|
||||||
|
'e2_note': 'new source buffer after E1 ACK; executed runner compared it with stale pre-E1 pointer'})
|
||||||
|
write('scene-parentage', {'ack_creation': [line for line in log.splitlines() if 'SB_TREE_CREATE ' in line and f'view={preflight["after"]["view"]}' in line][-2:],
|
||||||
|
'ack_saved_node_trace': [line for line in log.splitlines() if 'TARGET_SAVED ' in line and f'node={ack_node}' in line][-1:],
|
||||||
|
'source_path_contract': 'saved tree under view scene tree; Mosaic tiled clip is ancestor per source audit'})
|
||||||
|
write('saved-target-crossing', {'e1': saved[0]['global_box'], 'e1_crosses_x0': saved[0]['global_box'][0] < 0 < saved[0]['global_box'][2],
|
||||||
|
'e2': saved[1]['global_box'] if len(saved) > 1 else None,
|
||||||
|
'e2_crosses_x0': len(saved) > 1 and saved[1]['global_box'][0] < 0 < saved[1]['global_box'][2],
|
||||||
|
'e2_status': 'NOT EXERCISED through timeout due to fixture pointer correlation'})
|
||||||
|
write('transaction-timeline', {mode: [e for e in events(mode) if e['kind'] in ('episode_start', 'saved_target', 'held', 'timeout_observed', 'failure', 'cleanup')]
|
||||||
|
for mode in run_dirs})
|
||||||
|
write('configure-ack', {mode: [line for line in (run / 'client.log').read_text().splitlines()
|
||||||
|
if line.startswith(('HELD ', 'ACK ', 'COMMIT ', 'PONG '))]
|
||||||
|
for mode, run in run_dirs.items()})
|
||||||
|
write('render-entries', {'e1_target': render, 'e1_observed': bool(render), 'e2_target': [line for line in log.splitlines()
|
||||||
|
if len(saved) > 1 and 'SB_RENDER ' in line and f'node={saved[1]["node"]}' in line]})
|
||||||
|
write('clip-observations', {'e1_target_render_visible': [re.search(r'visible=([^ ]+)', x).group(1) for x in render],
|
||||||
|
'e1_applied_clip': [line for line in log.splitlines() if 'PI_SCENE output=HEADLESS-1' in line and 'scroll=160 tile_scene=-160,0 clip_valid=1 clip=160,0,800,600' in line][:2],
|
||||||
|
'source_audit': 'scene_node_intersect_clip and scene_node_point_inside_clip walk ancestors'})
|
||||||
|
write('pointer-hits', {'status': 'NOT TESTED during target hold', 'reason': 'ACK window prioritized target snapshot and protocol'})
|
||||||
|
write('client-delivery', {'status': 'NOT TESTED during target hold'})
|
||||||
|
write('damage-analysis', {'status': 'LIMITED', 'render_damage_e1': [re.search(r'damage=([^ ]+)', x).group(1) for x in render],
|
||||||
|
'complete_intermediate_frames': 'NOT TESTED'})
|
||||||
|
write('lifetime', {'e1_target_saved_tree_destroy_observed': bool(destroy), 'destroy_trace': destroy,
|
||||||
|
'e2_timeout_cleanup': 'NOT TESTED', 'all_gpu_references_released': 'NOT PROVEN',
|
||||||
|
'mapped_view_destroy_during_transaction': 'NOT TESTED'})
|
||||||
|
write('timeout', {mode: [line for line in (run / 'compositor.log').read_text().splitlines() if 'timed out' in line]
|
||||||
|
for mode, run in run_dirs.items()})
|
||||||
|
write('matrix', {'C2_E1': 'TARGET CROSSING/RENDER/ACK OBSERVED; H2 whole-image mismatch 253 pixels',
|
||||||
|
'C2_E2': 'TARGET CREATED AND CROSSES; fixture stale-buffer correlation stopped before timeout',
|
||||||
|
'C0_E1_E2': 'COMPLETED', 'C1_E1_E2': 'COMPLETED',
|
||||||
|
'target_hold_input': 'NOT TESTED', 'complete_damage': 'NOT TESTED'})
|
||||||
|
write('summary', {'result': 'PARTIALLY_FEASIBLE', 'architectural_conclusion': 'UNDECIDED',
|
||||||
|
'target_ack_crossing_and_render': True, 'target_ack_h1_color_pixels': pixels['private_mosaic']['target_color_on_h1_ack_held'],
|
||||||
|
'target_ack_h2_color_pixels': pixels['private_mosaic']['target_color_on_h2_held'],
|
||||||
|
'target_ack_h2_full_diff': pixels['private_mosaic']['right-ack-held'],
|
||||||
|
'target_timeout_cleanup': 'NOT TESTED',
|
||||||
|
'reason': 'E1 target crossing/render and allowed H1 pixels observed; H2 reference mismatch, input/damage and target E2 cleanup remain unqualified'})
|
||||||
|
|
||||||
|
source_files = sorted((repo / 'tooling/probes/step20c-saved-buffer-target-crossing').glob('*'))
|
||||||
|
source_files += [repo / 'tooling/probes/step20c-saved-transaction-buffer/sway-saved-trace.patch',
|
||||||
|
repo / 'tooling/probes/step20c-saved-transaction-buffer/scenefx-saved-trace.patch']
|
||||||
|
for source_root in (lab / 'mosaic', lab / 'standard', lab / 'scenefx'):
|
||||||
|
source_files.extend(source_root.rglob('*'))
|
||||||
|
source_files.extend(lab.glob('*.c'))
|
||||||
|
source_files.extend(lab.glob('*.h'))
|
||||||
|
source_files = [p for p in source_files if p.is_file()]
|
||||||
|
|
||||||
|
def manifest(paths, dest):
|
||||||
|
dest.write_text(''.join(f'{hashlib.sha256(p.read_bytes()).hexdigest()} {p}\n' for p in sorted(paths)))
|
||||||
|
|
||||||
|
manifest(source_files, results / 'source-sha256.txt')
|
||||||
|
evidence_files = (list((lab / 'runs').glob('**/*')) + list(results.glob('*.json')) +
|
||||||
|
list(lab.glob('*-log.txt')) +
|
||||||
|
[lab / 'target-budget.json', lab / 'source-sha256.txt'])
|
||||||
|
manifest([p for p in evidence_files if p.is_file()], results / 'evidence-sha256.txt')
|
||||||
|
print('RESULT=PARTIALLY_FEASIBLE', 'RUNS=3', 'CAPTURES=' + str(sum(budget['captures'].values())))
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
"""One persistent, exclusive ledger for the independent target campaign."""
|
||||||
|
import fcntl
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
MODES = ('control', 'private_standard', 'private_mosaic')
|
||||||
|
|
||||||
|
|
||||||
|
def reserve(lab: Path, mode: str, episode: str | None = None,
|
||||||
|
*, camera: bool = False, capture: bool = False) -> dict:
|
||||||
|
if mode not in MODES or episode not in (None, 'ack', 'timeout'):
|
||||||
|
raise ValueError('invalid reservation')
|
||||||
|
if int(camera) + int(capture) + int(episode is not None) > 1:
|
||||||
|
raise ValueError('reserve one resource per call')
|
||||||
|
path = lab / 'target-budget.json'
|
||||||
|
with path.open('a+') as file:
|
||||||
|
fcntl.flock(file, fcntl.LOCK_EX)
|
||||||
|
file.seek(0)
|
||||||
|
raw = file.read()
|
||||||
|
state = json.loads(raw) if raw else {
|
||||||
|
'runs': [], 'episodes': [], 'camera': [], 'captures': {m: 0 for m in MODES}}
|
||||||
|
if camera:
|
||||||
|
if mode != 'private_mosaic' or mode not in state['runs'] or state['camera']:
|
||||||
|
raise RuntimeError('camera budget exhausted')
|
||||||
|
state['camera'].append(mode)
|
||||||
|
elif capture:
|
||||||
|
if mode not in state['runs'] or state['captures'][mode] >= 16 or sum(state['captures'].values()) >= 48:
|
||||||
|
raise RuntimeError('capture budget exhausted')
|
||||||
|
state['captures'][mode] += 1
|
||||||
|
elif episode is None:
|
||||||
|
if mode in state['runs'] or len(state['runs']) >= 3:
|
||||||
|
raise RuntimeError('run budget exhausted')
|
||||||
|
state['runs'].append(mode)
|
||||||
|
else:
|
||||||
|
key = [mode, episode]
|
||||||
|
if mode not in state['runs'] or key in state['episodes'] or len(state['episodes']) >= 6:
|
||||||
|
raise RuntimeError('episode budget exhausted')
|
||||||
|
state['episodes'].append(key)
|
||||||
|
file.seek(0)
|
||||||
|
file.truncate()
|
||||||
|
json.dump(state, file, sort_keys=True)
|
||||||
|
file.write('\n')
|
||||||
|
file.flush()
|
||||||
|
os.fsync(file.fileno())
|
||||||
|
return state
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# EXPERIMENTAL source-only reconstruction; never launches a compositor.
|
||||||
|
set -eu
|
||||||
|
repo=$(CDPATH= cd -- "$(dirname -- "$0")/../../.." && pwd)
|
||||||
|
fixture="$repo/tooling/probes/step20c-saved-buffer-target-crossing"
|
||||||
|
old="$repo/tooling/probes/step20c-saved-transaction-buffer"
|
||||||
|
log=$(mktemp /tmp/saved-target-prepare-XXXXXXXX)
|
||||||
|
if ! sh "$repo/tooling/probes/step20c-rotated-partial-popup/prepare.sh" > "$log" 2>&1; then
|
||||||
|
printf 'BUILD_FAILED log=%s\n' "$log" >&2
|
||||||
|
tail -40 "$log" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
lab=$(sed -n 's/^LAB=//p' "$log" | head -1)
|
||||||
|
test -n "$lab" && test -d "$lab"
|
||||||
|
for source in standard mosaic; do
|
||||||
|
git -C "$lab/$source" apply --check "$old/sway-saved-trace.patch"
|
||||||
|
git -C "$lab/$source" apply "$old/sway-saved-trace.patch"
|
||||||
|
done
|
||||||
|
git -C "$lab/scenefx" apply --check "$old/scenefx-saved-trace.patch"
|
||||||
|
git -C "$lab/scenefx" apply "$old/scenefx-saved-trace.patch"
|
||||||
|
if test -f "$fixture/target-geometry-trace.patch"; then
|
||||||
|
for source in standard mosaic; do
|
||||||
|
git -C "$lab/$source" apply --check "$fixture/target-geometry-trace.patch"
|
||||||
|
git -C "$lab/$source" apply "$fixture/target-geometry-trace.patch"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
meson compile -C "$lab/scenefx-build" -j 8 >> "$log" 2>&1
|
||||||
|
meson compile -C "$lab/standard-build" -j 8 >> "$log" 2>&1
|
||||||
|
meson compile -C "$lab/mosaic-build" -j 8 >> "$log" 2>&1
|
||||||
|
cc -std=c17 -Wall -Wextra -Werror -Wno-unused-parameter -I"$lab" \
|
||||||
|
"$fixture/saved-client.c" "$lab/xdg-shell-protocol.c" -o "$lab/saved-client" \
|
||||||
|
$(pkg-config --cflags --libs wayland-client) >> "$log" 2>&1
|
||||||
|
cp "$log" "$lab/saved-target-build-log.txt"
|
||||||
|
printf 'LAB=%s\nBUILD_LOG=%s\n' "$lab" "$log"
|
||||||
343
tooling/probes/step20c-saved-buffer-target-crossing/run.py
Normal file
343
tooling/probes/step20c-saved-buffer-target-crossing/run.py
Normal file
|
|
@ -0,0 +1,343 @@
|
||||||
|
"""One reserved headless run with a real held XDG configure per episode."""
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
from PIL import Image
|
||||||
|
from budget import reserve
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument('lab', type=Path)
|
||||||
|
parser.add_argument('mode', choices=('control', 'private_standard', 'private_mosaic'))
|
||||||
|
args = parser.parse_args()
|
||||||
|
lab = args.lab.resolve()
|
||||||
|
if not (lab / 'saved-client').is_file():
|
||||||
|
parser.error('prepared LAB required')
|
||||||
|
state = reserve(lab, args.mode)
|
||||||
|
run = lab / 'runs' / f'saved-{args.mode}-{time.time_ns()}'
|
||||||
|
run.mkdir(parents=True, mode=0o700)
|
||||||
|
(run / 'fixture-source.py').write_text(Path(__file__).read_text())
|
||||||
|
runtime = Path(tempfile.mkdtemp(prefix='mst-', dir='/tmp'))
|
||||||
|
(run / 'runtime-path').write_text(str(runtime) + '\n')
|
||||||
|
(run / 'config').write_text(
|
||||||
|
'output HEADLESS-1 resolution 800x600 scale 1 transform normal position -800 0\n'
|
||||||
|
'output HEADLESS-2 resolution 800x600 scale 1 transform normal position 0 0\n'
|
||||||
|
'workspace 1 output HEADLESS-1\nworkspace 2 output HEADLESS-2\n'
|
||||||
|
'focus_follows_mouse no\n')
|
||||||
|
env = os.environ.copy()
|
||||||
|
for key in ('SWAYSOCK', 'I3SOCK', 'WAYLAND_DISPLAY', 'LD_LIBRARY_PATH'):
|
||||||
|
env.pop(key, None)
|
||||||
|
env.update(XDG_RUNTIME_DIR=str(runtime), WLR_BACKENDS='headless',
|
||||||
|
WLR_HEADLESS_OUTPUTS='2', WLR_RENDERER='pixman', SAVED_TRACE='1', SAVED_TARGET_TRACE='1')
|
||||||
|
binary = lab / ('standard-build/sway/sway' if args.mode == 'control'
|
||||||
|
else 'mosaic-build/sway/swayfx-mosaic')
|
||||||
|
if args.mode != 'control':
|
||||||
|
env['LD_LIBRARY_PATH'] = str(lab / 'scenefx-build')
|
||||||
|
started = time.monotonic()
|
||||||
|
captures = 0
|
||||||
|
events = []
|
||||||
|
processes = []
|
||||||
|
logs = []
|
||||||
|
failure = None
|
||||||
|
compositor = None
|
||||||
|
|
||||||
|
|
||||||
|
def record(kind, **kw):
|
||||||
|
event = {'ns': time.monotonic_ns(), 'kind': kind, **kw}
|
||||||
|
events.append(event)
|
||||||
|
with (run / 'events.jsonl').open('a') as file:
|
||||||
|
file.write(json.dumps(event) + '\n')
|
||||||
|
|
||||||
|
|
||||||
|
def spawn(argv, name, *, stdin=False, use_env=None):
|
||||||
|
file = (run / name).open('w')
|
||||||
|
logs.append(file)
|
||||||
|
proc = subprocess.Popen(argv, env=use_env or env, stdout=file,
|
||||||
|
stderr=subprocess.STDOUT, start_new_session=True,
|
||||||
|
stdin=subprocess.PIPE if stdin else None, text=stdin)
|
||||||
|
processes.append(proc)
|
||||||
|
return proc
|
||||||
|
|
||||||
|
|
||||||
|
def wait(predicate, seconds=8):
|
||||||
|
limit = time.monotonic() + seconds
|
||||||
|
while time.monotonic() < limit:
|
||||||
|
if time.monotonic() - started > 150:
|
||||||
|
raise RuntimeError('150 second run bound exceeded')
|
||||||
|
value = predicate()
|
||||||
|
if value:
|
||||||
|
return value
|
||||||
|
if compositor and compositor.poll() is not None:
|
||||||
|
raise RuntimeError(f'compositor exit {compositor.returncode}')
|
||||||
|
time.sleep(.025)
|
||||||
|
raise RuntimeError('wait timed out')
|
||||||
|
|
||||||
|
|
||||||
|
def ipc(*argv):
|
||||||
|
result = subprocess.run(['swaymsg', '-s', str(sock), *argv], env=env,
|
||||||
|
capture_output=True, text=True, timeout=5, check=True)
|
||||||
|
value = json.loads(result.stdout)
|
||||||
|
record('ipc', argv=list(argv), result=value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def command(value):
|
||||||
|
result = ipc(value)
|
||||||
|
if not result or not all(x.get('success') for x in result):
|
||||||
|
raise RuntimeError(f'IPC rejected {value}: {result}')
|
||||||
|
|
||||||
|
|
||||||
|
def find(node, predicate):
|
||||||
|
if isinstance(node, dict):
|
||||||
|
if predicate(node):
|
||||||
|
return node
|
||||||
|
for child in node.get('nodes', []) + node.get('floating_nodes', []):
|
||||||
|
match = find(child, predicate)
|
||||||
|
if match:
|
||||||
|
return match
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def client_log():
|
||||||
|
return (run / 'client.log').read_text()
|
||||||
|
|
||||||
|
|
||||||
|
def compositor_log():
|
||||||
|
return (run / 'compositor.log').read_text()
|
||||||
|
|
||||||
|
|
||||||
|
def client_command(value):
|
||||||
|
marker = f'COMMAND {value} DONE'
|
||||||
|
before = client_log().count(marker)
|
||||||
|
client.stdin.write(value + '\n')
|
||||||
|
client.stdin.flush()
|
||||||
|
wait(lambda: client_log().count(marker) > before)
|
||||||
|
record('client_command', value=value)
|
||||||
|
|
||||||
|
|
||||||
|
def inject(action, x, y):
|
||||||
|
if not -800 <= x < 800 or not 0 <= y < 600:
|
||||||
|
raise ValueError('pointer point outside applied outputs')
|
||||||
|
marker = f'INJECT_DONE {action} {x + 800},{y}'
|
||||||
|
before = (run / 'pointer.log').read_text().count(marker)
|
||||||
|
pointer.stdin.write(f'{action} {x + 800} {y} 1600 600\n')
|
||||||
|
pointer.stdin.flush()
|
||||||
|
wait(lambda: (run / 'pointer.log').read_text().count(marker) > before)
|
||||||
|
record('pointer', action=action, global_point=[x, y],
|
||||||
|
client_tail=client_log().splitlines()[-5:])
|
||||||
|
|
||||||
|
|
||||||
|
def snapshot(tag):
|
||||||
|
global captures
|
||||||
|
outputs = ipc('-t', 'get_outputs')
|
||||||
|
actual = {o['name']: (o['rect']['x'], o['rect']['y'],
|
||||||
|
o['rect']['width'], o['rect']['height']) for o in outputs if o['active']}
|
||||||
|
if actual != {'HEADLESS-1': (-800, 0, 800, 600), 'HEADLESS-2': (0, 0, 800, 600)}:
|
||||||
|
raise RuntimeError(f'output geometry invalid: {actual}')
|
||||||
|
for name, side in (('HEADLESS-1', 'left'), ('HEADLESS-2', 'right')):
|
||||||
|
reserve(lab, args.mode, capture=True)
|
||||||
|
path = run / f'{side}-{tag}.png'
|
||||||
|
before = time.monotonic_ns()
|
||||||
|
subprocess.run(['grim', '-s', '1', '-o', name, str(path)], env=env,
|
||||||
|
check=True, timeout=5, stdout=subprocess.DEVNULL)
|
||||||
|
after = time.monotonic_ns()
|
||||||
|
captures += 1
|
||||||
|
size = Image.open(path).size
|
||||||
|
if size != (800, 600):
|
||||||
|
raise RuntimeError(f'PNG dimensions invalid: {size}')
|
||||||
|
record('capture', tag=tag, output=name, path=path.name,
|
||||||
|
before_ns=before, after_ns=after, size=size)
|
||||||
|
(run / f'tree-{tag}.json').write_text(json.dumps(ipc('-t', 'get_tree'), indent=2) + '\n')
|
||||||
|
|
||||||
|
|
||||||
|
def source_measure():
|
||||||
|
tree = ipc('-t', 'get_tree')
|
||||||
|
workspace = find(tree, lambda n: n.get('type') == 'workspace' and n.get('name') == '1')
|
||||||
|
owner = find(tree, lambda n: n.get('app_id') == 'org.mosaic.SavedBufferProbe')
|
||||||
|
if not owner or not workspace:
|
||||||
|
raise RuntimeError('target view or workspace absent')
|
||||||
|
lines = [line for line in compositor_log().splitlines() if 'TARGET_SOURCE ns=' in line]
|
||||||
|
if not lines:
|
||||||
|
raise RuntimeError('target source scene buffer unmeasured')
|
||||||
|
line = lines[-1]
|
||||||
|
def field(name):
|
||||||
|
match = re.search(rf'{name}=([^ ]+)', line)
|
||||||
|
if not match:
|
||||||
|
raise RuntimeError(f'missing {name}: {line}')
|
||||||
|
return match.group(1)
|
||||||
|
xy = [int(x) for x in field('global').split(',')]
|
||||||
|
size = [int(x) for x in field('source_size').split(',')]
|
||||||
|
dest = [int(x) for x in field('dest').split(',')]
|
||||||
|
width = dest[0] or size[0]
|
||||||
|
result = {'view': field('view'), 'container': field('con'),
|
||||||
|
'source_node': field('node'), 'source_buffer': field('buffer'),
|
||||||
|
'global_valid': field('global_valid') == '1',
|
||||||
|
'left': xy[0], 'top': xy[1], 'width': width,
|
||||||
|
'right': xy[0] + width, 'source_size': size, 'dest': dest,
|
||||||
|
'src_box': field('src_box'), 'transform': field('transform'),
|
||||||
|
'enabled': field('enabled'), 'owner': owner,
|
||||||
|
'workspace': workspace, 'raw': line}
|
||||||
|
record('source_measure', measurement=result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def preflight_target():
|
||||||
|
global target_source
|
||||||
|
before = source_measure()
|
||||||
|
target_source = before
|
||||||
|
if not before['global_valid'] or before['width'] <= 0 or before['enabled'] != '1':
|
||||||
|
raise RuntimeError('source buffer geometry invalid')
|
||||||
|
if args.mode != 'private_mosaic':
|
||||||
|
record('standard_preflight', source=before)
|
||||||
|
return
|
||||||
|
row = before['workspace']['mosaic']['rows'][0]
|
||||||
|
current = row['scroll_x']
|
||||||
|
desired_left = -max(64, min(before['width'] // 2, before['width'] - 64))
|
||||||
|
delta = int(round(before['left'] - desired_left))
|
||||||
|
desired = current + delta
|
||||||
|
if before['width'] < 128 or desired < 0:
|
||||||
|
raise RuntimeError(f'crossing unreachable from measured source: current={current} delta={delta}')
|
||||||
|
record('camera_calculation', current_scroll=current, desired_scroll=desired,
|
||||||
|
delta=delta, desired_left=desired_left, source=before)
|
||||||
|
if delta:
|
||||||
|
reserve(lab, args.mode, camera=True)
|
||||||
|
command(f'layout mosaic scroll {delta}')
|
||||||
|
time.sleep(.2)
|
||||||
|
after = source_measure()
|
||||||
|
target_source = after
|
||||||
|
applied = after['workspace']['mosaic']['rows'][0]['scroll_x']
|
||||||
|
record('camera_applied', scroll=applied, source=after)
|
||||||
|
if applied != desired or after['left'] >= -64 or after['right'] <= 64:
|
||||||
|
raise RuntimeError(f'target source failed boundary preflight: {after["left"]},{after["right"]}, scroll={applied}')
|
||||||
|
if after['source_node'] != before['source_node'] or after['source_buffer'] != before['source_buffer']:
|
||||||
|
raise RuntimeError('source identity changed during camera adjustment')
|
||||||
|
(lab / 'results').mkdir(exist_ok=True)
|
||||||
|
(lab / 'results' / 'target-preflight.json').write_text(json.dumps({
|
||||||
|
'before': before, 'after': after, 'current_scroll': current,
|
||||||
|
'desired_scroll': desired, 'delta': delta}, indent=2) + '\n')
|
||||||
|
|
||||||
|
|
||||||
|
def episode(kind, gaps):
|
||||||
|
global target_source
|
||||||
|
# WHY: an ACK commits a new source buffer before the next episode.
|
||||||
|
# CONTRACT: remeasure it before reserving; never reuse the first pointer.
|
||||||
|
target_source = source_measure()
|
||||||
|
if args.mode == 'private_mosaic' and not (
|
||||||
|
target_source['left'] < 0 < target_source['right']):
|
||||||
|
raise RuntimeError('current target source no longer crosses x=0')
|
||||||
|
reserve(lab, args.mode, kind)
|
||||||
|
record('episode_start', episode=kind, gaps=gaps)
|
||||||
|
client_command('hold_next_configure')
|
||||||
|
old_held = client_log().count('HELD ns=')
|
||||||
|
old_tree = compositor_log().count('SB_TREE_CREATE')
|
||||||
|
command(f'gaps inner current set {gaps}')
|
||||||
|
wait(lambda: client_log().count('HELD ns=') > old_held, 3)
|
||||||
|
wait(lambda: compositor_log().count('SB_TREE_CREATE') > old_tree, 3)
|
||||||
|
record('held', client=client_log().splitlines()[-12:],
|
||||||
|
saved=compositor_log().splitlines()[-30:])
|
||||||
|
if args.mode == 'private_mosaic':
|
||||||
|
saved = [line for line in compositor_log().splitlines()
|
||||||
|
if 'TARGET_SAVED ns=' in line and f'view={target_source["view"]}' in line]
|
||||||
|
if not saved:
|
||||||
|
raise RuntimeError('target saved scene buffer absent')
|
||||||
|
line = saved[-1]
|
||||||
|
match = re.search(r'node=(0x[0-9a-f]+).*buffer=(0x[0-9a-f]+).*global_valid=(\d+) global=(-?\d+),(-?\d+).*source_size=(\d+),(\d+) dest=(\d+),(\d+).*enabled=(\d+)', line)
|
||||||
|
if not match:
|
||||||
|
raise RuntimeError(f'target saved trace incomplete: {line}')
|
||||||
|
node, buffer, valid, x, y, sw, sh, dw, dh, enabled = match.groups()
|
||||||
|
right = int(x) + (int(dw) or int(sw))
|
||||||
|
record('saved_target', node=node, buffer=buffer, source=target_source,
|
||||||
|
global_box=[int(x), int(y), right, int(y) + (int(dh) or int(sh))],
|
||||||
|
raw=line)
|
||||||
|
if buffer != target_source['source_buffer'] or valid != '1' or enabled != '1' or int(x) >= 0 or right <= 0:
|
||||||
|
raise RuntimeError('actual target saved buffer does not cross x=0')
|
||||||
|
snapshot(f'{kind}-held')
|
||||||
|
if args.mode == 'private_mosaic':
|
||||||
|
if not any(f'node={node}' in line for line in compositor_log().splitlines() if 'SB_RENDER ' in line):
|
||||||
|
raise RuntimeError('target saved node had no render entry')
|
||||||
|
if kind == 'ack':
|
||||||
|
old_destroy = compositor_log().count('SB_TREE_DESTROY')
|
||||||
|
client_command('release_configure')
|
||||||
|
wait(lambda: compositor_log().count('SB_TREE_DESTROY') > old_destroy, 3)
|
||||||
|
snapshot('ack-applied')
|
||||||
|
else:
|
||||||
|
wait(lambda: 'timed out' in compositor_log().split('SB_TREE_CREATE')[-1], 3)
|
||||||
|
record('timeout_observed')
|
||||||
|
snapshot('timeout-applied')
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
file = (run / 'compositor.log').open('w')
|
||||||
|
logs.append(file)
|
||||||
|
compositor = subprocess.Popen([str(binary), '-c', str(run / 'config'),
|
||||||
|
'-d', '-Dtxn-timeout=2500'], env=env,
|
||||||
|
stdout=file, stderr=subprocess.STDOUT,
|
||||||
|
start_new_session=True)
|
||||||
|
def sockets():
|
||||||
|
s = next(iter(runtime.glob('sway-ipc.*.sock')), None)
|
||||||
|
w = next((p for p in runtime.glob('wayland-*') if p.suffix != '.lock'), None)
|
||||||
|
return (s, w) if s and w else None
|
||||||
|
sock, way = wait(sockets)
|
||||||
|
env.update(SWAYSOCK=str(sock), WAYLAND_DISPLAY=way.name)
|
||||||
|
command('workspace 1')
|
||||||
|
command('layout mosaic' if args.mode == 'private_mosaic' else 'layout splith')
|
||||||
|
spawn(['kitty', '--class', 'A', '/usr/bin/sleep', '900'], 'A.log')
|
||||||
|
wait(lambda: len(find(ipc('-t', 'get_tree'),
|
||||||
|
lambda n: n.get('type') == 'workspace' and n.get('name') == '1')['nodes']) >= 1)
|
||||||
|
client = spawn([str(lab / 'saved-client')], 'client.log', stdin=True)
|
||||||
|
wait(lambda: 'COMMIT ns=' in client_log())
|
||||||
|
for name in ('C', 'D'):
|
||||||
|
spawn(['kitty', '--class', name, '/usr/bin/sleep', '900'], f'{name}.log')
|
||||||
|
time.sleep(.2)
|
||||||
|
command('workspace 2')
|
||||||
|
spawn(['kitty', '--class', 'REFERENCE', '-o', 'background=#303038',
|
||||||
|
'/usr/bin/sleep', '900'], 'reference.log')
|
||||||
|
wait(lambda: bool(find(ipc('-t', 'get_tree'),
|
||||||
|
lambda n: n.get('type') == 'workspace' and n.get('name') == '2')['nodes']))
|
||||||
|
command('workspace 1')
|
||||||
|
time.sleep(.2)
|
||||||
|
tree = ipc('-t', 'get_tree')
|
||||||
|
owner = find(tree, lambda n: n.get('app_id') == 'org.mosaic.SavedBufferProbe')
|
||||||
|
workspace = find(tree, lambda n: n.get('type') == 'workspace' and n.get('name') == '1')
|
||||||
|
if not owner or not workspace:
|
||||||
|
raise RuntimeError('owner/workspace calibration failed')
|
||||||
|
record('identity', owner=owner, workspace=workspace)
|
||||||
|
preflight_target()
|
||||||
|
snapshot('reference')
|
||||||
|
episode('ack', 30)
|
||||||
|
episode('timeout', 50)
|
||||||
|
except Exception as exc:
|
||||||
|
failure = repr(exc)
|
||||||
|
record('failure', message=failure)
|
||||||
|
finally:
|
||||||
|
for proc in processes + ([compositor] if compositor else []):
|
||||||
|
if proc.poll() is None:
|
||||||
|
try:
|
||||||
|
os.killpg(proc.pid, signal.SIGTERM)
|
||||||
|
except ProcessLookupError:
|
||||||
|
pass
|
||||||
|
for proc in processes + ([compositor] if compositor else []):
|
||||||
|
try:
|
||||||
|
proc.wait(timeout=5)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
os.killpg(proc.pid, signal.SIGKILL)
|
||||||
|
proc.wait(timeout=5)
|
||||||
|
for file in logs:
|
||||||
|
file.close()
|
||||||
|
(run / 'exit-status.json').write_text(json.dumps({
|
||||||
|
'failure': failure, 'captures': captures,
|
||||||
|
'duration_seconds': time.monotonic() - started,
|
||||||
|
'compositor': compositor.returncode if compositor else None,
|
||||||
|
'children': [{'args': p.args, 'code': p.returncode} for p in processes]},
|
||||||
|
indent=2) + '\n')
|
||||||
|
shutil.rmtree(runtime)
|
||||||
|
record('cleanup', runtime_removed=not runtime.exists())
|
||||||
|
print(f'RUN={run} FAILURE={failure} CAPTURES={captures}')
|
||||||
|
if failure:
|
||||||
|
raise SystemExit(1)
|
||||||
|
|
@ -0,0 +1,215 @@
|
||||||
|
#define _POSIX_C_SOURCE 200809L
|
||||||
|
#include <errno.h>
|
||||||
|
#include <poll.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/mman.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <wayland-client.h>
|
||||||
|
#include "xdg-shell-client.h"
|
||||||
|
|
||||||
|
/* WHY: the compositor must wait on a real, responsive XDG client.
|
||||||
|
* CONTRACT: hold only a received configure; ACK the latest received serial.
|
||||||
|
* INVARIANT: Wayland dispatch and ping replies continue while ACK is held. */
|
||||||
|
static struct wl_display *display;
|
||||||
|
static struct wl_compositor *compositor;
|
||||||
|
static struct wl_shm *shm;
|
||||||
|
static struct xdg_wm_base *wm;
|
||||||
|
static struct wl_surface *surface;
|
||||||
|
static struct xdg_surface *xdg;
|
||||||
|
static struct xdg_toplevel *toplevel;
|
||||||
|
static struct wl_seat *seat;
|
||||||
|
static struct wl_pointer *pointer;
|
||||||
|
static volatile sig_atomic_t running = 1;
|
||||||
|
static bool mapped, hold_next, held;
|
||||||
|
static uint32_t latest_serial;
|
||||||
|
static int32_t width = 420, height = 320, configured_width = 420, configured_height = 320;
|
||||||
|
static unsigned generation;
|
||||||
|
|
||||||
|
static long long stamp(void) {
|
||||||
|
struct timespec ts;
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||||
|
return (long long)ts.tv_sec * 1000000000LL + ts.tv_nsec;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void draw(void) {
|
||||||
|
if (configured_width > 0) width = configured_width;
|
||||||
|
if (configured_height > 0) height = configured_height;
|
||||||
|
if (width <= 0 || height <= 0 || width > 4096 || height > 4096) {
|
||||||
|
fprintf(stderr, "INVALID_SIZE %d %d\n", width, height);
|
||||||
|
exit(2);
|
||||||
|
}
|
||||||
|
size_t bytes = (size_t)width * (size_t)height * 4;
|
||||||
|
char name[] = "/tmp/mosaic-saved-shm-XXXXXX";
|
||||||
|
int fd = mkstemp(name);
|
||||||
|
if (fd < 0 || ftruncate(fd, (off_t)bytes) < 0) { perror("shm"); exit(2); }
|
||||||
|
unlink(name);
|
||||||
|
uint32_t *pixels = mmap(NULL, bytes, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
|
||||||
|
if (pixels == MAP_FAILED) { perror("mmap"); exit(2); }
|
||||||
|
uint32_t color = (generation++ % 2) ? 0x0020d0a0 : 0x002344cc;
|
||||||
|
for (size_t i = 0; i < bytes / 4; ++i) pixels[i] = color;
|
||||||
|
struct wl_shm_pool *pool = wl_shm_create_pool(shm, fd, (int)bytes);
|
||||||
|
struct wl_buffer *buffer = wl_shm_pool_create_buffer(pool, 0, width, height,
|
||||||
|
width * 4, WL_SHM_FORMAT_XRGB8888);
|
||||||
|
wl_surface_attach(surface, buffer, 0, 0);
|
||||||
|
wl_surface_damage_buffer(surface, 0, 0, width, height);
|
||||||
|
wl_surface_commit(surface);
|
||||||
|
fprintf(stderr, "COMMIT ns=%lld serial=%u size=%dx%d color=%08x\n",
|
||||||
|
stamp(), latest_serial, width, height, color);
|
||||||
|
wl_buffer_destroy(buffer);
|
||||||
|
wl_shm_pool_destroy(pool);
|
||||||
|
munmap(pixels, bytes);
|
||||||
|
close(fd);
|
||||||
|
mapped = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ack_and_commit(void) {
|
||||||
|
if (!latest_serial) return;
|
||||||
|
xdg_surface_ack_configure(xdg, latest_serial);
|
||||||
|
fprintf(stderr, "ACK ns=%lld serial=%u size=%dx%d\n",
|
||||||
|
stamp(), latest_serial, configured_width, configured_height);
|
||||||
|
draw();
|
||||||
|
held = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ping(void *data, struct xdg_wm_base *base, uint32_t serial) {
|
||||||
|
xdg_wm_base_pong(base, serial);
|
||||||
|
fprintf(stderr, "PONG ns=%lld serial=%u\n", stamp(), serial);
|
||||||
|
}
|
||||||
|
static const struct xdg_wm_base_listener wm_listener = {.ping = ping};
|
||||||
|
|
||||||
|
static void role_configure(void *data, struct xdg_toplevel *role,
|
||||||
|
int32_t w, int32_t h, struct wl_array *states) {
|
||||||
|
if (w > 0) configured_width = w;
|
||||||
|
if (h > 0) configured_height = h;
|
||||||
|
fprintf(stderr, "ROLE_CONFIGURE ns=%lld size=%dx%d\n", stamp(), w, h);
|
||||||
|
}
|
||||||
|
static void role_close(void *data, struct xdg_toplevel *role) { running = 0; }
|
||||||
|
static const struct xdg_toplevel_listener role_listener = {
|
||||||
|
.configure = role_configure, .close = role_close,
|
||||||
|
};
|
||||||
|
static void configure(void *data, struct xdg_surface *s, uint32_t serial) {
|
||||||
|
latest_serial = serial;
|
||||||
|
fprintf(stderr, "XDG_CONFIGURE ns=%lld serial=%u size=%dx%d hold=%d previous=%d\n",
|
||||||
|
stamp(), serial, configured_width, configured_height, hold_next || held, held);
|
||||||
|
if (hold_next || held) {
|
||||||
|
held = true;
|
||||||
|
hold_next = false;
|
||||||
|
fprintf(stderr, "HELD ns=%lld serial=%u\n", stamp(), serial);
|
||||||
|
} else {
|
||||||
|
ack_and_commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static const struct xdg_surface_listener surface_listener = {.configure = configure};
|
||||||
|
static void pointer_enter(void *data, struct wl_pointer *p, uint32_t serial,
|
||||||
|
struct wl_surface *s, wl_fixed_t x, wl_fixed_t y) {
|
||||||
|
fprintf(stderr, "ENTER ns=%lld x=%.2f y=%.2f\n", stamp(),
|
||||||
|
wl_fixed_to_double(x), wl_fixed_to_double(y));
|
||||||
|
}
|
||||||
|
static void pointer_leave(void *data, struct wl_pointer *p, uint32_t serial,
|
||||||
|
struct wl_surface *s) { fprintf(stderr, "LEAVE ns=%lld\n", stamp()); }
|
||||||
|
static void pointer_motion(void *data, struct wl_pointer *p, uint32_t time,
|
||||||
|
wl_fixed_t x, wl_fixed_t y) {
|
||||||
|
fprintf(stderr, "MOTION ns=%lld x=%.2f y=%.2f\n", stamp(),
|
||||||
|
wl_fixed_to_double(x), wl_fixed_to_double(y));
|
||||||
|
}
|
||||||
|
static void pointer_button(void *data, struct wl_pointer *p, uint32_t serial,
|
||||||
|
uint32_t time, uint32_t button, uint32_t state) {
|
||||||
|
fprintf(stderr, "BUTTON ns=%lld serial=%u button=%u state=%u\n",
|
||||||
|
stamp(), serial, button, state);
|
||||||
|
}
|
||||||
|
static void pointer_axis(void *data, struct wl_pointer *p, uint32_t t,
|
||||||
|
uint32_t axis, wl_fixed_t value) {}
|
||||||
|
static void pointer_frame(void *data, struct wl_pointer *p) {}
|
||||||
|
static const struct wl_pointer_listener pointer_listener = {
|
||||||
|
.enter = pointer_enter, .leave = pointer_leave, .motion = pointer_motion,
|
||||||
|
.button = pointer_button, .axis = pointer_axis, .frame = pointer_frame,
|
||||||
|
};
|
||||||
|
static void capabilities(void *data, struct wl_seat *s, uint32_t caps) {
|
||||||
|
if ((caps & WL_SEAT_CAPABILITY_POINTER) && !pointer) {
|
||||||
|
pointer = wl_seat_get_pointer(s);
|
||||||
|
wl_pointer_add_listener(pointer, &pointer_listener, NULL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static void seat_name(void *data, struct wl_seat *s, const char *name) {}
|
||||||
|
static const struct wl_seat_listener seat_listener = {
|
||||||
|
.capabilities = capabilities, .name = seat_name,
|
||||||
|
};
|
||||||
|
static void global(void *data, struct wl_registry *registry, uint32_t name,
|
||||||
|
const char *interface, uint32_t version) {
|
||||||
|
if (!strcmp(interface, wl_compositor_interface.name))
|
||||||
|
compositor = wl_registry_bind(registry, name, &wl_compositor_interface, version < 4 ? version : 4);
|
||||||
|
else if (!strcmp(interface, wl_shm_interface.name))
|
||||||
|
shm = wl_registry_bind(registry, name, &wl_shm_interface, 1);
|
||||||
|
else if (!strcmp(interface, xdg_wm_base_interface.name))
|
||||||
|
wm = wl_registry_bind(registry, name, &xdg_wm_base_interface, version < 3 ? version : 3);
|
||||||
|
else if (!strcmp(interface, wl_seat_interface.name)) {
|
||||||
|
seat = wl_registry_bind(registry, name, &wl_seat_interface, version < 5 ? version : 5);
|
||||||
|
wl_seat_add_listener(seat, &seat_listener, NULL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static void global_remove(void *data, struct wl_registry *registry, uint32_t name) {}
|
||||||
|
static const struct wl_registry_listener registry_listener = {
|
||||||
|
.global = global, .global_remove = global_remove,
|
||||||
|
};
|
||||||
|
static void stop(int signum) { running = 0; }
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
signal(SIGTERM, stop);
|
||||||
|
display = wl_display_connect(NULL);
|
||||||
|
if (!display) { fprintf(stderr, "CONNECT_FAILED\n"); return 1; }
|
||||||
|
struct wl_registry *registry = wl_display_get_registry(display);
|
||||||
|
wl_registry_add_listener(registry, ®istry_listener, NULL);
|
||||||
|
wl_display_roundtrip(display);
|
||||||
|
wl_display_roundtrip(display);
|
||||||
|
if (!compositor || !shm || !wm) { fprintf(stderr, "MISSING_GLOBAL\n"); return 2; }
|
||||||
|
xdg_wm_base_add_listener(wm, &wm_listener, NULL);
|
||||||
|
surface = wl_compositor_create_surface(compositor);
|
||||||
|
xdg = xdg_wm_base_get_xdg_surface(wm, surface);
|
||||||
|
xdg_surface_add_listener(xdg, &surface_listener, NULL);
|
||||||
|
toplevel = xdg_surface_get_toplevel(xdg);
|
||||||
|
xdg_toplevel_add_listener(toplevel, &role_listener, NULL);
|
||||||
|
xdg_toplevel_set_title(toplevel, "Mosaic Saved Buffer Probe");
|
||||||
|
xdg_toplevel_set_app_id(toplevel, "org.mosaic.SavedBufferProbe");
|
||||||
|
wl_surface_commit(surface);
|
||||||
|
fprintf(stderr, "READY ns=%lld\n", stamp());
|
||||||
|
while (running) {
|
||||||
|
wl_display_flush(display);
|
||||||
|
struct pollfd fds[2] = {
|
||||||
|
{.fd = wl_display_get_fd(display), .events = POLLIN},
|
||||||
|
{.fd = STDIN_FILENO, .events = POLLIN},
|
||||||
|
};
|
||||||
|
int ready = poll(fds, 2, 100);
|
||||||
|
if (ready < 0) { if (errno == EINTR) continue; perror("poll"); break; }
|
||||||
|
if (fds[0].revents & (POLLIN | POLLERR | POLLHUP)) {
|
||||||
|
if (wl_display_dispatch(display) < 0) break;
|
||||||
|
}
|
||||||
|
if (fds[1].revents & POLLIN) {
|
||||||
|
char cmd[64];
|
||||||
|
if (!fgets(cmd, sizeof(cmd), stdin)) break;
|
||||||
|
if (!strncmp(cmd, "hold_next_configure", 19)) {
|
||||||
|
hold_next = true;
|
||||||
|
fprintf(stderr, "COMMAND hold_next_configure DONE ns=%lld\n", stamp());
|
||||||
|
} else if (!strncmp(cmd, "release_configure", 17)) {
|
||||||
|
if (held) ack_and_commit();
|
||||||
|
fprintf(stderr, "COMMAND release_configure DONE ns=%lld\n", stamp());
|
||||||
|
} else if (!strncmp(cmd, "close", 5)) {
|
||||||
|
running = 0;
|
||||||
|
fprintf(stderr, "COMMAND close DONE ns=%lld\n", stamp());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fprintf(stderr, "EXIT ns=%lld mapped=%d held=%d error=%d\n",
|
||||||
|
stamp(), mapped, held, wl_display_get_error(display));
|
||||||
|
xdg_toplevel_destroy(toplevel);
|
||||||
|
xdg_surface_destroy(xdg);
|
||||||
|
wl_surface_destroy(surface);
|
||||||
|
wl_display_flush(display);
|
||||||
|
wl_display_disconnect(display);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
--- a/sway/ipc-json.c
|
||||||
|
+++ b/sway/ipc-json.c
|
||||||
|
@@ -3,6 +3,7 @@
|
||||||
|
#include <json.h>
|
||||||
|
#include <libevdev/libevdev.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
+#include <time.h>
|
||||||
|
#include <wlr/config.h>
|
||||||
|
#include <wlr/types/wlr_content_type_v1.h>
|
||||||
|
#include <wlr/types/wlr_output.h>
|
||||||
|
@@ -820,6 +821,33 @@
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
+/* WHY: pre-transaction geometry must be measured from the actual source node.
|
||||||
|
+ * CONTRACT: this private trace reads the mapped target only during get_tree.
|
||||||
|
+ * INVARIANT: no scene state, buffer reference or transaction is changed. */
|
||||||
|
+struct target_trace_context {
|
||||||
|
+ struct sway_container *container;
|
||||||
|
+};
|
||||||
|
+
|
||||||
|
+static void trace_target_source(struct wlr_scene_buffer *buffer,
|
||||||
|
+ int sx, int sy, void *data) {
|
||||||
|
+ struct target_trace_context *ctx = data;
|
||||||
|
+ int gx = 0, gy = 0;
|
||||||
|
+ bool valid = wlr_scene_node_coords(&buffer->node, &gx, &gy);
|
||||||
|
+ struct timespec ts;
|
||||||
|
+ clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||||
|
+ fprintf(stderr, "TARGET_SOURCE ns=%lld con=%p con_id=%zu view=%p node=%p buffer=%p parent=%p global_valid=%d global=%d,%d local=%d,%d source_size=%d,%d dest=%d,%d src_box=%.3f,%.3f,%.3f,%.3f transform=%d enabled=%d\n",
|
||||||
|
+ (long long)ts.tv_sec * 1000000000LL + ts.tv_nsec,
|
||||||
|
+ (void *)ctx->container, ctx->container->node.id,
|
||||||
|
+ (void *)ctx->container->view, (void *)&buffer->node,
|
||||||
|
+ (void *)buffer->buffer, (void *)buffer->node.parent, valid, gx, gy, sx, sy,
|
||||||
|
+ buffer->buffer ? buffer->buffer->width : 0,
|
||||||
|
+ buffer->buffer ? buffer->buffer->height : 0,
|
||||||
|
+ buffer->dst_width, buffer->dst_height,
|
||||||
|
+ buffer->src_box.x, buffer->src_box.y,
|
||||||
|
+ buffer->src_box.width, buffer->src_box.height,
|
||||||
|
+ buffer->transform, buffer->node.enabled);
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
static void ipc_json_describe_container(struct sway_container *c, json_object *object) {
|
||||||
|
json_object_object_add(object, "name",
|
||||||
|
c->title ? json_object_new_string(c->title) : NULL);
|
||||||
|
@@ -886,6 +914,14 @@
|
||||||
|
json_object_object_add(object, "marks", marks);
|
||||||
|
|
||||||
|
if (c->view) {
|
||||||
|
+ const char *app_id = view_get_app_id(c->view);
|
||||||
|
+ if (getenv("SAVED_TARGET_TRACE") && app_id &&
|
||||||
|
+ strcmp(app_id, "org.mosaic.SavedBufferProbe") == 0 &&
|
||||||
|
+ c->view->content_tree) {
|
||||||
|
+ struct target_trace_context ctx = {.container = c};
|
||||||
|
+ wlr_scene_node_for_each_buffer(&c->view->content_tree->node,
|
||||||
|
+ trace_target_source, &ctx);
|
||||||
|
+ }
|
||||||
|
ipc_json_describe_view(c, object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
--- a/sway/tree/view.c
|
||||||
|
+++ b/sway/tree/view.c
|
||||||
|
@@ -1328,6 +1328,23 @@
|
||||||
|
buffer->buffer ? buffer->buffer->height : 0, sx, sy, (void *)sbuf->node.parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
+/* WHY: copied local offsets cannot prove a global boundary crossing.
|
||||||
|
+ * CONTRACT: private observation only; the node and buffer remain borrowed.
|
||||||
|
+ * INVARIANT: no buffer lock or scene mutation is introduced. */
|
||||||
|
+static void trace_target_saved_buffer(struct wlr_scene_buffer *buffer,
|
||||||
|
+ int sx, int sy, void *data) {
|
||||||
|
+ struct sway_view *view = data;
|
||||||
|
+ int gx = 0, gy = 0;
|
||||||
|
+ bool valid = wlr_scene_node_coords(&buffer->node, &gx, &gy);
|
||||||
|
+ fprintf(stderr, "TARGET_SAVED ns=%lld view=%p con=%p node=%p buffer=%p parent=%p global_valid=%d global=%d,%d local=%d,%d source_size=%d,%d dest=%d,%d enabled=%d\n",
|
||||||
|
+ saved_ns(), (void *)view, (void *)view->container,
|
||||||
|
+ (void *)&buffer->node, (void *)buffer->buffer,
|
||||||
|
+ (void *)buffer->node.parent, valid, gx, gy, sx, sy,
|
||||||
|
+ buffer->buffer ? buffer->buffer->width : 0,
|
||||||
|
+ buffer->buffer ? buffer->buffer->height : 0,
|
||||||
|
+ buffer->dst_width, buffer->dst_height, buffer->node.enabled);
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
void view_save_buffer(struct sway_view *view) {
|
||||||
|
if (!sway_assert(!view->saved_surface_tree, "Didn't expect saved buffer")) {
|
||||||
|
view_remove_saved_buffer(view);
|
||||||
|
@@ -1354,6 +1371,9 @@
|
||||||
|
|
||||||
|
wlr_scene_node_set_enabled(&view->content_tree->node, false);
|
||||||
|
wlr_scene_node_set_enabled(&view->saved_surface_tree->node, true);
|
||||||
|
+ /* CONTRACT: measure the actual saved node after enabling the tree. */
|
||||||
|
+ wlr_scene_node_for_each_buffer(&view->saved_surface_tree->node,
|
||||||
|
+ trace_target_saved_buffer, view);
|
||||||
|
fprintf(stderr, "SB_ENABLE ns=%lld view=%p tree=%p content_enabled=%d saved_enabled=%d\n",
|
||||||
|
saved_ns(), (void *)view, (void *)view->saved_surface_tree,
|
||||||
|
view->content_tree->node.enabled, view->saved_surface_tree->node.enabled);
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
"""Offline exclusive-ledger regression; never touches a campaign LAB."""
|
||||||
|
import json
|
||||||
|
import multiprocessing
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
from budget import reserve
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as root:
|
||||||
|
lab = Path(root)
|
||||||
|
reserve(lab, 'control')
|
||||||
|
reserve(lab, 'control', 'ack')
|
||||||
|
reserve(lab, 'control', 'timeout')
|
||||||
|
for episode in (None, 'ack', 'timeout'):
|
||||||
|
try:
|
||||||
|
reserve(lab, 'control', episode)
|
||||||
|
raise AssertionError('duplicate accepted')
|
||||||
|
except RuntimeError:
|
||||||
|
pass
|
||||||
|
for mode in ('private_standard', 'private_mosaic'):
|
||||||
|
reserve(lab, mode)
|
||||||
|
reserve(lab, mode, 'ack')
|
||||||
|
reserve(lab, mode, 'timeout')
|
||||||
|
reserve(lab, 'private_mosaic', camera=True)
|
||||||
|
try:
|
||||||
|
reserve(lab, 'private_mosaic', camera=True)
|
||||||
|
raise AssertionError('second camera adjustment accepted')
|
||||||
|
except RuntimeError:
|
||||||
|
pass
|
||||||
|
for mode in ('control', 'private_standard', 'private_mosaic'):
|
||||||
|
for _ in range(16):
|
||||||
|
reserve(lab, mode, capture=True)
|
||||||
|
try:
|
||||||
|
reserve(lab, mode, capture=True)
|
||||||
|
raise AssertionError('seventeenth capture accepted')
|
||||||
|
except RuntimeError:
|
||||||
|
pass
|
||||||
|
state = json.loads((lab / 'target-budget.json').read_text())
|
||||||
|
assert len(state['episodes']) == 6
|
||||||
|
assert sum(state['captures'].values()) == 48
|
||||||
|
print('PASS persistent run/episode limits')
|
||||||
|
|
||||||
|
|
||||||
|
def contender(path, queue):
|
||||||
|
try:
|
||||||
|
reserve(Path(path), 'control')
|
||||||
|
queue.put('accepted')
|
||||||
|
except RuntimeError:
|
||||||
|
queue.put('rejected')
|
||||||
|
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as root:
|
||||||
|
context = multiprocessing.get_context('fork')
|
||||||
|
queue = context.Queue()
|
||||||
|
children = [context.Process(target=contender, args=(root, queue))
|
||||||
|
for _ in range(2)]
|
||||||
|
for child in children:
|
||||||
|
child.start()
|
||||||
|
for child in children:
|
||||||
|
child.join(5)
|
||||||
|
assert child.exitcode == 0
|
||||||
|
assert sorted(queue.get() for _ in children) == ['accepted', 'rejected']
|
||||||
|
print('PASS exclusive reservation')
|
||||||
Loading…
Reference in a new issue