docs(mosaic): preserve commit-present input ordering probe evidence

This commit is contained in:
fy59 2026-10-12 08:39:42 +02:00
parent 6fc849d172
commit c1f0e3621d
11 changed files with 688 additions and 1 deletions

View file

@ -4,6 +4,7 @@
### STEP20C experimental evidence
- Preserved and independently replayed a commit/present/input ordering probe. A passive private top-level listener and native headless traces found 20 applied input-region mutations but no natural mutation between an accepted output commit and its matching present; sampled hit and client delivery followed effective input. The temporal verdict is INCONCLUSIVE and the generation-contract decision remains UNDECIDED. A complete outside-Git raw evidence manifest now covers all 94 run/result files. No production or package files changed.
- Preserved a reproducible mutation-coverage fixture and source audit. A fresh-clone replay rebuilt private SceneFX/Mosaic and verified pinned wlroots sources in its own lab. Bounded trace-only runs separated camera/clip, popup lifecycle, same-size client content and input-region hits; the latter changed hit testing at an unchanged partial epoch. A real private output-destroy path released one held reference, while backend rejected-commit cleanup remains untested. The architectural generation decision is UNDECIDED; STEP20C stays NEXT.
- Preserved a private applied-scene/input probe. A separate corrective patch scopes retained output buffers to their owner, samples three buffers through present, and logs partial applied-scene epochs at build and real pointer hits with client delivery kept separate. The tested matrix is PARTIALLY_FEASIBLE; STEP20C remains NEXT and multi-output Mosaic unsupported.
- Preserved a private output-buffer identity fixture and protocol. In fixed headless geometry, 32 GBM buffers were read through SceneFX FX and linked to native commits and presents; three second reads matched. Four references were released early by a cross-output scope defect in the private instrumentation, not by demonstrated same-output buffer replacement. Readback changed timing, so transaction/input coherence and production readiness remain unproven. STEP20C stays NEXT and multi-output Mosaic unsupported.

View file

@ -171,6 +171,19 @@ candidate research boundaries; whether existing event-loop ordering alone
is enough across build, present and input remains **UNDECIDED**. No new
production generation or transaction contract is accepted by this probe.
The independently replayed [commit/present/input ordering probe](design/STEP20C_COMMIT_PRESENT_INPUT_ORDERING_PROBE.md)
confirms in source that wlroots headless queues `present` as an idle callback
after an accepted output commit, while an applied Wayland input-region
commit changes the region consulted by later scene hits. In five bounded
cycles per mode, all 20 applied target input mutations in private standard
and Mosaic occurred after the preceding matching `present`; none landed
inside a native commit-to-present interval. Input-only hit and client
delivery matched the effective region in sampled states. This leaves the
cross-time Mosaic clipping invariant **UNDECIDED** and adds no production
generation contract. A later hit may legitimately use newer input than a
previously committed output buffer; identity of these states must remain
distinct in further probes.
## IPC
On a Mosaic workspace, `get_tree` reports `layout: "mosaic"` and adds a `mosaic` object with `active_row` and `rows`. Each row entry contains ordered child node IDs, `scroll_x`, and focused node ID or null. Existing Sway fields retain their shapes; normal workspace/window events and `get_workspaces` paths remain in use. This is an extension to the Sway layout vocabulary: strict IPC consumers with a closed layout enum must recognize `mosaic`. Additional rows in the model are not a promise of usable multi-row IPC behavior.

View file

@ -129,6 +129,18 @@ boundary remain unqualified. The architecture decision is **UNDECIDED**;
physical presentation and production multi-output Mosaic remain
**NOT YET SUPPORTED**.
The independently replayed [commit/present/input ordering probe](design/STEP20C_COMMIT_PRESENT_INPUT_ORDERING_PROBE.md)
is **INCONCLUSIVE** for the discriminating temporal case. The headless
backend defers `present` to an idle callback, but none of 20 bounded
input-region mutations in private standard/Mosaic fell between a native
commit and its matching present. Sampled hits and client receipt followed
the applied input state, with unchanged sampled pixels; this is not a
demonstrated Mosaic clip defect. Client input-only commits need not cause
output redraw. Popup clip changes in that window, physical presentation,
rejected backend commit cleanup and broader output/effect paths remain
**NOT TESTED**. Existing ordering sufficiency and any added-generation
contract remain **UNDECIDED**.
The [isolated feasibility probe](design/STEP20C_FEASIBILITY_PROBE.md) is **PARTIALLY FEASIBLE**, not a release qualification. On two horizontal headless outputs, real Mosaic content leaked visually into an empty adjacent output; that earlier disposable candidate also delivered pointer enter from a partly visible client outside its viewport. Negative-origin, scaled and rotated arrangements showed visual leakage in that candidate. Popups, effects, saved buffers and complete damage correctness remain unqualified across the full matrix. The later probes above narrow individual cases without qualifying multi-output Mosaic, which remains **NOT YET SUPPORTED**.
The later [SceneFX subtree clipping spike](design/STEP20C_SCENEFX_CLIPPING_SPIKE.md) is also **PARTIALLY FEASIBLE**. Its private inherited tree clip stopped tested tiled pixels and pointer hits from crossing the output boundary, including a fully hidden cell, but a real GTK XDG popup rendered and received input on the neighboring output because its scene tree lives in the global popup layer. Dedicated subsurface, saved-buffer, nested-popup and complete effect/damage validation remain open. The spike changed no production library or compositor source; multi-output Mosaic remains **NOT YET SUPPORTED**.

View file

