docs(mosaic): preserve event-loop pending input probe evidence

This commit is contained in:
fy59 2026-10-12 09:12:05 +02:00
parent c1f0e3621d
commit 542d9b9c94
11 changed files with 688 additions and 1 deletions

View file

@ -4,6 +4,7 @@
### STEP20C experimental evidence
- Preserved and independently replayed a bounded event-loop pending-input probe. The client flush and server application were observed after decoupled request submission, but no input mutation fell between a matching native headless commit and present in five attempts per private mode. The temporal case remains INCONCLUSIVE / NOT EXERCISED; this research line is closed for the tested backend and geometry. No production or package files changed.
- 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.

View file

@ -184,6 +184,17 @@ 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.
The follow-up private [event-loop pending-input probe](design/STEP20C_EVENT_LOOP_PENDING_INPUT_PROBE.md)
submitted ordinary client region requests without waiting for each camera or
rendering IPC response. All 20 target mutations applied in two instrumented
modes, but none occurred between a matching native headless output commit
and present. This is **INCONCLUSIVE** for the unexercised temporal case;
neither a Mosaic clipping violation nor the need for an additional applied
generation was demonstrated. Client flush, server application, submitted
buffer and later hit remain separate identities. The temporal line is closed
for this backend/matrix after five attempts per mode, while the overall
cross-time architectural question remains **UNDECIDED**.
## 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

@ -141,6 +141,15 @@ rejected backend commit cleanup and broader output/effect paths remain
**NOT TESTED**. Existing ordering sufficiency and any added-generation
contract remain **UNDECIDED**.
The [pending-input event-loop follow-up](design/STEP20C_EVENT_LOOP_PENDING_INPUT_PROBE.md)
removed the runner's intermediate response waits during five bounded cycles
per private mode. It confirmed ten positive client flushes and ten applied
target input commits per mode, yet zero natural commit→mutation→present
interleavings. The sought same-interval hit and grabbed-popup variant are
**NOT TESTED**. The result remains **INCONCLUSIVE** for that temporal window;
repeat attempts on the same headless matrix are closed. Effects/damage,
scaling and resize/migration remain open STEP20C gates.
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

