docs(mosaic): preserve STEP20C mutation coverage evidence
This commit is contained in:
parent
1f59cb433b
commit
6fc849d172
13 changed files with 761 additions and 3 deletions
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
### STEP20C experimental evidence
|
||||
|
||||
- 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.
|
||||
- Added a source-only private presentation-identity probe at fixed negative-origin headless geometry. Native wlroots output present events paired with 470 accepted commits by output and `commit_seq`; applied scene/clip and sampled input agreed at the target states, but exact commit-buffer pixels remain unproven. STEP20C stays NEXT and production multi-output Mosaic unsupported.
|
||||
|
|
|
|||
|
|
@ -157,6 +157,20 @@ mutation. Client delivery under a real XDG grab was checked separately
|
|||
from the scene hit. This **PARTIALLY_FEASIBLE** result does not change
|
||||
production scene, transaction, input or SceneFX ABI contracts.
|
||||
|
||||
The [mutation coverage probe](design/STEP20C_MUTATION_COVERAGE_PROBE.md)
|
||||
audited compositor, private SceneFX and wlroots 0.20.2 mutation owners and
|
||||
ran a bounded trace-only headless matrix. Camera/clip and popup lifecycle
|
||||
observations agreed with scene hits and actual client events. A same-size
|
||||
client recolor changed pixels while applied geometry and the partial epoch
|
||||
stayed fixed; an empty surface input region changed the executed hit at the
|
||||
same point and epoch without changing sampled pixels. Thus
|
||||
`asi_applied_epoch` cannot identify all hit-relevant mutations. A native
|
||||
output `(output, commit_seq)` records a submission, not the later input
|
||||
state. Distinct geometry/hit, content and presentation identities are
|
||||
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.
|
||||
|
||||
## 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.
|
||||
|
|
|
|||
|
|
@ -117,6 +117,18 @@ Readback remains intrusive, and neither an atomic scene/render/input
|
|||
generation nor physical presentation is proven. Production multi-output
|
||||
Mosaic remains **NOT YET SUPPORTED**.
|
||||
|
||||
The [mutation coverage probe](design/STEP20C_MUTATION_COVERAGE_PROBE.md)
|
||||
is **PARTIALLY_FEASIBLE** overall. In the fixed headless matrix, the
|
||||
partial epoch missed a real input-region change that removed a scene hit,
|
||||
while a client recolor changed pixels without a geometry change. Normal
|
||||
private output destruction released one intentionally retained buffer
|
||||
reference exactly once; rejected-commit cleanup passed only a synthetic
|
||||
model and remains **NOT TESTED** on a real backend. Subsurfaces, effects,
|
||||
animations, output migration and the complete cross-time build/present/hit
|
||||
boundary remain unqualified. The architecture decision is **UNDECIDED**;
|
||||
physical presentation and production multi-output Mosaic remain
|
||||
**NOT YET SUPPORTED**.
|
||||
|
||||
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**.
|
||||
|
|
|
|||
|
|
@ -33,7 +33,9 @@ The [root README](../README.md) is the public summary. For a specific topic, use
|
|||
[Buffer source fixture](../tooling/probes/step20c-buffer-identity/README.md) reconstructs the disposable experiment; it is not production code.
|
||||
19. [STEP20C applied state/input probe](design/STEP20C_APPLIED_STATE_INPUT_PROBE.md): private per-output buffer-reference correction and partial applied-scene epoch tracing through render and pointer paths; no atomic generation is established.
|
||||
[Applied-state source fixture](../tooling/probes/step20c-applied-state-input/README.md) rebuilds the disposable experiment.
|
||||
20. [STEP20A](STEP20A.md) and [STEP20B](STEP20B.md): historical evidence.
|
||||
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.
|
||||
|
||||
## Project
|
||||
|
||||
|
|
@ -76,6 +78,7 @@ The [root README](../README.md) is the public summary. For a specific topic, use
|
|||
| [STEP20C presentation identity probe](design/STEP20C_PRESENTATION_IDENTITY_PROBE.md) | Experimental evidence | PARTIALLY_FEASIBLE; native headless events pair with commits, but buffer pixels and per-frame input identity remain unproven |
|
||||
| [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 |
|
||||
|
||||
A proposal does not supersede accepted canonical contracts or assert implementation status. Its open feasibility items must be resolved before implementation.
|
||||
|
||||
|
|
|
|||
|
|
@ -106,10 +106,23 @@ while a partial experimental epoch and real pointer traces exposed
|
|||
applied state at build and hit. Client delivery under grabs remained a
|
||||
separate observation. Incomplete mutation coverage, untested error paths
|
||||
and intrusive readback leave the temporal-consistency gate open. The
|
||||
smallest next experiment must close the mutation boundary and use a
|
||||
lower-impact observation before any production STEP20C decision. STEP20C
|
||||
next experiment was to map the mutation boundary with lower-impact
|
||||
observation before any production STEP20C decision. STEP20C
|
||||
stays **NEXT**; multi-output remains **NOT YET SUPPORTED**.
|
||||
|
||||
The [mutation coverage probe](design/STEP20C_MUTATION_COVERAGE_PROBE.md)
|
||||
is **PARTIALLY_FEASIBLE** overall. A bounded trace-only matrix separated
|
||||
camera/clip, popup hierarchy, same-size client content and surface input
|
||||
region changes. The input-region test changed the actual scene hit while
|
||||
`asi_applied_epoch` was unchanged, so that counter is not a complete
|
||||
geometry/hit identity. Output destruction released one held reference in
|
||||
the real private compositor; rejected-commit cleanup is still untested on
|
||||
a backend. Whether SwayFX's existing event-loop ordering suffices for
|
||||
build/present/input coherence is **UNDECIDED**. The smallest next gate is
|
||||
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.
|
||||
|
||||
## 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.
|
||||
|
|
|
|||
297
docs/design/STEP20C_MUTATION_COVERAGE_PROBE.md
Normal file
297
docs/design/STEP20C_MUTATION_COVERAGE_PROBE.md
Normal file
|
|
@ -0,0 +1,297 @@
|
|||
# STEP20C mutation coverage and lifetime probe
|
||||
|
||||
**Status: EXPERIMENTAL / PARTIALLY_FEASIBLE.** The protocol preceded the
|
||||
private tests and the findings follow below. STEP20C remains **NEXT** and production
|
||||
multi-output Mosaic remains **NOT YET SUPPORTED**. This probe uses disposable
|
||||
private sources and headless sockets; it changes no production compositor,
|
||||
SceneFX ABI, package, user interaction or installed dependency.
|
||||
|
||||
## Question and observation boundary
|
||||
|
||||
Determine which applied mutations can change pixels, scene hit geometry,
|
||||
pointer delivery or only metadata between output-state construction and an
|
||||
executed input test. The existing `asi_applied_epoch` increments at selected
|
||||
transaction and popup arrangements only. Equality of this partial counter
|
||||
cannot establish a complete scene generation. Native output `commit_seq`
|
||||
and headless `present` identify an output submission, not the scene used by
|
||||
a later pointer event or a physically displayed frame.
|
||||
|
||||
The candidate minimal boundary is workspace/owner identity, applied scene
|
||||
position, Mosaic tiled and owner-derived popup clips, surface hierarchy,
|
||||
visibility, order, hit geometry and spatial transform. Client buffer content
|
||||
is tracked separately from geometry. Output presentation identity is tracked
|
||||
per output. XDG grab state controls delivery after a scene hit and must be
|
||||
reported separately. These are hypotheses for comparison, not a new
|
||||
production generation contract.
|
||||
|
||||
## Source and owner audit plan
|
||||
|
||||
Inspect pinned Mosaic/SwayFX `transaction.c`, `output.c`, `xdg_shell.c`,
|
||||
`cursor.c`, `seat.c`, `arrange.c` and `mosaic.c`; private SceneFX clipping;
|
||||
and the wlroots 0.20.2 scene/output/surface paths. Trace callers and
|
||||
callbacks for node position, enabled state, reparent/order, creation and
|
||||
destruction, clips, surface map/unmap, subsurfaces, input region, buffer
|
||||
commits, scale/transform, damage, effects, animations, workspace/output
|
||||
changes, focus and XDG grabs. Record the owner of each mutation and whether
|
||||
it is immediate or transaction applied. A text search alone is insufficient.
|
||||
|
||||
The structured inventory in the private laboratory records source function,
|
||||
object, pixel/hit/damage effect, transaction timing, current epoch coverage,
|
||||
trigger and verdict. Every untriggered path stays **NOT TESTED**. Distinguish
|
||||
pixel only, hit only, both, grab delivery only and metadata only. Preserve
|
||||
monotonic timestamps, applied snapshots, native commits/presents, executed
|
||||
hits and client events without assigning a fictional frame ID to input.
|
||||
|
||||
## Bounded experiments and stop rules
|
||||
|
||||
Use HEADLESS-1 `(-800,0)` and HEADLESS-2 `(0,0)`, both 800×600 at scale 1
|
||||
and normal transform. Run an unmodified SwayFX `splith` control, private
|
||||
`splith`, and private `mosaic` with trace-only instrumentation first.
|
||||
|
||||
* T1: camera 400 → 1280 → 400; correlate actual scene/clip mutation sites,
|
||||
partial epoch, output state, commit and present.
|
||||
* T2: create grabbed A then B; close B while A remains, then close A;
|
||||
correlate hierarchy and hit changes, including uncovered sites.
|
||||
* T3: commit changed client pixels at fixed intended size/position; establish
|
||||
a visible new buffer and corresponding native commit/present, then compare
|
||||
geometry and partial epoch.
|
||||
* T4: choose one reproducible input-region, subsurface, order or geometry
|
||||
mutation from the source audit; compare scene hit and client delivery.
|
||||
* T5: compare ordinary `splith` behavior in unmodified and private builds.
|
||||
|
||||
Use targeted captures only. Read a true output buffer only if a specific
|
||||
remaining question requires it; time and bound any such copy per output.
|
||||
`grim` is an independent capture, not a commit-buffer identity proof.
|
||||
|
||||
Audit rejected-commit and output-destroy lock cleanup first in source. A
|
||||
safe private headless trigger may be used only if the backend and callback
|
||||
lifetime permit it. Otherwise preserve a clearly synthetic branch test or
|
||||
mark it **NOT TESTED**; no invalid state or unsafe output destruction is
|
||||
allowed. Count each extra lock/unlock and retain exit status and logs.
|
||||
|
||||
Separate verdicts for mapping, runtime mutation scenarios and cleanup.
|
||||
**FEASIBLE** covers only triggered bounded paths with matching geometry and
|
||||
input and no blocking regression. **PARTIALLY_FEASIBLE** retains useful
|
||||
evidence with important unqualified paths. **INCONCLUSIVE** means reliable
|
||||
observation was unavailable. **BLOCKED** requires a reproduced mismatch,
|
||||
unsafe lifetime, crash or blocking regression. The final architecture
|
||||
answer is **UNDECIDED** unless the tested and source-audited boundary can
|
||||
actually discriminate native SwayFX consistency from a required additional
|
||||
transaction/generation contract.
|
||||
|
||||
## Source inventory and ownership findings
|
||||
|
||||
The audit used private Mosaic derived from published `1f59cb433bcdad006c23df10496cb4709551c624`
|
||||
and the frozen patch chain, SceneFX 0.5 from
|
||||
`37ccd723bef49e6891156ffafce8f549f01446cc`, and wlroots 0.20.2
|
||||
source tag `0.20.2` (`d783533489e1f75d6886c2ab5c5960090ef268f8`).
|
||||
The [structured inventory](../../tooling/probes/step20c-mutation-coverage/inventory.json)
|
||||
is copied into the private lab with the scenario matrix. It distinguishes
|
||||
observed paths from untriggered callers; it is not a claim that all scene
|
||||
mutations have been exercised.
|
||||
|
||||
| Class | Actual owner and path | Effect and current coverage |
|
||||
| --- | --- | --- |
|
||||
| Camera, tiled clip, owner popup clip | `sway/desktop/transaction.c:arrange_output`, `arrange_popups`, `probe_clip_popup`; `sway/tree/mosaic.c` supplies logical camera | Pixels and hit. Transaction arrangement is marked; direct popup creation can call arrangement independently. T1 sampled. |
|
||||
| XDG hierarchy and map lifecycle | `sway/desktop/xdg_shell.c:popup_create`, `handle_new_popup`, popup destroy; wlroots `types/scene/subsurface_tree.c` map/unmap/new-subsurface listeners | Pixels and hit. Creation/destruction themselves have no `asi_mark`; incidental arrangements may mark later. T2 A/B lifecycle sampled; subsurface mutation open. |
|
||||
| Generic scene node and clip | SceneFX `types/scene/wlr_scene.c:wlr_scene_node_set_position`, `set_enabled`, `reparent`, `place_above/below`, `wlr_scene_tree_set_clip`, `scene_node_update` | Pixels, hit and conditional damage. The current counter covers only selected callers. Uninstrumented effects and SceneFX callbacks remain. |
|
||||
| Client content | wlroots `types/scene/surface.c:handle_scene_surface_surface_commit`, `surface_reconfigure`; SceneFX `wlr_scene_buffer_set_buffer_with_options` | Pixels/damage; hit geometry only if dimensions, crop, transform or related state change. T3 fixed-size recolor sampled with no geometry change. |
|
||||
| Client input region | wlroots `types/scene/surface.c:scene_buffer_point_accepts_input` → `wlr_surface_point_accepts_input`; SceneFX `scene_node_at_iterator` | Hit only for fixed pixels. T4 changed hit with the same partial epoch and unchanged sampled pixel. |
|
||||
| Effects and decorations | `sway/desktop/output.c` render update; SceneFX rect/shadow/blur/buffer setters | Mostly pixels/damage, sometimes interactive rect geometry. Broad effect paths **NOT TESTED**. |
|
||||
| Output and presentation | `sway/desktop/output.c:output_repaint_timer_handler`, commit/present listeners, destroy; wlroots output commit/state/headless deferred present | Per-output submission identity. Neither `commit_seq` nor `present` is an input generation. Fixed geometry only. |
|
||||
| Grab, focus and IPC | `sway/input/cursor.c:node_at_coords`, `sway/input/seat.c:seat_pointer_notify_button`, wlroots XDG grab handlers | Grab changes delivery after scene hit; focus/IPC can change metadata or decorations without moving hit geometry. Client receipt is separate evidence. |
|
||||
|
||||
SceneFX's private tree clip intersects both visible render/damage bounds and
|
||||
`wlr_scene_node_at()` traversal. Its setter calls `scene_node_update` when
|
||||
the clip changes. wlroots' subsurface tree reconfigures position and order
|
||||
on a surface commit and handles map/unmap/new-subsurface callbacks. A simple
|
||||
search of Mosaic `asi_mark` sites would miss these transitive changes.
|
||||
`transaction_progress()` applies queued state, arranges root, then marks the
|
||||
partial epoch; it is not a commit/present barrier. The compositor event loop
|
||||
orders individual callbacks, but an input callback need not observe the
|
||||
same scene that a previously built buffer used.
|
||||
|
||||
## Runtime matrix and correlations
|
||||
|
||||
Lab: `/tmp/swayfx-mosaic-nested-grab-OWIvboDB/`. All nine T1–T5 runs
|
||||
reported `failure=null`, used private headless sockets and exited through
|
||||
runner cleanup. The unmodified SwayFX control lacks private native
|
||||
commit/present logging, so its zero trace count is **no observation**, not
|
||||
zero actual commits. For each private run, the only unmatched accepted
|
||||
native commits were initial sequence 2 on each output; no headless present
|
||||
was observed for those two. No B case used an output-buffer readback.
|
||||
|
||||
| Scenario | Private Mosaic | Private `splith` | Unmodified `splith` |
|
||||
| --- | --- | --- | --- |
|
||||
| T1/T2 one camera and popup lifecycle cycle | 70 accepted commits / 68 presents; two real A/B grabs; A/B created and destroyed | 57/55; two grabs; A/B created and destroyed | Native counts uninstrumented; two grabs; A/B created and destroyed |
|
||||
| T3 same-size recolor | 34/32; pixel `(35,68,204)` → `(208,32,192)` | 36/34; same pixel change | Same pixel change; native counts uninstrumented |
|
||||
| T4 empty/full input region | 42/40; sampled pixel stays `(35,68,204)` | 43/41; same sampled pixel | Same sampled pixel; native counts uninstrumented |
|
||||
|
||||
**T1.** The private Mosaic output-state stages show 400/1280/400 with
|
||||
applied tiling positions `-400/-1280/-400` and tree clips
|
||||
`(400,0,800,600)/(1280,0,800,600)/(400,0,800,600)`.
|
||||
SceneFX logged the corresponding actual clip mutations, including a
|
||||
hidden-owner empty popup clip at 1280. The sampled output stages reached
|
||||
accepted native commits and headless presents. The epoch advanced at the
|
||||
transaction and popup arrangements, but it can also advance when sampled
|
||||
state is unchanged. The controls remained `splith` without a Mosaic camera.
|
||||
|
||||
**T2.** The private client created distinct A and B XDG surfaces with
|
||||
two pointer-derived grab serials. `close_b` destroyed B; a subsequent right
|
||||
button reached A, then `close_a` destroyed A. The same A-after-B input
|
||||
result and lifecycle occurred in both standard controls. Direct creation,
|
||||
map, and destruction of the scene objects are not individually covered by
|
||||
`asi_applied_epoch`; later transaction arrangements caused separate marks.
|
||||
The trace proves the sampled hierarchy/lifecycle and actual A delivery,
|
||||
not a complete mutation-generation boundary.
|
||||
|
||||
**T3.** The private client issued a second `wl_surface.attach` and commit
|
||||
for the toplevel at the same 420×320 size and unchanged intended position.
|
||||
The new color was visible in a targeted capture after HEADLESS-1 native
|
||||
commit/present 20 in the Mosaic run. Its COMMIT_ACCEPTED stage still had
|
||||
epoch 12, tile `-400,0` and clip `(400,0,800,600)`; the prior relevant
|
||||
stage had the same values. Thus a real content change was absent from the
|
||||
partial epoch, as expected for a content-only mutation. The capture is
|
||||
temporally correlated with the native event, not an exact read of its buffer.
|
||||
|
||||
**T4.** No popup or XDG grab was active. At one global point on the
|
||||
toplevel, the executed `ASI_HIT` changed from its surface to `nil` after
|
||||
the client committed an empty input region. Both hits recorded epoch 12,
|
||||
tile `-400,0` and clip `(400,0,800,600)`. The pixel at that point remained
|
||||
`(35,68,204)` in all three captures. Before and after restoration the
|
||||
toplevel received a right-button press; while empty it received none.
|
||||
Unmodified and private standard layouts likewise delivered two toplevel
|
||||
presses, before and after the empty region, with unchanged sampled pixels.
|
||||
An incidental later transaction mark does not erase the same-epoch hit
|
||||
counterexample. This is an actual hit-affecting mutation missed by the
|
||||
counter, without a demonstrated clipping defect.
|
||||
|
||||
**T5.** Standard layouts preserved the popup close order, A delivery after
|
||||
B closed, toplevel recolor, and input-region behavior. Private trace logging
|
||||
added native records but no reproduced blocking difference in these cases.
|
||||
No exhaustive timing or effects comparison was made.
|
||||
|
||||
## Output-buffer lifetime audit
|
||||
|
||||
`sway/desktop/output.c:output_repaint_timer_handler` takes an extra probe
|
||||
lock before `wlr_output_commit_state`. On a rejected commit it directly
|
||||
unlocks that reference, before the state is finished. On an accepted commit,
|
||||
it installs the lock in the output-scoped slot and starts a timer; a same
|
||||
output buffer replacement releases the old slot. `begin_destroy` releases
|
||||
an active slot before the output pointers/listeners are removed. The helper
|
||||
clears the slot before removing its timer and unlocking. wlroots 0.20.2
|
||||
`types/output/output.c:wlr_output_commit_state` emits the native commit
|
||||
event synchronously after acceptance; `types/output/state.c` owns a separate
|
||||
buffer reference until `wlr_output_state_finish`; headless `present` is
|
||||
deferred and cancellation on output destroy is handled by wlroots.
|
||||
|
||||
One controlled **real private headless** case retained buffer ID 1 after
|
||||
HEADLESS-1 `present` sequence 30, then stopped the compositor normally.
|
||||
`begin_destroy` emitted exactly one `ASI_RELEASE id=1
|
||||
reason=output_destroy`, age 863,608,059 ns; the compositor exited 0 and
|
||||
the runner removed its private runtime. This qualifies that sampled
|
||||
output-destroy path only. A C17 synthetic model passed once-only release
|
||||
for direct rejected commit and repeated destroy release, but **no real
|
||||
rejected commit was triggered**. Backend rejected-commit cleanup is
|
||||
**NOT TESTED**. The synthetic result cannot qualify a real backend.
|
||||
|
||||
## Verdicts and architectural boundary
|
||||
|
||||
* Mapping: **PARTIALLY_FEASIBLE**. Source ownership and major classes are
|
||||
identified, but output migration, animations, effects, subsurfaces,
|
||||
stacking and other scene callers remain untriggered.
|
||||
* Mutation scenarios T1–T5: **FEASIBLE for the bounded matrix**. Observed
|
||||
geometry, clip, hits and client delivery agree. T4 disproves completeness
|
||||
of `asi_applied_epoch`; T3 shows why pixels must not automatically be
|
||||
folded into a geometric generation.
|
||||
* Cleanup: **PARTIALLY_FEASIBLE**. A real held-reference output destruction
|
||||
passed; the backend rejected-commit branch remains **NOT TESTED**.
|
||||
* Overall probe: **PARTIALLY_FEASIBLE**. No sampled crash, invisible hit,
|
||||
clip leak or standard-layout regression was reproduced. This does not
|
||||
qualify production STEP20C or all SceneFX mutations.
|
||||
|
||||
The minimal candidate coherence boundary is an applied **geometry/hit**
|
||||
identity for workspace and owner, scene position, viewport and tiled/root
|
||||
popup clip, hierarchy, enabled state, order, surface hit geometry and
|
||||
spatial transforms. A separate **content** identity may track attached
|
||||
buffers and effect pixels without pretending they change hit geometry.
|
||||
Native **presentation identity** remains `(output, commit_seq)` with the
|
||||
headless present observation. Grab state must be associated with delivery,
|
||||
not retroactively treated as the scene hit. This separation is a research
|
||||
hypothesis, not an authorized production API or counter design.
|
||||
|
||||
**Architecture answer: UNDECIDED.** The current SwayFX/SceneFX live scene
|
||||
was consistent with rendering and input in the sampled states, including
|
||||
the input-region mutation. The partial epoch is demonstrably insufficient
|
||||
as a generation identity. The evidence does not show whether existing
|
||||
event-loop ordering alone can enforce the required cross-time relation
|
||||
between an already built output buffer and a later hit during all relevant
|
||||
Mosaic mutations. The smallest discriminating next experiment is a
|
||||
controlled private mutation of hit geometry **between a known output-state
|
||||
build and its deferred headless present**, with scene state sampled at
|
||||
build, immediately before and after the mutation, at present, and at an
|
||||
executed hit. It must retain one output's native `(output, commit_seq)` and
|
||||
client delivery evidence. It should first use the proven input-region path,
|
||||
then a Mosaic clip/owner change if the first case cannot distinguish the
|
||||
contracts. Broader output/effect coverage follows only after this temporal
|
||||
question is resolved.
|
||||
|
||||
## Reproduction, hashes and validation
|
||||
|
||||
The [fixture README](../../tooling/probes/step20c-mutation-coverage/README.md)
|
||||
provides exact rebuild and replay commands. `mutation-results/` contains
|
||||
the structured scenario matrix, lifetime record, copied inventory, and
|
||||
source/artifact SHA-256 manifests. All generated builds, buffers, logs,
|
||||
captures, sockets and manifests stay outside Git. The private client patch
|
||||
applies on the frozen nested-client source; it does not touch Mosaic or
|
||||
installed SceneFX/wlroots. The trace-only scenarios use no probe locks.
|
||||
|
||||
The private client patch SHA-256 is
|
||||
`abee78ffa80729c395374c037f71b4a3e1158b81df6079ce6effc36afdf5eef6`.
|
||||
The 15-source manifest SHA-256 is
|
||||
`2875c22de8bb3a86d8d2f05296e9e141033980bb765e5e8958d5565ef47530ae`.
|
||||
The 420-artifact manifest SHA-256 is
|
||||
`f0761406414f3131017fc135c01caa4c643463117ae3f7a135d2b12ffa6ae2bc`.
|
||||
The scenario matrix and lifetime record SHA-256 values are
|
||||
`db2924f300fa139c9d16729ee62a17782698ecb517d42110e9507821c53d5aa1`
|
||||
and `3a1e4900a83ebdcd9b9725caf18519f05de99aad8ab0b58104bd8c73f9bb1658`.
|
||||
Both manifests passed `sha256sum --quiet -c` in the retained lab.
|
||||
|
||||
The original source manifest records two wlroots paths under the historical
|
||||
`/tmp/mosaic-wlroots-0202-source/`; it remains unchanged evidence, not a
|
||||
replay instruction. The corrected preparator fetches the audited wlroots
|
||||
0.20.2 commit `d783533489e1f75d6886c2ab5c5960090ef268f8` into each new
|
||||
lab and checks the two source hashes before analysis. The corrected analyzer
|
||||
reads `LAB/wlroots/types/scene/{surface,subsurface_tree}.c`. New runs generate
|
||||
new manifests with their own lab paths; the historical manifest hashes above
|
||||
are preserved. This changes source provenance handling only, not T1–T5
|
||||
instrumentation or the verdict.
|
||||
|
||||
An independent clone of `main` at `1f59cb433bcdad006c23df10496cb4709551c624`
|
||||
with this source fixture overlaid rebuilt private SceneFX and Mosaic, then
|
||||
replayed all nine T1–T5 mode/scenario runs and one output-destroy run in
|
||||
`/tmp/swayfx-mosaic-nested-grab-no8fPnyy/`. All ten runners reported
|
||||
`failure=null`. The three T1/T2 controls each made two real grabs. T3's
|
||||
sampled color changed `(35,68,204)` → `(208,32,192)` in all three modes;
|
||||
T4's sampled color remained `(35,68,204)` and each mode delivered exactly
|
||||
two toplevel presses around the empty-region interval. Private Mosaic had
|
||||
76/74 native commits/presents in T1/T2, 40/38 in T3 and 42/40 in T4;
|
||||
private standard had 56/54, 33/31 and 42/40. Unmodified control native
|
||||
counts remain uninstrumented. The output-destroy replay retained one lock,
|
||||
released it once with `reason=output_destroy`, and exited 0; the C17 model
|
||||
passed separately. The replay does not upgrade rejected-commit status.
|
||||
|
||||
The corrected `prepare.sh` SHA-256 is
|
||||
`eb8482fc4cb6f96636d89a9510cd1e5de9750ebd16d55630e3af6585fe8933e6`;
|
||||
the corrected `analyze.py` SHA-256 is
|
||||
`be96ed839d957a51e724be79b78b0124b63a4c67691d5d409f88ab05dd60c4ab`.
|
||||
The new 15-source and 364-artifact manifests hash to
|
||||
`61a4e856e0f6cc26bccf661d7ec81f88256a0a3ee34e5bc7cbdf4063faa44f53`
|
||||
and `11233d9e2f08f9c503d74e43157b787423bda50ef507b16a6670e0d64e235471`;
|
||||
every entry passed `sha256sum -c`. The new scenario matrix and lifetime
|
||||
record hash to `001ed3f324156692ae4b1e71298ac4c00f631b5d0a6839db024bc2e6489eed90`
|
||||
and `8e862792d15b021a61f88b0e926b4d67678ada5a8e2d5ee930dc68511365a0b5`.
|
||||
Artifact counts differ because the bounded replay produced different native
|
||||
timings and capture files; the historical 420-file manifest was not rewritten.
|
||||
The remaining **NOT TESTED** paths are not upgraded by a successful compile
|
||||
or a synthetic model.
|
||||
75
tooling/probes/step20c-mutation-coverage/README.md
Normal file
75
tooling/probes/step20c-mutation-coverage/README.md
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
# STEP20C mutation coverage fixture — EXPERIMENTAL
|
||||
|
||||
The [protocol and findings](../../../docs/design/STEP20C_MUTATION_COVERAGE_PROBE.md)
|
||||
apply only to a disposable private headless laboratory. This fixture reuses
|
||||
the frozen applied-state, SceneFX clip, XDG client and pointer fixtures.
|
||||
It adds one private client patch for a fixed-size recolor and a toplevel
|
||||
input-region toggle. The runner is generated from the preceding trace-only
|
||||
runner inside `/tmp`; this repository contains no duplicate compositor or
|
||||
runner source. Neither private patch is a production dependency.
|
||||
|
||||
## Rebuild and targeted replay
|
||||
|
||||
```sh
|
||||
prep=$(sh tooling/probes/step20c-mutation-coverage/prepare.sh)
|
||||
printf '%s\n' "$prep"
|
||||
LAB=$(printf '%s\n' "$prep" | sed -n 's/^LAB=//p')
|
||||
PYTHONDONTWRITEBYTECODE=1 python tooling/probes/step20c-applied-state-input/run.py \
|
||||
"$LAB" private_mosaic cycles --cycles 1 --asi-mode 0 \
|
||||
--max-seconds 120 --max-captures 40
|
||||
for mode in control private_standard; do
|
||||
PYTHONDONTWRITEBYTECODE=1 python tooling/probes/step20c-applied-state-input/run.py \
|
||||
"$LAB" "$mode" cycles --cycles 1 --asi-mode 0 \
|
||||
--max-seconds 120 --max-captures 40
|
||||
done
|
||||
for mode in control private_standard private_mosaic; do
|
||||
for scenario in content input_region; do
|
||||
PYTHONDONTWRITEBYTECODE=1 python tooling/probes/step20c-mutation-coverage/run.py \
|
||||
"$LAB" "$mode" "$scenario"
|
||||
done
|
||||
done
|
||||
PYTHONDONTWRITEBYTECODE=1 python tooling/probes/step20c-mutation-coverage/run.py \
|
||||
"$LAB" private_mosaic hold_exit
|
||||
cc -std=c17 -Wall -Wextra -Werror -pedantic \
|
||||
tooling/probes/step20c-mutation-coverage/lifetime-model.c \
|
||||
-o "$LAB/lifetime-model"
|
||||
"$LAB/lifetime-model"
|
||||
PYTHONDONTWRITEBYTECODE=1 python tooling/probes/step20c-mutation-coverage/analyze.py "$LAB"
|
||||
sha256sum --quiet -c "$LAB/mutation-results/source-sha256.txt"
|
||||
sha256sum --quiet -c "$LAB/mutation-results/artifact-sha256.txt"
|
||||
```
|
||||
|
||||
Run each case once in a newly prepared lab; the analyzer rejects missing or
|
||||
ambiguous runs. It writes structured `inventory.json`, `scenario-matrix.json`,
|
||||
`lifetime.json` and SHA-256 manifests under `LAB/mutation-results/`. Raw
|
||||
client/compositor logs, exact exit statuses, IPC states and targeted captures
|
||||
remain under `LAB/runs/`. Only the compositor receives the private SceneFX
|
||||
library path. Every run has its own mode-0700 runtime and explicit IPC socket;
|
||||
the previous runner removes it and terminates its process groups.
|
||||
|
||||
`hold_exit` is the only B case that takes one extra output-buffer lock and
|
||||
read. It intentionally retains that lock after a real headless `present`,
|
||||
then lets normal private compositor shutdown destroy the output. This tests
|
||||
the actual output-destroy callback once. `lifetime-model.c` is a **synthetic**
|
||||
C17 state-machine check of the direct rejected-commit unlock and idempotent
|
||||
destroy release; it does **not** test a wlroots/backend rejected commit.
|
||||
The latter remains **NOT TESTED**. Normal T1–T5 runs are trace-only.
|
||||
|
||||
The fixed output geometry is HEADLESS-1 `(-800,0)` and HEADLESS-2 `(0,0)`,
|
||||
both 800×600, scale 1, normal transform. The standard controls receive no
|
||||
Mosaic camera. `grim` captures establish sampled visible pixels, never the
|
||||
identity of a committed output buffer. Native `(output, commit_seq)` and
|
||||
headless `present` are correlated separately from actual client input.
|
||||
|
||||
## Pinned source-audit input
|
||||
|
||||
`prepare.sh` clones wlroots from
|
||||
`https://gitlab.freedesktop.org/wlroots/wlroots.git` into the **new** lab,
|
||||
checks out 0.20.2 commit `d783533489e1f75d6886c2ab5c5960090ef268f8`,
|
||||
and archives it to `LAB/wlroots/`. It verifies SHA-256
|
||||
`b8c83f292006bc44d349891b1d460245db9dae891e986e6dc2006572f99a7f97`
|
||||
for `types/scene/surface.c` and
|
||||
`e73655cc589321f18a824ed4b153c9b1973f9c41a0f8d52b1d380e28a42671cc`
|
||||
for `types/scene/subsurface_tree.c`. The analyzer hashes these lab-owned files.
|
||||
The earlier `/tmp/mosaic-wlroots-0202-source/` is not a replay input.
|
||||
The full wlroots checkout, build products and manifests remain outside Git.
|
||||
140
tooling/probes/step20c-mutation-coverage/analyze.py
Normal file
140
tooling/probes/step20c-mutation-coverage/analyze.py
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
"""Correlate bounded headless mutations without assigning input a frame ID."""
|
||||
import argparse
|
||||
from collections import Counter
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
|
||||
from PIL import Image
|
||||
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument('lab', type=Path)
|
||||
args = ap.parse_args()
|
||||
lab = args.lab.resolve()
|
||||
repo = Path(__file__).resolve().parents[3]
|
||||
out = lab / 'mutation-results'
|
||||
out.mkdir(exist_ok=True)
|
||||
fixture = Path(__file__).parent
|
||||
shutil.copyfile(fixture / 'inventory.json', out / 'inventory.json')
|
||||
|
||||
|
||||
def fields(line, marker):
|
||||
if marker not in line:
|
||||
return None
|
||||
return dict(re.findall(r'(\w+)=([^ ]+)', line.split(marker, 1)[1]))
|
||||
|
||||
|
||||
def read_run(mode, kind):
|
||||
matches = sorted((lab / 'runs').glob(f'{mode}-{kind}-asi0-*'))
|
||||
if len(matches) != 1:
|
||||
raise RuntimeError(f'expected one {mode}/{kind} run, got {len(matches)}')
|
||||
run = matches[0]
|
||||
events = [json.loads(x) for x in (run / 'events.jsonl').read_text().splitlines()]
|
||||
log = (run / 'compositor.log').read_text(errors='replace').splitlines()
|
||||
records = {key: [f for line in log if (f := fields(line, key + ' '))]
|
||||
for key in ('ASI_MUTATION', 'ASI_HIT', 'BI_STAGE',
|
||||
'PI_OUTPUT_COMMIT', 'PI_OUTPUT_PRESENT',
|
||||
'ASI_LOCKED', 'ASI_RELEASE')}
|
||||
commits = {(x['output'], x['commit_seq']) for x in records['PI_OUTPUT_COMMIT']}
|
||||
presents = {(x['output'], x['commit_seq']) for x in records['PI_OUTPUT_PRESENT']}
|
||||
client = (run / 'client.log').read_text(errors='replace')
|
||||
status = json.loads((run / 'exit-status.json').read_text())
|
||||
return run, events, records, client, status, commits, presents
|
||||
|
||||
|
||||
matrix = {}
|
||||
for mode in ('control', 'private_standard', 'private_mosaic'):
|
||||
for kind in ('cycles', 'mutation_content', 'mutation_input_region'):
|
||||
run, events, rec, client, status, commits, presents = read_run(mode, kind)
|
||||
entry = {
|
||||
'run': str(run), 'mode': mode, 'scenario': kind,
|
||||
'runner_failure': status['failure'], 'captures': status['captures'],
|
||||
'commits': len(commits), 'presents': len(presents),
|
||||
'unpaired_commits': sorted([list(x) for x in commits - presents]),
|
||||
'hit_count': len(rec['ASI_HIT']),
|
||||
'epoch_events': len(rec['ASI_MUTATION']),
|
||||
'buffer_locks': len(rec['ASI_LOCKED']),
|
||||
'buffer_releases': len(rec['ASI_RELEASE']),
|
||||
'popup_grabs': client.count('GRAB_REQUEST'),
|
||||
'popup_creates': [x for x in client.splitlines() if 'POPUP_' in x and ' CREATE ' in x],
|
||||
'popup_destroys': [x for x in client.splitlines() if 'POPUP_' in x and ' DESTROY ' in x],
|
||||
}
|
||||
if kind == 'cycles':
|
||||
entry['A_buttons_after_B_close'] = len(re.findall(
|
||||
r'POINTER_BUTTON target=POPUP_A[^\n]*button=273 state=1', client))
|
||||
entry['camera_stages'] = [
|
||||
{k: x.get(k) for k in ('output', 'scroll', 'tile_scene', 'clip', 'epoch')}
|
||||
for x in rec['BI_STAGE'] if x.get('phase') == 'COMMIT_ACCEPTED'
|
||||
and x.get('output') == 'HEADLESS-1'
|
||||
and x.get('scroll') in ('400', '1280')]
|
||||
elif kind == 'mutation_content':
|
||||
entry['client_new_buffer'] = 'COMMAND recolor_top DONE' in client and (
|
||||
client.count('TOPLEVEL MAP') >= 2)
|
||||
entry['pixels'] = {tag: Image.open(run / f'left-content-{tag}.png').getpixel((342, 175))
|
||||
for tag in ('before', 'after')}
|
||||
boundary = [e for e in events if e['kind'] == 'client_request'
|
||||
and e['value'] == 'recolor_top'][0]['monotonic_ns']
|
||||
entry['commits_after_request'] = [x for x in rec['PI_OUTPUT_COMMIT']
|
||||
if x['output'] == 'HEADLESS-1' and int(x['ns']) > boundary]
|
||||
entry['stages_after_request'] = [{k: x.get(k) for k in
|
||||
('commit_seq', 'scroll', 'tile_scene', 'clip', 'epoch', 'ns')}
|
||||
for x in rec['BI_STAGE'] if x.get('phase') == 'COMMIT_ACCEPTED'
|
||||
and x['output'] == 'HEADLESS-1' and int(x['ns']) > boundary]
|
||||
else:
|
||||
entry['pixels'] = {tag: Image.open(run / f'left-region-{tag}.png').getpixel((342, 175))
|
||||
for tag in ('full-before', 'empty', 'full-after')}
|
||||
entry['client_top_button_presses'] = len(re.findall(
|
||||
r'POINTER_BUTTON target=TOPLEVEL[^\n]*button=273 state=1', client))
|
||||
entry['hit_sequence'] = [
|
||||
{k: x.get(k) for k in ('ns', 'epoch', 'lx', 'ly', 'node', 'surface',
|
||||
'logical_scroll', 'tile_scene', 'clip')}
|
||||
for x in rec['ASI_HIT'] if x.get('lx') == '-458.00' or
|
||||
(mode == 'private_standard' and x.get('lx') == '-458.00')]
|
||||
matrix[f'{mode}/{kind}'] = entry
|
||||
|
||||
(out / 'scenario-matrix.json').write_text(json.dumps(matrix, indent=2) + '\n')
|
||||
hold_runs = sorted((lab / 'runs').glob('private_mosaic-mutation_hold_exit-asi1-*'))
|
||||
if len(hold_runs) != 1:
|
||||
raise RuntimeError(f'expected one real output-destroy run, got {len(hold_runs)}')
|
||||
hold = hold_runs[0]
|
||||
hold_log = (hold / 'compositor.log').read_text(errors='replace').splitlines()
|
||||
hold_status = json.loads((hold / 'exit-status.json').read_text())
|
||||
lifetime = {
|
||||
'runtime_output_destroy': {
|
||||
'run': str(hold), 'runner_failure': hold_status['failure'],
|
||||
'compositor_exit': hold_status['compositor'],
|
||||
'locks': [fields(x, 'ASI_LOCKED ') for x in hold_log if 'ASI_LOCKED ' in x],
|
||||
'releases': [fields(x, 'ASI_RELEASE ') for x in hold_log if 'ASI_RELEASE ' in x],
|
||||
'selected_present_retained': any('ASI_TEST_HOLD id=1 ' in x for x in hold_log),
|
||||
},
|
||||
'runtime_rejected_commit': 'NOT TESTED',
|
||||
'synthetic_rejected_commit': 'see lifetime-model.c; no backend triggered',
|
||||
}
|
||||
(out / 'lifetime.json').write_text(json.dumps(lifetime, indent=2) + '\n')
|
||||
sources = [
|
||||
fixture / 'mutation-client.patch', fixture / 'prepare.sh', fixture / 'run.py', fixture / 'analyze.py',
|
||||
fixture / 'inventory.json', fixture / 'lifetime-model.c',
|
||||
lab / 'nested-client.c', lab / 'mosaic/sway/desktop/transaction.c',
|
||||
lab / 'mosaic/sway/desktop/output.c', lab / 'mosaic/sway/desktop/xdg_shell.c',
|
||||
lab / 'mosaic/sway/input/cursor.c', lab / 'mosaic/sway/input/seat.c',
|
||||
lab / 'scenefx/types/scene/wlr_scene.c',
|
||||
lab / 'wlroots/types/scene/surface.c',
|
||||
lab / 'wlroots/types/scene/subsurface_tree.c',
|
||||
]
|
||||
with (out / 'source-sha256.txt').open('w') as dst:
|
||||
for p in sources:
|
||||
dst.write(f'{hashlib.sha256(p.read_bytes()).hexdigest()} {p}\n')
|
||||
with (out / 'artifact-sha256.txt').open('w') as dst:
|
||||
for p in sorted((lab / 'runs').glob('*')):
|
||||
if not p.is_dir() or ('asi0' not in p.name and 'mutation_hold_exit-asi1' not in p.name):
|
||||
continue
|
||||
for f in sorted(p.rglob('*')):
|
||||
if f.is_file():
|
||||
dst.write(f'{hashlib.sha256(f.read_bytes()).hexdigest()} {f}\n')
|
||||
for p in (out / 'inventory.json', out / 'scenario-matrix.json',
|
||||
out / 'lifetime.json', out / 'source-sha256.txt'):
|
||||
dst.write(f'{hashlib.sha256(p.read_bytes()).hexdigest()} {p}\n')
|
||||
print(f'MATRIX={out / "scenario-matrix.json"} SOURCES={len(sources)} '
|
||||
f'ARTIFACTS={len((out / "artifact-sha256.txt").read_text().splitlines())}')
|
||||
18
tooling/probes/step20c-mutation-coverage/inventory.json
Normal file
18
tooling/probes/step20c-mutation-coverage/inventory.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
[
|
||||
{"mutation":"Mosaic camera and tiled viewport clip","source":"mosaic:sway/desktop/transaction.c:arrange_output (workspace_tiling_scene_x, wlr_scene_tree_set_clip)","object":"workspace tiling scene tree","class":"pixels_and_hit","render":"yes","hit":"yes","damage":"yes","transaction":"applied in arrange_root after transaction_apply","epoch":"observed at transaction_progress end; not atomic","experiment":"T1 400/1280/400","verdict":"PASS"},
|
||||
{"mutation":"Owner-derived root popup position and clip","source":"mosaic:sway/desktop/transaction.c:arrange_popups, probe_clip_popup","object":"XDG popup scene tree","class":"pixels_and_hit","render":"yes","hit":"yes","damage":"yes","transaction":"arrange_root or direct handle_new_popup call","epoch":"partial: increment after arrangement, not before direct creation","experiment":"T1/T2","verdict":"PARTIAL"},
|
||||
{"mutation":"XDG popup scene creation and destruction","source":"mosaic:sway/desktop/xdg_shell.c:popup_create, popup_destroy, handle_new_popup","object":"popup scene tree and XDG role","class":"pixels_and_hit","render":"yes","hit":"yes","damage":"yes","transaction":"immediate callbacks; XDG configure/map later","epoch":"absent at creation/destroy; possible later incidental arrangement","experiment":"T2 A/B open then B/A close","verdict":"PASS"},
|
||||
{"mutation":"Surface map and unmap","source":"wlroots:types/scene/subsurface_tree.c:subsurface_tree_handle_surface_map/unmap","object":"scene subsurface tree","class":"pixels_and_hit","render":"yes","hit":"yes","damage":"yes","transaction":"immediate surface callback","epoch":"absent unless unrelated arrangement","experiment":"T2 popup map/close; direct callback not individually traced","verdict":"PARTIAL"},
|
||||
{"mutation":"Client buffer replacement at fixed geometry","source":"wlroots:types/scene/surface.c:handle_scene_surface_surface_commit, surface_reconfigure; SceneFX:types/scene/wlr_scene.c:wlr_scene_buffer_set_buffer_with_options","object":"surface and scene buffer","class":"pixels_only","render":"yes","hit":"conditional if dimensions, opacity or transform change; fixed case no","damage":"yes","transaction":"immediate client surface commit","epoch":"absent at fixed geometry","experiment":"T3 toplevel recolor, same size","verdict":"PASS"},
|
||||
{"mutation":"Surface input region","source":"wlroots:types/scene/surface.c:scene_buffer_point_accepts_input -> wlr_surface_point_accepts_input; SceneFX:types/scene/wlr_scene.c:scene_node_at_iterator","object":"wlr_surface input region","class":"hit_only","render":"no at fixed content","hit":"yes","damage":"not required by input semantics; incidental output commits possible","transaction":"immediate client surface commit","epoch":"absent at first empty-region hit","experiment":"T4 empty/full toplevel region without grab","verdict":"PASS"},
|
||||
{"mutation":"Subsurface creation, position and stacking","source":"wlroots:types/scene/subsurface_tree.c:subsurface_tree_handle_surface_new_subsurface, subsurface_tree_reconfigure","object":"subsurface scene hierarchy","class":"pixels_and_hit","render":"yes","hit":"yes","damage":"yes","transaction":"immediate surface callback","epoch":"absent","experiment":"candidate T4, not selected","verdict":"NOT TESTED"},
|
||||
{"mutation":"Generic scene node position, enable, reparent or order","source":"SceneFX:types/scene/wlr_scene.c:wlr_scene_node_set_position/set_enabled/reparent/place_above/place_below","object":"scene node","class":"pixels_and_hit","render":"conditional","hit":"conditional","damage":"conditional via scene_node_update","transaction":"caller dependent","epoch":"only arrangement callers counted","experiment":"T1/T2 callers sampled; other callers open","verdict":"PARTIAL"},
|
||||
{"mutation":"Generic SceneFX tree-local clip","source":"SceneFX:types/scene/wlr_scene.c:wlr_scene_tree_set_clip, scene_node_point_inside_clip, scene_node_intersect_clip","object":"scene tree","class":"pixels_and_hit","render":"yes","hit":"yes","damage":"yes via scene_node_update","transaction":"caller dependent","epoch":"only private arrangement callers counted","experiment":"T1 root/tiled clip","verdict":"PARTIAL"},
|
||||
{"mutation":"Effects, decoration, shadow, blur and opacity","source":"mosaic:sway/desktop/output.c:update_container_*; SceneFX:types/scene/wlr_scene.c:rect/shadow/blur/buffer setters","object":"scene effect nodes","class":"pixels_only_or_conditional_hit","render":"yes","hit":"conditional for interactive decoration rect","damage":"yes","transaction":"caller dependent, including render update","epoch":"mostly absent","experiment":"not in targeted matrix","verdict":"NOT TESTED"},
|
||||
{"mutation":"Animation current dimensions and workspace fade","source":"mosaic:sway/desktop/transaction.c:arrange_container, arrange_output; animation callbacks elsewhere","object":"container and owner-visibility guard inputs","class":"pixels_and_hit","render":"conditional","hit":"conditional","damage":"conditional","transaction":"animation tick or arrangement","epoch":"partial; animation ticks not covered","experiment":"T1 snapshots only","verdict":"PARTIAL"},
|
||||
{"mutation":"Output geometry, scale, transform, migration or removal","source":"mosaic:sway/desktop/output.c:handle_request_state/begin_destroy; transaction.c:arrange_output; SceneFX:types/scene/wlr_scene.c:scene_output_update_geometry","object":"output and scene output","class":"pixels_and_hit","render":"yes","hit":"yes","damage":"yes","transaction":"configuration and arrangement","epoch":"partial or absent","experiment":"fixed geometry only","verdict":"NOT TESTED"},
|
||||
{"mutation":"Scene damage without geometry change","source":"SceneFX:types/scene/wlr_scene.c:scene_node_update, scene_output_damage; wlroots:types/scene/surface.c:surface_reconfigure","object":"damage ring and output","class":"pixels_only","render":"conditional","hit":"no by itself","damage":"yes","transaction":"immediate","epoch":"absent","experiment":"T3 content update","verdict":"PARTIAL"},
|
||||
{"mutation":"XDG popup grab and dismissal","source":"mosaic:sway/input/seat.c:seat_pointer_notify_button; wlroots XDG popup grab handlers","object":"seat/grab and XDG roles","class":"delivery_only","render":"no","hit":"no scene change by grab itself","damage":"conditional on dismissal","transaction":"immediate input callback","epoch":"absent","experiment":"T2 valid A/B grabs; prior visible/clipped control","verdict":"PARTIAL"},
|
||||
{"mutation":"Focus and IPC metadata","source":"mosaic:sway/input/seat.c:focus handlers; sway IPC tree reporting","object":"seat focus and metadata","class":"metadata_or_delivery","render":"conditional if decoration changes","hit":"not necessarily","damage":"conditional","transaction":"mixed","epoch":"not a general focus generation","experiment":"T5 controls","verdict":"PARTIAL"},
|
||||
{"mutation":"Output buffer commit and present","source":"mosaic:sway/desktop/output.c:output_repaint_timer_handler, presentation_probe_handle_commit, handle_present","object":"per-output state and presentation","class":"presentation_identity","render":"yes","hit":"no scene mutation implied","damage":"consumes pending damage","transaction":"native output submission","epoch":"recorded but not a geometry generation","experiment":"T1-T4 native commit/present","verdict":"PASS"}
|
||||
]
|
||||
47
tooling/probes/step20c-mutation-coverage/lifetime-model.c
Normal file
47
tooling/probes/step20c-mutation-coverage/lifetime-model.c
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/* EXPERIMENTAL synthetic branch model, not a wlroots backend test.
|
||||
* WHY: rejected commits and output destruction have no safe, controlled
|
||||
* trigger in the fixed headless matrix. Model only extra probe ownership.
|
||||
* CONTRACT: one extra lock must be released exactly once on each branch.
|
||||
* INVARIANT: clear the output slot before invoking the unlock callback. */
|
||||
#include <assert.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
struct fake_buffer {
|
||||
unsigned locks;
|
||||
unsigned unlocks;
|
||||
};
|
||||
struct fake_hold {
|
||||
struct fake_buffer *buffer;
|
||||
bool timer;
|
||||
unsigned output_id;
|
||||
};
|
||||
|
||||
static void unlock(struct fake_buffer *buffer) {
|
||||
assert(buffer->locks == buffer->unlocks + 1);
|
||||
++buffer->unlocks;
|
||||
}
|
||||
|
||||
static void release_hold(struct fake_hold *hold) {
|
||||
if (!hold || !hold->buffer) return;
|
||||
struct fake_buffer *buffer = hold->buffer;
|
||||
memset(hold, 0, sizeof(*hold));
|
||||
unlock(buffer);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
struct fake_buffer rejected = {.locks = 1};
|
||||
/* output.c direct rejection branch: the slot was never installed. */
|
||||
unlock(&rejected);
|
||||
assert(rejected.unlocks == 1);
|
||||
|
||||
struct fake_buffer destroyed = {.locks = 1};
|
||||
struct fake_hold hold = {.buffer = &destroyed, .timer = true, .output_id = 1};
|
||||
/* output.c begin_destroy branch and a late idempotent release request. */
|
||||
release_hold(&hold);
|
||||
release_hold(&hold);
|
||||
assert(destroyed.unlocks == 1 && hold.buffer == NULL && !hold.timer);
|
||||
puts("SYNTHETIC_REJECT_AND_DESTROY=PASS (no backend triggered)");
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
--- a/nested-client.c
|
||||
+++ b/nested-client.c
|
||||
@@ -252,7 +252,26 @@
|
||||
if (fds[1].revents & POLLIN) {
|
||||
char cmd[64];
|
||||
if (fgets(cmd,sizeof(cmd),stdin)) {
|
||||
- if (!strncmp(cmd,"close_b",7)) { destroy_popup(&b); fprintf(stderr,"COMMAND close_b DONE\n"); }
|
||||
+ /* WHY: isolate client content from scene geometry and input region.
|
||||
+ * CONTRACT: same toplevel size/position; only private headless client.
|
||||
+ * INVARIANT: region updates carry no new client buffer. */
|
||||
+ if (!strncmp(cmd,"recolor_top",11)) {
|
||||
+ top.color = 0x00d020c0;
|
||||
+ draw(&top);
|
||||
+ fprintf(stderr,"COMMAND recolor_top DONE color=%08x wl=%u\n",
|
||||
+ top.color, wl_proxy_get_id((struct wl_proxy *)top.wl));
|
||||
+ } else if (!strncmp(cmd,"region_empty_top",16) ||
|
||||
+ !strncmp(cmd,"region_full_top",15)) {
|
||||
+ bool empty = !strncmp(cmd,"region_empty_top",16);
|
||||
+ struct wl_region *region = wl_compositor_create_region(compositor);
|
||||
+ if (!empty) wl_region_add(region, 0, 0, top.width, top.height);
|
||||
+ wl_surface_set_input_region(top.wl, region);
|
||||
+ wl_surface_commit(top.wl);
|
||||
+ wl_region_destroy(region);
|
||||
+ fprintf(stderr,"COMMAND region_%s_top DONE wl=%u\n",
|
||||
+ empty ? "empty" : "full",
|
||||
+ wl_proxy_get_id((struct wl_proxy *)top.wl));
|
||||
+ } else if (!strncmp(cmd,"close_b",7)) { destroy_popup(&b); fprintf(stderr,"COMMAND close_b DONE\n"); }
|
||||
else if (!strncmp(cmd,"close_a",7)) { destroy_popup(&b); destroy_popup(&a); fprintf(stderr,"COMMAND close_a DONE\n"); }
|
||||
else if (!strncmp(cmd,"close_owner",11)) { fprintf(stderr,"COMMAND close_owner\n"); running=0; }
|
||||
}
|
||||
36
tooling/probes/step20c-mutation-coverage/prepare.sh
Normal file
36
tooling/probes/step20c-mutation-coverage/prepare.sh
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
#!/bin/sh
|
||||
# EXPERIMENTAL: all modified compositor/client sources stay in /tmp.
|
||||
set -eu
|
||||
repo=$(CDPATH= cd -- "$(dirname -- "$0")/../../.." && pwd)
|
||||
fixture="$repo/tooling/probes/step20c-mutation-coverage"
|
||||
prep_log=$(mktemp /tmp/mutation-prepare-XXXXXXXX)
|
||||
if ! sh "$repo/tooling/probes/step20c-applied-state-input/prepare.sh" > "$prep_log" 2>&1; then
|
||||
printf 'PRIVATE_BUILD_FAILED log=%s\n' "$prep_log" >&2
|
||||
tail -40 "$prep_log" >&2
|
||||
exit 1
|
||||
fi
|
||||
lab=$(sed -n 's/^LAB=//p' "$prep_log")
|
||||
test -n "$lab" && test -d "$lab"
|
||||
trap 'rm -rf -- "$lab"' 0
|
||||
# CONTRACT: source-audit inputs belong to this lab and are pinned to the
|
||||
# audited wlroots release. Never use an older machine-local source tree.
|
||||
wlroots_commit=d783533489e1f75d6886c2ab5c5960090ef268f8
|
||||
git clone --quiet https://gitlab.freedesktop.org/wlroots/wlroots.git "$lab/wlroots-git"
|
||||
git -C "$lab/wlroots-git" checkout --quiet --detach "$wlroots_commit"
|
||||
test "$(git -C "$lab/wlroots-git" rev-parse HEAD)" = "$wlroots_commit"
|
||||
mkdir "$lab/wlroots"
|
||||
git -C "$lab/wlroots-git" archive HEAD | tar -xf - -C "$lab/wlroots"
|
||||
printf '%s\n' \
|
||||
'b8c83f292006bc44d349891b1d460245db9dae891e986e6dc2006572f99a7f97 types/scene/surface.c' \
|
||||
'e73655cc589321f18a824ed4b153c9b1973f9c41a0f8d52b1d380e28a42671cc types/scene/subsurface_tree.c' |
|
||||
(cd "$lab/wlroots" && sha256sum --quiet -c -)
|
||||
(
|
||||
cd "$lab"
|
||||
git apply --check "$fixture/mutation-client.patch"
|
||||
git apply "$fixture/mutation-client.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" "$prep_log"
|
||||
72
tooling/probes/step20c-mutation-coverage/run.py
Normal file
72
tooling/probes/step20c-mutation-coverage/run.py
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
"""Adapt the frozen trace-only runner in a disposable lab for T3/T4.
|
||||
|
||||
The generated copy, sockets and captures stay outside Git. Existing process
|
||||
cleanup, output geometry checks and explicit swaymsg socket remain intact.
|
||||
"""
|
||||
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('scenario', choices=('content', 'input_region', 'hold_exit'))
|
||||
args = ap.parse_args()
|
||||
lab = args.lab.resolve()
|
||||
repo = Path(__file__).resolve().parents[3]
|
||||
original = (repo / 'tooling/probes/step20c-applied-state-input/run.py').read_text()
|
||||
old_choices = "choices=('cycles', 'buffer_cycles', 'identity', 'outside', 'hidden_click', 'toggle')"
|
||||
new_choices = "choices=('cycles', 'buffer_cycles', 'identity', 'outside', 'hidden_click', 'toggle', 'mutation_content', 'mutation_input_region', 'mutation_hold_exit')"
|
||||
assert original.count(old_choices) == 1
|
||||
adapted = original.replace(old_choices, new_choices)
|
||||
anchor = " if args.kind == 'cycles':\n"
|
||||
assert adapted.count(anchor) == 1
|
||||
scenario_code = ''' if args.kind == 'mutation_content':
|
||||
# WHY: a client buffer replacement must not be mistaken for a
|
||||
# geometry/clip mutation merely because it creates output damage.
|
||||
snapshot('content-before')
|
||||
client_command('recolor_top')
|
||||
time.sleep(.25)
|
||||
snapshot('content-after')
|
||||
record('mutation_content', owner_id=owner()['id'],
|
||||
top_maps=count('TOPLEVEL MAP'))
|
||||
elif args.kind == 'mutation_input_region':
|
||||
# CONTRACT: no XDG popup/grab is active. Both hits address the same
|
||||
# global point; the client log proves actual toplevel delivery.
|
||||
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)
|
||||
inject('move', x, y)
|
||||
inject('right', x, y)
|
||||
wait(lambda: count('POINTER_BUTTON target=TOPLEVEL') >= 1)
|
||||
snapshot('region-full-before')
|
||||
client_command('region_empty_top')
|
||||
inject('move', x, y)
|
||||
inject('right', x, y)
|
||||
snapshot('region-empty')
|
||||
client_command('region_full_top')
|
||||
before = count('POINTER_BUTTON target=TOPLEVEL')
|
||||
inject('move', x, y)
|
||||
inject('right', x, y)
|
||||
wait(lambda: count('POINTER_BUTTON target=TOPLEVEL') > before)
|
||||
snapshot('region-full-after')
|
||||
record('mutation_input_region', point=[x,y], owner_id=o['id'])
|
||||
elif args.kind == 'mutation_hold_exit':
|
||||
# CONTRACT: terminate the private compositor from the runner after a
|
||||
# normal event-loop present has intentionally retained one extra lock.
|
||||
# Never destroy an output inside its own callback.
|
||||
open_chain('hold-exit')
|
||||
wait(lambda: 'ASI_TEST_HOLD id=1' in (run / 'compositor.log').read_text())
|
||||
record('hold_exit_requested', first_hold_retained=True)
|
||||
elif args.kind == 'cycles':
|
||||
'''
|
||||
adapted = adapted.replace(anchor, scenario_code)
|
||||
generated = lab / 'mutation-run-generated.py'
|
||||
generated.write_text(adapted)
|
||||
kind = 'mutation_' + args.scenario
|
||||
flags = ['--asi-mode', '1', '--test-hold'] if args.scenario == 'hold_exit' else ['--asi-mode', '0']
|
||||
raise SystemExit(subprocess.call([sys.executable, str(generated), str(lab),
|
||||
args.mode, kind, *flags, '--max-seconds', '120',
|
||||
'--max-captures', '12']))
|
||||
Loading…
Reference in a new issue