@ -35,7 +35,9 @@ The [root README](../README.md) is the public summary. For a specific topic, use
[Applied-state source fixture](../tooling/probes/step20c-applied-state-input/README.md) rebuilds the disposable experiment.
20. [STEP20C mutation coverage probe](design/STEP20C_MUTATION_COVERAGE_PROBE.md): source ownership inventory and bounded trace-only camera, popup, client-content and input-region scenarios; output-destroy cleanup sampled, rejected commit still untested.
[Mutation source fixture](../tooling/probes/step20c-mutation-coverage/README.md) replays the disposable matrix without production integration.
21. [STEP20A](STEP20A.md) and [STEP20B](STEP20B.md): historical evidence.
21. [STEP20C commit/present/input ordering probe](design/STEP20C_COMMIT_PRESENT_INPUT_ORDERING_PROBE.md): independently replayed headless ordering evidence; 20 applied input-region mutations, no natural commit-to-present interleaving, temporal question INCONCLUSIVE.
[Ordering source fixture](../tooling/probes/step20c-commit-present-input/README.md) rebuilds the passive private trace.
22. [STEP20A](STEP20A.md) and [STEP20B](STEP20B.md): historical evidence.
## Project
@ -79,6 +81,7 @@ The [root README](../README.md) is the public summary. For a specific topic, use
| [STEP20C output buffer identity probe](design/STEP20C_OUTPUT_BUFFER_IDENTITY_PROBE.md) | Experimental evidence | PARTIALLY_FEASIBLE; sampled true buffers were read and paired with headless presents; four early releases came from a private cross-output instrumentation defect, while readback perturbs timing and atomic scene/input coherence remains unproven |
| [STEP20C applied state/input probe](design/STEP20C_APPLIED_STATE_INPUT_PROBE.md) | Experimental evidence | PARTIALLY_FEASIBLE; per-output holds survive cross-output repaint in the sampled matrix, and applied scene/hit/client streams are observed with a partial experimental epoch |
| [STEP20C mutation coverage probe](design/STEP20C_MUTATION_COVERAGE_PROBE.md) | Experimental evidence | PARTIALLY_FEASIBLE overall; an input-region change alters the hit at unchanged partial epoch, client recolor changes pixels without geometry, and a held output reference is released on sampled output destruction; cross-time generation contract remains UNDECIDED |
| [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 |
A proposal does not supersede accepted canonical contracts or assert implementation status. Its open feasibility items must be resolved before implementation.

View file

@ -123,6 +123,17 @@ a controlled hit-geometry mutation between one known output-state build
and its deferred headless present, with hits and client delivery sampled
on both sides. This remains scientific evidence, not production STEP20C.
The independently replayed [commit/present/input ordering probe](design/STEP20C_COMMIT_PRESENT_INPUT_ORDERING_PROBE.md)
tested that gate without changing backend scheduling. Twenty real input
region mutations applied in two private modes, but none occurred between
an accepted output commit and its matching headless present; P2 is
**INCONCLUSIVE**. The sampled hit and client delivery matched effective
input state, and no distinct Mosaic clipping defect was reproduced. Before
deciding whether an extra generation contract is necessary, the smallest
next gate is passive event-loop phase tracing with a client request already
pending during ordinary repaint, then a same-interval hit if naturally
observable. STEP20C remains **NEXT** and multi-output **NOT YET SUPPORTED**.
## STEP20D — multi-row Spatial Canvas · PLANNED
**Goal:** connect rows to runtime tree, transactions, scene, input, and IPC. **Planned deliverables:** active-row navigation, row-local scroll/focus, deterministic insertion/reflow, in-row reorder, and between-row movement. **Exit criteria:** real tiled containers preserve identity and order across row changes; focus and camera restore correctly; headless multi-row tests pass. **Docs first:** settle exact interactions and row invariants in UX and Architecture. Unset key combinations remain TBD.

View file

@ -0,0 +1,291 @@
# STEP20C commit, present and input ordering probe
**Status: EXPERIMENTAL / INCONCLUSIVE.** STEP20C stays
**NEXT** and multi-output Mosaic **NOT YET SUPPORTED**. All compositor and
client changes for this probe belong to disposable private copies. No
production, packaging, ABI or UX decision follows from this protocol.
## Question and distinct states
The mutation coverage probe found T3 client pixels changing at fixed intended
geometry and T4 input hit changing while `asi_applied_epoch=12` and sampled
pixels stayed fixed. T4 is a normal possible Wayland transition, not proof of
a clipping defect. The question here is whether a real client input-region
commit can execute between one accepted output commit and that commit's
headless present, and what the actual scene hit and client delivery then do.
Keep five identities separate: client **content** buffer; effective Wayland
**input** region; applied **scene geometry** and clip; native output
**presentation** `(output, commit_seq)`; and pointer **grab/delivery** state.
A submitted output buffer is not changed retroactively when a later surface
commit changes input. A later pointer hit may legally consult newer input
than the pixels in that buffer. A mismatch alone is not a Mosaic defect.
No experimental epoch is treated as an atomic native generation.
## Source audit and scheduling hypotheses
Audited pinned inputs are SwayFX/Mosaic based on SwayFX 0.6, private SceneFX
0.5 at `37ccd723bef49e6891156ffafce8f549f01446cc`, and wlroots 0.20.2
at `d783533489e1f75d6886c2ab5c5960090ef268f8`. The frozen private
instrumentation is the Mutation Coverage chain. Wayland server API semantics
are checked against `wayland-server-core.h` and the executed callbacks.
`sway/desktop/output.c:output_repaint_timer_handler` checks
`wlr_scene_output_needs_frame`, logs private `BI_STAGE` before and after
`wlr_scene_output_build_state`, then calls `wlr_output_commit_state`.
wlroots `types/output/output.c` calls the backend commit, increments native
`commit_seq`, applies state, and emits `events.commit` synchronously on
acceptance. The private `PI_OUTPUT_COMMIT` listener runs in that callback.
The headless backend `backend/headless/output.c:output_commit` queues a
present with `commit_seq + 1` through `output_defer_present` when enabled.
`types/output/output.c` puts this on a Wayland event-loop idle source. The
idle callback calls `wlr_output_send_present`; an output destroy removes
the idle source and cancels that pending present. Allocation failure could
also prevent queuing. Thus `present` is deferred in source but not guaranteed
for every commit. A deferred idle is not evidence that a client request
actually ran between commit and present.
`types/wlr_compositor.c:surface_handle_set_input_region` changes pending
state. The subsequent `wl_surface.commit` moves it to current state,
intersects it with current surface size into `surface->input_region`, and
emits the surface commit signal. A cached/synchronized surface can delay
application; the top-level used here is not synchronized beneath a parent.
`wlr_surface_point_accepts_input` consults effective `input_region` at hit
time. wlroots `types/scene/surface.c` installs that predicate on the scene
buffer. Private SceneFX `scene_node_at_iterator` applies scene clip and
calls it. SwayFX `sway/input/cursor.c:node_at_coords` performs the scene
lookup; seat button notification follows. XDG grabs can redirect effective
delivery, so the initial scenario has none and still checks the client log.
The scene surface commit callback calls `surface_reconfigure`. It schedules
a frame only if the surface has a pending frame callback and intersects an
enabled output; an input-only commit does not itself guarantee a new output
buffer. The Wayland idle callback is documented as running before blocking
in `wl_event_loop_dispatch`; the exact order of already-ready client fd
callbacks against that idle depends on the loop's executed dispatch order.
Passive same-process timestamps, not this source inference, will decide P2.
No recursive dispatch, backend delay, compositor sleep, forged present or
direct write to `current.input` is permitted.
## Fixture, identities and lifetime
Use HEADLESS-1 `(-800,0)` and HEADLESS-2 `(0,0)`, both 800×600, scale 1,
normal transform. Compare unmodified SwayFX `splith`, private Mosaic binary
`splith`, and private Mosaic binary `mosaic`. The T4 client keeps one mapped
420×320 colored top-level and changes its input region full → empty → full
through normal Wayland requests without a new client buffer. No popup or
grab is active in P0–P5. A stable visible point is used for all hits and
actual right-button deliveries. Each run owns a mode-0700 runtime, explicit
`WAYLAND_DISPLAY` and `SWAYSOCK`, process groups and private logs.
Passive server traces record monotonic nanoseconds for surface commit,
output build, accepted output commit, headless present, executed scene hit,
and seat notification. Output identity is valid only during its lifetime;
surface IDs are temporary. Use native output `commit_seq` and an observed
surface commit sequence when available. Client events retain their own
order; client and server clocks are not merged without calibration. The
runner must stop process groups and remove sockets. The earlier private
output lock has per-output ownership, one unlock per lock, and timer cleanup;
normal P0–P5 use trace-only mode. Rejected backend commit remains
**NOT TESTED** and will not be induced unsafely.
## Bounded matrix and validity
* **P0**: verify two outputs, layout, mapped client, reference pixel, hit,
real button receipt, and native commit/present availability where traced.
* **P1**: commit empty input before an identified later output build, if
one occurs; observe applied region, hit, button and output timeline.
* **P2**: seek naturally `OUTPUT_COMMIT(N) → SURFACE_COMMIT(empty) →
SCENE_HIT(P) → OUTPUT_PRESENT(N)`, retaining actual order if hit differs.
Count only accepted native commits and a real Wayland client mutation.
* **P3**: observe a mutation after a present as temporal control.
* **P4**: restore full input on the same surface, with stable sampled pixels,
restored hit and actual button receipt.
* **P5**: repeat at most five full/empty cycles per mode. Preserve failed
attempts; if no P2 interleaving occurs, mark P2 **INCONCLUSIVE**.
Only after the input-only matrix, consider a private Mosaic scroll
`400 → 1280 → 400` with two valid XDG popups if source and observations show
it can distinguish a Mosaic clip invariant. An input region alone does not
exercise the Mosaic viewport clip. A clip changing after an output commit
cannot rewrite its already submitted buffer. No scaling, rotation, migration
or hotplug is part of this matrix.
## Analysis, neutral measurement and verdict
Keep raw traces and derive `event-order.jsonl`, `surface-commits.json`,
`output-commits.json`, `presentations.json`, `input-regions.json`,
`scene-hits.json`, `client-delivery.json`, `timelines.json`, `matrix.json`
and `summary.json` in the lab. Each hit must identify the effective input
region, last accepted output commit, last observed present, next present,
actual client receipt, and whether any relation is inferred. Captures are
pixel samples, not native buffer reads. Measure log volume, commit/present
counts, commit-to-present gaps and scenario duration in standard and
private runs; headless gaps are not physical display latency.
**FEASIBLE** requires sufficient observed transitions with matching hit and
client delivery inside the tested boundary. **PARTIALLY_FEASIBLE** retains
useful observations with important gaps. **INCONCLUSIVE** applies when the
natural commit-to-present window cannot be exercised or distinguished.
**BLOCKED** needs a reproducible clipping, targeting or lifetime violation;
normal input-only change without redraw is insufficient. The architecture
answer remains **UNDECIDED** unless a concrete required invariant is either
shown to hold within the tested boundary or violated by a real counterexample.
## Executed source-audit answers
1. Headless `present` is queued as a Wayland idle callback, not emitted in
the backend commit call. The accepted `events.commit` signal is synchronous.
2. Output destruction cancels the queued idle. A failed allocation in
`output_defer_present` could omit it. The retained private runs also lack
the two initial sequence-2 presents per instrumented mode; the reason for
those initial omissions is not assigned without further callback tracing.
3. For the top-level fixture, an applied `wl_surface.commit` updates
`surface->input_region` before the SwayFX top-level commit listener logs.
Subsequent `wlr_scene_node_at` uses that effective region.
4. An input-only commit does not necessarily schedule a frame. Scene surface
code schedules one conditionally for pending client frame callbacks;
camera, cursor and other activity can separately cause output commits.
5. Only callbacks actually dispatched by the Wayland event loop can occur
between a backend commit and its idle present. Client requests, pointer
input or other timers are candidates, not guaranteed intervening events.
6. The public idle API says the callback runs before blocking in dispatch;
it does not by itself establish the order against every ready client fd.
The executed traces are the authority for this bounded backend run.
7. This fixture sought true client Wayland mutations without changing the
backend. None landed in the commit-to-present interval.
8. An empty interval proves only that these 20 attempts did not exercise the
proposed temporal case. It cannot prove that the interval is impossible.
## Replayed results
### Runner timing limit
The historical `run.py` inserts the scenario into the older runner. Each
`command()` waits for a complete `swaymsg` IPC response. Each
`client_command()` writes a line to the client and waits until its `COMMAND
... DONE` log appears. Each `inject()` writes to the virtual pointer client
and waits for `INJECT_DONE`. In Mosaic, the camera command is therefore
acknowledged before the input mutation is submitted; the next pointer action
also waits for client confirmation. The full-to-empty and empty-to-full
transitions repeat this sequential order. These are process and IPC
round trips, whereas many private commit-to-present gaps were only tens of
microseconds. This fixture can thus miss an otherwise possible pending-client
request during that narrow interval. The timing limit does not invalidate
the native callbacks, actual applied mutations, hits, pixels or client
delivery recorded in the runs. It limits the conclusion to the order that
was executed, and motivates a separately documented nonblocking runner.
The final fixture was overlaid on a new clone of published `main` at
`6fc849d172a1728a5a4b3b7fa31c7dc930736fe5`; its private SceneFX,
Mosaic, wlroots source-audit input and client were rebuilt in
`/tmp/swayfx-mosaic-nested-grab-D0AMNl62/`. The three runs used the fixed
two-output geometry and five region cycles each. All reported
`failure=null`, eight captures, five empty and five full client commands,
six actual top-level right-button presses, zero popup grabs and no retained
probe buffer. The sampled `(342,175)` left-output pixel remained
`(35,68,204)` at P0, first empty P1 and restored P4 in every mode.
| Mode | Native commits/presents | Applied target input commits | Matching commit→mutation→present | Target-point hits after known input commit | Duration |
| --- | ---: | ---: | ---: | --- | ---: |
| Unmodified SwayFX `splith` | Uninstrumented | Uninstrumented | Unobservable | Client receipt only | 8.870 s |
| Private `splith` | 62/60 | 10 | 0 | 25 empty: no target hit; 25 full: target hit | 8.906 s |
| Private `mosaic` | 84/82 | 10 | 0 | 30 empty: no target hit; 29 full: target hit; 2 full hits at a changed camera position reached another node | 8.923 s |
Both instrumented modes had 10/10 target input mutations after the last
matching observed present and before a later output build. This directly
supports P1 and P3 in their observed ordering. P0 verified output geometry,
native tracing, baseline pixels and button receipt. P4 restored the same
surface's input and receipt. P5 stopped after five cycles. P2 had **zero**
actual natural interleavings and is **INCONCLUSIVE**. The two unmatched
commits per private mode were initial sequence 2 on the two outputs. The
unmodified control's zero trace records mean no instrumentation, not zero
native commits. The client and server clocks were kept separate; server
nanoseconds establish all reported commit/present/mutation/hit ordering.
Private standard commit-to-present gaps ranged 20,779–24,620,493 ns
(median 76,131 ns); private Mosaic ranged 21,580–24,875,267 ns
(median 85,915 ns). These are headless event-loop gaps, not display
latencies. A separate same-workload comparison in an earlier private lab
disabled only the new surface-commit log site: private standard measured
62/60 commits/presents and 8.968 s with it, versus 63/61 and 8.893 s
without it; private Mosaic measured 77/75 and 8.891 s with it, versus
83/81 and 8.920 s without it. Both variants delivered six presses. Native
counts and gaps vary between runs; this pair does not isolate a causal
logging cost or establish a scheduling-neutral measurement.
The input-only test did not modify the Mosaic viewport clip. A second
`400 → 1280 → 400` popup transition could test clipping, but this campaign
did not produce the temporal window required to distinguish a newer clip
from an already submitted output buffer. Running that variant now would add
spatial coverage without answering P2, so it remains **NOT TESTED** in this
probe. No separate Mosaic clipping defect, crash or lifetime failure was
observed. Backend rejected commit remains **NOT TESTED**.
## Structured evidence and verdict
The [source fixture](../../tooling/probes/step20c-commit-present-input/README.md)
produces `event-order.jsonl`, `surface-commits.json`,
`output-commits.json`, `presentations.json`, `input-regions.json`,
`scene-hits.json`, `client-delivery.json`, `timelines.json`, `matrix.json`
and `summary.json` outside Git. Scene-hit records carry the last known
target input mutation, last accepted native commit and surrounding present
sequences; a nil hit's target identity is explicitly an inference from the
fixed test point. Captures are sampled pixels, not exact submitted buffers.
The final patch SHA-256 is
`0abdd61a45dc5cc63a59b5279fcd3d94a01b52ed394048259270edcc6ca582fc`.
The final source and artifact manifests hash to
`767755b02b505af2467ea9b40f67c250f9406ee720a29237d877f9c69dc2f3c2`
and `7835941aaae98ca3e5a3484defdfe0e14f246ac4693bdb13ff437c1fa29ffbd1`;
every manifest entry passed `sha256sum -c`. The matrix, summary and event
order hashes are `347f74ec86fb54691660dde6098effde2ee2a90d4055df3613f8e7810aebd9d1`,
`8202008e649c3b5782ebac2ae32e9e80875f5763ec76936ae6849c2c37598a39`
and `399c9e52087e426d2c6448dfa2bbc761f23a348ace4275b0d6472cf409993100`.
The historical artifact manifest contains 11 derived files and omits raw
run logs and captures. It remains unchanged. A second, outside-Git manifest
at `commit-present-input-results/raw-evidence-sha256.txt` in that lab
covers all 84 regular files under the three `runs/` directories and the 10
structured result files, excluding the two historical manifests themselves.
This includes configs, IPC output, compositor/client/pointer logs, captures,
output states and exit codes. It has 94 entries, all verified, and SHA-256
`9b74584936d44be5accb1f3882482fbd1eb85bc5c696b4551d45076b76ff4f8d`.
It does not cover build trees, binaries or sockets.
### Independent freeze replay
On 2026-10-12 the preserved fixture was reconstructed again in a new
disposable lab, `/tmp/swayfx-mosaic-nested-grab-a7AsuMuV/`, from the pinned
source preparation chain, not from the historical build. Pinned SceneFX
`37ccd723bef49e6891156ffafce8f549f01446cc` and wlroots
`d783533489e1f75d6886c2ab5c5960090ef268f8` were checked out in the
new lab. The three five-cycle runs all had `failure=null`, eight captures,
six real top-level right-button receipts and the same sampled pixel at P0,
P1 and P4. The standard control remains uninstrumented. Private `splith`
again gave 62 commits, 60 presents and 10 applied target mutations; private
Mosaic gave 84, 82 and 10. Both had zero target mutation between matched
commit and present. The two sequence-2 commits without observed presents
remain **UNEXPLAINED / NO PRESENT OBSERVED**. Timing values and exact event
stream hashes differed from the historical run, as normal scheduling can
vary; the measured counts and classification did not. Meson `mosaic-model`
passed in the new private build.
The independent lab's outside-Git raw evidence manifest uses the same
94-file scope and passed every hash check. Its SHA-256 is
`7d4e5c3860cc5ea3b63668441efa58bf31ef3197c6d22890cbaccee70543d41a`.
The independent matrix and event stream SHA-256 values are
`00a70483bd8ad1f30fd9e544f8253d516f4b571f4cb876dc2a960a474ee532dc`
and `71cab46cac1e41d1a08ab0d1460599074a1a0924c27e64af6c98e1afa6180ef9`.
**Probe verdict: INCONCLUSIVE** for the commit/present/input ordering
question. The sampled steady-state transitions and delivery passed, but the
discriminating temporal window was never exercised. **Architecture:
UNDECIDED.** No observed invariant requires an added applied-generation
contract, and these samples cannot prove existing ordering sufficient for
all clipping mutations. The smallest follow-up is a passive event-loop
phase trace with a client input request already pending during ordinary
repaint, preserving the native backend and five-attempt bound, to determine
whether fd dispatch can naturally precede the queued present. If that is
observed, execute a hit and client receipt in the same interval; only then
consider an owner-popup clip transition at the same output geometry.

View file

@ -0,0 +1,62 @@
# STEP20C commit/present/input ordering — EXPERIMENTAL
The [protocol](../../../docs/design/STEP20C_COMMIT_PRESENT_INPUT_ORDERING_PROBE.md)
was written before this fixture. Everything here is a source fixture for a
disposable private laboratory; no code enters the production compositor,
SceneFX ABI, package, cumulative Mosaic patch or daily session.
## Replay
```sh
prep=$(sh tooling/probes/step20c-commit-present-input/prepare.sh)
LAB=$(printf '%s\n' "$prep" | sed -n 's/^LAB=//p')
for mode in control private_standard private_mosaic; do
PYTHONDONTWRITEBYTECODE=1 python tooling/probes/step20c-commit-present-input/run.py \
"$LAB" "$mode" --cycles 5
done
PYTHONDONTWRITEBYTECODE=1 python tooling/probes/step20c-commit-present-input/analyze.py "$LAB"
```
The prepare chain rebuilds the pinned private SceneFX/Mosaic fixtures and
fetches wlroots 0.20.2 at commit
`d783533489e1f75d6886c2ab5c5960090ef268f8` inside the new lab.
Its two audited scene files are hash-checked by the Mutation Coverage
preparator. `surface-ordering.patch` adds a passive top-level surface commit
trace after wlroots applies the client input region. Existing private traces
provide build, commit, present, hit and seat notification. The client uses
ordinary `wl_surface.set_input_region` and `wl_surface.commit` requests.
The runner reuses the frozen headless runtime, explicit IPC socket, process
group cleanup and geometry checks. It defaults to five bounded cycles and
`ASI_MODE=0` (trace-only). Standard controls are unmodified SwayFX `splith`
and private SwayFX `splith`. Native output events are uninstrumented in the
unmodified control. Camera requests in the private Mosaic case supply
ordinary scene damage; no backend scheduling is changed. A missing natural
commit/mutation/present interleaving must be reported **INCONCLUSIVE**.
The runner is sequential: `command()` waits for IPC response,
`client_command()` waits for a client log confirmation, and `inject()` waits
for pointer confirmation. A Mosaic camera reply precedes submission of the
next input-region request. Those process round trips can exceed the native
commit-to-present interval. The executed callbacks and delivery remain valid;
the timing question requires a separately recorded nonblocking experiment.
For a complete evidence manifest outside Git, hash every regular file under
the three `runs/` directories plus the ten structured results, excluding
the two older source/artifact manifests and the new manifest itself. The
historical frozen lab's `raw-evidence-sha256.txt` contains 94 entries and
hashes to `9b74584936d44be5accb1f3882482fbd1eb85bc5c696b4551d45076b76ff4f8d`.
The independent replay manifest has the same scope and hashes to
`7d4e5c3860cc5ea3b63668441efa58bf31ef3197c6d22890cbaccee70543d41a`.
Set `CPI_SURFACE_TRACE=0` before `run.py` for a paired workload with only the
additional `ORDER_SURFACE_COMMIT` log site disabled. Keep these paired runs
in a separate lab from the three analysis runs; `analyze.py` deliberately
rejects ambiguous duplicate mode runs. The old private build, hit and
commit/present traces still execute in this comparison.
Raw logs, screenshots, generated runner, binaries, build trees, sockets and
structured JSON analysis remain in `LAB`, outside Git. Stop or terminate the
runner normally to remove its private runtime. A killed runner may leave its
lab and sockets for manual, targeted cleanup; never touch the live Sway
socket. The real rejected-commit backend path remains **NOT TESTED**.

View file

@ -0,0 +1,168 @@
"""Order executed server callbacks; keep client receipt on its own clock."""
import argparse
import hashlib
import json
from pathlib import Path
import re
from PIL import Image
ap = argparse.ArgumentParser()
ap.add_argument('lab', type=Path)
args = ap.parse_args()
lab = args.lab.resolve()
out = lab / 'commit-present-input-results'
out.mkdir(exist_ok=True)
markers = {
'ORDER_SURFACE_COMMIT': 'surface_commit',
'BI_STAGE': 'output_build',
'PI_OUTPUT_COMMIT': 'output_commit',
'PI_OUTPUT_PRESENT': 'output_present',
'ASI_HIT': 'scene_hit',
'ASI_DELIVERY': 'pointer_delivery',
}
def save(name, value):
(out / name).write_text(json.dumps(value, indent=2) + '\n')
events = []
matrix = {}
client_delivery = {}
pixels = {}
for mode in ('control', 'private_standard', 'private_mosaic'):
matches = sorted((lab / 'runs').glob(f'{mode}-ordering-asi0-*'))
if len(matches) != 1:
raise RuntimeError(f'expected one {mode} run, found {len(matches)}')
run = matches[0]
status = json.loads((run / 'exit-status.json').read_text())
if status['failure'] is not None:
raise RuntimeError(f'{mode} runner failed: {status["failure"]}')
for line_number, line in enumerate((run / 'compositor.log').read_text(errors='replace').splitlines()):
for marker, kind in markers.items():
if marker + ' ' not in line:
continue
fields = dict(re.findall(r'(\w+)=([^ ]+)', line.split(marker + ' ', 1)[1]))
if 'ns' not in fields:
continue
events.append({'mode': mode, 'kind': kind, 'line': line_number,
**fields, 'ns': int(fields['ns'])})
break
client = (run / 'client.log').read_text(errors='replace')
client_delivery[mode] = {
'run': str(run),
'top_button_presses': len(re.findall(
r'POINTER_BUTTON target=TOPLEVEL[^\n]*button=273 state=1', client)),
'region_empty_commands': client.count('COMMAND region_empty_top DONE'),
'region_full_commands': client.count('COMMAND region_full_top DONE'),
'popup_grabs': client.count('GRAB_REQUEST'),
'client_lines': [x for x in client.splitlines() if
'COMMAND region_' in x or
'POINTER_BUTTON target=TOPLEVEL' in x],
}
pixels[mode] = {}
for tag in ('ordering-P0-reference', 'ordering-P1-empty', 'ordering-P4-restored'):
pixels[mode][tag] = Image.open(run / f'left-{tag}.png').getpixel((342, 175))
matrix[mode] = {'run': str(run), 'runner_failure': None,
'duration_seconds': status['duration_seconds'],
'captures': status['captures'],
'pixels': pixels[mode], **{k: v for k, v in
client_delivery[mode].items() if k != 'client_lines'}}
# Same-process timestamps can be ordered. Equal timestamps retain log order.
events.sort(key=lambda e: (e['mode'], e['ns'], e['line']))
target_mutations = [e for e in events if e['kind'] == 'surface_commit'
and e.get('input_changed') == '1'
and e.get('width') == '420' and e.get('height') == '320']
with (out / 'event-order.jsonl').open('w') as dst:
for event in events:
dst.write(json.dumps(event) + '\n')
for name, kind in (
('surface-commits.json', 'surface_commit'),
('output-commits.json', 'output_commit'),
('presentations.json', 'output_present'),
):
save(name, [e for e in events if e['kind'] == kind])
save('input-regions.json', target_mutations)
save('client-delivery.json', client_delivery)
hits = []
for hit in (e for e in events if e['kind'] == 'scene_hit'):
prior_inputs = [e for e in target_mutations if e['mode'] == hit['mode']
and e['ns'] <= hit['ns']]
prior_commits = [e for e in events if e['mode'] == hit['mode']
and e['kind'] == 'output_commit'
and e.get('output') == hit.get('output') and e['ns'] <= hit['ns']]
prior_presents = [e for e in events if e['mode'] == hit['mode']
and e['kind'] == 'output_present'
and e.get('output') == hit.get('output') and e['ns'] <= hit['ns']]
next_presents = [e for e in events if e['mode'] == hit['mode']
and e['kind'] == 'output_present'
and e.get('output') == hit.get('output') and e['ns'] > hit['ns']]
hits.append({**hit,
'last_target_input': prior_inputs[-1] if prior_inputs else None,
'last_output_commit_seq': prior_commits[-1].get('commit_seq')
if prior_commits else None,
'last_present_seq': prior_presents[-1].get('commit_seq')
if prior_presents else None,
'next_present_seq': next_presents[0].get('commit_seq')
if next_presents else None,
'input_identity_inferred': hit.get('surface') == '(nil)'})
save('scene-hits.json', hits)
timelines = []
for mode in ('control', 'private_standard', 'private_mosaic'):
group = [e for e in events if e['mode'] == mode]
commits = [e for e in group if e['kind'] == 'output_commit']
presents = {(e.get('output'), e.get('commit_seq')): e for e in group
if e['kind'] == 'output_present'}
for commit in commits:
present = presents.get((commit.get('output'), commit.get('commit_seq')))
between = [e for e in group if present and
commit['ns'] < e['ns'] < present['ns'] and
e['kind'] in ('surface_commit', 'scene_hit', 'pointer_delivery')]
timelines.append({'mode': mode, 'output': commit.get('output'),
'commit_seq': commit.get('commit_seq'),
'commit_ns': commit['ns'],
'present_ns': present['ns'] if present else None,
'presented': present.get('presented') if present else None,
'gap_ns': present['ns'] - commit['ns'] if present else None,
'between': between,
'input_mutation_between': any(e in target_mutations
for e in between),
'hit_between': any(e['kind'] == 'scene_hit' for e in between)})
save('timelines.json', timelines)
for mode in matrix:
group = [e for e in events if e['mode'] == mode]
intervals = [x for x in timelines if x['mode'] == mode]
matrix[mode].update({
'surface_input_commits': sum(e in target_mutations for e in group),
'native_commits': sum(e['kind'] == 'output_commit' for e in group),
'native_presents': sum(e['kind'] == 'output_present' for e in group),
'commit_present_interleavings': sum(x['input_mutation_between'] for x in intervals),
'commit_mutation_hit_present': sum(x['input_mutation_between'] and
any(e['kind'] == 'scene_hit' for e in x['between']) for x in intervals),
'gap_ns_min': min((x['gap_ns'] for x in intervals if x['gap_ns'] is not None),
default=None),
'gap_ns_max': max((x['gap_ns'] for x in intervals if x['gap_ns'] is not None),
default=None),
})
save('matrix.json', matrix)
summary = {'modes': list(matrix), 'interleavings': sum(x['input_mutation_between']
for x in timelines), 'commit_mutation_hit_present': sum(
x['input_mutation_between'] and x['hit_between'] for x in timelines),
'clock_contract': 'server timestamps only; client order separate',
'p2': 'OBSERVED' if any(x['input_mutation_between'] for x in timelines)
else 'INCONCLUSIVE', 'rejected_backend_commit': 'NOT TESTED'}
save('summary.json', summary)
with (out / 'source-sha256.txt').open('w') as dst:
for p in (Path(__file__), Path(__file__).parent / 'run.py',
Path(__file__).parent / 'prepare.sh',
Path(__file__).parent / 'surface-ordering.patch'):
dst.write(f'{hashlib.sha256(p.read_bytes()).hexdigest()} {p.resolve()}\n')
with (out / 'artifact-sha256.txt').open('w') as dst:
for p in sorted(out.glob('*.json')) + [out / 'event-order.jsonl',
out / 'source-sha256.txt']:
dst.write(f'{hashlib.sha256(p.read_bytes()).hexdigest()} {p}\n')
print(json.dumps(summary))

View file

@ -0,0 +1,21 @@
#!/bin/sh
# EXPERIMENTAL: reconstruct only private sources in a new temporary lab.
set -eu
repo=$(CDPATH= cd -- "$(dirname -- "$0")/../../.." && pwd)
fixture="$repo/tooling/probes/step20c-commit-present-input"
log=$(mktemp /tmp/cpi-prepare-XXXXXXXX)
if ! sh "$repo/tooling/probes/step20c-mutation-coverage/prepare.sh" > "$log" 2>&1; then
printf 'PRIVATE_BUILD_FAILED log=%s\n' "$log" >&2
tail -40 "$log" >&2
exit 1
fi
lab=$(sed -n 's/^LAB=//p' "$log")
test -n "$lab" && test -d "$lab"
trap 'rm -rf -- "$lab"' 0
# CONTRACT: the additional listener only reads already-applied top-level
# surface state. No installed wlroots, compositor or backend is changed.
(cd "$lab/mosaic" && git apply --check "$fixture/surface-ordering.patch" &&
git apply "$fixture/surface-ordering.patch")
meson compile -C "$lab/mosaic-build" -j 8 >> "$log" 2>&1
trap - 0
printf 'LAB=%s\nBUILD_LOG=%s\n' "$lab" "$log"

View file

@ -0,0 +1,61 @@
"""Adapt the frozen headless runner for bounded input-only ordering attempts."""
import argparse
from pathlib import Path
import subprocess
import sys
ap = argparse.ArgumentParser()
ap.add_argument('lab', type=Path)
ap.add_argument('mode', choices=('control', 'private_standard', 'private_mosaic'))
ap.add_argument('--cycles', type=int, default=5)
args = ap.parse_args()
if not 1 <= args.cycles <= 5:
ap.error('at most five region cycles')
lab = args.lab.resolve()
repo = Path(__file__).resolve().parents[3]
original = (repo / 'tooling/probes/step20c-applied-state-input/run.py').read_text()
old = "choices=('cycles', 'buffer_cycles', 'identity', 'outside', 'hidden_click', 'toggle')"
assert original.count(old) == 1
adapted = original.replace(old, old[:-1] + ", 'ordering')")
anchor = " if args.kind == 'cycles':\n"
assert adapted.count(anchor) == 1
scenario = ''' if args.kind == 'ordering':
# CONTRACT: all surface input changes originate in the normal client;
# only private Mosaic gets a camera request to supply output damage.
o = owner()
camera = workspace()['mosaic']['rows'][0]['scroll_x'] if args.mode == 'private_mosaic' else 0
x = int(o['rect']['x'] - camera + o['window_rect']['x'] + 100)
y = int(o['rect']['y'] + o['window_rect']['y'] + 150)
snapshot('ordering-P0-reference')
inject('move', x, y)
inject('right', x, y)
wait(lambda: count('POINTER_BUTTON target=TOPLEVEL') >= 1)
record('ordering_point', global_point=[x, y], owner_id=o['id'])
for i in range(args.cycles):
record('ordering_cycle_start', cycle=i + 1)
if args.mode == 'private_mosaic':
command('layout mosaic scroll 200')
record('ordering_camera_request', cycle=i + 1, delta=200)
client_command('region_empty_top')
inject('move', x, y)
inject('right', x, y)
if i == 0:
snapshot('ordering-P1-empty')
if args.mode == 'private_mosaic':
command('layout mosaic scroll -200')
record('ordering_camera_request', cycle=i + 1, delta=-200)
before = count('POINTER_BUTTON target=TOPLEVEL')
client_command('region_full_top')
inject('move', x, y)
inject('right', x, y)
wait(lambda: count('POINTER_BUTTON target=TOPLEVEL') > before)
record('ordering_cycle_end', cycle=i + 1)
snapshot('ordering-P4-restored')
elif args.kind == 'cycles':
'''
adapted = adapted.replace(anchor, scenario)
generated = lab / 'commit-present-input-run-generated.py'
generated.write_text(adapted)
raise SystemExit(subprocess.call([sys.executable, str(generated), str(lab),
args.mode, 'ordering', '--cycles', str(args.cycles), '--asi-mode', '0',
'--max-seconds', '120', '--max-captures', '12']))

View file

@ -0,0 +1,44 @@
--- a/sway/desktop/xdg_shell.c
+++ b/sway/desktop/xdg_shell.c
@@ -1,6 +1,8 @@
#include <float.h>
+#include <inttypes.h>
#include <stdbool.h>
#include <stdlib.h>
+#include <time.h>
#include <wayland-server-core.h>
#include <wlr/types/wlr_xdg_shell.h>
#include <wlr/types/wlr_xdg_toplevel_tag_v1.h>
@@ -293,6 +295,32 @@
wl_container_of(listener, xdg_shell_view, commit);
struct sway_view *view = &xdg_shell_view->view;
struct wlr_xdg_surface *xdg_surface = view->wlr_xdg_toplevel->base;
+ // CONTRACT: opt-out permits a same-workload trace-overhead comparison;
+ // the switch changes only this log site, not the backend scheduler.
+ if (getenv("CPI_SURFACE_TRACE") == NULL ||
+ getenv("CPI_SURFACE_TRACE")[0] != '0') {
+ struct wlr_surface *surface = xdg_surface->surface;
+ struct timespec ordering_now;
+ clock_gettime(CLOCK_MONOTONIC, &ordering_now);
+ // WHY: this callback observes the applied Wayland input state, independently
+ // of output submission and presentation. Surface IDs expire at destruction.
+ // CONTRACT: record state only; never alter scheduling or current.input.
+ // INVARIANT: an input-only commit may change hit without a new client buffer.
+ const pixman_box32_t *input_bounds = pixman_region32_extents(&surface->input_region);
+ sway_log(SWAY_INFO, "ORDER_SURFACE_COMMIT ns=%" PRIu64
+ " surface=%p resource=%u seq=%" PRIu32 " committed=%" PRIu32
+ " input_changed=%d buffer_changed=%d input_rects=%d"
+ " input_bounds=%d,%d,%d,%d width=%d height=%d sample_accepts=%d",
+ (uint64_t)ordering_now.tv_sec * UINT64_C(1000000000) + ordering_now.tv_nsec,
+ (void *)surface, wl_resource_get_id(surface->resource), surface->current.seq,
+ surface->current.committed,
+ !!(surface->current.committed & WLR_SURFACE_STATE_INPUT_REGION),
+ !!(surface->current.committed & WLR_SURFACE_STATE_BUFFER),
+ pixman_region32_n_rects(&surface->input_region),
+ input_bounds->x1, input_bounds->y1, input_bounds->x2, input_bounds->y2,
+ surface->current.width, surface->current.height,
+ wlr_surface_point_accepts_input(surface, 100, 150));
+ }
if (xdg_surface->initial_commit) {
if (view->xdg_decoration != NULL) {