@ -37,7 +37,9 @@ The [root README](../README.md) is the public summary. For a specific topic, use
[Mutation source fixture](../tooling/probes/step20c-mutation-coverage/README.md) replays the disposable matrix without production integration.
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.
22. [STEP20C event-loop pending-input probe](design/STEP20C_EVENT_LOOP_PENDING_INPUT_PROBE.md): bounded asynchronous client submission and native callback order; no natural commit-to-present input mutation, INCONCLUSIVE.
[Pending-input source fixture](../tooling/probes/step20c-event-loop-pending-input/README.md) replays the disposable matrix.
23. [STEP20A](STEP20A.md) and [STEP20B](STEP20B.md): historical evidence.
## Project
@ -82,6 +84,7 @@ The [root README](../README.md) is the public summary. For a specific topic, use
| [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 |
| [STEP20C event-loop pending-input probe](design/STEP20C_EVENT_LOOP_PENDING_INPUT_PROBE.md) | Experimental, independently replayed evidence | INCONCLUSIVE / NOT EXERCISED for natural commit→input mutation→present despite decoupled client submission; the temporal line is closed for this backend/matrix |
A proposal does not supersede accepted canonical contracts or assert implementation status. Its open feasibility items must be resolved before implementation.

View file

@ -134,6 +134,16 @@ 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**.
The bounded [event-loop pending-input probe](design/STEP20C_EVENT_LOOP_PENDING_INPUT_PROBE.md)
decoupled normal client submission from IPC/client acknowledgements, then
stopped after five attempts in each private mode. All 20 target mutations
applied, with no matching commit-to-present interleaving. Close this
particular temporal line as **INCONCLUSIVE / NOT EXERCISED** for the tested
headless backend and fixed geometry. Do not infer an extra generation
contract or production readiness. The next separate STEP20C gate should
address complete effects/damage, scaling, or resize/migration. STEP20C stays
**NEXT** and multi-output Mosaic **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,228 @@
# STEP20C event-loop pending input probe
**Status: EXPERIMENTAL / INCONCLUSIVE after bounded execution.** STEP20C remains
**NEXT** and multi-output Mosaic **NOT YET SUPPORTED**. This probe changes no
production compositor, package, UX or backend scheduling policy.
## Question and historical limit
The [ordering probe](STEP20C_COMMIT_PRESENT_INPUT_ORDERING_PROBE.md) observed
20 applied input-region changes, with none between a matching native output
commit and headless present. Its runner waited for each camera IPC response,
client confirmation and pointer confirmation. Those cross-process waits can
outlast a commit-to-present interval. The old observations remain valid for
their executed order. This probe asks whether a normal Wayland request,
submitted without all those intermediate runner waits, is naturally applied
in that interval.
## Dispatch and repaint model
The pinned wlroots 0.20.2 and installed Wayland server 1.26.0 source (tag
commit `87cc8a8728a923fc57938faa81ba0e74f34ecdc7`) show that
`wl_event_loop_dispatch` runs queued idles before polling, dispatches ready
epoll sources with timers first, then dispatches idles created by those
sources. Thus a client fd already in the same returned epoll batch can run
after a repaint timer and before its newly queued present idle; a request
arriving later may encounter the idle first. The event loop does not promise
that an already written client request wins over an idle callback. A Wayland client `flush` sends
queued bytes toward its connection; it does not run a server handler. Distinct
states are **written by client**, **possibly readable at server**,
**dispatched by server**, and **applied to effective input**. The latter is
observed in the server surface-commit callback, after wlroots has committed
the new `input_region`. No readiness is inferred solely from client logs.
SwayFX's `output_repaint_timer_handler` builds scene output state and calls
`wlr_output_commit_state`. The accepted commit signal is synchronous. The
headless backend queues `output_defer_present`; wlroots adds a Wayland idle
callback for the matching native `commit_seq`. A later callback emits
`present`, but destruction or allocation failure can prevent it. A normal
surface `set_input_region` changes pending state and `wl_surface.commit`
makes it effective. SceneFX's scene hit predicate reads that effective region
through `wlr_surface_point_accepts_input`; SwayFX `node_at_coords` and seat
notifications follow. Surface input-only commits need not render a new
output buffer. Output build/commit/present and later input hit are different
identities; a later hit may legally use newer input than submitted pixels.
## Identities and clocks
Keep native `(output, commit_seq)` for accepted commits and presents; never
predict a sequence. Keep surface resource ID, observed `current.seq`,
`input_changed`, effective bounds and client command identity separately.
Private compositor callbacks use `CLOCK_MONOTONIC` nanoseconds to establish
server order. Runner and client logs have separate process order unless
their clocks are checked; a runner timestamp is not treated as a server
handler timestamp. `OUTPUT_BUILD`, `OUTPUT_COMMIT`, `OUTPUT_PRESENT`,
`SURFACE_COMMIT`, `SCENE_HIT` and `POINTER_DELIVERY` are passive existing
private trace sites. The unmodified SwayFX control supplies client events
and sampled pixels, not native output event traces.
## Fixed experiment
Two headless outputs remain HEADLESS-1 `(-800,0)` and HEADLESS-2 `(0,0)`,
both 800×600, scale 1, normal transform. The same mapped T4 client performs
normal `wl_surface.set_input_region`, `wl_surface.commit` and client flush.
Compare standard SwayFX `splith`, private `splith` and private `mosaic`, with
no popup or grab. Runs own private mode-0700 runtime directories, discovered
IPC sockets, logs and process groups. Captures sample fixed pixels only.
* **E0:** verify outputs, mapping, full-region right button receipt, empty
region no target hit, and fixed pixels.
* **E1:** two sequential full/empty transitions using the historical waits.
Retain runner submissions/acknowledgements and server callback order.
* **E2:** submit a camera IPC request when applicable, then a client input
mutation without awaiting the IPC response or client confirmation. Await
completion only after both submissions. Preserve whether application was
before commit, between commit/present, after present, or without a new
output commit. Up to five transitions per private mode.
* **E3:** submit a normal client region request before an ordinary rendering
transition. Record client flush and server handler times; do not call the
request server-ready without proof.
* **E4:** attempt a same-interval hit only if E2 naturally creates the
commit/mutation/present order. Record actual scene hit and client delivery;
no destination may be inferred for a hit that did not run.
* **E5:** restore full input on the same surface and verify client receipt,
target hit and stable sampled pixels.
The runner offers a sequential control and an asynchronous submission mode.
It records each request and checks completion after submission; an IPC
request sent is not treated as an applied camera change. No sleep or special
idle callback is added to the compositor, headless backend, or Wayland server.
No recursive dispatch, source-priority change, direct `current.input` write,
forged `present` or output geometry change is allowed. At most five attempts
per experimental configuration; if none interleave, stop with
**INCONCLUSIVE / NOT EXERCISED**. The grabbed-popup camera variant is allowed
only after a natural E2 interleaving creates a Mosaic clipping question.
## Analysis and stop rule
Emit raw and structured `event-order.jsonl`, `client-requests.json`,
`surface-commits.json`, `output-builds.json`, `output-commits.json`,
`presentations.json`, `input-regions.json`, `scene-hits.json`,
`client-delivery.json`, `timelines.json`, `matrix.json` and `summary.json`
outside Git. Each timeline must state submitted request, observed server
application, last accepted commit, last observed and next present, actual
hit/input/client receipt, and which relations remain inferred or unknown.
Compare one minimal-trace run with a run disabling only the additional
surface log site: commit/present counts, duration, gap distribution, applied
surface mutations and button receipts. Functional agreement alone does not
prove timing neutrality. SHA-256 manifests cover all raw evidence used.
If five attempts produce no natural interleaving, close this temporal line
as **INCONCLUSIVE** for this backend and matrix. Existing ordering may be
sufficient for sampled states, but a global architectural sufficiency claim
would require more evidence. An added Mosaic transaction contract requires
a concrete required clipping/hit invariant and a real counterexample, not
merely a later input state than an already committed output buffer.
## Executed campaign and result
The source fixture rebuilt pinned private SceneFX, Mosaic and wlroots in
`/tmp/swayfx-mosaic-nested-grab-6C8kpcQB/`. The first sequential run failed
because the generated runner lacked its `sequential` constant. A later
asynchronous control run failed because an untargeted `border pixel 3` IPC
command ran after a click on an empty input region had changed focus. Both
failed runs are retained under `failed-runs/`; neither supplies temporal
evidence. The corrected fixture then ran two sequential region cycles per
mode, followed by five asynchronous cycles per mode. The standard control
remained uninstrumented. The separate trace-off comparison used two
sequential cycles per private mode.
After the five-attempt timing bound, a separate provenance check cloned
published `c1f0e3621ddfba29fde9660d044d595c00ee8610` into
`/tmp/step20c-ordering-clone-final2/`, overlaid only this uncommitted
fixture, and rebuilt both the historical and pending-input private sources
without rerunning timing attempts. Recursive source comparisons found no
differences between either clone-built private Mosaic/SceneFX tree and its
corresponding measured lab. The copied T4 client source SHA-256 matched
`2f06888fbb77d541545aa54280567b8571ef5001e6880686bc9667b4091eddeb`.
The clone-built private Meson model test passed. This establishes source
equivalence of the measured builds to reconstruction from a Git clone; it
does not make the runs byte-identical or add temporal observations.
For the evidence freeze, another fresh clone at
`c1f0e3621ddfba29fde9660d044d595c00ee8610` independently rebuilt the
same pinned chain in `/tmp/swayfx-mosaic-nested-grab-9wm8BpSJ/`. One private
Mosaic sequential cycle yielded 45 accepted output commits, 43 observed
presents and two applied target input mutations; one asynchronous cycle
yielded 48/46 and two mutations. Both runs had two positive client flushes,
two actual top-level right-button receipts, eight captures, `failure=null`
and zero commit-to-mutation-to-present intervals. The asynchronous runner
recorded two distinct client submissions before their confirmations. This
small replay verifies fixture behavior, not a new scheduling conclusion.
Its outside-Git manifest covers all 56 regular raw run files, passed every
entry check and has SHA-256
`f8b54f2ef7f9caca3837e956341ddb5a62f261d0115840d6fd8b0b3540c1e8b9`.
Every successful mode had two correctly placed 800×600 outputs, the mapped
420×320 toplevel, eight captures, six received right-button presses (initial
full region plus five restorations), five empty and five full commands in
the asynchronous campaign, and zero popup grabs. The sampled left-output
pixel `(342,175)` was `(35,68,204)` at E0, E2 empty and E5 restored in all
three modes. The control verifies pixels and client receipt; it has no
native server commit/present trace.
| Asynchronous mode | Accepted native commits | Observed presents | Applied target input mutations | Client flushes | Natural commit→mutation→present |
| --- | ---: | ---: | ---: | ---: | ---: |
| Standard SwayFX `splith` | Uninstrumented | Uninstrumented | Uninstrumented | 10 | Unobservable |
| Private `splith` | 82 | 80 | 10 | 10 | 0 |
| Private `mosaic` | 83 | 81 | 10 | 10 | 0 |
All client flush calls reported positive byte counts (40 or 64), and the
server applied the 10 corresponding input-only mutations in each private
mode. Client and server both used `CLOCK_MONOTONIC`; paired flush-to-handler
gaps were 30,176–113,331 ns in private `splith` and 15,058–111,367 ns in
private Mosaic. In E2, the runner submitted the IPC request then the client
request before waiting for either completion. In E3 it submitted the client
request before the IPC rendering request. The server traces nevertheless
place every applied target mutation outside a matching accepted
commit-to-present interval. A submitted request is still not proof that its
fd was ready in a particular epoll batch. The native counts differ from the
historical 62/60 and 84/82 because this runner also changes border state in
standard layout and overlaps normal camera requests in Mosaic. Both private
modes again have two initial sequence-2 commits with **NO PRESENT OBSERVED**;
their cause remains **UNEXPLAINED**.
E0 passed the sampled geometry, client and pixel controls. E1 passed two
sequential transitions per mode. E2 submitted five bounded overlapping
cycles per mode and found zero natural interleavings. E3 proved the client
submission and subsequent normal server application, without proving fd
readiness before repaint. E4 was **NOT TESTED**, because no E2 interval
existed in which to attempt a hit. E5 restored full input on the same
surface; actual button receipt resumed. Private scene hits after effective
empty input did not target the toplevel. Some transient full-state private
standard hits were nil during ordinary border changes, but later full-state
hits and the client deliveries succeeded; no distinct Mosaic clipping
failure was demonstrated. There was no popup/grab variant.
Trace-on two-cycle sequential controls measured private `splith` 47/45
commit/present, 4 applied target mutations, 3 buttons, 4.808 s and a
76,762 ns median commit→present gap; trace-off measured 48/46,
4 client flushes, 3 buttons, 4.799 s and 96,800.5 ns. Private Mosaic gave
59/57, 4 mutations, 3 buttons, 4.826 s and 86,591 ns trace-on; trace-off
gave 60/58, 4 client flushes, 3 buttons, 4.837 s and 86,020 ns. Applied
mutation counts cannot be observed directly with the new log site disabled;
the client and hit behavior still pass. Native counts and gap distributions
vary, so perfect timing neutrality is not claimed.
The lab contains `event-order.jsonl`, `client-requests.json`,
`surface-commits.json`, `output-builds.json`, `output-commits.json`,
`presentations.json`, `input-regions.json`, `scene-hits.json`,
`client-delivery.json`, `timelines.json`, `matrix.json` and `summary.json`.
`timelines.json` records each submitted client flush, applied server handler,
surrounding native events and executed later hit; per-hit client delivery is
left unknown where the client log has order but no same-clock event stamp.
The outside-Git `raw-evidence-sha256.txt` covers all 280 regular files in
`runs/`, `sequential-runs/`, `trace-off-runs/`, `failed-runs/` and structured
results, excluding the two source/derived manifest files and itself. All
entries passed SHA-256 verification. The manifest hash is
`6eaeee3fcd77c4f2b7682924d2ff8ebdd5b306acab715bead68ce9e655f5490a`.
Builds, binaries, generated protocol source and sockets are outside this
raw-evidence scope and outside Git.
**Verdict: INCONCLUSIVE / NOT EXERCISED** for the sought temporal order.
The request/confirmation decoupling worked, and applied mutations and later
hit/client behavior are real, but the discriminating interval was still not
exercised. No concrete Mosaic clipping invariant was violated and no added
transaction contract is established. **Architecture: UNDECIDED.** Stop this
temporal line for the headless backend and fixed matrix; STEP20C can next
examine effects/damage, scaling, or resize/migration as separate gates.

View file

@ -0,0 +1,40 @@
# STEP20C pending-input event-loop fixture — EXPERIMENTAL
See the [protocol](../../../docs/design/STEP20C_EVENT_LOOP_PENDING_INPUT_PROBE.md).
This fixture rebuilds the pinned private compositor and the same T4 client
in a disposable laboratory. `client-flush.patch` adds a client-side
`wl_display_flush` result and monotonic timestamp after each normal
input-region commit. It changes no server handler or scheduler. The existing
private commit, present, surface, hit and delivery traces remain in use.
```sh
prep=$(sh tooling/probes/step20c-event-loop-pending-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-event-loop-pending-input/run.py \
"$LAB" "$mode" --cycles 5
done
PYTHONDONTWRITEBYTECODE=1 python tooling/probes/step20c-event-loop-pending-input/analyze.py "$LAB"
```
The default runner submits an ordinary IPC rendering/camera request and a
normal client region request before waiting for both results. `--sequential`
keeps the earlier wait-after-each-operation control. `--surface-trace 0`
disables only the additional private surface log site for a limited timing
comparison. The control SwayFX is not instrumented. Up to five cycles per
mode are allowed. The runner logs submission and completion separately;
the client logs its flush result, and server traces determine application.
The lab contains runtime-private sockets, process-owned logs, captures,
generated runner, builds and binaries; none belongs in Git. A manifest
should hash all regular files under the run and structured result directories,
excluding the manifest itself. Never target the daily Sway IPC socket. Stop
after five unsuccessful interleaving attempts; report
`INCONCLUSIVE / NOT EXERCISED` and leave the popup variant untested.
The frozen five-cycle campaign is described in the protocol. A later
independent clone replay used one sequential and one asynchronous private
Mosaic cycle only. It confirmed two applied mutations per run, native
commit/present traces and restored client delivery without reopening the
timing search; its 56-file raw manifest hash is
`f8b54f2ef7f9caca3837e956341ddb5a62f261d0115840d6fd8b0b3540c1e8b9`.

View file

@ -0,0 +1,230 @@
"""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 / 'event-loop-pending-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}-pending-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 ('pending-P0-reference', 'pending-P1-empty', 'pending-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('output-builds.json', [e for e in events if e['kind'] == 'output_build'])
requests = {}
for mode in ('control', 'private_standard', 'private_mosaic'):
run_path = Path(matrix[mode]['run'])
requests[mode] = [json.loads(line) for line in (run_path / 'events.jsonl').read_text().splitlines()
if 'submitted' in json.loads(line)['kind'] or 'completed' in json.loads(line)['kind']]
requests[mode] += [line for line in (run_path / 'client.log').read_text().splitlines()
if 'CLIENT_FLUSH' in line]
save('client-requests.json', requests)
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)})
request_timelines = []
for mode in ('control', 'private_standard', 'private_mosaic'):
run_path = Path(matrix[mode]['run'])
flushes = [dict(re.findall(r'(\w+)=([^ ]+)', line)) for line in
(run_path / 'client.log').read_text().splitlines()
if 'CLIENT_FLUSH ' in line]
applied = [e for e in target_mutations if e['mode'] == mode]
if mode != 'control' and len(flushes) != len(applied):
raise RuntimeError(f'{mode}: flush/application count mismatch')
for index, flush in enumerate(flushes):
mutation = applied[index] if mode != 'control' else None
if mutation and ((flush['region'] == 'empty') !=
(mutation.get('sample_accepts') == '0')):
raise RuntimeError(f'{mode}: input state order mismatch')
ns = mutation['ns'] if mutation else None
mode_events = [e for e in events if e['mode'] == mode]
prior_commit = [e for e in mode_events if e['kind'] == 'output_commit'
and ns is not None and e['ns'] <= ns]
prior_present = [e for e in mode_events if e['kind'] == 'output_present'
and ns is not None and e['ns'] <= ns]
next_present = [e for e in mode_events if e['kind'] == 'output_present'
and ns is not None and e['ns'] > ns]
next_hit = [e for e in mode_events if e['kind'] == 'scene_hit'
and ns is not None and e['ns'] > ns]
enclosing = [t for t in timelines if t['mode'] == mode and ns is not None
and t['commit_ns'] < ns and t['present_ns'] is not None
and ns < t['present_ns']]
request_timelines.append({
'mode': mode, 'index': index + 1, 'region': flush['region'],
'client_flush_ns': int(flush['ns']), 'client_flush_rc': int(flush['rc']),
'server_apply_ns': ns, 'server_surface_seq': mutation.get('seq') if mutation else None,
'flush_to_apply_ns': ns - int(flush['ns']) if ns else None,
'last_accepted_commit': prior_commit[-1] if prior_commit else None,
'last_observed_present': prior_present[-1] if prior_present else None,
'next_observed_present': next_present[0] if next_present else None,
'enclosing_commit_present': enclosing,
'next_executed_hit': next_hit[0] if next_hit else None,
'effective_target_input_at_hit': next((e.get('sample_accepts') for e in
reversed(applied) if next_hit and e['ns'] <= next_hit[0]['ns']), None),
'client_delivery_for_this_hit': 'unknown; client log order only',
'proven': 'client flush and server callback use CLOCK_MONOTONIC' if mutation else
'client flush only; standard server uninstrumented',
'unknown': ['server fd readiness', 'per-hit client delivery clock']})
save('timelines.json', {'output_intervals': timelines,
'request_applications': request_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': 'client flush and server traces use CLOCK_MONOTONIC; '
'client delivery log has order without per-event timestamp',
'e2': 'OBSERVED' if any(x['input_mutation_between'] for x in timelines)
else 'INCONCLUSIVE / NOT EXERCISED', 'rejected_backend_commit': 'NOT TESTED'}
summary['e0'] = 'PASS sampled geometry, client delivery and pixels'
summary['e1'] = 'PASS two sequential cycles in each mode; separate sequential-runs/'
summary['e3'] = 'PASS client submission preceded ordinary IPC request in five full transitions per mode'
summary['e4'] = 'NOT TESTED: E2 produced no natural interleaving'
summary['e5'] = 'PASS same-surface full-region restoration and six total right-button receipts per mode'
summary['popup_variant'] = 'NOT TESTED: E2 prerequisite absent'
summary['attempts_per_mode'] = 5
summary['matrix'] = matrix
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 / 'client-flush.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,27 @@
--- a/nested-client.c
+++ b/nested-client.c
@@ -8,6 +8,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
+#include <time.h>
#include <sys/mman.h>
#include <unistd.h>
#include <wayland-client.h>
@@ -268,6 +269,16 @@
wl_surface_set_input_region(top.wl, region);
wl_surface_commit(top.wl);
wl_region_destroy(region);
+ /* CONTRACT: log an actual client flush separately from the
+ * later server handler; a successful write is not application. */
+ int flushed = wl_display_flush(display);
+ struct timespec sent_at;
+ clock_gettime(CLOCK_MONOTONIC, &sent_at);
+ fprintf(stderr, "CLIENT_FLUSH ns=%lld region=%s rc=%d wl=%u\n",
+ (long long)sent_at.tv_sec * 1000000000LL + sent_at.tv_nsec,
+ empty ? "empty" : "full", flushed,
+ wl_proxy_get_id((struct wl_proxy *)top.wl));
+ if (flushed < 0 && errno != EAGAIN) running = false;
fprintf(stderr,"COMMAND region_%s_top DONE wl=%u\n",
empty ? "empty" : "full",
wl_proxy_get_id((struct wl_proxy *)top.wl));

View file

@ -0,0 +1,22 @@
#!/bin/sh
# EXPERIMENTAL: rebuild private sources only; no installed component is touched.
set -eu
repo=$(CDPATH= cd -- "$(dirname -- "$0")/../../.." && pwd)
fixture="$repo/tooling/probes/step20c-event-loop-pending-input"
log=$(mktemp /tmp/epi-prepare-XXXXXXXX)
if ! sh "$repo/tooling/probes/step20c-commit-present-input/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: only the disposable client reports its own flush result/time.
(cd "$lab" && git apply --check "$fixture/client-flush.patch" &&
git apply "$fixture/client-flush.patch")
cc -std=c17 -Wall -Wextra -Werror -Wno-unused-parameter -I"$lab" \
"$lab/nested-client.c" "$lab/xdg-shell-protocol.c" \
-o "$lab/nested-client" $(pkg-config --cflags --libs wayland-client)
trap - 0
printf 'LAB=%s\nBUILD_LOG=%s\n' "$lab" "$log"

View file

@ -0,0 +1,106 @@
"""Bounded private headless runner with distinct submission and completion."""
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)
ap.add_argument('--sequential', action='store_true')
ap.add_argument('--surface-trace', choices=('0', '1'), default='1')
args = ap.parse_args()
if not 1 <= args.cycles <= 5:
ap.error('at most five timing attempts')
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] + ", 'pending')")
anchor = " if args.kind == 'cycles':\n"
assert adapted.count(anchor) == 1
scenario = ''' if args.kind == 'pending':
# CONTRACT: requests retain normal IPC/Wayland paths. A submission
# record never claims server application; native traces decide order.
def submit_ipc(value):
record('ipc_submitted', value=value)
return subprocess.Popen(['swaymsg', '-s', str(sock), value], env=env,
text=True, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
def complete_ipc(proc, value):
out, err = proc.communicate(timeout=8)
record('ipc_completed', value=value, exit_code=proc.returncode,
output=out, error=err)
result = json.loads(out)
if proc.returncode or not isinstance(result, list) or not result or not all(
x.get('success') for x in result):
raise RuntimeError(f'IPC command rejected: {value}: {out} {err}')
def submit_client(value):
before = count(f'COMMAND {value} DONE')
record('client_submitted', value=value)
client.stdin.write(value + '\\n')
client.stdin.flush()
return before
def complete_client(value, before):
wait(lambda: count(f'COMMAND {value} DONE') > before)
record('client_completed', value=value)
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('pending-P0-reference')
inject('move', x, y)
inject('right', x, y)
wait(lambda: count('POINTER_BUTTON target=TOPLEVEL') >= 1)
record('pending_point', global_point=[x, y], owner_id=o['id'])
for i in range(args.cycles):
record('pending_cycle_start', cycle=i + 1)
if args.sequential:
if args.mode == 'private_mosaic': command('layout mosaic scroll 200')
client_command('region_empty_top')
else:
# E2: launch ordinary camera activity and submit the client
# request before waiting for either acknowledgement.
value = ('layout mosaic scroll 200' if args.mode == 'private_mosaic'
else '[app_id="org.mosaic.NestedGrabProbe"] border pixel 2')
proc = submit_ipc(value)
before = submit_client('region_empty_top')
complete_ipc(proc, value)
complete_client('region_empty_top', before)
inject('move', x, y)
inject('right', x, y)
if i == 0: snapshot('pending-P1-empty')
if args.sequential:
if args.mode == 'private_mosaic': command('layout mosaic scroll -200')
client_command('region_full_top')
else:
# E3: the client request is submitted first. This does not
# assert that the server fd is ready before repaint.
before = submit_client('region_full_top')
value = ('layout mosaic scroll -200' if args.mode == 'private_mosaic'
else '[app_id="org.mosaic.NestedGrabProbe"] border pixel 3')
proc = submit_ipc(value)
complete_client('region_full_top', before)
complete_ipc(proc, value)
prior = count('POINTER_BUTTON target=TOPLEVEL')
inject('move', x, y)
inject('right', x, y)
wait(lambda: count('POINTER_BUTTON target=TOPLEVEL') > prior)
record('pending_cycle_end', cycle=i + 1)
snapshot('pending-P4-restored')
elif args.kind == 'cycles':
'''
adapted = adapted.replace(anchor, scenario.replace('args.sequential', str(args.sequential)))
generated = lab / 'event-loop-pending-input-run-generated.py'
generated.write_text(adapted)
env = __import__('os').environ.copy()
env['CPI_SURFACE_TRACE'] = args.surface_trace
raise SystemExit(subprocess.call([sys.executable, str(generated), str(lab),
args.mode, 'pending', '--cycles', str(args.cycles), '--asi-mode', '0',
'--max-seconds', '120', '--max-captures', '12'], env=env))