docs(mosaic): preserve saved target crossing evidence

This commit is contained in:
fy59 2026-10-12 18:48:32 +02:00
parent 642c61aaaf
commit 75f057d471
14 changed files with 1253 additions and 1 deletions

View file

@ -4,6 +4,10 @@
### STEP20C experimental evidence
- Local target-crossing saved-buffer probe: one measured signed camera delta placed the real target source and saved node across x=0. The target node rendered within H1's clip and retained its SHM pixels; H2 showed no target color but had a repeated 253-pixel reference difference. Mosaic E2 timeout stopped through stale fixture pointer correlation. PARTIALLY_FEASIBLE, architecture UNDECIDED; no production change, commit or push for this campaign.
- Freeze audit of that target crossing: the repeated H2 difference is one filled 11×23 rectangle with Kitty cursor/background colors in pristine and Mosaic runs, strongly compatible with cursor blinking but not causally proven. The E2 visible bound at x=-30 matches the applied tiled clip narrowed by gaps. Timeout/input remain untested.
- Preserved saved-transaction-buffer probe: real held XDG configures produced nonempty saved scene buffers; private saved nodes entered rendering and sampled ACK/timeout paths removed their trees. Mosaic's target was outside H1 after the applied camera reached 1760, so partial-boundary clipping is NOT EXERCISED and the verdict is INCONCLUSIVE. Three distinct executed runner copies and the private-standard fixture timeout are retained; no production change.
- Local static-R90 partial-popup probe: a legal nested B popup crossed the Mosaic viewport by 72 logical pixels; 46,455 B pixels and a real button survived on H1, while H2 was pixel-identical to its reference and delivered no B button. Both standard controls showed 35,360 B pixels and a B button on H2. A crossed visually, but its forbidden-side input was not independently clicked. PARTIALLY_FEASIBLE for the tested matrix; no production change or phase-B commit/push.

View file

@ -288,3 +288,15 @@ The experimental Mosaic tiled clip is an ancestor of the view tree, so a
saved tree has a plausible inherited path. The measured target was outside
H1 after the applied camera reached 1760, preventing a boundary-clipping
conclusion. This remains INCONCLUSIVE, with no production architecture change.
The separate [target-crossing saved-buffer probe](design/STEP20C_SAVED_BUFFER_TARGET_CROSSING_PROBE.md)
then measured the actual source and saved scene buffer crossing x=0 after a
single calculated camera correction. Its target saved node rendered with a
visible region ending at x=0 and retained target pixels on H1; no target SHM
color appeared on H2. A 253-pixel whole-H2 reference difference and incomplete
Mosaic timeout/input/damage/lifetime evidence limit the result to
PARTIALLY_FEASIBLE and architecture UNDECIDED. No production contract changes.
Offline freeze analysis strongly supports a blinking Kitty block cursor for
the repeated H2 reference difference, without proving its cause. E2's
`x=-30` visible bound matches the applied tiled clip narrowed by gaps; it is
not evidence of a distinct content clip. Target timeout cleanup remains open.

View file

@ -229,3 +229,18 @@ clipped input, transaction-scoped damage, destruction while pending and
reference lifetime after texture conversion remain unqualified. One
private-standard episode failed through fixture timing. STEP20C remains
**NEXT** and production multi-output **NOT YET SUPPORTED**.
The [new target-crossing probe](design/STEP20C_SAVED_BUFFER_TARGET_CROSSING_PROBE.md)
is **PARTIALLY_FEASIBLE** for one real saved target that crossed x=0 and
rendered only its permitted H1 portion in the sampled ACK episode. H2 had no
target-color pixels but differed from its whole-image reference by 253 pixels
of unproven cause, also seen in pristine controls. A stale fixture pointer
stopped Mosaic E2 before natural timeout; target input, complete damage and
GPU reference lifetime are unqualified. STEP20C remains **NEXT** and
production multi-output **NOT YET SUPPORTED**.
Offline RGB analysis found the same filled 11×23 `#F5E0DC`→`#303038`
rectangle in pristine and Mosaic H2 comparisons. Kitty's recorded local
block-cursor color and blink setting strongly support a dynamic reference,
but no effective cursor phase was logged. Whole-H2 equality is still
unproven; the new timeout probe must use a static reference.

View file

@ -45,7 +45,9 @@ The [root README](../README.md) is the public summary. For a specific topic, use
[R90 source fixture](../tooling/probes/step20c-rotated-partial-popup/README.md) preserves the published experimental inputs.
25. [STEP20C saved transaction buffer probe](design/STEP20C_SAVED_TRANSACTION_BUFFER_PROBE.md): real saved nodes and lifetime evidence, but target partial-overflow geometry NOT EXERCISED; result INCONCLUSIVE.
[Saved-buffer source fixture](../tooling/probes/step20c-saved-transaction-buffer/README.md) preserves the experimental inputs.
26. [STEP20A](STEP20A.md) and [STEP20B](STEP20B.md): historical evidence.
26. [STEP20C saved target crossing probe](design/STEP20C_SAVED_BUFFER_TARGET_CROSSING_PROBE.md): measured saved target crossing and clipped H1 render; H2 reference difference and incomplete timeout/input gate keep it PARTIALLY_FEASIBLE.
[Target-crossing source fixture](../tooling/probes/step20c-saved-buffer-target-crossing/README.md) remains local and experimental.
27. [STEP20A](STEP20A.md) and [STEP20B](STEP20B.md): historical evidence.
## Project
@ -94,6 +96,7 @@ The [root README](../README.md) is the public summary. For a specific topic, use
| [STEP20C effects/damage probe](design/STEP20C_EFFECTS_DAMAGE_PROBE.md) | Experimental, independent private replay | PARTIALLY_FEASIBLE for visible blur, excluded sampled shadow overflow, settled camera restoration and sampled damage; shadow interior, optimized cache, popup effects and saved buffers remain unqualified |
| [STEP20C static R90 partial-popup probe](design/STEP20C_ROTATED_PARTIAL_POPUP_CLIP_PROBE.md) | Experimental evidence | PARTIALLY_FEASIBLE; sampled B crossing clips under R90, independent A input remains untested |
| [STEP20C saved transaction buffer probe](design/STEP20C_SAVED_TRANSACTION_BUFFER_PROBE.md) | Experimental, local evidence | INCONCLUSIVE; real saved nodes rendered, target partial crossing not exercised |
| [STEP20C saved target crossing probe](design/STEP20C_SAVED_BUFFER_TARGET_CROSSING_PROBE.md) | Experimental, local evidence | PARTIALLY_FEASIBLE; target saved node crossed and rendered within H1, but whole-H2 reference and timeout/input evidence remain open |
A proposal does not supersede accepted canonical contracts or assert implementation status. Its open feasibility items must be resolved before implementation.
@ -141,3 +144,14 @@ EXPERIMENTAL / INCONCLUSIVE. Real held XDG configures created nonempty saved
scene buffers; private render entries and ACK/timeout removal were observed.
The Mosaic target was wholly off H1 after an applied relative camera scroll,
so the partial-boundary clipping gate was NOT EXERCISED. STEP20C stays NEXT.
[Saved target crossing probe](design/STEP20C_SAVED_BUFFER_TARGET_CROSSING_PROBE.md)
and [local source fixture](../tooling/probes/step20c-saved-buffer-target-crossing/README.md):
EXPERIMENTAL / PARTIALLY_FEASIBLE. One calculated camera correction produced
a real saved target crossing x=0 with clipped render and retained H1 pixels.
H2's target color was absent, but its whole-image reference differed by 253
pixels; Mosaic target timeout and held input remain unqualified. STEP20C
stays NEXT and production multi-output remains NOT YET SUPPORTED.
The offline freeze audit found an identical filled 11×23 Kitty cursor-color
rectangle across pristine and Mosaic captures. Cursor blinking is strongly
supported, not causally proven; a static H2 reference is the next gate.

View file

@ -231,3 +231,20 @@ authorized saved-buffer experiment must calibrate the applied camera and
target crossing before the first transaction. Optimized blur/cache and
SceneFX ABI/package remain separate unqualified gates. STEP20C stays
**NEXT**, multi-output **NOT YET SUPPORTED**.
The independent [saved target crossing campaign](design/STEP20C_SAVED_BUFFER_TARGET_CROSSING_PROBE.md)
used one measured signed camera correction and observed the actual target
saved node cross x=0, render inside H1 and retain permitted pixels. H2 showed
no target color, but whole-image equality failed by a repeated 253-pixel
reference difference. Mosaic timeout cleanup and held input remain untested
after an E2 fixture pointer error. Result **PARTIALLY_FEASIBLE**, architecture
**UNDECIDED**. The next narrow gate should first resolve the H2 reference
artifact and complete a fresh target timeout/input observation; optimized
blur/cache and SceneFX ABI/package remain later separate gates. STEP20C is
still **NEXT** and multi-output **NOT YET SUPPORTED**.
The saved-target freeze audit identifies the repeated 253-pixel H2 difference
as strongly compatible with Kitty's blinking block cursor, not causally
proven. A separate static-reference Mosaic timeout/input gate is authorized
before optimized blur/cache and SceneFX ABI/package work. No production
readiness or changed UX contract follows.

