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.*
|
||||
.SRCINFO.tmp
|
||||
.SRCINFO
|
||||
|
||||
# Generated Python bytecode from experimental fixtures
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
|
||||
### 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.
|
||||
|
||||
- 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
|
||||
the clipped-away part of A was not independently clicked and no atomic
|
||||
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,
|
||||
atomic generation and SceneFX/wlroots ABI/package remain unqualified.
|
||||
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.
|
||||
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.
|
||||
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
|
||||
|
||||
|
|
@ -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 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 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.
|
||||
|
||||
|
|
@ -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.
|
||||
Independent clipped-away A input and saved buffers remain open. STEP20C is
|
||||
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,
|
||||
interior shadow and ABI/package maintenance. STEP20C remains **NEXT**;
|
||||
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