17 KiB
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 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,
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.