View file

@ -0,0 +1,243 @@
# STEP20C saved target boundary crossing probe
**EXPERIMENTAL / PARTIALLY_FEASIBLE, measured 2026-10-12.** STEP20C remains NEXT and
production multi-output Mosaic is NOT YET SUPPORTED. This is a new, independent
campaign following the [saved transaction buffer freeze](STEP20C_SAVED_TRANSACTION_BUFFER_PROBE.md),
whose verdict remains INCONCLUSIVE. No Mosaic production, cumulative patch,
package, UX or SceneFX algorithm change is authorized here.
## Scientific identity and source path
The target is the XDG toplevel with `app_id=org.mosaic.SavedBufferProbe`.
Its Sway tiled container, borrowed workspace tiling row, view scene tree,
live content tree, temporary saved tree and saved scene buffer are distinct.
The source buffer of this view must be identified before the transaction.
An output buffer or optimized blur cache is never evidence for that source.
`transaction_commit` configures a changed-size visible view when
`should_configure` holds, then `view_save_buffer` creates a sibling saved tree
under `view->scene_tree`. `view_save_buffer_iterator` visits enabled live
buffers via `wlr_scene_node_for_each_buffer`, creates saved scene nodes via
`wlr_scene_buffer_create`, and calls `wlr_scene_buffer_set_buffer` with the
source buffer. Current live content is disabled and the saved tree enabled.
`transaction_progress` applies on readiness or `handle_timeout`; the normal
`apply_container_state` path calls `view_remove_saved_buffer`. SceneFX's
`scene_node_intersect_clip` intersects ancestral tree clips for visible/damage
regions and `scene_node_point_inside_clip` guards node-at; `scene_entry_render`
is the actual render-entry observation. This source path warrants a test, not
a clipping conclusion. The target node ID must be correlated from source to
saved creation to render entry and destruction. Parentage and enabled state
must be recorded. Pointer equality alone does not prove all GPU references
were released.
## Geometry gate before reservation
Use only two normal scale-1 headless outputs: H1 `(-800,0),800x600` and H2
`(0,0),800x600`. Map the deterministic C17 XDG client and ordinary reference
clients, settle focus, query actual outputs and workspace Mosaic state, and
measure the target **source scene buffer**: node pointer, source buffer
pointer, global node coordinates, destination width/height, source box,
transform, owner container/view, applied camera and tiled clip. A container
rect alone is insufficient. The previous runner issued relative
`layout mosaic scroll 400` and reached applied scroll 1760, leaving its target
outside H1. Here `desired_scroll` is computed from the measured source buffer
left/right and applied `current_scroll`; issue exactly one signed
`delta=desired_scroll-current_scroll`, then remeasure. Require
`left<0<right`, preferably at least 64 logical pixels on each side, a visible
H1 intersection and a theoretical H2 intersection. The source must be
nonempty, have a tiled Mosaic ancestor clip, and remain stable after focus.
If any gate fails, stop that run before reserving an episode. Do not scan
camera positions or create a second lab to evade the budget.
The transaction trigger may alter pending dimensions. During the hold,
remeasure the **saved** scene buffer and require all of:
`SAVED_TARGET_CREATED`, `SAVED_TARGET_ENABLED`,
`SAVED_TARGET_BUFFER_NONEMPTY`, `SAVED_TARGET_CROSSES_X0`, and
`SAVED_TARGET_RENDER_ENTRY_OBSERVED`. Correlate exact view, owner, source
buffer and saved node. If the actual saved node does not cross, classify the
episode NOT EXERCISED even if another saved node renders.
## Episodes, observations and bounds
Use pinned SwayFX 0.6 `fd71a6bdc061bd633b488ae7b83e8a6981d22f86`,
private Mosaic `cd550382b311efdbfe3ed892f4e1d281d852e754`, SceneFX 0.5
`37ccd723bef49e6891156ffafce8f549f01446cc` and wlroots 0.20.2.
Reapply the preserved clipping and trace patches without changing their bytes.
Any additional passive trace is a separate hashed private patch. Build and
run only in one disposable `/tmp` laboratory, with private 0700 runtime,
Wayland and Sway IPC sockets. `-Dtxn-timeout=2500` is a lab policy only; source
parsing and equal use in comparable modes must be confirmed. Do not use
`-Dtxn-wait`, `-Dnoatomic`, direct save calls, compositor sleeps or altered
pending transaction counts.
At most one run each: C2 private Mosaic (priority), C0 pristine splith and C1
private splith. Each has at most two reserved episodes: E1 real configure
hold then latest-valid-serial ACK and commit before timeout; E2 a second real
hold until natural timeout with the client alive and pings serviced. The
ordinary `gaps inner current set` trigger is acceptable only when a changed
target configure and waiting instruction are observed. During E1 collect the
target saved node, render entry and minimal capture, then release promptly;
no six pointer injections during the hold. Observe applied transaction,
saved-tree removal and live-content return in both episodes.
Define H1 permitted and H2 forbidden regions before pixel analysis. Require
target SHM color on H1 while the saved node renders, no target color on H2,
and whole-H2 RGB equality against its reference; retain every difference.
Sample allowed and forbidden pointer points only if the hold window permits.
Distinguish `wlr_scene_node_at`, descriptor/surface association, seat notice
and client delivery; a saved scene buffer need not deliver clicks to the
held client. Record available damage around creation, activation, removal and
new live image, native commits/presents when traced, and do not infer every
intermediate frame from stabilized PNGs. Do not compare unrelated clocks.
Mapped-view destruction is NOT TESTED unless separately shown safe and within
budget. No rotation, scale, migration, resize, popup, effects or camera
hide/return pair belongs to this campaign.
The locked persistent ledger permits three runs, two episodes per mode, one
calculated camera adjustment in the Mosaic run, at most 16 captures per run
and 48 overall, and 150 seconds per run. Reserve before an episode; a failed
reservation is consumed and never refunded. Preserve the exact runner copy,
raw logs, outputs, captures, exit codes, structured JSON and SHA-256 source
and evidence manifests. Offline ledger tests precede any run. Stop early on
uncalibrated geometry, a noncrossing saved target, or an incident; preserve
the facts and do not call placement error BLOCKED.
FEASIBLE requires correct target identity/crossing/render, permitted H1 pixels,
no forbidden H2 pixels/hits, ACK and timeout cleanup and no blocking standard
regression. PARTIALLY_FEASIBLE requires crossing and rendering with limited
input, lifetime or damage evidence. INCONCLUSIVE covers absent target
crossing/render or inseparable identities. BLOCKED requires a reproduced
forbidden pixel/hit, lifetime defect, crash or blocking regression. Any
unstudied matrix cell remains NOT TESTED. Neither an experimental success nor
a compile qualifies STEP20C production.
## Measured campaign and incidents
The independent LAB is `/tmp/swayfx-mosaic-nested-grab-eVtLtpYd`. Its private
passive geometry patch is `target-geometry-trace.patch`, SHA-256
`7bbb27bc8617645e1acfd0589a410d470426363bd8a108bd43fc1006747f012e`.
The old saved-buffer Sway and SceneFX trace patches retained their published
bytes. The private build used `-Dtxn-timeout=2500` in all three modes; source
parses `txn-timeout=` into `server.txn_timeout_ms`, whose default is 200 ms.
All outputs were queried as H1 `(-800,0),800x600` and H2 `(0,0),800x600`,
scale 1, normal. The target's workspace was ID 6 on H1, container ID 8,
view `0x55997bcb85b0`; pointers identify objects only within this process.
Before correction the applied camera read **1760** and the actual source
scene buffer was `[-1918,-1282)` with 636×573 destination/source size,
normal transform and source box `(0,0,636,573)`. The fixture computed
`desired_scroll=160` and issued exactly one relative delta **−1600**.
Afterward `get_tree` read scroll **160** and the same source node/buffer at
`[-318,318)`, y=25..598. H1's applied tiled scene was `(-160,0)` with clip
`(160,0,800,600)`, equivalent to global x `[-800,0)`. No subsequent focus
action moved the camera. The 318-pixel permitted and theoretical forbidden
horizontal portions exceed the 64-pixel preflight target. This run passed
source preflight before E1 reservation.
In E1, a genuine held configure (serial 50) followed a changed target size.
The target saved tree `0x55997bcecd00` was created under its view tree,
and target saved node `0x55997bd6a560` reused source buffer
`0x55997bd4ec00`. Its measured global box was `[-318,318)×[25,598)`;
`content_enabled=0`, `saved_enabled=1`. The **same node** entered `SB_RENDER`
with global visible extent `[-318,0)×[25,598)` and damage extent in output
coordinates `[(482,25),(800,598))`. The held H1 capture contained **182,214**
pixels of the target's prior SHM color `(32,208,160)`; the H2 capture
contained **zero** of that color. The client ACKed the real serial 50 and
committed a new buffer before 2500 ms; the target saved tree was destroyed,
and the applied H1 capture contained the new target color. This is direct
partial saved-target crossing and render evidence, rather than evidence from
the other three saved nodes in the transaction.
Whole-H2 RGB comparison did **not** equal its reference during E1: 253 pixels
changed in `[4,35,15,58)`. Each changed pixel went from `(245,224,220)` in
the reference to `(48,48,56)` in the held image. The same count, box and color
transition appeared in the new pristine control after timeout and both old
pristine/Mosaic final-timeout comparisons. It is consistent with a
reference-specific disappearing shape, but its cause is **UNDECIDED**. It is
not target SHM color, and this evidence does not establish a Mosaic leak.
The whole-H2 equality requirement nevertheless remains unmet.
E2 created a new saved buffer after E1's ACK, as the ordinary client commit
changed its source pointer. The new target snapshot was observed at
`[-258,378)×[55,568)`, and its own node `0x55997bcea350` entered render
with visible extent `[-258,-30)×[55,568)`. The **executed runner** still
compared that new pointer with the pre-E1 source pointer and stopped with
`actual target saved buffer does not cross x=0`. That error is misleading:
the trace proves crossing; the failed condition was stale buffer identity.
The runner was corrected locally to remeasure before each episode, but the
exact executed copy remains in `runs/*/fixture-source.py`. No Mosaic E2
capture or natural timeout was obtained; shutdown follows the fixture
exception. Do not treat this as a compositor timeout or clipping failure.
The run was not repeated and both episode reservations remain spent.
C0 (instrumented pristine SwayFX source, unmodified SceneFX behavior) and C1
(private standard `splith`) completed their held ACK and natural 2500 ms
timeout episodes, with saved-tree cleanup. Their standard tiling geometry is
not camera-equivalent to C2. Neither had target-color pixels on H2 during its
ACK hold. No blocking standard regression was demonstrated. The new C0 final
timeout image showed the same 253-pixel H2 difference; C1's H2 remained
identical throughout. C2's target input during hold, surface/client delivery,
complete damage and native buffer/texture release remain **NOT TESTED** or
unqualified. Mapped-view destruction, output changes, effects and intermediate
frames were outside the matrix.
The persistent ledger reads **3/3 runs**, **6/6 episode reservations**,
**26/48 captures** (C0 10, C1 10, C2 6), and **1/1** calculated Mosaic
camera adjustment. All run durations were below 150 seconds. No extra run or
camera retry is authorized. The raw runs, structured results, runner copies,
exit codes, build log and SHA-256 manifests are under the LAB. The result is
`SWAYFX_MOSAIC_SAVED_BUFFER_TARGET_CROSSING_PROBE=PARTIALLY_FEASIBLE`:
target crossing/render and permitted H1 pixels are established, while whole-H2
equality, input, full damage/lifetime and Mosaic target timeout cleanup are
unqualified. Architectural conclusion: `UNDECIDED`. No SceneFX algorithm or
production Mosaic correction is justified by a reproduced counterexample.
The final source manifest covers **913** entries (including the three private
source trees and fixture), SHA-256
`816b55d94936fa32c7272845cd2f0514a55ad73b3cb04d32d442d5910e3da09b`.
The evidence manifest covers **99** raw and derived entries, SHA-256
`0ea45da93396810cf63c451f64d47101c4d4b8c7fb27900043494a0ec591096e`.
Both passed entry-by-entry verification. They exclude the report itself, whose
wording was synchronized after the measurements.
## Freeze audit: H2 reference and E2 clip (offline, no replay)
The exact executed runner is the manifest-covered `fixture-source.py` in each
run (SHA-256 `51eb1274b757701e80f54004016944b7fa23f731d194193d1baabbe25a6cdc82`).
The preserved local `run.py` differs: it remeasures `TARGET_SOURCE` before
each episode, so E2 no longer compares a post-ACK source buffer against the
pre-E1 pointer. This fixes the identified correlation condition in source;
the corrected runner was **NOT EXECUTED** in this exhausted campaign.
An offline full-RGB comparison of the old and new LABs found five nonempty
H2-reference differences: old pristine timeout-applied, old Mosaic
timeout-applied, new pristine timeout-applied, and new Mosaic ACK-held and
ACK-applied. **Each** has exactly 253 changed pixels filling the complete
rectangle `[4,15)×[35,58)` (11×23), with every pixel changing from
`#F5E0DC` `(245,224,220)` to `#303038` `(48,48,56)`. No other H2 pixel
differs in those comparisons, and none of the changed pixels is a target SHM
color. The corresponding new-reference-to-image capture-start intervals are
about 2.725 s for pristine timeout, 0.083 s for Mosaic ACK-held and 0.180 s
for Mosaic ACK-applied; the old pristine/Mosaic timeout intervals are about
1.455/1.476 s. Other sampled H2 pairs remained equal. These timings can
sample either phase of a blinking cursor; they do not prove a blink event.
The H2 reference process was Kitty with the fixture argument
`background=#303038`. The local `~/.config/kitty/kitty.conf` and included
`generated-appearance.conf` predate these runs and contain `cursor_shape
block`, a later `cursor_blink_interval 0.5`, and cursor color `#f5e0dc`.
The matching filled rectangle, exact colors and repeated pristine/Mosaic
observations strongly support a blinking Kitty block cursor as the cause.
The recorded logs do not capture Kitty's effective parsed configuration or
its cursor phase, so **causal confirmation remains open**. Whole-H2 RGB
equality remains historically unsatisfied; no pixels were masked or images
rewritten. The next lab must use a static reference.
The E2 saved target itself crossed x=0. Its render visible extent ended at
`x=-30`, matching the **applied tiled Mosaic clip after E1's 30-pixel gaps**:
H1 logged `tile_scene=-130,30`, local clip `(160,0,740,540)` and global
viewport `[-770,-30)×[30,570)`. The source in `transaction.c` derives clip
width/height from output usable area minus current gaps. Thus the measured
right bound is explained by this outer tiled clip and gap geometry; it does
not establish an additional content/view clip. E2's timeout and cleanup
remain **NOT TESTED** because the fixture stopped before expiration.

