240 lines
15 KiB
Markdown
240 lines
15 KiB
Markdown
# 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.
|