docs(mosaic): preserve saved transaction buffer probe evidence
This commit is contained in:
parent
7561120b55
commit
642c61aaaf
16 changed files with 1178 additions and 1 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -23,3 +23,7 @@ opt/
|
||||||
*.pkg.tar.*
|
*.pkg.tar.*
|
||||||
.SRCINFO.tmp
|
.SRCINFO.tmp
|
||||||
.SRCINFO
|
.SRCINFO
|
||||||
|
|
||||||
|
# Generated Python bytecode from experimental fixtures
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@
|
||||||
|
|
||||||
### STEP20C experimental evidence
|
### STEP20C experimental evidence
|
||||||
|
|
||||||
|
- 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.
|
||||||
|
|
||||||
- Preserved rotated-output probe: actual 90° output keeps an 800×600 logical viewport while the physical mode becomes 750×1000 and `grim` remains logical-oriented 1000×750. Private standard shows 15,860 B-popup pixels on H2; private Mosaic shows zero, but its B popup ends before the boundary, so partial clipping is unchallenged. Fully hidden owner, pointer, measured physical damage and R0/standard returns pass sampled checks. Pristine R90 capture stopped on a fixture IPC-direction assertion; no replay. PARTIALLY_FEASIBLE, architectural conclusion UNDECIDED; no production change.
|
- Preserved rotated-output probe: actual 90° output keeps an 800×600 logical viewport while the physical mode becomes 750×1000 and `grim` remains logical-oriented 1000×750. Private standard shows 15,860 B-popup pixels on H2; private Mosaic shows zero, but its B popup ends before the boundary, so partial clipping is unchallenged. Fully hidden owner, pointer, measured physical damage and R0/standard returns pass sampled checks. Pristine R90 capture stopped on a fixture IPC-direction assertion; no replay. PARTIALLY_FEASIBLE, architectural conclusion UNDECIDED; no production change.
|
||||||
|
|
|
||||||
|
|
@ -279,3 +279,12 @@ viewport in the sampled state; pristine and private-standard controls both
|
||||||
drew and handled the crossed portion. Result **PARTIALLY_FEASIBLE** because
|
drew and handled the crossed portion. Result **PARTIALLY_FEASIBLE** because
|
||||||
the clipped-away part of A was not independently clicked and no atomic
|
the clipped-away part of A was not independently clicked and no atomic
|
||||||
render/input generation was proved. The architecture remains experimental.
|
render/input generation was proved. The architecture remains experimental.
|
||||||
|
|
||||||
|
The local [saved transaction buffer probe](design/STEP20C_SAVED_TRANSACTION_BUFFER_PROBE.md)
|
||||||
|
found `saved_surface_tree` under `view->scene_tree` as a sibling of live
|
||||||
|
`content_tree`; SceneFX retains the source buffer at creation and a private
|
||||||
|
trace observed saved render entries and normal removal after ACK/timeout.
|
||||||
|
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.
|
||||||
|
|
|
||||||
|
|
@ -219,3 +219,13 @@ but independent input on its forbidden portion remains **NOT TESTED**.
|
||||||
Saved buffers, optimized blur caches, inner shadows, intermediate frames,
|
Saved buffers, optimized blur caches, inner shadows, intermediate frames,
|
||||||
atomic generation and SceneFX/wlroots ABI/package remain unqualified.
|
atomic generation and SceneFX/wlroots ABI/package remain unqualified.
|
||||||
Multi-output stays **NOT YET SUPPORTED**.
|
Multi-output stays **NOT YET SUPPORTED**.
|
||||||
|
|
||||||
|
The [saved transaction buffer probe](design/STEP20C_SAVED_TRANSACTION_BUFFER_PROBE.md)
|
||||||
|
is **INCONCLUSIVE** for the intended partial-overflow gate. The target
|
||||||
|
snapshot was outside H1 because its applied camera differed from the
|
||||||
|
fixture's assumed initial position. Other saved nodes rendered in the
|
||||||
|
private scene; ACK and natural timeout removed sampled trees. Target pixels,
|
||||||
|
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**.
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,11 @@ The [root README](../README.md) is the public summary. For a specific topic, use
|
||||||
[Pending-input source fixture](../tooling/probes/step20c-event-loop-pending-input/README.md) replays the disposable matrix.
|
[Pending-input source fixture](../tooling/probes/step20c-event-loop-pending-input/README.md) replays the disposable matrix.
|
||||||
23. [STEP20C effects/damage probe](design/STEP20C_EFFECTS_DAMAGE_PROBE.md): real translucent blur and shadow-overflow pixels compared across standard and private Mosaic layouts; sampled damage and camera returns PARTIALLY_FEASIBLE, optimized blur cache unqualified.
|
23. [STEP20C effects/damage probe](design/STEP20C_EFFECTS_DAMAGE_PROBE.md): real translucent blur and shadow-overflow pixels compared across standard and private Mosaic layouts; sampled damage and camera returns PARTIALLY_FEASIBLE, optimized blur cache unqualified.
|
||||||
[Effects/damage source fixture](../tooling/probes/step20c-effects-damage/README.md) rebuilds the disposable matrix.
|
[Effects/damage source fixture](../tooling/probes/step20c-effects-damage/README.md) rebuilds the disposable matrix.
|
||||||
24. [STEP20A](STEP20A.md) and [STEP20B](STEP20B.md): historical evidence.
|
24. [STEP20C static R90 partial-popup probe](design/STEP20C_ROTATED_PARTIAL_POPUP_CLIP_PROBE.md): sampled genuine B crossing and input with a completed pristine control; broader STEP20C gates remain open.
|
||||||
|
[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.
|
||||||
|
|
||||||
## Project
|
## Project
|
||||||
|
|
||||||
|
|
@ -88,6 +92,8 @@ The [root README](../README.md) is the public summary. For a specific topic, use
|
||||||
| [STEP20C commit/present/input ordering probe](design/STEP20C_COMMIT_PRESENT_INPUT_ORDERING_PROBE.md) | Experimental, independently replayed evidence | INCONCLUSIVE for the discriminating temporal window; headless input-region commits and hits agree in observed states, but none of 20 applied mutations occurred between a native output commit and its present |
|
| [STEP20C commit/present/input ordering probe](design/STEP20C_COMMIT_PRESENT_INPUT_ORDERING_PROBE.md) | Experimental, independently replayed evidence | INCONCLUSIVE for the discriminating temporal window; headless input-region commits and hits agree in observed states, but none of 20 applied mutations occurred between a native output commit and its present |
|
||||||
| [STEP20C event-loop pending-input probe](design/STEP20C_EVENT_LOOP_PENDING_INPUT_PROBE.md) | Experimental, independently replayed evidence | INCONCLUSIVE / NOT EXERCISED for natural commit→input mutation→present despite decoupled client submission; the temporal line is closed for this backend/matrix |
|
| [STEP20C event-loop pending-input probe](design/STEP20C_EVENT_LOOP_PENDING_INPUT_PROBE.md) | Experimental, independently replayed evidence | INCONCLUSIVE / NOT EXERCISED for natural commit→input mutation→present despite decoupled client submission; the temporal line is closed for this backend/matrix |
|
||||||
| [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 saved transaction buffer probe](design/STEP20C_SAVED_TRANSACTION_BUFFER_PROBE.md) | Experimental, local evidence | INCONCLUSIVE; real saved nodes rendered, target partial crossing not exercised |
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
|
|
@ -128,3 +134,10 @@ its H1 pixels and real button remained, while H2 matched its reference and
|
||||||
delivered no B button. Both standard controls drew and handled the overflow.
|
delivered no B button. Both standard controls drew and handled the overflow.
|
||||||
Independent clipped-away A input and saved buffers remain open. STEP20C is
|
Independent clipped-away A input and saved buffers remain open. STEP20C is
|
||||||
NEXT; production multi-output is NOT YET SUPPORTED.
|
NEXT; production multi-output is NOT YET SUPPORTED.
|
||||||
|
|
||||||
|
[Saved transaction buffer probe](design/STEP20C_SAVED_TRANSACTION_BUFFER_PROBE.md)
|
||||||
|
and [source fixture](../tooling/probes/step20c-saved-transaction-buffer/README.md):
|
||||||
|
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.
|
||||||
|
|
|
||||||
|
|
@ -221,3 +221,13 @@ temporal/effect cases remain open. Next prioritize saved transaction buffers
|
||||||
against the same owner-derived viewport before optimized blur cache,
|
against the same owner-derived viewport before optimized blur cache,
|
||||||
interior shadow and ABI/package maintenance. STEP20C remains **NEXT**;
|
interior shadow and ABI/package maintenance. STEP20C remains **NEXT**;
|
||||||
production multi-output remains **NOT YET SUPPORTED**.
|
production multi-output remains **NOT YET SUPPORTED**.
|
||||||
|
|
||||||
|
The subsequent [saved transaction buffer campaign](design/STEP20C_SAVED_TRANSACTION_BUFFER_PROBE.md)
|
||||||
|
observed real held XDG configure transactions, reused saved buffers, private
|
||||||
|
render entries and sampled ACK/timeout removal. Its target never crossed the
|
||||||
|
H1/H2 boundary after the applied Mosaic camera settled at 1760. Overall
|
||||||
|
**INCONCLUSIVE**; all three run reservations are consumed. A new, separately
|
||||||
|
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**.
|
||||||
|
|
|
||||||
240
docs/design/STEP20C_SAVED_TRANSACTION_BUFFER_PROBE.md
Normal file
240
docs/design/STEP20C_SAVED_TRANSACTION_BUFFER_PROBE.md
Normal file
|
|
@ -0,0 +1,240 @@
|
||||||
|
# STEP20C saved transaction buffer clipping and lifetime probe
|
||||||
|
|
||||||
|
**EXPERIMENTAL / INCONCLUSIVE, preserved after execution.** STEP20C remains NEXT and
|
||||||
|
multi-output Mosaic production is NOT YET SUPPORTED. This campaign follows
|
||||||
|
the [static R90 popup evidence](STEP20C_ROTATED_PARTIAL_POPUP_CLIP_PROBE.md)
|
||||||
|
without changing production Mosaic, SceneFX algorithms, UX or packaging.
|
||||||
|
|
||||||
|
## Source contract and ownership
|
||||||
|
|
||||||
|
Pinned inputs: SwayFX 0.6 `fd71a6bdc061bd633b488ae7b83e8a6981d22f86`,
|
||||||
|
private Mosaic `cd550382b311efdbfe3ed892f4e1d281d852e754`, SceneFX 0.5
|
||||||
|
`37ccd723bef49e6891156ffafce8f549f01446cc`, wlroots 0.20.2. The
|
||||||
|
private source tree is disposable; installed libraries remain untouched.
|
||||||
|
|
||||||
|
`transaction_commit()` configures a visible changed-size view when
|
||||||
|
`should_configure()` sees different current/instruction content dimensions,
|
||||||
|
then calls `view_save_buffer()` for every visible view instruction without an
|
||||||
|
existing saved tree. `view_save_buffer()` creates `saved_surface_tree` under
|
||||||
|
`view->scene_tree`, places it below `output_handler`, initially disables it,
|
||||||
|
iterates enabled buffers of `content_tree`, creates new scene buffer nodes,
|
||||||
|
and calls `wlr_scene_buffer_set_buffer(sbuf, buffer->buffer)`. This reuses a
|
||||||
|
referenced source buffer; it does not copy pixels to a new SHM image.
|
||||||
|
SceneFX's setter calls `wlr_buffer_lock()` on a non-null buffer and unlocks
|
||||||
|
the previously owned buffer when replacing it; its texture conversion path
|
||||||
|
may release that lock after obtaining a texture. Node destruction releases
|
||||||
|
its owned buffer/texture through the same scene lifetime. Pointer equality at
|
||||||
|
creation is therefore evidence of reuse, not proof that one particular buffer
|
||||||
|
lock remains held throughout every rendered frame.
|
||||||
|
The live content tree is then disabled and the saved tree enabled.
|
||||||
|
|
||||||
|
The saved tree is a **sibling** of `content_tree`, not its descendant.
|
||||||
|
`apply_container_state()` normally removes it via `view_remove_saved_buffer()`:
|
||||||
|
destroy saved scene nodes and enable live content. The destroying-container
|
||||||
|
branch skips this immediate removal; close animation and container teardown
|
||||||
|
must be audited separately. XDG commit uses the actual current configure
|
||||||
|
serial to notify transaction readiness; a nonmatching commit can send frame
|
||||||
|
done to a saved view. `transaction_progress()` applies when all waiting
|
||||||
|
instructions are ready. Its timeout callback sets waiting to zero and uses
|
||||||
|
the same progress path. A new pending transaction cannot commit while a
|
||||||
|
queued transaction exists. `-Dtxn-timeout=1200` sets the private test policy;
|
||||||
|
the code default is 200 ms. No `-Dtxn-wait` or `-Dnoatomic` is permitted.
|
||||||
|
|
||||||
|
`view->scene_tree` is normally reparented under tiled container
|
||||||
|
`content_tree`. The private Mosaic clip is set on the owning workspace's
|
||||||
|
`layers.tiling`; SceneFX's `scene_node_intersect_clip()` walks all tree
|
||||||
|
ancestors for render visibility and damage, and `scene_node_point_inside_clip()`
|
||||||
|
guards node-at. Thus inheritance is plausible for the saved sibling, but
|
||||||
|
runtime parentage, enabled state, actual buffer, render contribution and pixels
|
||||||
|
must be observed. The `SWAY_SCENE_DESC_SAVED_BUFFER` descriptor marks saved
|
||||||
|
nodes. `SWAY_SCENE_DESC_VIEW` marks the view tree; container descriptors
|
||||||
|
identify ownership. `output_handler` is an input-rejecting buffer above the
|
||||||
|
saved tree and tracks output updates. A saved scene buffer is not itself a
|
||||||
|
Wayland surface: node-at, surface lookup, seat notification and client delivery
|
||||||
|
are separate observations. An output buffer and an optimized blur cache are
|
||||||
|
different objects with different lifetimes.
|
||||||
|
|
||||||
|
For destruction, XDG unmap calls `view_unmap()`, which starts container
|
||||||
|
destruction. `container_destroy()` asserts zero transaction references before
|
||||||
|
detaching the view and destroying its scene tree; `view_destroy()` also asserts
|
||||||
|
the view is unmapped, marked destroying and detached. The destroying branch
|
||||||
|
of `apply_container_state()` deliberately skips immediate saved-tree removal.
|
||||||
|
`transaction_destroy()` can defer final container deletion through a close
|
||||||
|
animation, whose completion callback removes the saved tree before destroying
|
||||||
|
the container. The observed ACK/timeout episodes did not exercise mapped-view
|
||||||
|
destruction while a transaction was waiting; that path remains NOT TESTED.
|
||||||
|
|
||||||
|
## Controlled fixture and evidence gates
|
||||||
|
|
||||||
|
Use one C17 XDG SHM client in all modes. It maps a deterministic initial
|
||||||
|
buffer, logs configure serial/size/monotonic time, keeps dispatching pings and
|
||||||
|
input, and accepts `hold_next_configure` / `release_configure` over a private
|
||||||
|
control channel. Release ACKs the latest valid serial and commits the
|
||||||
|
corresponding size; it never invents a serial. A normal geometry command
|
||||||
|
(candidate: `gaps inner` after source and IPC calibration) must create a real
|
||||||
|
visible view instruction, a nonempty saved tree, live-content disablement
|
||||||
|
and a waiting transaction. A camera change alone is insufficient. A source
|
||||||
|
trace without an active nonempty saved buffer does not pass S1.
|
||||||
|
|
||||||
|
Use H1 `800×600`, scale 1, normal, origin `(-800,0)`, and H2 `800×600`,
|
||||||
|
scale 1, normal, origin `(0,0)`. H1 hosts four tiled cells and a Mosaic camera
|
||||||
|
near 400; H2 hosts an ordinary reference surface. Recalculate owner, scene,
|
||||||
|
snapshot and viewport boxes from applied IPC and trace. The clipping gate
|
||||||
|
requires `saved_buffer_left < 0 < saved_buffer_right`; otherwise partial
|
||||||
|
exclusion is NOT EXERCISED. Capture a full H2 reference before transaction.
|
||||||
|
|
||||||
|
One run each: C0 pristine `splith`, C1 private `splith`, C2 private `mosaic`.
|
||||||
|
Controls validate ordinary snapshot/ACK/timeout/lifetime behavior, though
|
||||||
|
standard tiling need not overflow. S0 calibrates outputs, renderer, PNGs,
|
||||||
|
view/owner, viewport, pointer and ordinary input. S1 retains a real configure
|
||||||
|
and proves creation, active buffer and wait. S2 captures active snapshot,
|
||||||
|
render entries, ancestor clip, H1/H2 pixels, damage and commits/presents.
|
||||||
|
S3 samples allowed, forbidden and ordinary H2 hit points, separating node-at
|
||||||
|
from client delivery. S4 releases and observes ACK, commit, apply, saved-tree
|
||||||
|
destruction, restored content, pixels and damage. S5 holds a second configure
|
||||||
|
past the real timeout with the client alive and checks cleanup. S6 may use
|
||||||
|
one camera hide/return pair only if S1–S4 show actual saved rendering and
|
||||||
|
source/runtime prove overlap with the active snapshot. A queued camera
|
||||||
|
request alone proves nothing. Mapped-view destruction is audited; unsafe
|
||||||
|
runtime destruction remains NOT TESTED.
|
||||||
|
|
||||||
|
Passive private traces may record tree/buffer creation and destruction,
|
||||||
|
source buffer identity, parent chain, clip, enabled state, render entry,
|
||||||
|
damage, transaction wait/ready/timeout/apply, node-at and output commit/present.
|
||||||
|
Use CLOCK_MONOTONIC and ephemeral identities scoped to lifetime. Logging must
|
||||||
|
not add a buffer lock or change clipping, render, scheduler or ABI behavior.
|
||||||
|
Compare whole PNGs and measured damage in their actual coordinates. A pair of
|
||||||
|
captures cannot qualify every intermediate frame, and grim does not identify
|
||||||
|
the native presented buffer.
|
||||||
|
|
||||||
|
## Budget and classification
|
||||||
|
|
||||||
|
The single persistent locked ledger reserves before execution: at most three
|
||||||
|
main runs, one per mode; two targeted transaction episodes per mode (ACK and
|
||||||
|
natural timeout); one Mosaic camera hide/return pair; 20 captures per run,
|
||||||
|
60 overall; 150 seconds per run unless explicitly justified before launch.
|
||||||
|
Failed attempts consume reservations. No reset, refund, clone bypass or extra
|
||||||
|
run to repair placement. Historical probe ledgers remain unchanged.
|
||||||
|
|
||||||
|
FEASIBLE requires an actual rendered saved buffer crossing x=0 under Mosaic,
|
||||||
|
correct H1/H2 pixels and hits, ACK and timeout removal, no blocking standard
|
||||||
|
regression and supporting measured damage. PARTIALLY_FEASIBLE means a real
|
||||||
|
saved tree works but lifetime, input or pixels remain unqualified. INCONCLUSIVE
|
||||||
|
means the snapshot, boundary or live/saved distinction cannot be exercised.
|
||||||
|
BLOCKED requires a reproducible forbidden pixel/hit, reference defect, crash
|
||||||
|
or blocking standard regression. Stop the affected scenario for a protocol
|
||||||
|
error, uncalibrated geometry or proven leak. Do not patch algorithms to pass.
|
||||||
|
|
||||||
|
All builds, sockets, logs, PNGs and results stay outside Git under one private
|
||||||
|
LAB. Every headless run uses a private 0700 runtime, WAYLAND_DISPLAY and
|
||||||
|
explicit SWAYSOCK. The source fixture is under
|
||||||
|
[tooling/probes/step20c-saved-transaction-buffer](../../tooling/probes/step20c-saved-transaction-buffer/README.md).
|
||||||
|
Saved buffers do not qualify optimized blur, all effects/animations, all
|
||||||
|
transforms/scales/backends, complete intermediate frames, SceneFX ABI/package
|
||||||
|
or production STEP20C.
|
||||||
|
|
||||||
|
## Measured campaign
|
||||||
|
|
||||||
|
**INCONCLUSIVE / measured 2026-10-12.** One headless run was reserved per
|
||||||
|
mode. The pristine standard control completed held-configure/ACK and natural
|
||||||
|
timeout episodes with `-Dtxn-timeout=1200`. Four nonempty saved buffers were
|
||||||
|
created per target episode, with source/saved buffer pointers equal and
|
||||||
|
`content_enabled=0 saved_enabled=1`; all four trees were destroyed about
|
||||||
|
56 ms after ACK in episode one and about 1200 ms after creation in episode
|
||||||
|
two. The pristine SceneFX installation did not carry the private render-entry
|
||||||
|
trace, so its exact saved-node render contribution is unqualified. H2 stayed
|
||||||
|
identical through both held captures; the final timeout H2 capture changed
|
||||||
|
253 pixels in box `[4,35,15,58)`, not yet causally attributed; the Mosaic
|
||||||
|
timeout capture has the same count and box.
|
||||||
|
|
||||||
|
The private-standard ACK episode created four saved buffers and four of their
|
||||||
|
nodes reached `SB_RENDER`, but the fixture injected six slow pointer actions
|
||||||
|
during the 1200 ms hold. New XDG serials superseded the first retained serial;
|
||||||
|
the client correctly ACKed the latest serial after the compositor's natural
|
||||||
|
timeout. The runner then failed waiting for a post-ACK destruction that had
|
||||||
|
already occurred. Its single run and ACK reservation remain consumed; its
|
||||||
|
timeout episode is NOT EXERCISED. This is a fixture timing incident, not a
|
||||||
|
demonstrated compositor regression.
|
||||||
|
|
||||||
|
The private Mosaic run completed both episodes. Each created four saved
|
||||||
|
buffers; two saved nodes reached the private render-entry trace before each
|
||||||
|
held capture, and all four saved trees were destroyed after ACK or natural
|
||||||
|
timeout. The `layout mosaic scroll 400` command was relative to the existing
|
||||||
|
camera: applied IPC/scene state settled at scroll **1760**, with H1's tiled
|
||||||
|
scene at `-1760` and clip `(1760,0,800,600)`. The target owner's IPC rect
|
||||||
|
was `[-160,480)` and its camera-adjusted box was `[-1920,-1280)`, wholly
|
||||||
|
outside H1. Its deterministic SHM colors had zero pixels on both H1 and H2
|
||||||
|
through the Mosaic captures. Consequently, the target snapshot did not
|
||||||
|
exercise `saved_buffer_left < 0 < saved_buffer_right`, and the central partial
|
||||||
|
clipping test is **NOT EXERCISED**. Other visible saved nodes rendered; the
|
||||||
|
trace does not identify them as the target. H2 was identical to reference
|
||||||
|
through the held/ACK captures, then changed 253 pixels in the final timeout
|
||||||
|
capture, in the same `[4,35,15,58)` box as the pristine control. This is
|
||||||
|
unqualified without a full causal frame trace. No forbidden target
|
||||||
|
pixel or crash was demonstrated. Input, node-at, damage coverage, camera hide
|
||||||
|
while a target snapshot is active and mapped-view destruction remain NOT TESTED.
|
||||||
|
|
||||||
|
The campaign budget ended at 3/3 runs and 5/6 targeted episodes: control
|
||||||
|
ACK+timeout, private standard ACK attempt, Mosaic ACK+timeout. Captures were
|
||||||
|
10/20, 4/20 and 10/20 respectively, 24/60 total. No camera hide/return pair
|
||||||
|
was used. Failed reservations were not refunded. The private LAB is
|
||||||
|
`/tmp/swayfx-mosaic-nested-grab-KYJ2pj1o`; `results/` contains raw-derived
|
||||||
|
JSON and source/evidence manifests. No additional run is authorized by this
|
||||||
|
campaign. Verdict:
|
||||||
|
`SWAYFX_MOSAIC_SAVED_TRANSACTION_BUFFER_PROBE=INCONCLUSIVE` and architectural
|
||||||
|
conclusion `UNDECIDED`. The next saved-buffer gate needs a new explicitly
|
||||||
|
authorized campaign with a pre-run applied camera/owner crossing calibration;
|
||||||
|
this campaign gives no production STEP20C authorization.
|
||||||
|
|
||||||
|
The final source manifest contains **906 entries**, SHA-256
|
||||||
|
`e11a0800b84f6a7b12a383faa87d9d04bf8d53232fb83e6d12574b8569cc384c`.
|
||||||
|
The evidence manifest contains **90 entries**, SHA-256
|
||||||
|
`5c00b78b19fcffacb3922bdb600300188972e85b83fe8356aebbc65280dc7e30`.
|
||||||
|
Both passed entry-by-entry verification. The passive Sway trace patch is
|
||||||
|
`106544aade58e05e7245601ee380e763ac19521f7d51e1ada410b4e1e7a19997`;
|
||||||
|
the SceneFX trace patch is
|
||||||
|
`82ebdd4fb003be1729ecef443a92190b010f65b62ed858c546c019743998037e`.
|
||||||
|
These hashes identify the local source fixture; no phase-B commit or push
|
||||||
|
was made.
|
||||||
|
|
||||||
|
## Executed runner provenance and camera correction
|
||||||
|
|
||||||
|
The exact runner used for each run is retained in its `runs/*/fixture-source.py`,
|
||||||
|
covered by the evidence manifest. Their SHA-256 values are: pristine
|
||||||
|
`bc041eb7be89da9e198bb9ca2e25cfe86467c264e3687e6d9d3f3836e82ce40c`,
|
||||||
|
private standard `67aa3fb94901739bea0b1d0e0cf881081d2388a02256dddc45282d7a4bc86b25`,
|
||||||
|
and private Mosaic `cdece69cf956e550779bd992b0d38234629ca1e611dfb3fb0348c04841c47f59`.
|
||||||
|
The checked-in `run.py` matches only the final private Mosaic copy. The
|
||||||
|
pristine copy had no pointer injector. The private-standard copy added the
|
||||||
|
pointer daemon and six move/right actions during the retained ACK episode.
|
||||||
|
The Mosaic copy retained the daemon but removed those six actions after they
|
||||||
|
consumed the 1200 ms window in the private-standard run. The last standard
|
||||||
|
ACK used a real serial, but followed the natural timeout; the fixture then
|
||||||
|
waited for an already completed saved-tree destruction. This is a fixture
|
||||||
|
failure, not a demonstrated SwayFX regression. The corresponding raw run
|
||||||
|
source and timeline must be used for historical reconstruction.
|
||||||
|
|
||||||
|
`sway/commands/layout.c` computes `next = row->state.scroll_x + delta`.
|
||||||
|
Consequently `layout mosaic scroll 400` added 400 to the existing camera;
|
||||||
|
the observed applied `scroll_x` was 1760. The target IPC owner box
|
||||||
|
`[-160,480)` would translate to `[-1920,-1280)`, wholly outside the viewport.
|
||||||
|
This owner estimate is not a measurement of the source buffer's scene box.
|
||||||
|
A new campaign must measure that buffer, calculate one signed correction from
|
||||||
|
the applied camera, and reject the run before any transaction reservation if
|
||||||
|
the buffer itself does not cross `x=0`.
|
||||||
|
|
||||||
|
The final timeout H2 images of pristine and Mosaic both differ from their
|
||||||
|
references by 253 pixels in `[4,35,15,58)`. The common extent does not
|
||||||
|
identify the cause. A cursor or reference artifact remains a hypothesis;
|
||||||
|
the captures were not altered and no Mosaic attribution follows.
|
||||||
|
|
||||||
|
The historic ledger is exhausted at 3/3 runs, 5/6 reserved episodes,
|
||||||
|
24/60 captures and 0/1 camera pairs. The unused episode cannot replay an
|
||||||
|
old run. Saved-tree removal in completed episodes establishes node cleanup,
|
||||||
|
not exhaustive GPU texture/reference release. Transactional input, damage,
|
||||||
|
mapped-view destruction and distinct effects/caches remain NOT TESTED.
|
||||||
|
The private Mosaic trace retained 57 output commits and 55 headless presents
|
||||||
|
over the whole run; private standard retained 100/98. These are run totals,
|
||||||
|
including startup, capture and teardown activity, and do not identify the
|
||||||
|
exact native buffer content of a particular screenshot. The pristine
|
||||||
|
control lacked the private commit/present trace.
|
||||||
37
tooling/probes/step20c-saved-transaction-buffer/README.md
Normal file
37
tooling/probes/step20c-saved-transaction-buffer/README.md
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
# Saved transaction buffer source fixture
|
||||||
|
|
||||||
|
**EXPERIMENTAL / INCONCLUSIVE.** The protocol and source audit are in
|
||||||
|
[the report](../../../docs/design/STEP20C_SAVED_TRANSACTION_BUFFER_PROBE.md).
|
||||||
|
This directory preserves only reviewed source. It does not add a production
|
||||||
|
dependency or authorize a SceneFX merge. Generated builds, sockets, captures,
|
||||||
|
large logs and results belong under a private LAB outside Git.
|
||||||
|
|
||||||
|
`prepare.sh` reconstructs pinned disposable sources and compiles the C17
|
||||||
|
controlled XDG client. `run.py LAB MODE` reserves a locked persistent budget
|
||||||
|
before a headless run; modes are `control`, `private_standard` and
|
||||||
|
`private_mosaic`, once each. `analyze.py LAB` derives a bounded matrix from
|
||||||
|
the raw event/PNG files and writes manifests. `test-budget.py` exercises
|
||||||
|
the ledger without launching a compositor. No command may use the daily IPC
|
||||||
|
socket; runner IPC always passes `swaymsg -s` with the private socket.
|
||||||
|
|
||||||
|
For the completed campaign, use each run's manifest-covered
|
||||||
|
`runs/*/fixture-source.py` to reconstruct the exact executed code. The current
|
||||||
|
`run.py` matches the final private Mosaic copy (SHA-256 `cdece69cf956e550779bd992b0d38234629ca1e611dfb3fb0348c04841c47f59`).
|
||||||
|
The pristine copy (`bc041eb7be89da9e198bb9ca2e25cfe86467c264e3687e6d9d3f3836e82ce40c`)
|
||||||
|
had no pointer injector. The private-standard copy
|
||||||
|
(`67aa3fb94901739bea0b1d0e0cf881081d2388a02256dddc45282d7a4bc86b25`)
|
||||||
|
added six retained-window pointer actions; they delayed its valid ACK past
|
||||||
|
timeout. The final copy removed those actions. Do not rerun this exhausted
|
||||||
|
campaign or treat the current source as identical to all historical runs.
|
||||||
|
|
||||||
|
The default transaction timeout is 200 ms; the fixture requests
|
||||||
|
`-Dtxn-timeout=1200` for each comparable mode. A source trace, configure
|
||||||
|
hold and a screenshot alone do not establish saved-buffer rendering: the
|
||||||
|
saved node must be nonempty, enabled, in the render list, and geometrically
|
||||||
|
cross the tested boundary. Missing observations are NOT EXERCISED or
|
||||||
|
INCONCLUSIVE. Historical ledgers are separate and never reset. The completed
|
||||||
|
LAB is `/tmp/swayfx-mosaic-nested-grab-KYJ2pj1o`; its ledger has one run
|
||||||
|
per mode. **Do not rerun a mode or reset this ledger.** The private-standard
|
||||||
|
ACK episode timed out during slow fixture input, and the Mosaic target camera
|
||||||
|
settled at 1760, leaving its snapshot outside H1. The partial-overflow gate
|
||||||
|
is NOT EXERCISED; see the report for exact limits.
|
||||||
188
tooling/probes/step20c-saved-transaction-buffer/analyze.py
Normal file
188
tooling/probes/step20c-saved-transaction-buffer/analyze.py
Normal file
|
|
@ -0,0 +1,188 @@
|
||||||
|
"""Offline analysis; preserves raw files and never launches a compositor."""
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
from PIL import Image, ImageChops
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument('lab', type=Path)
|
||||||
|
args = parser.parse_args()
|
||||||
|
lab = args.lab.resolve()
|
||||||
|
repo = Path(__file__).resolve().parents[3]
|
||||||
|
results = lab / 'results'
|
||||||
|
results.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def write(name, value):
|
||||||
|
(results / name).write_text(json.dumps(value, indent=2, sort_keys=True) + '\n')
|
||||||
|
|
||||||
|
|
||||||
|
def fields(line):
|
||||||
|
return dict(re.findall(r'(\w+)=([^\s]+)', line))
|
||||||
|
|
||||||
|
|
||||||
|
def ns(line):
|
||||||
|
match = re.search(r'\bns=(\d+)', line)
|
||||||
|
return int(match.group(1)) if match else None
|
||||||
|
|
||||||
|
|
||||||
|
def count_changed(left, right):
|
||||||
|
a = Image.open(left).convert('RGB')
|
||||||
|
b = Image.open(right).convert('RGB')
|
||||||
|
diff = ImageChops.difference(a, b)
|
||||||
|
box = diff.getbbox()
|
||||||
|
n = sum(any(px) for px in diff.get_flattened_data())
|
||||||
|
return {'pixels': n, 'box': box, 'dimensions': a.size}
|
||||||
|
|
||||||
|
|
||||||
|
def color_count(path, color):
|
||||||
|
im = Image.open(path).convert('RGB')
|
||||||
|
return sum(px == color for px in im.get_flattened_data())
|
||||||
|
|
||||||
|
|
||||||
|
matrix = {}
|
||||||
|
lifecycle = {}
|
||||||
|
timeline = {}
|
||||||
|
identities = {}
|
||||||
|
buffers = {}
|
||||||
|
parentage = {}
|
||||||
|
clips = {}
|
||||||
|
renders = {}
|
||||||
|
hits = {}
|
||||||
|
delivery = {}
|
||||||
|
commits = {}
|
||||||
|
presents = {}
|
||||||
|
damage = {}
|
||||||
|
pixels = {}
|
||||||
|
timeouts = {}
|
||||||
|
for mode in ('control', 'private_standard', 'private_mosaic'):
|
||||||
|
paths = sorted((lab / 'runs').glob(f'saved-{mode}-*'))
|
||||||
|
if len(paths) != 1:
|
||||||
|
raise RuntimeError(f'{mode}: expected exactly one run, found {len(paths)}')
|
||||||
|
run = paths[0]
|
||||||
|
events = [json.loads(line) for line in (run / 'events.jsonl').read_text().splitlines()]
|
||||||
|
lines = (run / 'compositor.log').read_text().splitlines()
|
||||||
|
client = (run / 'client.log').read_text().splitlines()
|
||||||
|
exit_state = json.loads((run / 'exit-status.json').read_text())
|
||||||
|
identity = next((x for x in events if x['kind'] == 'identity'), None)
|
||||||
|
identities[mode] = {'owner': identity['owner'] if identity else None,
|
||||||
|
'workspace': identity['workspace'] if identity else None}
|
||||||
|
tree_events = [{'event': line.split()[0], **fields(line)} for line in lines
|
||||||
|
if line.startswith(('SB_TREE_CREATE', 'SB_TREE_DESTROY', 'SB_ENABLE'))]
|
||||||
|
buffer_events = [fields(line) for line in lines if line.startswith('SB_BUFFER_CREATE')]
|
||||||
|
render_events = [fields(line) for line in lines if line.startswith('SB_RENDER')]
|
||||||
|
lifecycle[mode] = tree_events
|
||||||
|
buffers[mode] = buffer_events
|
||||||
|
parentage[mode] = [{'tree': x.get('tree'), 'parent': x.get('parent'),
|
||||||
|
'content': x.get('content'), 'view': x.get('view'),
|
||||||
|
'container': x.get('con'), 'ns': x.get('ns')}
|
||||||
|
for x in tree_events if x['event'] == 'SB_TREE_CREATE']
|
||||||
|
clips[mode] = [line for line in lines if 'PI_SCENE output=HEADLESS-1' in line]
|
||||||
|
renders[mode] = render_events
|
||||||
|
hits[mode] = [x for x in events if x['kind'] in ('pointer', 'hit_sample')]
|
||||||
|
delivery[mode] = [line for line in client if line.startswith(('ENTER', 'LEAVE', 'MOTION', 'BUTTON'))]
|
||||||
|
commits[mode] = [line for line in lines if 'PI_OUTPUT_COMMIT ' in line]
|
||||||
|
presents[mode] = [line for line in lines if 'PI_OUTPUT_PRESENT ' in line]
|
||||||
|
damage[mode] = {'status': 'NOT TESTED', 'reason': 'no transaction-scoped physical damage extents captured'}
|
||||||
|
timeouts[mode] = [line for line in lines if 'timed out' in line]
|
||||||
|
timeline[mode] = {
|
||||||
|
'client': [line for line in client if line.startswith(('XDG_CONFIGURE', 'HELD', 'ACK', 'COMMIT', 'PONG', 'EXIT'))],
|
||||||
|
'episodes': [x for x in events if x['kind'] in ('episode_start', 'held', 'timeout_observed', 'failure')],
|
||||||
|
'transactions': [line for line in lines if 'Transaction ' in line and
|
||||||
|
any(word in line for word in ('committing', 'ready', 'timed out'))],
|
||||||
|
}
|
||||||
|
px = {}
|
||||||
|
for side in ('left', 'right'):
|
||||||
|
reference = run / f'{side}-reference.png'
|
||||||
|
px[side] = {}
|
||||||
|
for tag in ('ack-held', 'ack-applied', 'timeout-held', 'timeout-applied'):
|
||||||
|
path = run / f'{side}-{tag}.png'
|
||||||
|
if path.exists():
|
||||||
|
px[side][tag] = {
|
||||||
|
'full_reference_diff': count_changed(reference, path),
|
||||||
|
'initial_client_color_pixels': color_count(path, (35, 68, 204)),
|
||||||
|
'alternate_client_color_pixels': color_count(path, (32, 208, 160)),
|
||||||
|
}
|
||||||
|
pixels[mode] = px
|
||||||
|
episode_data = {}
|
||||||
|
for kind in ('ack', 'timeout'):
|
||||||
|
start = next((x['ns'] for x in events if x['kind'] == 'episode_start' and x['episode'] == kind), None)
|
||||||
|
capture = next((x['after_ns'] for x in events if x['kind'] == 'capture'
|
||||||
|
and x['tag'] == f'{kind}-held' and x['output'] == 'HEADLESS-2'), None)
|
||||||
|
created = [x for x in buffer_events if start and capture and
|
||||||
|
start < int(x['ns']) < capture]
|
||||||
|
nodes = {x['node'] for x in created}
|
||||||
|
rendered = [x for x in render_events if start and capture and
|
||||||
|
start < int(x['ns']) < capture and x.get('node') in nodes]
|
||||||
|
episode_data[kind] = {'start_ns': start, 'held_capture_end_ns': capture,
|
||||||
|
'saved_buffers_created': len(created),
|
||||||
|
'created_nodes_rendered': len({x['node'] for x in rendered}),
|
||||||
|
'render_entries': len(rendered),
|
||||||
|
'created_nodes': sorted(nodes),
|
||||||
|
'rendered_nodes': sorted({x['node'] for x in rendered})}
|
||||||
|
owner = identities[mode]['owner'] or {}
|
||||||
|
workspace = identities[mode]['workspace'] or {}
|
||||||
|
scroll = workspace.get('mosaic', {}).get('rows', [{}])[0].get('scroll_x', 0)
|
||||||
|
# IPC rect is the logical owner; applied tile_scene in PI_SCENE is the
|
||||||
|
# private camera translation. Actual render entries remain the authority.
|
||||||
|
predicted_left = owner.get('rect', {}).get('x', 0) - scroll
|
||||||
|
predicted_right = predicted_left + owner.get('rect', {}).get('width', 0)
|
||||||
|
boundary = predicted_left < 0 < predicted_right
|
||||||
|
matrix[mode] = {'exit': exit_state, 'episodes': episode_data,
|
||||||
|
'owner_id': owner.get('id'), 'workspace_id': workspace.get('id'),
|
||||||
|
'layout': workspace.get('layout'), 'observed_scroll': scroll,
|
||||||
|
'owner_camera_box_estimate': [predicted_left, predicted_right],
|
||||||
|
'target_boundary_estimate': boundary,
|
||||||
|
'target_boundary_gate': 'NOT EXERCISED' if mode == 'private_mosaic' and not boundary else 'CONTROL_OR_CANDIDATE',
|
||||||
|
'input_during_snapshot': 'NOT TESTED' if not hits[mode] else 'PARTIAL',
|
||||||
|
'damage_coverage': 'NOT TESTED'}
|
||||||
|
|
||||||
|
budget = json.loads((lab / 'saved-budget.json').read_text())
|
||||||
|
write('saved-buffer-lifecycle.json', lifecycle)
|
||||||
|
write('transaction-timeline.json', timeline)
|
||||||
|
write('view-identities.json', identities)
|
||||||
|
write('buffer-identities.json', buffers)
|
||||||
|
write('scene-parentage.json', parentage)
|
||||||
|
write('clip-observations.json', clips)
|
||||||
|
write('render-entries.json', renders)
|
||||||
|
write('pointer-hits.json', hits)
|
||||||
|
write('client-delivery.json', delivery)
|
||||||
|
write('output-commits.json', commits)
|
||||||
|
write('presentations.json', presents)
|
||||||
|
write('damage-analysis.json', damage)
|
||||||
|
write('pixel-analysis.json', pixels)
|
||||||
|
write('timeout-analysis.json', timeouts)
|
||||||
|
write('budget.json', budget)
|
||||||
|
write('matrix.json', matrix)
|
||||||
|
summary = {'result': 'INCONCLUSIVE', 'architectural_conclusion': 'UNDECIDED',
|
||||||
|
'reason': 'Mosaic target snapshot did not cross x=0; target was outside H1 after relative scroll applied to existing camera',
|
||||||
|
'standard_fixture_incident': 'pointer sampling overran the 1200 ms ACK window; its reserved ACK episode failed and no second standard run was made',
|
||||||
|
'runs': {mode: matrix[mode]['exit']['failure'] for mode in matrix},
|
||||||
|
'budget': budget,
|
||||||
|
'unexercised': ['target partial-boundary clipping', 'saved-target pixels on H1',
|
||||||
|
'node-at/input during Mosaic target snapshot', 'camera hide while target snapshot active',
|
||||||
|
'mapped-view destruction', 'transaction-scoped damage coverage',
|
||||||
|
'native output buffer identity', 'all intermediate frames']}
|
||||||
|
write('summary.json', summary)
|
||||||
|
|
||||||
|
|
||||||
|
def manifest(paths, destination):
|
||||||
|
with destination.open('w') as out:
|
||||||
|
for path in sorted(set(paths)):
|
||||||
|
digest = hashlib.sha256(path.read_bytes()).hexdigest()
|
||||||
|
out.write(f'{digest} {path}\n')
|
||||||
|
|
||||||
|
|
||||||
|
source_files = [p for root in ('standard', 'mosaic', 'scenefx')
|
||||||
|
for p in (lab / root).rglob('*') if p.is_file() and '.git' not in p.parts]
|
||||||
|
source_files += [p for p in (repo / 'tooling/probes/step20c-saved-transaction-buffer').iterdir()
|
||||||
|
if p.is_file()]
|
||||||
|
manifest(source_files, results / 'source-sha256.txt')
|
||||||
|
evidence_files = [p for p in (lab / 'runs').rglob('*') if p.is_file()]
|
||||||
|
evidence_files += [p for p in results.iterdir() if p.is_file() and p.name not in
|
||||||
|
('source-sha256.txt', 'evidence-sha256.txt')]
|
||||||
|
evidence_files += [lab / 'saved-budget.json', lab / 'saved-build-log.txt']
|
||||||
|
manifest(evidence_files, results / 'evidence-sha256.txt')
|
||||||
|
print(f'RESULT=INCONCLUSIVE RESULTS={results}')
|
||||||
35
tooling/probes/step20c-saved-transaction-buffer/budget.py
Normal file
35
tooling/probes/step20c-saved-transaction-buffer/budget.py
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
"""Persistent campaign reservations. Failed attempts are never refunded."""
|
||||||
|
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) -> dict:
|
||||||
|
if mode not in MODES or episode not in (None, 'ack', 'timeout'):
|
||||||
|
raise ValueError('invalid reservation')
|
||||||
|
path = lab / 'saved-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': []}
|
||||||
|
file.seek(0)
|
||||||
|
if 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
|
||||||
27
tooling/probes/step20c-saved-transaction-buffer/prepare.sh
Normal file
27
tooling/probes/step20c-saved-transaction-buffer/prepare.sh
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# EXPERIMENTAL source-only reconstruction. Never launches a compositor.
|
||||||
|
set -eu
|
||||||
|
repo=$(CDPATH= cd -- "$(dirname -- "$0")/../../.." && pwd)
|
||||||
|
fixture="$repo/tooling/probes/step20c-saved-transaction-buffer"
|
||||||
|
log=$(mktemp /tmp/saved-buffer-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 "$fixture/sway-saved-trace.patch"
|
||||||
|
git -C "$lab/$source" apply "$fixture/sway-saved-trace.patch"
|
||||||
|
done
|
||||||
|
git -C "$lab/scenefx" apply --check "$fixture/scenefx-saved-trace.patch"
|
||||||
|
git -C "$lab/scenefx" apply "$fixture/scenefx-saved-trace.patch"
|
||||||
|
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-build-log.txt"
|
||||||
|
printf 'LAB=%s\nBUILD_LOG=%s\n' "$lab" "$log"
|
||||||
258
tooling/probes/step20c-saved-transaction-buffer/run.py
Normal file
258
tooling/probes/step20c-saved-transaction-buffer/run.py
Normal file
|
|
@ -0,0 +1,258 @@
|
||||||
|
"""One reserved headless run with a real held XDG configure per episode."""
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
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')
|
||||||
|
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')):
|
||||||
|
if captures >= 20:
|
||||||
|
raise RuntimeError('capture budget exceeded')
|
||||||
|
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 episode(kind, gaps):
|
||||||
|
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:])
|
||||||
|
snapshot(f'{kind}-held')
|
||||||
|
# The private-standard ACK episode exhausted its 1200 ms window during
|
||||||
|
# six pointer commands. Preserve that incident and prioritize the actual
|
||||||
|
# Mosaic snapshot lifetime; no unmeasured input claim follows.
|
||||||
|
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=1200'], 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)
|
||||||
|
if args.mode != 'control':
|
||||||
|
pointer = spawn([str(lab / 'pointer-daemon'), '1600', '600'],
|
||||||
|
'pointer.log', stdin=True)
|
||||||
|
wait(lambda: 'POINTER_READY' in (run / 'pointer.log').read_text())
|
||||||
|
command('workspace 1')
|
||||||
|
command('layout mosaic' if args.mode == 'private_mosaic' else 'layout splith')
|
||||||
|
spawn(['kitty', '--class', 'A', '/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')
|
||||||
|
if args.mode == 'private_mosaic':
|
||||||
|
command('layout mosaic scroll 400')
|
||||||
|
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)
|
||||||
|
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)
|
||||||
215
tooling/probes/step20c-saved-transaction-buffer/saved-client.c
Normal file
215
tooling/probes/step20c-saved-transaction-buffer/saved-client.c
Normal file
|
|
@ -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,23 @@
|
||||||
|
--- a/types/scene/wlr_scene.c
|
||||||
|
+++ b/types/scene/wlr_scene.c
|
||||||
|
@@ -2108,6 +2108,20 @@
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
+ if (node->type == WLR_SCENE_NODE_BUFFER && getenv("SAVED_TRACE")) {
|
||||||
|
+ struct timespec ts;
|
||||||
|
+ clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||||
|
+ struct wlr_scene_buffer *sb = wlr_scene_buffer_from_node(node);
|
||||||
|
+ struct pixman_box32 *box = pixman_region32_extents(&render_region);
|
||||||
|
+ fprintf(stderr, "SB_RENDER ns=%lld node=%p buffer=%p parent=%p visible=%d,%d,%d,%d damage=%d,%d,%d,%d\n",
|
||||||
|
+ (long long)ts.tv_sec * 1000000000LL + ts.tv_nsec,
|
||||||
|
+ (void *)node, (void *)sb->buffer, (void *)node->parent,
|
||||||
|
+ pixman_region32_extents(&node->visible)->x1,
|
||||||
|
+ pixman_region32_extents(&node->visible)->y1,
|
||||||
|
+ pixman_region32_extents(&node->visible)->x2,
|
||||||
|
+ pixman_region32_extents(&node->visible)->y2,
|
||||||
|
+ box->x1, box->y1, box->x2, box->y2);
|
||||||
|
+ }
|
||||||
|
int x = entry->x - data->logical.x;
|
||||||
|
int y = entry->y - data->logical.y;
|
||||||
|
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
--- a/sway/tree/view.c
|
||||||
|
+++ b/sway/tree/view.c
|
||||||
|
@@ -1,4 +1,6 @@
|
||||||
|
#include <stdlib.h>
|
||||||
|
+#include <stdio.h>
|
||||||
|
+#include <time.h>
|
||||||
|
#include <strings.h>
|
||||||
|
#include <wayland-server-core.h>
|
||||||
|
#include <wlr/config.h>
|
||||||
|
@@ -1276,7 +1278,18 @@
|
||||||
|
return view->urgent.tv_sec || view->urgent.tv_nsec;
|
||||||
|
}
|
||||||
|
|
||||||
|
+/* WHY: passive private evidence identifies actual saved nodes and lifetime. */
|
||||||
|
+static long long saved_ns(void) {
|
||||||
|
+ struct timespec ts;
|
||||||
|
+ clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||||
|
+ return (long long)ts.tv_sec * 1000000000LL + ts.tv_nsec;
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
void view_remove_saved_buffer(struct sway_view *view) {
|
||||||
|
+ fprintf(stderr, "SB_TREE_DESTROY ns=%lld view=%p con=%p tree=%p content=%p\n",
|
||||||
|
+ saved_ns(), (void *)view, (void *)view->container,
|
||||||
|
+ (void *)view->saved_surface_tree, (void *)view->content_tree);
|
||||||
|
+
|
||||||
|
if (!sway_assert(view->saved_surface_tree, "Expected a saved buffer")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
@@ -1309,6 +1322,10 @@
|
||||||
|
wlr_scene_buffer_set_buffer(sbuf, buffer->buffer);
|
||||||
|
wlr_scene_buffer_set_corner_radii(sbuf, buffer->corners);
|
||||||
|
scene_descriptor_assign(&sbuf->node, SWAY_SCENE_DESC_SAVED_BUFFER, tree);
|
||||||
|
+ fprintf(stderr, "SB_BUFFER_CREATE ns=%lld node=%p source_node=%p source_buffer=%p buffer=%p size=%dx%d pos=%d,%d parent=%p\n",
|
||||||
|
+ saved_ns(), (void *)&sbuf->node, (void *)&buffer->node,
|
||||||
|
+ (void *)buffer->buffer, (void *)sbuf->buffer, buffer->buffer ? buffer->buffer->width : 0,
|
||||||
|
+ buffer->buffer ? buffer->buffer->height : 0, sx, sy, (void *)sbuf->node.parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
void view_save_buffer(struct sway_view *view) {
|
||||||
|
@@ -1317,6 +1334,10 @@
|
||||||
|
}
|
||||||
|
|
||||||
|
view->saved_surface_tree = wlr_scene_tree_create(view->scene_tree);
|
||||||
|
+ fprintf(stderr, "SB_TREE_CREATE ns=%lld view=%p con=%p tree=%p parent=%p content=%p\n",
|
||||||
|
+ saved_ns(), (void *)view, (void *)view->container,
|
||||||
|
+ (void *)view->saved_surface_tree, (void *)view->scene_tree,
|
||||||
|
+ (void *)view->content_tree);
|
||||||
|
if (!view->saved_surface_tree) {
|
||||||
|
sway_log(SWAY_ERROR, "Could not allocate a scene tree node when saving a surface");
|
||||||
|
return;
|
||||||
|
@@ -1333,6 +1354,9 @@
|
||||||
|
|
||||||
|
wlr_scene_node_set_enabled(&view->content_tree->node, false);
|
||||||
|
wlr_scene_node_set_enabled(&view->saved_surface_tree->node, true);
|
||||||
|
+ 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool view_is_transient_for(struct sway_view *child,
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
"""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')
|
||||||
|
assert len(json.loads((lab / 'saved-budget.json').read_text())['episodes']) == 6
|
||||||
|
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