View file

@ -0,0 +1,27 @@
# Saved target crossing source fixture
**EXPERIMENTAL / PARTIALLY_FEASIBLE.** See the [report](../../../docs/design/STEP20C_SAVED_BUFFER_TARGET_CROSSING_PROBE.md).
This new campaign has its own locked budget and one disposable `/tmp` LAB.
The prior saved-buffer ledger and captures are read-only historical evidence.
Production source, cumulative patch, package and installed libraries are not
changed. No commit or push is authorized for this fixture.
`prepare.sh` reconstructs the pinned private builds using the preserved
upstream clipping and trace patches, then applies any separately hashed
passive target-geometry instrumentation. It never starts a compositor.
`run.py LAB MODE` uses only private sockets and must pass the source-buffer
geometry preflight before reserving a transaction episode. A failed preflight
ends the run without an exploratory camera retry. The exact runner used is
copied into each run directory. All generated data remain under LAB, outside
Git. Run the offline ledger test before a graphical run.
Completed LAB: `/tmp/swayfx-mosaic-nested-grab-eVtLtpYd`. It used one
calculated Mosaic camera delta, three runs, six episode reservations and 26
captures. The exact executed `fixture-source.py` in each run preserves the
runner before the post-run E2 pointer-correlation fix. Do not replay this
exhausted campaign. The geometry trace patch has SHA-256
`7bbb27bc8617645e1acfd0589a410d470426363bd8a108bd43fc1006747f012e`;
the prior trace patches are referenced by the source manifest. `analyze.py`
regenerates the offline JSON summaries and both manifests without launching
another compositor. Target E1 crossing/render and H1 pixels are measured;
Mosaic E2 timeout cleanup and input remain untested.

View file

