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
|
||||
|
||||
- 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.
|
||||
|
||||
- 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
|
||||
H1 after the applied camera reached 1760, preventing a boundary-clipping
|
||||
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
|
||||
private-standard episode failed through fixture timing. STEP20C remains
|
||||
**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.
|
||||
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.
|
||||
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
|
||||
|
||||
|
|
@ -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 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 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.
|
||||
|
||||
|
|
@ -141,3 +144,14 @@ EXPERIMENTAL / INCONCLUSIVE. Real held XDG configures created nonempty saved
|
|||
scene buffers; private render entries and ACK/timeout removal were observed.
|
||||
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.
|
||||
|
||||
[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
|
||||
SceneFX ABI/package remain separate unqualified gates. STEP20C stays
|
||||
**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