swayfx-mosaic/docs/design/STEP20C_OUTPUT_BUFFER_IDENTITY_PROBE.md

274 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# STEP20C output buffer identity probe
**Status: EXPERIMENTAL / PARTIALLY_FEASIBLE.** STEP20C
remains **NEXT** and production multi-output Mosaic is **NOT YET SUPPORTED**.
This probe changes no production source, public ABI, package, clipping rule,
transaction rule or UX policy.
## Question and prior evidence
The [presentation probe](STEP20C_PRESENTATION_IDENTITY_PROBE.md) paired 470
headless `present` events with 482 accepted native output commits by
`(output, commit_seq)`. Twelve initial commits had no observed present. It
did not identify the committed buffer or its pixels. Five initial Mosaic
commits had logical camera 1760 with applied scene -1120 and clip x=1120;
this was a transient logical/applied distinction, not a proven pixel or hit
defect. A `grim` image and an IPC reply cannot establish buffer identity.
The fixed experiment uses HEADLESS-1 `(-800,0)` and HEADLESS-2 `(0,0)`, each
800×600, scale 1, normal transform, with the genuine grabbed XDG chain
TOPLEVEL → POPUP_A → POPUP_B. No output reconfiguration is part of this probe.
## Audited API and lifetime model
The installed wlroots is 0.20.2; the source audit uses upstream tag commit
`d783533489e1f75d6886c2ab5c5960090ef268f8`, especially
`types/output/{state,output,render}.c`, `types/buffer/buffer.c`,
`render/{swapchain,allocator/gbm,wlr_texture}.c`, and
`backend/headless/output.c`. Private SceneFX base is
`37ccd723bef49e6891156ffafce8f549f01446cc`; its modified scene build
and FX texture implementation were inspected. The existing private runner
requests `WLR_RENDERER=pixman`, but the actual log shows SceneFX FX renderer,
OpenGL ES 3.2 on AMD Radeon 780M, with 800×600 GBM buffers and AB24/XB24
DRM formats. The requested environment setting is not evidence of the
renderer actually used.
`wlr_scene_output_build_state()` may return without a new buffer when no
render is needed. On the render path it acquires a swapchain buffer, submits
the render pass, calls `wlr_output_state_set_buffer()`, then unlocks its
temporary acquisition. `set_buffer()` locks the buffer for the output state.
`wlr_output_state_finish()` unlocks that state reference and nulls its field.
The compositor must never keep the state pointer after this call.
`wlr_output_commit_state()` shallow-copies and may filter unchanged fields,
test, fail or succeed. The headless backend accepts the state and defers a
`presented=true` event for an enabled output; it does not supply buffer
bytes in that event. On success wlroots increments the output's native
`uint32_t commit_seq` and synchronously emits `events.commit`, whose `state`
pointer is borrowed for the callback. On failure the sequence is not
advanced. A no-buffer configuration commit must not be assigned old pixels.
The headless deferred event can be absent, for example after output
destruction or allocation failure. Its `(output, commit_seq)` is the only
native link to the accepted commit; the counter can wrap, so the probe must
bound its run and preserve output lifetime/epoch in the key.
`wlr_buffer_lock()` increments a consumer lock. Its matching unlock emits
`release` when the last consumer leaves. The swapchain's release listener
marks its slot reusable only then. A probe-owned lock taken while the
output state is live therefore preserves the buffer object and prevents
swapchain reuse until release. It must be unlocked on rejection, present,
timeout and output destruction. Addresses alone are not durable identities:
assign a probe-local monotonically increasing ID while the owned lock is
held. Bound retained buffers to two per output, each to 800×600 and at most
8 MiB of copied bytes; release by 2 seconds even without `present`. Never
retain `event->state` or a data pointer beyond its callback/access scope.
The direct `wlr_buffer_begin_data_ptr_access(READ)` method may return false.
The GBM allocator in this renderer exposes DMA-BUF, not a data-pointer
implementation; no CPU mapping is assumed. On success only the returned
pointer, format and stride are valid until `end_data_ptr_access`, which is
mandatory. The fallback `wlr_texture_from_buffer()` and
`wlr_texture_read_pixels()` must be called only with a valid FX renderer and
texture on the compositor thread. SceneFX FX implements texture readback
using an EGL context, an internal `glFinish()` and `glReadPixels`; this is a
real synchronization and timing perturbation, not a free observation. The
probe must measure it against the same private compositor without readback.
No direct framebuffer read or extra `glFinish()` is permitted. If the
texture cannot be constructed or read safely, content remains UNOBSERVED.
## Protocol and safety gate
Before readback, verify the exact source/ABI and active renderer, then log
PRE_BUILD, POST_BUILD, COMMIT_ACCEPTED and PRESENT_CALLBACK separately.
POST_BUILD may expose `pending.buffer` when `WLR_OUTPUT_STATE_BUFFER` is set.
Take one additional lock only for such a live buffer, copy dimensions,
format, crop/destination box and state flags, and release on every path.
Do not predict success from `commit_seq+1`; record the native value only
after accepted commit. A commit callback may be used to check the actual
state's buffer identity while its borrowed pointer is valid. Distinguish
rejected commits, observed but uncommitted buffers, configuration commits,
accepted commits without present and `presented=false`.
Try CPU data-pointer access first. If unavailable, audit and try the FX
texture API. Read at most 8 MiB into a private copy, SHA-256 the raw bytes,
record format/stride/width/height and padding separately, and decode only
known DRM formats. Transform, source crop and destination box govern ROI
mapping. A second read at present is allowed only while the owned lock is
still valid; otherwise byte stability until present is UNPROVEN. Never infer
that `present` cryptographically attests to pixels. Compare applied scene
and clip at build with buffer pixels, while preserving any PRE_BUILD to
COMMIT_ACCEPTED differences. Pointer deliveries are a separate event stream.
Run one stable 400 case first. If ownership or synchronization fails, stop
readback and conclude INCONCLUSIVE. Otherwise run 400 → 1280 → 400, at most
three pairs, separate pointer targeting runs and unmodified/private `splith`
controls. Keep source fixtures under `tooling/probes/step20c-buffer-identity/`
and generated builds, logs, sockets and snapshots in one private `/tmp` lab.
Bound trace size, process duration and locks; reap only owned processes.
Preserve negative results and label fixture failures separately.
## Verdict criteria
- **FEASIBLE:** safe true-buffer readback, format-aware pixels matching the
sampled applied scene/clip, native commit/present pairing, bounded cleanup
and no blocking regression in this fixed matrix.
- **PARTIALLY_FEASIBLE:** true buffer access exists, but content decoding,
retention through present or some correlation remains unproven.
- **INCONCLUSIVE:** no safe supported readback or insufficient identity data;
a clean access failure is a capability result, not a compositor bug.
- **BLOCKED:** reproducible memory violation, crash, confirmed scene/buffer
inconsistency or blocking renderer regression.
An accepted headless commit is not a physical presentation. Any scaling,
rotation, migration, complete effects/damage or SceneFX ABI qualification is
**NOT TESTED** here. No verdict authorizes STEP20C production work.
## Measured private laboratory
The disposable laboratory is `/tmp/swayfx-mosaic-nested-grab-QwCf4FLm/`.
The private patch is preserved at
[`tooling/probes/step20c-buffer-identity/buffer-identity.patch`](../../tooling/probes/step20c-buffer-identity/buffer-identity.patch),
SHA-256 `d62e8c3a166e60a6eb394de58f51da1e37c3e7ac49671a361bd106ac64e2b427`.
It was applied after the published presentation fixture in a fresh private
checkout, compiled, and compared byte-for-byte with the separately edited
laboratory source. The final fixture uses a stricter cap than the protocol:
one extra buffer lock on HEADLESS-1, 80 snapshots maximum of 1,920,000 bytes
each, and a two-second timer. Each snapshot is written to a private file
after an FX texture read. The source comment, release log and native event
trace document every lock/unlock path. The patch changes no clip/hit rule.
The final three-pair run `private_mosaic-buffer_cycles-1791774958990683685`
kept owner IPC ID 8 and exactly one A/B map through all three 400 → 1280 →
400 pairs. It observed 80 accepted native output commits and 78 headless
present events. Both unmatched commits were initial sequence 2, one per
output, before workspace assignment: **UNEXPLAINED / NO PRESENT OBSERVED**.
The logged `BI_RELEASE age_ns` starts when the post-commit hold record is
installed; it excludes the earlier probe-owned lock time spent in readback
and commit. The two-second timeout likewise starts after that work. It
bounds waiting for an absent `present`, not a stalled GPU readback call.
Thirty-two selected HEADLESS-1 output-state buffers were GBM DMA-BUFs at
800×600, source format XB24 (`0x34325258`), with default full-buffer source
crop and destination. Direct CPU data-pointer access failed cleanly for all
32. SceneFX FX's `wlr_texture_from_buffer()` and
`wlr_texture_read_pixels()` read all 32 into XRGB8888, stride 3200,
1,920,000 bytes, with no row padding. Raw and top-down normalized RGB
SHA-256 are recorded for every selected buffer in `results/`.
For all 32, the native synchronous commit callback contained that same
live buffer, the commit was accepted with its actual `commit_seq`, a matching
headless `presented=true` event arrived, and 28 extra locks were released on
that event. Four other selected locks, IDs 3, 15, 21 and 27, were released
early by a **private instrumentation scope defect**: after their HEADLESS-1
commits, a HEADLESS-2 repaint reached the unconditional global
`bi_release_hold("superseded")` immediately after `POST_BUILD`. The
HEADLESS-2 build had a buffer but the probe did not select it, and no new
HEADLESS-1 submission caused those four releases. Their native presents
arrived later, after the probe had relinquished its references; this does
not prove byte stability through presentation. The old `superseded` log
reason is the instrumentation's label, not a demonstrated same-output
buffer replacement. No sample needed timeout or
output-destroy release. Only three buffers were read a second time while
still locked at `present`; all three raw SHA-256 values matched their
precommit values. Byte stability through `present` for the other 29 is
**NOT TESTED**; four of those were not retained until presentation. The headless event does not itself attest to bytes.
Three representative identified buffers are:
| Probe ID / native commit | Applied scene and clip | Exact A/B pixels | Raw SHA-256 | Present reread |
| --- | --- | --- | --- | --- |
| 8 / HEADLESS-1:23 | `-400,0` / `400,0,800,600` | 42,200 / 17,940 | `9e6d2f7b094fd5124c0207a30cb294acec3a01da15b90008d7f7449d27e1ee7c` | equal |
| 12 / HEADLESS-1:27 | `-1280,0` / `1280,0,800,600` | 0 / 0 | `03e815da50d0f04b16660340db19c1a03225f31bac0824d490c2ffbab0ee165c` | equal |
| 16 / HEADLESS-1:31 | `-400,0` / `400,0,800,600` | 42,200 / 17,940 | `9e6d2f7b094fd5124c0207a30cb294acec3a01da15b90008d7f7449d27e1ee7c` | equal |
All nine selected 1280 snapshots across the three pairs had zero exact A/B
pixels. All 32 sampled commit scenes agreed with their applied clip at 400
or 1280. The 400 snapshots before A/B mapped or after their explicit closure
are not classified as missing popups; the runner event stream preserves
those lifecycle states. PNG captures remain supplementary and are never
assigned a buffer sequence solely by timestamp.
The separate transient run
`private_mosaic-buffer_cycles-1791775118486687664` observed PRE_BUILD,
POST_BUILD and COMMIT_ACCEPTED at logical `scroll_x=1760`, applied tiled
scene `-1120,0` and applied clip `1120,0,800,600`. Its actual HEADLESS-1
buffer at native sequence 10 was read (raw SHA-256
`9a8c1cc62f439f812ce501fe49b99c883c9839f377bdd7f1caaecc05ca5a4c86`),
presented in headless, and released. A/B were not yet in the settled
state; the read does not show an incorrect pixel or invisible hit. The
applied scene/clip, rather than the newer logical camera, are the valid
state to compare with this buffer.
The private stage trace records PRE_BUILD, POST_BUILD and COMMIT_ACCEPTED
workspace, logical camera, applied tiled scene and tiled clip. The native
commit callback additionally records current owner geometry and root-popup
clip; the client trace records A/B mapping and real grab serials. The
owner-visibility guard's Boolean decision and nested-popup mapping are not
atomically exported at each stage and remain **UNOBSERVED** there. The
runner's requested camera commands and IPC replies are kept separately.
No stage record is treated as an atomic rendering or input snapshot.
Separate fixed-geometry pointer runs against the instrumented private
binary used two valid XDG button serials. The visible case delivered a B
button. The clipped B-crossing click delivered an ordinary client button
and child-first B/A `popup_done`, with no B button from that clipped point.
The hidden-owner click delivered no popup button and dismissed B then A.
These events have no native frame ID and are not attributed to a buffer.
Both unmodified SwayFX and private `splith` controls completed their
ordinary sampled cycle without runner failure; they have no Mosaic camera.
### Instrumentation impact and limits
Sequential three-pair runs with the same private instrumentation family,
with and without `BI_OUTPUT_DIR`, observed 80/78 native commits/presents
in both. Median build time was 0.089/0.085 ms; median POST_BUILD to
COMMIT_ACCEPTED was 1.542/0.141 ms; median commit callback to present
callback was 0.384/0.317 ms. The no-readback control used the same stage
logging; the final lock-selection correction only executes with readback.
Total run duration was about 5.5/5.35 s. These are
small samples with host scheduling noise, but the readback path clearly
perturbs timing. SceneFX's internal `glFinish()` is a plausible contributor;
the experiment does not isolate its cost from copying, file I/O and logs.
No crash, renderer error, rejected sampled commit, protocol failure or
repeatable pixel/input inconsistency appeared. Headless presentation is not
physical presentation. The current test does not prove input and pixel
coherence for every transaction generation, all effects/saved buffers,
other renderers or all 80 commits.
**Verdict: PARTIALLY_FEASIBLE.** The actual submitted buffer and sampled
pixels can be read safely through the FX texture API, then tied to a native
commit and headless present while an owned lock prevents swapchain reuse.
The readback's timing cost, four cross-output early releases, three-of-32 second-read
coverage and lack of an atomic scene/input generation prevent a general
temporal-coherence claim.
The next smallest experiment is a bounded lower-impact readback method or
explicit render/transaction generation tag, validated against the same
native buffer identity and pointer event stream. Do not integrate this
private patch into production.
The five-file `buffer-source-sha256.txt` in the lab has SHA-256
`0fed9458f3793d05018af6ce9bf4a674208c700829726c1a1b8e0e434088bfef`;
all five entries passed. The main lab's `buffer-artifacts-sha256.txt` has
501 entries and SHA-256
`94a3097686d47dc3cd06b6c0b24f630544bff5e7b798ccc9934461799310339b`;
all 501 passed. It covers selected raw/derived buffers, the main and
transient results, controls, pointer runs and source manifests. The
historical five-source manifest was verified before the scientific wording
in the fixture README was corrected. Its original README had SHA-256
`add56c3de06671fd14bbb504456f1339eaaa7d0209f11444a9cb940f3435aa59`
and is retained as `buffer-fixture-README-before-rectification.md` in the
private lab. The corrected README has a new hash; re-running the old
manifest directly against the current repository README must flag that
intentional documentation change. The experimental patch, runner,
analyzer and preparation script retain their historical hashes.
fixed patch was independently rebuilt in
`/tmp/swayfx-mosaic-nested-grab-I3RluIc1/`; its instrumented compositor
source was byte-identical and a one-pair run read and committed 20 selected
buffers, paired all 20 with headless present, released 18 on present and
two earlier from the same cross-output scope defect, with three equal second reads. Its separate
`buffer-artifacts-sha256.txt` covers the final-patch pointer and `splith`
controls, one-pair replay and raw buffers: 379 entries, SHA-256
`1ae1b391fc4a0664d852830141d8d012e6e55966a7e38ce6362b4337eb51d5af`;
all 379 passed. The main lab's earlier pointer/control runs were made during
patch development; the independent lab repeats them against the final
fixture. Neither set assigns input to a native commit. These are private
experimental artifacts, not a permanent library dependency.