@ -0,0 +1,125 @@
"""Offline, bounded analysis of the one target-crossing LAB; no compositor run."""
import argparse
import hashlib
import json
import re
from pathlib import Path
from PIL import Image, ImageChops
parser = argparse.ArgumentParser()
parser.add_argument('lab', type=Path)
args = parser.parse_args()
lab = args.lab.resolve()
results = lab / 'results'
results.mkdir(exist_ok=True)
repo = Path(__file__).resolve().parents[3]
run_dirs = {mode: next((lab / 'runs').glob(f'saved-{mode}-*'))
for mode in ('control', 'private_standard', 'private_mosaic')}
def write(name, value):
(results / f'{name}.json').write_text(json.dumps(value, indent=2, sort_keys=True) + '\n')
def events(mode):
return [json.loads(line) for line in (run_dirs[mode] / 'events.jsonl').read_text().splitlines()]
def diff(a, b):
first = Image.open(a).convert('RGB')
second = Image.open(b).convert('RGB')
d = ImageChops.difference(first, second)
return {'box': d.getbbox(), 'changed_pixels': sum(1 for p in d.get_flattened_data() if p != (0, 0, 0))}
def count_color(path, color):
im = Image.open(path).convert('RGB')
return sum(1 for pixel in im.get_flattened_data() if pixel == color)
budget = json.loads((lab / 'target-budget.json').read_text())
write('budget', budget)
preflight = json.loads((results / 'target-preflight.json').read_text())
write('target-geometry', {'source_before': {k: preflight['before'][k] for k in ('left', 'right', 'width', 'source_size', 'dest', 'src_box', 'transform')},
'source_after': {k: preflight['after'][k] for k in ('left', 'right', 'width', 'source_size', 'dest', 'src_box', 'transform')}})
write('camera-calibration', {k: preflight[k] for k in ('current_scroll', 'desired_scroll', 'delta')})
mosaic = run_dirs['private_mosaic']
me = events('private_mosaic')
saved = [e for e in me if e['kind'] == 'saved_target']
log = (mosaic / 'compositor.log').read_text()
client = (mosaic / 'client.log').read_text()
ack_node = saved[0]['node']
ack_buffer = saved[0]['buffer']
render = [line for line in log.splitlines() if 'SB_RENDER ' in line and f'node={ack_node}' in line]
destroy = [line for line in log.splitlines() if 'SB_TREE_DESTROY ' in line and f'view={preflight["after"]["view"]}' in line]
held_color = (32, 208, 160)
pixels = {}
for mode, run in run_dirs.items():
ref = run / 'right-reference.png'
pixels[mode] = {p.stem: diff(ref, p) for p in sorted(run.glob('right-*.png'))}
pixels[mode]['target_color_on_h2_held'] = (count_color(run / 'right-ack-held.png', held_color)
if (run / 'right-ack-held.png').exists() else None)
pixels['private_mosaic']['target_color_on_h1_ack_held'] = count_color(mosaic / 'left-ack-held.png', held_color)
pixels['private_mosaic']['target_color_on_h1_reference'] = count_color(mosaic / 'left-reference.png', held_color)
write('pixel-analysis', pixels)
write('saved-tree-identity', {'target_view': preflight['after']['view'], 'target_container': preflight['after']['container'],
'ack_saved_node': ack_node, 'ack_destroy_observations': destroy,
'e2_saved_node': saved[1]['node'] if len(saved) > 1 else None})
write('saved-buffer-identity', {'source_node': preflight['after']['source_node'], 'source_buffer': preflight['after']['source_buffer'],
'ack_saved_buffer': ack_buffer, 'ack_same_source': ack_buffer == preflight['after']['source_buffer'],
'e2_saved_buffer': saved[1]['buffer'] if len(saved) > 1 else None,
'e2_note': 'new source buffer after E1 ACK; executed runner compared it with stale pre-E1 pointer'})
write('scene-parentage', {'ack_creation': [line for line in log.splitlines() if 'SB_TREE_CREATE ' in line and f'view={preflight["after"]["view"]}' in line][-2:],
'ack_saved_node_trace': [line for line in log.splitlines() if 'TARGET_SAVED ' in line and f'node={ack_node}' in line][-1:],
'source_path_contract': 'saved tree under view scene tree; Mosaic tiled clip is ancestor per source audit'})
write('saved-target-crossing', {'e1': saved[0]['global_box'], 'e1_crosses_x0': saved[0]['global_box'][0] < 0 < saved[0]['global_box'][2],
'e2': saved[1]['global_box'] if len(saved) > 1 else None,
'e2_crosses_x0': len(saved) > 1 and saved[1]['global_box'][0] < 0 < saved[1]['global_box'][2],
'e2_status': 'NOT EXERCISED through timeout due to fixture pointer correlation'})
write('transaction-timeline', {mode: [e for e in events(mode) if e['kind'] in ('episode_start', 'saved_target', 'held', 'timeout_observed', 'failure', 'cleanup')]
for mode in run_dirs})
write('configure-ack', {mode: [line for line in (run / 'client.log').read_text().splitlines()
if line.startswith(('HELD ', 'ACK ', 'COMMIT ', 'PONG '))]
for mode, run in run_dirs.items()})
write('render-entries', {'e1_target': render, 'e1_observed': bool(render), 'e2_target': [line for line in log.splitlines()
if len(saved) > 1 and 'SB_RENDER ' in line and f'node={saved[1]["node"]}' in line]})
write('clip-observations', {'e1_target_render_visible': [re.search(r'visible=([^ ]+)', x).group(1) for x in render],
'e1_applied_clip': [line for line in log.splitlines() if 'PI_SCENE output=HEADLESS-1' in line and 'scroll=160 tile_scene=-160,0 clip_valid=1 clip=160,0,800,600' in line][:2],
'source_audit': 'scene_node_intersect_clip and scene_node_point_inside_clip walk ancestors'})
write('pointer-hits', {'status': 'NOT TESTED during target hold', 'reason': 'ACK window prioritized target snapshot and protocol'})
write('client-delivery', {'status': 'NOT TESTED during target hold'})
write('damage-analysis', {'status': 'LIMITED', 'render_damage_e1': [re.search(r'damage=([^ ]+)', x).group(1) for x in render],
'complete_intermediate_frames': 'NOT TESTED'})
write('lifetime', {'e1_target_saved_tree_destroy_observed': bool(destroy), 'destroy_trace': destroy,
'e2_timeout_cleanup': 'NOT TESTED', 'all_gpu_references_released': 'NOT PROVEN',
'mapped_view_destroy_during_transaction': 'NOT TESTED'})
write('timeout', {mode: [line for line in (run / 'compositor.log').read_text().splitlines() if 'timed out' in line]
for mode, run in run_dirs.items()})
write('matrix', {'C2_E1': 'TARGET CROSSING/RENDER/ACK OBSERVED; H2 whole-image mismatch 253 pixels',
'C2_E2': 'TARGET CREATED AND CROSSES; fixture stale-buffer correlation stopped before timeout',
'C0_E1_E2': 'COMPLETED', 'C1_E1_E2': 'COMPLETED',
'target_hold_input': 'NOT TESTED', 'complete_damage': 'NOT TESTED'})
write('summary', {'result': 'PARTIALLY_FEASIBLE', 'architectural_conclusion': 'UNDECIDED',
'target_ack_crossing_and_render': True, 'target_ack_h1_color_pixels': pixels['private_mosaic']['target_color_on_h1_ack_held'],
'target_ack_h2_color_pixels': pixels['private_mosaic']['target_color_on_h2_held'],
'target_ack_h2_full_diff': pixels['private_mosaic']['right-ack-held'],
'target_timeout_cleanup': 'NOT TESTED',
'reason': 'E1 target crossing/render and allowed H1 pixels observed; H2 reference mismatch, input/damage and target E2 cleanup remain unqualified'})
source_files = sorted((repo / 'tooling/probes/step20c-saved-buffer-target-crossing').glob('*'))
source_files += [repo / 'tooling/probes/step20c-saved-transaction-buffer/sway-saved-trace.patch',
repo / 'tooling/probes/step20c-saved-transaction-buffer/scenefx-saved-trace.patch']
for source_root in (lab / 'mosaic', lab / 'standard', lab / 'scenefx'):
source_files.extend(source_root.rglob('*'))
source_files.extend(lab.glob('*.c'))
source_files.extend(lab.glob('*.h'))
source_files = [p for p in source_files if p.is_file()]
def manifest(paths, dest):
dest.write_text(''.join(f'{hashlib.sha256(p.read_bytes()).hexdigest()} {p}\n' for p in sorted(paths)))
manifest(source_files, results / 'source-sha256.txt')
evidence_files = (list((lab / 'runs').glob('**/*')) + list(results.glob('*.json')) +
list(lab.glob('*-log.txt')) +
[lab / 'target-budget.json', lab / 'source-sha256.txt'])
manifest([p for p in evidence_files if p.is_file()], results / 'evidence-sha256.txt')
print('RESULT=PARTIALLY_FEASIBLE', 'RUNS=3', 'CAPTURES=' + str(sum(budget['captures'].values())))

View file

