docs(mosaic): preserve saved transaction buffer probe evidence

This commit is contained in:
fy59 2026-10-12 18:02:08 +02:00
parent 7561120b55
commit 642c61aaaf
16 changed files with 1178 additions and 1 deletions

4
.gitignore vendored
View file

@ -23,3 +23,7 @@ opt/
*.pkg.tar.* *.pkg.tar.*
.SRCINFO.tmp .SRCINFO.tmp
.SRCINFO .SRCINFO
# Generated Python bytecode from experimental fixtures
__pycache__/
*.pyc

View file

@ -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.

View file

@ -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.

View file

@ -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**.

View file

@ -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.

View file

@ -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**.

View 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.

View 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.

View 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}')

View 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

View 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"

View 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)

View 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, &registry_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;
}

View file

@ -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;

View file

@ -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,

View file

@ -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')