@ -0,0 +1,46 @@
"""One persistent, exclusive ledger for the independent target campaign."""
import fcntl
import json
import os
from pathlib import Path
MODES = ('control', 'private_standard', 'private_mosaic')
def reserve(lab: Path, mode: str, episode: str | None = None,
*, camera: bool = False, capture: bool = False) -> dict:
if mode not in MODES or episode not in (None, 'ack', 'timeout'):
raise ValueError('invalid reservation')
if int(camera) + int(capture) + int(episode is not None) > 1:
raise ValueError('reserve one resource per call')
path = lab / 'target-budget.json'
with path.open('a+') as file:
fcntl.flock(file, fcntl.LOCK_EX)
file.seek(0)
raw = file.read()
state = json.loads(raw) if raw else {
'runs': [], 'episodes': [], 'camera': [], 'captures': {m: 0 for m in MODES}}
if camera:
if mode != 'private_mosaic' or mode not in state['runs'] or state['camera']:
raise RuntimeError('camera budget exhausted')
state['camera'].append(mode)
elif capture:
if mode not in state['runs'] or state['captures'][mode] >= 16 or sum(state['captures'].values()) >= 48:
raise RuntimeError('capture budget exhausted')
state['captures'][mode] += 1
elif episode is None:
if mode in state['runs'] or len(state['runs']) >= 3:
raise RuntimeError('run budget exhausted')
state['runs'].append(mode)
else:
key = [mode, episode]
if mode not in state['runs'] or key in state['episodes'] or len(state['episodes']) >= 6:
raise RuntimeError('episode budget exhausted')
state['episodes'].append(key)
file.seek(0)
file.truncate()
json.dump(state, file, sort_keys=True)
file.write('\n')
file.flush()
os.fsync(file.fileno())
return state

View file

@ -0,0 +1,34 @@
#!/bin/sh
# EXPERIMENTAL source-only reconstruction; never launches a compositor.
set -eu
repo=$(CDPATH= cd -- "$(dirname -- "$0")/../../.." && pwd)
fixture="$repo/tooling/probes/step20c-saved-buffer-target-crossing"
old="$repo/tooling/probes/step20c-saved-transaction-buffer"
log=$(mktemp /tmp/saved-target-prepare-XXXXXXXX)
if ! sh "$repo/tooling/probes/step20c-rotated-partial-popup/prepare.sh" > "$log" 2>&1; then
printf 'BUILD_FAILED log=%s\n' "$log" >&2
tail -40 "$log" >&2
exit 1
fi
lab=$(sed -n 's/^LAB=//p' "$log" | head -1)
test -n "$lab" && test -d "$lab"
for source in standard mosaic; do
git -C "$lab/$source" apply --check "$old/sway-saved-trace.patch"
git -C "$lab/$source" apply "$old/sway-saved-trace.patch"
done
git -C "$lab/scenefx" apply --check "$old/scenefx-saved-trace.patch"
git -C "$lab/scenefx" apply "$old/scenefx-saved-trace.patch"
if test -f "$fixture/target-geometry-trace.patch"; then
for source in standard mosaic; do
git -C "$lab/$source" apply --check "$fixture/target-geometry-trace.patch"
git -C "$lab/$source" apply "$fixture/target-geometry-trace.patch"
done
fi
meson compile -C "$lab/scenefx-build" -j 8 >> "$log" 2>&1
meson compile -C "$lab/standard-build" -j 8 >> "$log" 2>&1
meson compile -C "$lab/mosaic-build" -j 8 >> "$log" 2>&1
cc -std=c17 -Wall -Wextra -Werror -Wno-unused-parameter -I"$lab" \
"$fixture/saved-client.c" "$lab/xdg-shell-protocol.c" -o "$lab/saved-client" \
$(pkg-config --cflags --libs wayland-client) >> "$log" 2>&1
cp "$log" "$lab/saved-target-build-log.txt"
printf 'LAB=%s\nBUILD_LOG=%s\n' "$lab" "$log"

View file

@ -0,0 +1,343 @@
"""One reserved headless run with a real held XDG configure per episode."""
import argparse
import json
import os
import re
from pathlib import Path
import shutil
import signal
import subprocess
import tempfile
import time
from PIL import Image
from budget import reserve
parser = argparse.ArgumentParser()
parser.add_argument('lab', type=Path)
parser.add_argument('mode', choices=('control', 'private_standard', 'private_mosaic'))
args = parser.parse_args()
lab = args.lab.resolve()
if not (lab / 'saved-client').is_file():
parser.error('prepared LAB required')
state = reserve(lab, args.mode)
run = lab / 'runs' / f'saved-{args.mode}-{time.time_ns()}'
run.mkdir(parents=True, mode=0o700)
(run / 'fixture-source.py').write_text(Path(__file__).read_text())
runtime = Path(tempfile.mkdtemp(prefix='mst-', dir='/tmp'))
(run / 'runtime-path').write_text(str(runtime) + '\n')
(run / 'config').write_text(
'output HEADLESS-1 resolution 800x600 scale 1 transform normal position -800 0\n'
'output HEADLESS-2 resolution 800x600 scale 1 transform normal position 0 0\n'
'workspace 1 output HEADLESS-1\nworkspace 2 output HEADLESS-2\n'
'focus_follows_mouse no\n')
env = os.environ.copy()
for key in ('SWAYSOCK', 'I3SOCK', 'WAYLAND_DISPLAY', 'LD_LIBRARY_PATH'):
env.pop(key, None)
env.update(XDG_RUNTIME_DIR=str(runtime), WLR_BACKENDS='headless',
WLR_HEADLESS_OUTPUTS='2', WLR_RENDERER='pixman', SAVED_TRACE='1', SAVED_TARGET_TRACE='1')
binary = lab / ('standard-build/sway/sway' if args.mode == 'control'
else 'mosaic-build/sway/swayfx-mosaic')
if args.mode != 'control':
env['LD_LIBRARY_PATH'] = str(lab / 'scenefx-build')
started = time.monotonic()
captures = 0
events = []
processes = []
logs = []
failure = None
compositor = None
def record(kind, **kw):
event = {'ns': time.monotonic_ns(), 'kind': kind, **kw}
events.append(event)
with (run / 'events.jsonl').open('a') as file:
file.write(json.dumps(event) + '\n')
def spawn(argv, name, *, stdin=False, use_env=None):
file = (run / name).open('w')
logs.append(file)
proc = subprocess.Popen(argv, env=use_env or env, stdout=file,
stderr=subprocess.STDOUT, start_new_session=True,
stdin=subprocess.PIPE if stdin else None, text=stdin)
processes.append(proc)
return proc
def wait(predicate, seconds=8):
limit = time.monotonic() + seconds
while time.monotonic() < limit:
if time.monotonic() - started > 150:
raise RuntimeError('150 second run bound exceeded')
value = predicate()
if value:
return value
if compositor and compositor.poll() is not None:
raise RuntimeError(f'compositor exit {compositor.returncode}')
time.sleep(.025)
raise RuntimeError('wait timed out')
def ipc(*argv):
result = subprocess.run(['swaymsg', '-s', str(sock), *argv], env=env,
capture_output=True, text=True, timeout=5, check=True)
value = json.loads(result.stdout)
record('ipc', argv=list(argv), result=value)
return value
def command(value):
result = ipc(value)
if not result or not all(x.get('success') for x in result):
raise RuntimeError(f'IPC rejected {value}: {result}')
def find(node, predicate):
if isinstance(node, dict):
if predicate(node):
return node
for child in node.get('nodes', []) + node.get('floating_nodes', []):
match = find(child, predicate)
if match:
return match
return None
def client_log():
return (run / 'client.log').read_text()
def compositor_log():
return (run / 'compositor.log').read_text()
def client_command(value):
marker = f'COMMAND {value} DONE'
before = client_log().count(marker)
client.stdin.write(value + '\n')
client.stdin.flush()
wait(lambda: client_log().count(marker) > before)
record('client_command', value=value)
def inject(action, x, y):
if not -800 <= x < 800 or not 0 <= y < 600:
raise ValueError('pointer point outside applied outputs')
marker = f'INJECT_DONE {action} {x + 800},{y}'
before = (run / 'pointer.log').read_text().count(marker)
pointer.stdin.write(f'{action} {x + 800} {y} 1600 600\n')
pointer.stdin.flush()
wait(lambda: (run / 'pointer.log').read_text().count(marker) > before)
record('pointer', action=action, global_point=[x, y],
client_tail=client_log().splitlines()[-5:])
def snapshot(tag):
global captures
outputs = ipc('-t', 'get_outputs')
actual = {o['name']: (o['rect']['x'], o['rect']['y'],
o['rect']['width'], o['rect']['height']) for o in outputs if o['active']}
if actual != {'HEADLESS-1': (-800, 0, 800, 600), 'HEADLESS-2': (0, 0, 800, 600)}:
raise RuntimeError(f'output geometry invalid: {actual}')
for name, side in (('HEADLESS-1', 'left'), ('HEADLESS-2', 'right')):
reserve(lab, args.mode, capture=True)
path = run / f'{side}-{tag}.png'
before = time.monotonic_ns()
subprocess.run(['grim', '-s', '1', '-o', name, str(path)], env=env,
check=True, timeout=5, stdout=subprocess.DEVNULL)
after = time.monotonic_ns()
captures += 1
size = Image.open(path).size
if size != (800, 600):
raise RuntimeError(f'PNG dimensions invalid: {size}')
record('capture', tag=tag, output=name, path=path.name,
before_ns=before, after_ns=after, size=size)
(run / f'tree-{tag}.json').write_text(json.dumps(ipc('-t', 'get_tree'), indent=2) + '\n')
def source_measure():
tree = ipc('-t', 'get_tree')
workspace = find(tree, lambda n: n.get('type') == 'workspace' and n.get('name') == '1')
owner = find(tree, lambda n: n.get('app_id') == 'org.mosaic.SavedBufferProbe')
if not owner or not workspace:
raise RuntimeError('target view or workspace absent')
lines = [line for line in compositor_log().splitlines() if 'TARGET_SOURCE ns=' in line]
if not lines:
raise RuntimeError('target source scene buffer unmeasured')
line = lines[-1]
def field(name):
match = re.search(rf'{name}=([^ ]+)', line)
if not match:
raise RuntimeError(f'missing {name}: {line}')
return match.group(1)
xy = [int(x) for x in field('global').split(',')]
size = [int(x) for x in field('source_size').split(',')]
dest = [int(x) for x in field('dest').split(',')]
width = dest[0] or size[0]
result = {'view': field('view'), 'container': field('con'),
'source_node': field('node'), 'source_buffer': field('buffer'),
'global_valid': field('global_valid') == '1',
'left': xy[0], 'top': xy[1], 'width': width,
'right': xy[0] + width, 'source_size': size, 'dest': dest,
'src_box': field('src_box'), 'transform': field('transform'),
'enabled': field('enabled'), 'owner': owner,
'workspace': workspace, 'raw': line}
record('source_measure', measurement=result)
return result
def preflight_target():
global target_source
before = source_measure()
target_source = before
if not before['global_valid'] or before['width'] <= 0 or before['enabled'] != '1':
raise RuntimeError('source buffer geometry invalid')
if args.mode != 'private_mosaic':
record('standard_preflight', source=before)
return
row = before['workspace']['mosaic']['rows'][0]
current = row['scroll_x']
desired_left = -max(64, min(before['width'] // 2, before['width'] - 64))
delta = int(round(before['left'] - desired_left))
desired = current + delta
if before['width'] < 128 or desired < 0:
raise RuntimeError(f'crossing unreachable from measured source: current={current} delta={delta}')
record('camera_calculation', current_scroll=current, desired_scroll=desired,
delta=delta, desired_left=desired_left, source=before)
if delta:
reserve(lab, args.mode, camera=True)
command(f'layout mosaic scroll {delta}')
time.sleep(.2)
after = source_measure()
target_source = after
applied = after['workspace']['mosaic']['rows'][0]['scroll_x']
record('camera_applied', scroll=applied, source=after)
if applied != desired or after['left'] >= -64 or after['right'] <= 64:
raise RuntimeError(f'target source failed boundary preflight: {after["left"]},{after["right"]}, scroll={applied}')
if after['source_node'] != before['source_node'] or after['source_buffer'] != before['source_buffer']:
raise RuntimeError('source identity changed during camera adjustment')
(lab / 'results').mkdir(exist_ok=True)
(lab / 'results' / 'target-preflight.json').write_text(json.dumps({
'before': before, 'after': after, 'current_scroll': current,
'desired_scroll': desired, 'delta': delta}, indent=2) + '\n')
def episode(kind, gaps):
global target_source
# WHY: an ACK commits a new source buffer before the next episode.
# CONTRACT: remeasure it before reserving; never reuse the first pointer.
target_source = source_measure()
if args.mode == 'private_mosaic' and not (
target_source['left'] < 0 < target_source['right']):
raise RuntimeError('current target source no longer crosses x=0')
reserve(lab, args.mode, kind)
record('episode_start', episode=kind, gaps=gaps)
client_command('hold_next_configure')
old_held = client_log().count('HELD ns=')
old_tree = compositor_log().count('SB_TREE_CREATE')
command(f'gaps inner current set {gaps}')
wait(lambda: client_log().count('HELD ns=') > old_held, 3)
wait(lambda: compositor_log().count('SB_TREE_CREATE') > old_tree, 3)
record('held', client=client_log().splitlines()[-12:],
saved=compositor_log().splitlines()[-30:])
if args.mode == 'private_mosaic':
saved = [line for line in compositor_log().splitlines()
if 'TARGET_SAVED ns=' in line and f'view={target_source["view"]}' in line]
if not saved:
raise RuntimeError('target saved scene buffer absent')
line = saved[-1]
match = re.search(r'node=(0x[0-9a-f]+).*buffer=(0x[0-9a-f]+).*global_valid=(\d+) global=(-?\d+),(-?\d+).*source_size=(\d+),(\d+) dest=(\d+),(\d+).*enabled=(\d+)', line)
if not match:
raise RuntimeError(f'target saved trace incomplete: {line}')
node, buffer, valid, x, y, sw, sh, dw, dh, enabled = match.groups()
right = int(x) + (int(dw) or int(sw))
record('saved_target', node=node, buffer=buffer, source=target_source,
global_box=[int(x), int(y), right, int(y) + (int(dh) or int(sh))],
raw=line)
if buffer != target_source['source_buffer'] or valid != '1' or enabled != '1' or int(x) >= 0 or right <= 0:
raise RuntimeError('actual target saved buffer does not cross x=0')
snapshot(f'{kind}-held')
if args.mode == 'private_mosaic':
if not any(f'node={node}' in line for line in compositor_log().splitlines() if 'SB_RENDER ' in line):
raise RuntimeError('target saved node had no render entry')
if kind == 'ack':
old_destroy = compositor_log().count('SB_TREE_DESTROY')
client_command('release_configure')
wait(lambda: compositor_log().count('SB_TREE_DESTROY') > old_destroy, 3)
snapshot('ack-applied')
else:
wait(lambda: 'timed out' in compositor_log().split('SB_TREE_CREATE')[-1], 3)
record('timeout_observed')
snapshot('timeout-applied')
try:
file = (run / 'compositor.log').open('w')
logs.append(file)
compositor = subprocess.Popen([str(binary), '-c', str(run / 'config'),
'-d', '-Dtxn-timeout=2500'], env=env,
stdout=file, stderr=subprocess.STDOUT,
start_new_session=True)
def sockets():
s = next(iter(runtime.glob('sway-ipc.*.sock')), None)
w = next((p for p in runtime.glob('wayland-*') if p.suffix != '.lock'), None)
return (s, w) if s and w else None
sock, way = wait(sockets)
env.update(SWAYSOCK=str(sock), WAYLAND_DISPLAY=way.name)
command('workspace 1')
command('layout mosaic' if args.mode == 'private_mosaic' else 'layout splith')
spawn(['kitty', '--class', 'A', '/usr/bin/sleep', '900'], 'A.log')
wait(lambda: len(find(ipc('-t', 'get_tree'),
lambda n: n.get('type') == 'workspace' and n.get('name') == '1')['nodes']) >= 1)
client = spawn([str(lab / 'saved-client')], 'client.log', stdin=True)
wait(lambda: 'COMMIT ns=' in client_log())
for name in ('C', 'D'):
spawn(['kitty', '--class', name, '/usr/bin/sleep', '900'], f'{name}.log')
time.sleep(.2)
command('workspace 2')
spawn(['kitty', '--class', 'REFERENCE', '-o', 'background=#303038',
'/usr/bin/sleep', '900'], 'reference.log')
wait(lambda: bool(find(ipc('-t', 'get_tree'),
lambda n: n.get('type') == 'workspace' and n.get('name') == '2')['nodes']))
command('workspace 1')
time.sleep(.2)
tree = ipc('-t', 'get_tree')
owner = find(tree, lambda n: n.get('app_id') == 'org.mosaic.SavedBufferProbe')
workspace = find(tree, lambda n: n.get('type') == 'workspace' and n.get('name') == '1')
if not owner or not workspace:
raise RuntimeError('owner/workspace calibration failed')
record('identity', owner=owner, workspace=workspace)
preflight_target()
snapshot('reference')
episode('ack', 30)
episode('timeout', 50)
except Exception as exc:
failure = repr(exc)
record('failure', message=failure)
finally:
for proc in processes + ([compositor] if compositor else []):
if proc.poll() is None:
try:
os.killpg(proc.pid, signal.SIGTERM)
except ProcessLookupError:
pass
for proc in processes + ([compositor] if compositor else []):
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
os.killpg(proc.pid, signal.SIGKILL)
proc.wait(timeout=5)
for file in logs:
file.close()
(run / 'exit-status.json').write_text(json.dumps({
'failure': failure, 'captures': captures,
'duration_seconds': time.monotonic() - started,
'compositor': compositor.returncode if compositor else None,
'children': [{'args': p.args, 'code': p.returncode} for p in processes]},
indent=2) + '\n')
shutil.rmtree(runtime)
record('cleanup', runtime_removed=not runtime.exists())
print(f'RUN={run} FAILURE={failure} CAPTURES={captures}')
if failure:
raise SystemExit(1)

View file

@ -0,0 +1,215 @@
#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <poll.h>
#include <signal.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <time.h>
#include <unistd.h>
#include <wayland-client.h>
#include "xdg-shell-client.h"
/* WHY: the compositor must wait on a real, responsive XDG client.
* CONTRACT: hold only a received configure; ACK the latest received serial.
* INVARIANT: Wayland dispatch and ping replies continue while ACK is held. */
static struct wl_display *display;
static struct wl_compositor *compositor;
static struct wl_shm *shm;
static struct xdg_wm_base *wm;
static struct wl_surface *surface;
static struct xdg_surface *xdg;
static struct xdg_toplevel *toplevel;
static struct wl_seat *seat;
static struct wl_pointer *pointer;
static volatile sig_atomic_t running = 1;
static bool mapped, hold_next, held;
static uint32_t latest_serial;
static int32_t width = 420, height = 320, configured_width = 420, configured_height = 320;
static unsigned generation;
static long long stamp(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (long long)ts.tv_sec * 1000000000LL + ts.tv_nsec;
}
static void draw(void) {
if (configured_width > 0) width = configured_width;
if (configured_height > 0) height = configured_height;
if (width <= 0 || height <= 0 || width > 4096 || height > 4096) {
fprintf(stderr, "INVALID_SIZE %d %d\n", width, height);
exit(2);
}
size_t bytes = (size_t)width * (size_t)height * 4;
char name[] = "/tmp/mosaic-saved-shm-XXXXXX";
int fd = mkstemp(name);
if (fd < 0 || ftruncate(fd, (off_t)bytes) < 0) { perror("shm"); exit(2); }
unlink(name);
uint32_t *pixels = mmap(NULL, bytes, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (pixels == MAP_FAILED) { perror("mmap"); exit(2); }
uint32_t color = (generation++ % 2) ? 0x0020d0a0 : 0x002344cc;
for (size_t i = 0; i < bytes / 4; ++i) pixels[i] = color;
struct wl_shm_pool *pool = wl_shm_create_pool(shm, fd, (int)bytes);
struct wl_buffer *buffer = wl_shm_pool_create_buffer(pool, 0, width, height,
width * 4, WL_SHM_FORMAT_XRGB8888);
wl_surface_attach(surface, buffer, 0, 0);
wl_surface_damage_buffer(surface, 0, 0, width, height);
wl_surface_commit(surface);
fprintf(stderr, "COMMIT ns=%lld serial=%u size=%dx%d color=%08x\n",
stamp(), latest_serial, width, height, color);
wl_buffer_destroy(buffer);
wl_shm_pool_destroy(pool);
munmap(pixels, bytes);
close(fd);
mapped = true;
}
static void ack_and_commit(void) {
if (!latest_serial) return;
xdg_surface_ack_configure(xdg, latest_serial);
fprintf(stderr, "ACK ns=%lld serial=%u size=%dx%d\n",
stamp(), latest_serial, configured_width, configured_height);
draw();
held = false;
}
static void ping(void *data, struct xdg_wm_base *base, uint32_t serial) {
xdg_wm_base_pong(base, serial);
fprintf(stderr, "PONG ns=%lld serial=%u\n", stamp(), serial);
}
static const struct xdg_wm_base_listener wm_listener = {.ping = ping};
static void role_configure(void *data, struct xdg_toplevel *role,
int32_t w, int32_t h, struct wl_array *states) {
if (w > 0) configured_width = w;
if (h > 0) configured_height = h;
fprintf(stderr, "ROLE_CONFIGURE ns=%lld size=%dx%d\n", stamp(), w, h);
}
static void role_close(void *data, struct xdg_toplevel *role) { running = 0; }
static const struct xdg_toplevel_listener role_listener = {
.configure = role_configure, .close = role_close,
};
static void configure(void *data, struct xdg_surface *s, uint32_t serial) {
latest_serial = serial;
fprintf(stderr, "XDG_CONFIGURE ns=%lld serial=%u size=%dx%d hold=%d previous=%d\n",
stamp(), serial, configured_width, configured_height, hold_next || held, held);
if (hold_next || held) {
held = true;
hold_next = false;
fprintf(stderr, "HELD ns=%lld serial=%u\n", stamp(), serial);
} else {
ack_and_commit();
}
}
static const struct xdg_surface_listener surface_listener = {.configure = configure};
static void pointer_enter(void *data, struct wl_pointer *p, uint32_t serial,
struct wl_surface *s, wl_fixed_t x, wl_fixed_t y) {
fprintf(stderr, "ENTER ns=%lld x=%.2f y=%.2f\n", stamp(),
wl_fixed_to_double(x), wl_fixed_to_double(y));
}
static void pointer_leave(void *data, struct wl_pointer *p, uint32_t serial,
struct wl_surface *s) { fprintf(stderr, "LEAVE ns=%lld\n", stamp()); }
static void pointer_motion(void *data, struct wl_pointer *p, uint32_t time,
wl_fixed_t x, wl_fixed_t y) {
fprintf(stderr, "MOTION ns=%lld x=%.2f y=%.2f\n", stamp(),
wl_fixed_to_double(x), wl_fixed_to_double(y));
}
static void pointer_button(void *data, struct wl_pointer *p, uint32_t serial,
uint32_t time, uint32_t button, uint32_t state) {
fprintf(stderr, "BUTTON ns=%lld serial=%u button=%u state=%u\n",
stamp(), serial, button, state);
}
static void pointer_axis(void *data, struct wl_pointer *p, uint32_t t,
uint32_t axis, wl_fixed_t value) {}
static void pointer_frame(void *data, struct wl_pointer *p) {}
static const struct wl_pointer_listener pointer_listener = {
.enter = pointer_enter, .leave = pointer_leave, .motion = pointer_motion,
.button = pointer_button, .axis = pointer_axis, .frame = pointer_frame,
};
static void capabilities(void *data, struct wl_seat *s, uint32_t caps) {
if ((caps & WL_SEAT_CAPABILITY_POINTER) && !pointer) {
pointer = wl_seat_get_pointer(s);
wl_pointer_add_listener(pointer, &pointer_listener, NULL);
}
}
static void seat_name(void *data, struct wl_seat *s, const char *name) {}
static const struct wl_seat_listener seat_listener = {
.capabilities = capabilities, .name = seat_name,
};
static void global(void *data, struct wl_registry *registry, uint32_t name,
const char *interface, uint32_t version) {
if (!strcmp(interface, wl_compositor_interface.name))
compositor = wl_registry_bind(registry, name, &wl_compositor_interface, version < 4 ? version : 4);
else if (!strcmp(interface, wl_shm_interface.name))
shm = wl_registry_bind(registry, name, &wl_shm_interface, 1);
else if (!strcmp(interface, xdg_wm_base_interface.name))
wm = wl_registry_bind(registry, name, &xdg_wm_base_interface, version < 3 ? version : 3);
else if (!strcmp(interface, wl_seat_interface.name)) {
seat = wl_registry_bind(registry, name, &wl_seat_interface, version < 5 ? version : 5);
wl_seat_add_listener(seat, &seat_listener, NULL);
}
}
static void global_remove(void *data, struct wl_registry *registry, uint32_t name) {}
static const struct wl_registry_listener registry_listener = {
.global = global, .global_remove = global_remove,
};
static void stop(int signum) { running = 0; }
int main(void) {
signal(SIGTERM, stop);
display = wl_display_connect(NULL);
if (!display) { fprintf(stderr, "CONNECT_FAILED\n"); return 1; }
struct wl_registry *registry = wl_display_get_registry(display);
wl_registry_add_listener(registry, &registry_listener, NULL);
wl_display_roundtrip(display);
wl_display_roundtrip(display);
if (!compositor || !shm || !wm) { fprintf(stderr, "MISSING_GLOBAL\n"); return 2; }
xdg_wm_base_add_listener(wm, &wm_listener, NULL);
surface = wl_compositor_create_surface(compositor);
xdg = xdg_wm_base_get_xdg_surface(wm, surface);
xdg_surface_add_listener(xdg, &surface_listener, NULL);
toplevel = xdg_surface_get_toplevel(xdg);
xdg_toplevel_add_listener(toplevel, &role_listener, NULL);
xdg_toplevel_set_title(toplevel, "Mosaic Saved Buffer Probe");
xdg_toplevel_set_app_id(toplevel, "org.mosaic.SavedBufferProbe");
wl_surface_commit(surface);
fprintf(stderr, "READY ns=%lld\n", stamp());
while (running) {
wl_display_flush(display);
struct pollfd fds[2] = {
{.fd = wl_display_get_fd(display), .events = POLLIN},
{.fd = STDIN_FILENO, .events = POLLIN},
};
int ready = poll(fds, 2, 100);
if (ready < 0) { if (errno == EINTR) continue; perror("poll"); break; }
if (fds[0].revents & (POLLIN | POLLERR | POLLHUP)) {
if (wl_display_dispatch(display) < 0) break;
}
if (fds[1].revents & POLLIN) {
char cmd[64];
if (!fgets(cmd, sizeof(cmd), stdin)) break;
if (!strncmp(cmd, "hold_next_configure", 19)) {
hold_next = true;
fprintf(stderr, "COMMAND hold_next_configure DONE ns=%lld\n", stamp());
} else if (!strncmp(cmd, "release_configure", 17)) {
if (held) ack_and_commit();
fprintf(stderr, "COMMAND release_configure DONE ns=%lld\n", stamp());
} else if (!strncmp(cmd, "close", 5)) {
running = 0;
fprintf(stderr, "COMMAND close DONE ns=%lld\n", stamp());
}
}
}
fprintf(stderr, "EXIT ns=%lld mapped=%d held=%d error=%d\n",
stamp(), mapped, held, wl_display_get_error(display));
xdg_toplevel_destroy(toplevel);
xdg_surface_destroy(xdg);
wl_surface_destroy(surface);
wl_display_flush(display);
wl_display_disconnect(display);
return 0;
}

View file

@ -0,0 +1,95 @@
--- a/sway/ipc-json.c
+++ b/sway/ipc-json.c
@@ -3,6 +3,7 @@
#include <json.h>
#include <libevdev/libevdev.h>
#include <stdio.h>
+#include <time.h>
#include <wlr/config.h>
#include <wlr/types/wlr_content_type_v1.h>
#include <wlr/types/wlr_output.h>
@@ -820,6 +821,33 @@
#endif
}
+/* WHY: pre-transaction geometry must be measured from the actual source node.
+ * CONTRACT: this private trace reads the mapped target only during get_tree.
+ * INVARIANT: no scene state, buffer reference or transaction is changed. */
+struct target_trace_context {
+ struct sway_container *container;
+};
+
+static void trace_target_source(struct wlr_scene_buffer *buffer,
+ int sx, int sy, void *data) {
+ struct target_trace_context *ctx = data;
+ int gx = 0, gy = 0;
+ bool valid = wlr_scene_node_coords(&buffer->node, &gx, &gy);
+ struct timespec ts;
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ fprintf(stderr, "TARGET_SOURCE ns=%lld con=%p con_id=%zu view=%p node=%p buffer=%p parent=%p global_valid=%d global=%d,%d local=%d,%d source_size=%d,%d dest=%d,%d src_box=%.3f,%.3f,%.3f,%.3f transform=%d enabled=%d\n",
+ (long long)ts.tv_sec * 1000000000LL + ts.tv_nsec,
+ (void *)ctx->container, ctx->container->node.id,
+ (void *)ctx->container->view, (void *)&buffer->node,
+ (void *)buffer->buffer, (void *)buffer->node.parent, valid, gx, gy, sx, sy,
+ buffer->buffer ? buffer->buffer->width : 0,
+ buffer->buffer ? buffer->buffer->height : 0,
+ buffer->dst_width, buffer->dst_height,
+ buffer->src_box.x, buffer->src_box.y,
+ buffer->src_box.width, buffer->src_box.height,
+ buffer->transform, buffer->node.enabled);
+}
+
static void ipc_json_describe_container(struct sway_container *c, json_object *object) {
json_object_object_add(object, "name",
c->title ? json_object_new_string(c->title) : NULL);
@@ -886,6 +914,14 @@
json_object_object_add(object, "marks", marks);
if (c->view) {
+ const char *app_id = view_get_app_id(c->view);
+ if (getenv("SAVED_TARGET_TRACE") && app_id &&
+ strcmp(app_id, "org.mosaic.SavedBufferProbe") == 0 &&
+ c->view->content_tree) {
+ struct target_trace_context ctx = {.container = c};
+ wlr_scene_node_for_each_buffer(&c->view->content_tree->node,
+ trace_target_source, &ctx);
+ }
ipc_json_describe_view(c, object);
}
}
--- a/sway/tree/view.c
+++ b/sway/tree/view.c
@@ -1328,6 +1328,23 @@
buffer->buffer ? buffer->buffer->height : 0, sx, sy, (void *)sbuf->node.parent);
}
+/* WHY: copied local offsets cannot prove a global boundary crossing.
+ * CONTRACT: private observation only; the node and buffer remain borrowed.
+ * INVARIANT: no buffer lock or scene mutation is introduced. */
+static void trace_target_saved_buffer(struct wlr_scene_buffer *buffer,
+ int sx, int sy, void *data) {
+ struct sway_view *view = data;
+ int gx = 0, gy = 0;
+ bool valid = wlr_scene_node_coords(&buffer->node, &gx, &gy);
+ fprintf(stderr, "TARGET_SAVED ns=%lld view=%p con=%p node=%p buffer=%p parent=%p global_valid=%d global=%d,%d local=%d,%d source_size=%d,%d dest=%d,%d enabled=%d\n",
+ saved_ns(), (void *)view, (void *)view->container,
+ (void *)&buffer->node, (void *)buffer->buffer,
+ (void *)buffer->node.parent, valid, gx, gy, sx, sy,
+ buffer->buffer ? buffer->buffer->width : 0,
+ buffer->buffer ? buffer->buffer->height : 0,
+ buffer->dst_width, buffer->dst_height, buffer->node.enabled);
+}
+
void view_save_buffer(struct sway_view *view) {
if (!sway_assert(!view->saved_surface_tree, "Didn't expect saved buffer")) {
view_remove_saved_buffer(view);
@@ -1354,6 +1371,9 @@
wlr_scene_node_set_enabled(&view->content_tree->node, false);
wlr_scene_node_set_enabled(&view->saved_surface_tree->node, true);
+ /* CONTRACT: measure the actual saved node after enabling the tree. */
+ wlr_scene_node_for_each_buffer(&view->saved_surface_tree->node,
+ trace_target_saved_buffer, view);
fprintf(stderr, "SB_ENABLE ns=%lld view=%p tree=%p content_enabled=%d saved_enabled=%d\n",
saved_ns(), (void *)view, (void *)view->saved_surface_tree,
view->content_tree->node.enabled, view->saved_surface_tree->node.enabled);

View file

@ -0,0 +1,62 @@
"""Offline exclusive-ledger regression; never touches a campaign LAB."""
import json
import multiprocessing
import tempfile
from pathlib import Path
from budget import reserve
with tempfile.TemporaryDirectory() as root:
lab = Path(root)
reserve(lab, 'control')
reserve(lab, 'control', 'ack')
reserve(lab, 'control', 'timeout')
for episode in (None, 'ack', 'timeout'):
try:
reserve(lab, 'control', episode)
raise AssertionError('duplicate accepted')
except RuntimeError:
pass
for mode in ('private_standard', 'private_mosaic'):
reserve(lab, mode)
reserve(lab, mode, 'ack')
reserve(lab, mode, 'timeout')
reserve(lab, 'private_mosaic', camera=True)
try:
reserve(lab, 'private_mosaic', camera=True)
raise AssertionError('second camera adjustment accepted')
except RuntimeError:
pass
for mode in ('control', 'private_standard', 'private_mosaic'):
for _ in range(16):
reserve(lab, mode, capture=True)
try:
reserve(lab, mode, capture=True)
raise AssertionError('seventeenth capture accepted')
except RuntimeError:
pass
state = json.loads((lab / 'target-budget.json').read_text())
assert len(state['episodes']) == 6
assert sum(state['captures'].values()) == 48
print('PASS persistent run/episode limits')
def contender(path, queue):
try:
reserve(Path(path), 'control')
queue.put('accepted')
except RuntimeError:
queue.put('rejected')
with tempfile.TemporaryDirectory() as root:
context = multiprocessing.get_context('fork')
queue = context.Queue()
children = [context.Process(target=contender, args=(root, queue))
for _ in range(2)]
for child in children:
child.start()
for child in children:
child.join(5)
assert child.exitcode == 0
assert sorted(queue.get() for _ in children) == ['accepted', 'rejected']
print('PASS exclusive reservation')