docs(mosaic): preserve saved target timeout and input evidence

This commit is contained in:
fy59 2026-10-12 19:36:01 +02:00
parent 75f057d471
commit 6fd8945d10
16 changed files with 1264 additions and 0 deletions

1
.gitattributes vendored
View file

@ -1,2 +1,3 @@
# Preserved experimental patches include diff markers and source-context tabs.
tooling/probes/step20c-valid-grab/*.patch -whitespace
tooling/probes/step20c-saved-target-timeout-input/target-hit-trace.patch -whitespace

View file

@ -4,6 +4,8 @@
### STEP20C experimental evidence
- Preserved saved-target natural timeout gate: one Mosaic run used a static SHM H2 reference with four identical whole-RGB captures. The measured target saved node crossed x=0, rendered 182,214 retained-color pixels on H1, contributed no H2 pixels and was removed after a real 2500 ms timeout while the client held serial 50 alive. Two sampled pointer points found the H2 reference and visible H1 saved node. PARTIALLY_FEASIBLE because complete damage, broader input and GPU/texture lifetime remain unqualified; no production change.
- 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.

View file

@ -300,3 +300,17 @@ 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.
The separate [natural timeout and input gate](design/STEP20C_SAVED_TARGET_TIMEOUT_INPUT_PROBE.md)
uses a static SHM H2 reference and one Mosaic transaction. The actual target
source and saved node both crossed x=0; the saved node rendered only its H1
portion and its tree was removed after a real 2500 ms timeout with serial 50
held and the client alive. The whole H2 RGB image was identical across two
preflight, held and applied captures. Two sampled pointer positions found
the H2 reference surface and the visible H1 saved node respectively; the
latter is not a Wayland surface. This closes the sampled saved-target ACK and
timeout clipping matrix, **PARTIALLY_FEASIBLE**, without proving full damage,
all input paths or native GPU lifetime. The earlier crossing campaign's own
architectural verdict remains UNDECIDED. The new limited-matrix conclusion
does not authorize a production implementation. Optimized blur/cache and
SceneFX ABI/package remain separate gates.

View file

@ -244,3 +244,14 @@ 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.
The subsequent [static-reference saved-target timeout gate](design/STEP20C_SAVED_TARGET_TIMEOUT_INPUT_PROBE.md)
is **PARTIALLY_FEASIBLE**. In one private horizontal Mosaic run, all four
full H2 RGB captures matched exactly; the actual crossing saved target
rendered its permitted H1 pixels, remained absent on H2 and its saved tree
was removed after a natural timeout with the configure withheld. Two pointer
points showed no invisible H2 target hit. Full frame-by-frame damage,
unmeasured input paths, seat notification for the saved node and complete
GPU/texture reference lifetime remain **NOT TESTED**. The old Kitty cursor
explanation remains strongly probable, not causally proven. STEP20C remains
**NEXT** and production multi-output Mosaic **NOT YET SUPPORTED**.

View file

@ -155,3 +155,12 @@ 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.
[Saved target natural timeout and input gate](design/STEP20C_SAVED_TARGET_TIMEOUT_INPUT_PROBE.md)
and [local source fixture](../tooling/probes/step20c-saved-target-timeout-input/README.md):
EXPERIMENTAL / PARTIALLY_FEASIBLE. A static SHM H2 reference stayed
pixel-identical before, during and after one held Mosaic transaction. The
crossing target saved node rendered inside H1 and was removed after a real
timeout with the XDG client alive; sampled H2 input reached the reference.
Complete damage and GPU lifetime remain open. STEP20C stays NEXT and
production multi-output remains NOT YET SUPPORTED.

View file

@ -248,3 +248,14 @@ 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.
The completed [static-reference saved-target natural timeout gate](design/STEP20C_SAVED_TARGET_TIMEOUT_INPUT_PROBE.md)
then measured a real crossing target saved node, clipped H1 rendering,
stable full-image H2 RGB, two targeted pointer hits and natural timeout
cleanup with the XDG client alive and serial 50 withheld. Combined with the
earlier E1 ACK, the tested saved-target clipping matrix is viable, while
damage across every frame, broader input and GPU/texture lifetime remain
unqualified. Its verdict is **PARTIALLY_FEASIBLE**; the earlier probe's own
UNDECIDED conclusion is preserved. The next separate STEP20C gate is
optimized blur/cache, followed by SceneFX ABI/package. STEP20C is still
**NEXT**, production multi-output **NOT YET SUPPORTED**.

View file

@ -0,0 +1,230 @@
# STEP20C saved target natural timeout and input probe
**EXPERIMENTAL / PARTIALLY_FEASIBLE.** STEP20C remains NEXT;
production multi-output Mosaic is NOT YET SUPPORTED. This is a new, single-run
laboratory. The prior [target crossing report](STEP20C_SAVED_BUFFER_TARGET_CROSSING_PROBE.md)
remains PARTIALLY_FEASIBLE and architecturally UNDECIDED. Its E1 established
one actual target saved node crossing `x=0`, clipped render in H1, 182,214
retained target pixels and a real ACK/cleanup. Its E2 timeout stopped through
fixture pointer correlation; it did not test Mosaic target timeout cleanup.
The old H2 Kitty reference changed one complete 11×23 rectangle from its
block-cursor color `#F5E0DC` to background `#303038` across pristine and
Mosaic runs. Blink causality is strongly supported but unproven. This probe
uses an opaque, static XDG SHM reference with app ID
`org.mosaic.StaticReference`, fixed RGB and no text cursor or animation.
The reference still handles real configure/ACK and Wayland events. Two full
H2 RGB captures separated by more than one historic Kitty blink interval
must match exactly before the target episode is reserved. No region is
masked, and reference failure stops the run.
## Source identity and geometry
Pinned inputs: SwayFX 0.6 `fd71a6bdc061bd633b488ae7b83e8a6981d22f86`,
private Mosaic `cd550382b311efdbfe3ed892f4e1d281d852e754`, SceneFX 0.5
`37ccd723bef49e6891156ffafce8f549f01446cc`, wlroots 0.20.2.
Preserved clipping, saved-buffer and target-geometry trace patches retain
their bytes. Any further passive hit trace has its own patch and hash.
No production source, SceneFX algorithm, public ABI, package, UX or active
desktop session is changed. The private timeout policy is
`-Dtxn-timeout=2500`, parsed into `server.txn_timeout_ms`; no `-Dtxn-wait`,
`-Dnoatomic`, direct save call or scheduler change is used.
Source audit of the pinned private copies located `transaction_commit`,
`should_configure`, `transaction_progress`, `apply_container_state` and
`handle_timeout` in `sway/desktop/transaction.c`: an ordinary changed-size
instruction calls `view_save_buffer`, and timeout advances the transaction
through the normal progress path. `sway/tree/view.c` implements
`view_save_buffer_iterator`, `view_save_buffer` and
`view_remove_saved_buffer`; its iterator creates the saved scene buffer and
passes the current source buffer to `wlr_scene_buffer_set_buffer`.
The private SceneFX `types/scene/wlr_scene.c` contains
`wlr_scene_buffer_create`, `wlr_scene_buffer_set_buffer`,
`wlr_scene_node_for_each_buffer`, `scene_node_intersect_clip`,
`scene_node_point_inside_clip`, `wlr_scene_node_at` and
`scene_entry_render`. The latter two paths are separately observed by the
passive hit and render traces. A saved scene buffer is distinct from its
source Wayland buffer, output buffer, and optimized blur cache.
Only H1 `(-800,0),800×600` and H2 `(0,0),800×600`, scale 1, normal, are
allowed. Map and settle all clients first. Record H2 reference app ID,
output, geometry, two PNG hashes and full RGB equality. Then identify the
target app ID `org.mosaic.SavedBufferProbe`, workspace, output, tiled owner,
view, source scene node/buffer, destination/source dimensions, source box,
transform, applied camera, tiled clip and global source box. Compute one
signed relative scroll from the **current** camera and measured source node.
Remeasure after application. Require nonempty source and `left<0<right`,
preferably at least 64 pixels on each side. Do not substitute the IPC
container rectangle. No later focus action may silently change the camera.
Failure stops before an episode reservation; no scroll scan or retry.
## One timeout episode
Remeasure the current source immediately before reserving the sole episode.
Ask the C17 target to `hold_next_configure`, then make one normal geometry
change (`gaps inner current set 30`) and require an actual changed-size
configure. The client retains the real serial, stays alive, dispatches
Wayland events and responds to pings. It sends no ACK during the hold and
none is forged after expiration.
Correlate the target view and current source buffer to its saved tree and
saved scene node. Require a nonempty, enabled saved node whose global box
crosses `x=0`, current content disabled, ancestral tiled clip and a render
entry for **that node**. Distinguish the outer Mosaic clip, its gap-adjusted
viewport and any content clip; the old E2 `x=-30` bound matched a tiled clip
narrowed by 30-pixel gaps. Record permitted target SHM color on H1, zero
target SHM color on H2 and **full-H2 RGB equality** against the static
reference during the hold. Preserve all mismatches if equality fails.
If the remaining 2500 ms window permits, inject at most two prepared pointer
motions: one in the saved target's permitted H1 portion and one in its
geometrically forbidden H2 portion over the static reference. Record global
points, `wlr_scene_node_at` result/descriptor, surface association and
actual client events; distinguish seat notification when traceable. These
motions must not delay the natural timeout. If not safely measured, input is
NOT TESTED. A saved scene buffer need not itself deliver a Wayland click.
Wait for the real SwayFX timeout with configure held and client alive.
Observe transaction apply, target saved-tree destruction, enabled live
content, no later render entry from the destroyed saved node, final H1/H2
images and available damage extents. Node destruction does not prove all GPU
texture references were released. Stabilized PNGs do not cover all frames.
No output transition, camera hide/return, popup, scaling, rotation, effects,
animation, migration, stress test or new standard run is part of this gate.
## Budget, retention and classification
A single persistent locked ledger allows **one Mosaic run, one episode, one
calculated camera adjustment, at most 12 captures and 150 seconds**. It
reserves before each resource use; failures consume reservations. The ledger
cannot reset or be copied to justify a second laboratory. Test it offline
before the run. All builds, generated protocols, sockets, captures and logs
stay in one disposable `/tmp` LAB. The exact executed runner is copied into
its run. Source and evidence SHA-256 manifests cover the retained inputs and
observations. Private 0700 `XDG_RUNTIME_DIR`, `WAYLAND_DISPLAY` and explicit
`swaymsg -s` isolate the session; cleanup is by owned process groups only.
FEASIBLE requires stable whole-H2 RGB before/during/after, actual target
crossing/render and H1 pixels, no forbidden H2 contribution, natural timeout
with live held client, target saved-tree cleanup, and no measured invisible
hit. PARTIALLY_FEASIBLE means the timeout/clipping core succeeds with limited
input, damage or GPU lifetime qualification. INCONCLUSIVE means reference,
crossing, target rendering or natural timeout is not actually exercised.
BLOCKED requires a reproducible forbidden pixel/hit, concrete lifetime defect,
crash or blocking regression. A fixture/calibration error is documented as
such, without inventing a Mosaic defect. No result qualifies STEP20C
production or authorizes a clipping algorithm change.
## Retained execution and measured result
The single run is retained at
`/tmp/swayfx-mosaic-nested-grab-iKwhINe5/runs/timeout-mosaic-1791824080327425269`.
Its exact runner is `fixture-source.py`; the analysis in
`LAB/results/*.json` derives from the retained PNGs, logs, IPC trees and
monotonic timestamps. The private passive hit patch SHA-256 is
`25b7ffd909de3ccbab5b0ac542216b2b321fee78f7e73d80aa919c02ef829e83`.
The source manifest has 913 verified entries and SHA-256
`bcc1a921268dc1cf032aeb68824fefd27e3c1861c872920548a8db4ba33e070b`;
the evidence manifest has 46 verified entries and SHA-256
`5b085ddf58c63225a6940987b775de805543ad5711b65a6ed20d5eb9418577ac`.
There was one Mosaic run, one episode, one calculated camera delta, eight PNGs
and a 5.062-second run. The locked ledger retains these reservations. No
second run or standard control was launched.
The H2 reference had app ID `org.mosaic.StaticReference`, container ID 11,
opaque fixed RGB `#303038`, a normal XDG configure/ACK path and no cursor.
Two preflight H2 PNGs captured about 0.754 seconds apart were identical
over all 800×600 RGB pixels. The held and post-timeout H2 PNGs had the same
SHA-256 `fb69a3b1fb61484b03c41f988fba7de7b09223261b7e63c0e09d9b5cb81ddaf1`.
Each full-image difference contains zero pixels; neither target SHM color
appeared on H2. No mask or excluded region was used.
The target `org.mosaic.SavedBufferProbe` occupied tiled container ID 8,
Mosaic workspace ID 6 on H1. Its actual source node
`0x557502aff7e0` carried buffer `0x557502b652b0`, size 636×573 with
equal destination, source box 636×573 and normal transform. Before camera
calibration the global source box was `[-1918,-1282)×[25,598)` at applied
`scroll_x=1760`. One signed delta `-1600` produced applied `scroll_x=160`
and the remeasured source box `[-318,318)×[25,598)`. A second pre-episode
measurement confirmed the same source buffer; no prior episode pointer was
used. The requested `gaps inner current set 30` changed the target configure
to 636×513.
The transaction created target saved tree `0x557502b820e0` under its view,
and saved scene node `0x557502bada50` referenced that **same current source
buffer**. The node was enabled, the current content disabled, and its
effective saved box was `[-318,318)×[25,598)`. Its own `SB_RENDER` entry at
monotonic ns `123224008686383` had visible bounds
`[-318,0)×[25,598)` and damage extent `(482,25,800,598)` in H1 output
coordinates. The outer tiled Mosaic scene clip at camera 160 ended at the
global `x=0` boundary. H1 held capture retained **182,214** pixels of the
old target SHM color; H2 contained zero. After timeout, the gap-adjusted
outer clip ended at `x=-30`, matching the later live rendering limit. The
trace does not isolate a separate content clip as the cause of that limit.
The target received real XDG configure serial 50 and logged `HELD` at
monotonic ns `123224009029100` without ACK. Two prepared pointer motions
were sampled during that hold. At global `(100,100)` on H2,
`wlr_scene_node_at` returned a non-saved reference node with an associated
Wayland surface, and the static reference received `MOTION x=98,y=75`.
At global `(-159,311)` on H1, it returned the target saved scene node with
no associated Wayland surface; the held target client received no motion.
No invisible saved-target hit was observed at the sampled H2 point. The
seat notification path for the H1 saved node was not directly traced and
broader input behavior remains unqualified.
SwayFX logged `Transaction ... timed out (1 waiting)` at about 2.5 seconds
after `HELD`. The target client was still alive, continued dispatching,
and had not ACKed serial 50. The target saved tree was destroyed at
monotonic ns `123226508834753`; live source rendering and an ordinary
surface hit reappeared after application. No same-node render entry was
observed between this destruction and the controlled client exit. The
post-timeout H1 PNG contained 116,964 pixels of the old target color in the
new geometry; the full H2 PNG stayed unchanged. Additional short-lived
saved trees in the log begin during controlled client/window cleanup **after**
the measured timeout; their reused pointer values do not belong to the
timeout tree. Process exit codes and socket cleanup are retained in
`exit-status.json` and `events.jsonl`.
This establishes the target saved buffer's crossing, partial clipping,
render, H2 exclusion and natural timeout cleanup in the tested horizontal
matrix, alongside the previously preserved E1 ACK. The sampled input points
show no invisible H2 hit. Damage trace extents and stable PNGs do not cover
all intermediate frames; scene-node destruction does not prove full GPU or
texture reference release. These limits set
`SWAYFX_MOSAIC_SAVED_TARGET_TIMEOUT_INPUT_PROBE=PARTIALLY_FEASIBLE`.
The architectural conclusion for **this tested matrix** is
`SAVED_TARGET_TIMEOUT_CLIPPING_FEASIBLE_FOR_TESTED_MATRIX`; production
STEP20C remains NEXT and multi-output remains NOT YET SUPPORTED. The next
separate gates are optimized blur/cache, then SceneFX ABI/package.
## Offline evidence freeze (2026-10-12)
The freeze started on `main` at
`75f057d4717668fcec32755b79cff70b16ea355d`, also returned by direct
Forgejo and GitHub `ls-remote` queries. Only this report, its source fixture
and the five expected canonical/index/changelog additions were pending.
All 913 source entries and all 46 evidence entries were available and
matched their historical manifests; neither manifest was rewritten. The
four H2 PNG hashes and complete RGB images were independently rechecked
without a mask. Raw compositor and client lines confirm the source/saved
buffer identity, target render, held serial, natural timeout, transaction
application, tree removal and the two distinct hit/surface observations.
The historical budget remains closed at 1 run, 1 episode, 1 camera
adjustment and 8 of 12 captures. No graphical run was added.
Shell syntax, Python AST without bytecode, both C17 clients with fatal
warnings, offline ledger limits/contention, private patch applicability and
source equivalence passed. The reconstructed patch chain includes the
inherited clip, hidden-owner guard, temporal output trace, native
presentation trace, output ownership trace, saved trace, target geometry
and target hit trace. The private SceneFX chain also matched its retained
modified source. Compilation of the retained private Mosaic build and
Meson `mosaic-model` passed (1/1). The first archive extraction into `/tmp`
failed with `Disk quota exceeded`; a bounded temporary archive on the home
filesystem completed the offline reconstruction and was removed. This
recovered validation failure did not consume a graphical reservation.
Markdown local targets, generated-artifact exclusion and the complete
tracked/new-file delta were checked before publication. This freeze
preserves PARTIALLY_FEASIBLE and the limited-matrix architectural conclusion
above; it does not qualify GPU references or every transaction frame.

View file

@ -0,0 +1,28 @@
# Saved target natural timeout and input fixture
**EXPERIMENTAL / PARTIALLY_FEASIBLE.** The [protocol and result](../../../docs/design/STEP20C_SAVED_TARGET_TIMEOUT_INPUT_PROBE.md)
defines a single new Mosaic run with one natural timeout episode. The
previous crossing campaign and its exhausted ledger are immutable historical
evidence. This fixture reconstructs the pinned compositor privately, uses a
fixed opaque XDG SHM H2 reference, and stores all generated data under one
new `/tmp` LAB. No production source, package or installed library is changed.
Run `prepare.sh` to reconstruct the private binaries without launching a
compositor. Run the offline `test-budget.py` before the sole `run.py LAB`
invocation. The runner requires two identical H2 captures before any
transaction reservation, calculates no more than one relative camera delta,
and copies its exact source into the run directory. `analyze.py LAB` derives
structured results and SHA-256 manifests from retained raw files. Never
reuse an old LAB or reset a consumed ledger.
The sole run used `/tmp/swayfx-mosaic-nested-grab-iKwhINe5`. Its ledger
records one run, one episode, one camera adjustment and eight captures.
The exact executed Python source is saved as `runs/*/fixture-source.py`.
The static reference and the target retained their real configure/ACK
processing; the target held serial 50 until natural SwayFX timeout. Its
crossing saved node rendered on H1, the whole H2 RGB image remained
identical before/during/after, and the target tree was destroyed. One H2
pointer sample reached the static reference; the visible H1 sample hit the
saved scene buffer without a Wayland surface. Full damage and native GPU
lifetime remain unqualified. `analyze.py` is offline only and may be rerun
without spending a graphics budget; run it with `PYTHONDONTWRITEBYTECODE=1`.

View file

@ -0,0 +1,203 @@
#!/usr/bin/env python3
"""Derive bounded, reproducible facts from the single retained timeout run."""
import hashlib
import json
import re
import sys
from pathlib import Path
from PIL import Image, ImageChops
lab = Path(sys.argv[1]).resolve()
repo = Path(__file__).resolve().parents[3]
run = next((lab / 'runs').glob('timeout-mosaic-*'))
out = lab / 'results'
events = [json.loads(line) for line in (run / 'events.jsonl').read_text().splitlines()]
log = (run / 'compositor.log').read_text().splitlines()
client = (run / 'client.log').read_text().splitlines()
reference = (run / 'reference.log').read_text().splitlines()
preflight = json.loads((out / 'target-preflight.json').read_text())
stability = json.loads((out / 'reference-stability.json').read_text())
budget = json.loads((lab / 'timeout-budget.json').read_text())
status = json.loads((run / 'exit-status.json').read_text())
def write(name, obj):
(out / name).write_text(json.dumps(obj, indent=2, sort_keys=True) + '\n')
def first(kind):
return next(e for e in events if e['kind'] == kind)
def fields(line):
return dict(re.findall(r'([A-Za-z_]+)=([^ ]+)', line))
def rgb(name):
return Image.open(run / name).convert('RGB')
def diff(a, b):
image = ImageChops.difference(rgb(a), rgb(b))
return {'changed_pixels': sum(p != (0, 0, 0) for p in image.get_flattened_data()),
'box': image.getbbox()}
def color(name, triplet):
return sum(p == triplet for p in rgb(name).get_flattened_data())
def sha(path):
return hashlib.sha256(path.read_bytes()).hexdigest()
saved = first('saved_target')
node = saved['node']
tree = saved['tree']
view = saved['view']
source = saved['source']
start_ns = int(fields(saved['saved_trace'])['ns'])
created = next(x for x in log if x.startswith('SB_BUFFER_CREATE ') and
f'node={node}' in x and f'parent={tree}' in x and
0 <= start_ns - int(fields(x)['ns']) < 1_000_000_000)
enabled = next(x for x in log if x.startswith('SB_ENABLE ') and
f'view={view}' in x and f'tree={tree}' in x and
int(fields(x)['ns']) >= start_ns)
destroyed = next(x for x in log if x.startswith('SB_TREE_DESTROY ') and
f'view={view}' in x and f'tree={tree}' in x and
int(fields(x)['ns']) > start_ns)
render = [x for x in log if x.startswith('SB_RENDER ') and f'node={node}' in x
and int(fields(x)['ns']) >= start_ns and
int(fields(x)['ns']) < int(fields(destroyed)['ns'])]
timeout = next(x for x in log if 'timed out (1 waiting)' in x)
timeout_event = first('timeout_observed')
held = next(x for x in client if x.startswith('HELD ') and 'serial=50' in x)
hits = [x for x in log if x.startswith('TARGET_HIT ') and
int(fields(x)['ns']) < int(fields(destroyed)['ns']) and
int(fields(x)['ns']) > int(fields(created)['ns'])]
surfaces = [next((log[i + 1] for i, line in enumerate(log) if line == hit and
i + 1 < len(log) and
log[i + 1].startswith('TARGET_HIT_SURFACE ')), None)
for hit in hits]
right_names = ['right-reference-a.png', 'right-reference-b.png',
'right-held.png', 'right-timeout-applied.png']
right = {name: {**diff(right_names[0], name), 'sha256': sha(run / name),
'old_target_pixels': color(name, (32, 208, 160)),
'new_target_pixels': color(name, (35, 68, 204))}
for name in right_names}
left = {name: {'old_target_pixels': color(name, (32, 208, 160)),
'new_target_pixels': color(name, (35, 68, 204))}
for name in ['left-reference-a.png', 'left-reference-b.png',
'left-held.png', 'left-timeout-applied.png']}
reference_events = [x for x in events if x['kind'] == 'capture' and
x['path'].startswith('right-reference')]
client_exit_ns = int(fields(next(x for x in client if x.startswith('EXIT ')))['ns'])
post_destroy_same_node = [x for x in log if x.startswith('SB_RENDER ') and
f'node={node}' in x and
int(fields(destroyed)['ns']) < int(fields(x)['ns']) <
client_exit_ns]
write('reference-pixel-diff.json', {'preflight': stability,
'capture_gap_seconds': (reference_events[1]['before_ns'] -
reference_events[0]['before_ns']) / 1e9,
'full_rgb': right})
write('camera-applied.json', {k: preflight[k] for k in
('current_scroll', 'desired_scroll', 'delta')})
write('source-buffer.json', {k: source[k] for k in
('view', 'container', 'source_node', 'source_buffer', 'left', 'top',
'width', 'right', 'source_size', 'dest', 'src_box', 'transform')})
write('saved-target.json', {'view': view, 'tree': tree, 'node': node,
'buffer': saved['buffer'], 'box': saved['box'],
'source_buffer_equal': saved['buffer'] == source['source_buffer'],
'created': created, 'enabled': enabled, 'destroyed': destroyed})
write('scene-parentage.json', {'source_parent': fields(source['raw'])['parent'],
'saved_parent': fields(created)['parent'], 'saved_tree': tree,
'tree_parent': fields(saved['tree_trace'])['parent'],
'source_node': source['source_node'], 'saved_node': node,
'target_owner_id': source['owner']['id'],
'target_workspace_id': source['workspace']['id']})
write('render-entries.json', {'target_node': node, 'held_entries': render,
'same_node_entries_between_destroy_and_client_exit': post_destroy_same_node,
'pointer_reuse_after_client_exit_must_not_be_counted_as_same_tree': True})
scene = [x for x in log if 'PI_SCENE output=HEADLESS-1' in x and
int(fields(x)['ns']) >= int(fields(created)['ns']) and
int(fields(x)['ns']) < int(fields(destroyed)['ns'])]
write('clip-analysis.json', {'saved_global_box': saved['box'],
'held_render_visible': fields(render[0])['visible'],
'outer_tiled_clip': scene[0] if scene else None,
'post_timeout_clip': next((x for x in log if
'PI_SCENE output=HEADLESS-1' in x and 'clip=160,0,740,540' in x), None),
'interpretation': 'Held visible right edge x=0 matches outer Mosaic clip; '
'after timeout x=-30 matches gap-adjusted outer clip. No independent '
'content-clip causality is inferred.'})
write('target-pixel-analysis.json', {'full_h1_target_color_counts': left,
'saved_held_pixels': left['left-held.png']['old_target_pixels']})
write('whole-h2-comparison.json', {'full_rgb': right,
'all_equal': all(v['changed_pixels'] == 0 for v in right.values()),
'no_target_color': all(v['old_target_pixels'] == 0 and
v['new_target_pixels'] == 0 for v in right.values())})
write('input-hits.json', {'held_hits': hits, 'surface_associations': surfaces,
'seat_notification': 'not directly traced',
'h2_reference_motion_received': any(x.startswith('MOTION ') and
'x=98.00 y=75.00' in x for x in reference),
'target_motion_during_hold': [x for x in client if x.startswith('MOTION ') and
int(fields(x)['ns']) >= int(fields(held)['ns']) and
int(fields(x)['ns']) < int(fields(destroyed)['ns'])]})
write('client-delivery.json', {'held_configure': held,
'target_ack_serial_50': [x for x in client if x.startswith('ACK ') and
'serial=50' in x], 'reference_motion': [x for x in reference if
x.startswith('MOTION ')], 'target_exit': [x for x in client if
x.startswith('EXIT ')]})
write('transaction-timeline.json', {'held': held, 'saved_created': created,
'render': render[0], 'timeout': timeout,
'saved_destroyed': destroyed,
'hold_to_destroy_seconds': (int(fields(destroyed)['ns']) -
int(fields(held)['ns'])) / 1e9,
'other_transactions_after_controlled_client_cleanup': True})
write('timeout.json', {'natural_timeout': timeout, 'event': timeout_event,
'no_target_ack': not any(x.startswith('ACK ') and 'serial=50' in x
for x in client), 'client_alive_at_timeout': timeout_event['client_alive'],
'target_tree_destroyed': destroyed})
write('lifetime.json', {'source_buffer': source['source_buffer'],
'saved_buffer': saved['buffer'], 'saved_tree_destroyed': destroyed,
'no_same_node_render_between_destroy_and_client_exit':
not bool(post_destroy_same_node),
'gpu_texture_release': 'NOT TESTED'})
write('damage-analysis.json', {'held_target_render': render[0],
'held_damage_extent': fields(render[0])['damage'],
'post_timeout_h1_target_pixels': left['left-timeout-applied.png'],
'h2_changed_pixels': right['right-timeout-applied.png']['changed_pixels'],
'all_intermediate_frames': 'NOT TESTED'})
write('budget.json', {'reserved': budget, 'limits': {'run': 1, 'episode': 1,
'camera': 1, 'capture': 12, 'seconds': 150},
'duration_seconds': status['duration_seconds'], 'failure': status['failure']})
core = (stability['stable'] and saved['buffer'] == source['source_buffer'] and
saved['box'][0] < 0 < saved['box'][2] and bool(render) and
left['left-held.png']['old_target_pixels'] > 0 and
all(v['changed_pixels'] == 0 for v in right.values()) and
bool(destroyed) and timeout_event['client_alive'])
verdict = 'PARTIALLY_FEASIBLE' if core else 'INCONCLUSIVE'
write('matrix.json', {'ack_e1_historical': 'PARTIALLY_FEASIBLE',
'new_timeout_core': 'PASS' if core else 'FAIL',
'input_sampled': len(hits) == 2, 'damage_complete': False,
'native_gpu_lifetime_complete': False})
write('summary.json', {'verdict': verdict, 'core_pass': core,
'architecture': 'SAVED_TARGET_TIMEOUT_CLIPPING_FEASIBLE_FOR_TESTED_MATRIX'
if core else 'UNDECIDED', 'run': str(run),
'limits': ['full transactional damage', 'all GPU/texture references',
'intermediate frames', 'seat notification on saved node',
'unmeasured input paths']})
# Source manifest covers exact executed fixture and private source trees; no
# build products enter this source list. Evidence manifest covers all retained
# raw run inputs, captures and derived JSON without a self-hash cycle.
fixture = repo / 'tooling/probes/step20c-saved-target-timeout-input'
sources = [p for p in fixture.iterdir() if p.is_file()] + [run / 'fixture-source.py']
for root in (lab / 'mosaic', lab / 'standard', lab / 'scenefx-git'):
sources.extend(p for p in root.rglob('*') if p.is_file() and
'.git' not in p.parts and 'subprojects/packagecache' not in str(p))
sources.extend(p for p in (lab / 'nested-client.c', lab / 'pointer-daemon.c',
lab / 'virtual-pointer-client.h', lab / 'virtual-pointer-protocol.c',
lab / 'xdg-shell-client.h', lab / 'xdg-shell-protocol.c')
if p.is_file())
sources = sorted(set(sources))
evidence = sorted(set(p for p in run.iterdir() if p.is_file()) |
set(p for p in out.iterdir() if p.is_file() and
not p.name.endswith('-sha256.txt')) |
{lab / 'timeout-budget.json', lab / 'saved-timeout-build-log.txt'})
for name, paths in [('source-sha256.txt', sources), ('evidence-sha256.txt', evidence)]:
(out / name).write_text(''.join(f'{sha(p)} {p}\n' for p in paths))
print(f'{verdict} source={len(sources)} evidence={len(evidence)}')

View file

@ -0,0 +1,30 @@
"""Persistent one-run target-timeout budget; every reservation is durable."""
import fcntl
import json
import os
from pathlib import Path
LIMITS = {'run': 1, 'episode': 1, 'camera': 1, 'capture': 12}
def reserve(lab: Path, resource: str) -> dict:
if resource not in LIMITS:
raise ValueError('unknown budget resource')
path = lab / 'timeout-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 {key: 0 for key in LIMITS}
if state[resource] >= LIMITS[resource]:
raise RuntimeError(f'{resource} budget exhausted')
if resource != 'run' and state['run'] != 1:
raise RuntimeError('run must be reserved first')
state[resource] += 1
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,23 @@
#!/bin/sh
# EXPERIMENTAL source-only build; never launches a compositor.
set -eu
repo=$(CDPATH= cd -- "$(dirname -- "$0")/../../.." && pwd)
fixture="$repo/tooling/probes/step20c-saved-target-timeout-input"
log=$(mktemp /tmp/saved-timeout-prepare-XXXXXXXX)
if ! sh "$repo/tooling/probes/step20c-saved-buffer-target-crossing/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"
git -C "$lab/mosaic" apply --check "$fixture/target-hit-trace.patch"
git -C "$lab/mosaic" apply "$fixture/target-hit-trace.patch"
meson compile -C "$lab/mosaic-build" -j 8 >> "$log" 2>&1
for client in saved-client static-reference; do
cc -std=c17 -Wall -Wextra -Werror -Wno-unused-parameter -I"$lab" \
"$fixture/$client.c" "$lab/xdg-shell-protocol.c" -o "$lab/$client" \
$(pkg-config --cflags --libs wayland-client) >> "$log" 2>&1
done
cp "$log" "$lab/saved-timeout-build-log.txt"
printf 'LAB=%s\nBUILD_LOG=%s\n' "$lab" "$log"

View file

@ -0,0 +1,391 @@
"""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, ImageChops
from budget import reserve
parser = argparse.ArgumentParser()
parser.add_argument('lab', type=Path)
args = parser.parse_args()
args.mode = 'private_mosaic'
lab = args.lab.resolve()
if not (lab / 'saved-client').is_file() or not (lab / 'static-reference').is_file():
parser.error('prepared LAB required')
state = reserve(lab, 'run')
run = lab / 'runs' / f'timeout-mosaic-{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',
SAVED_HIT_TRACE='1')
binary = lab / 'mosaic-build/sway/swayfx-mosaic'
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, 'capture')
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')
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, 'camera')
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 reference_stability():
snapshot('reference-a')
time.sleep(.7)
snapshot('reference-b')
left = Image.open(run / 'right-reference-a.png').convert('RGB')
right = Image.open(run / 'right-reference-b.png').convert('RGB')
difference = ImageChops.difference(left, right)
box = difference.getbbox()
stable = box is None
target_colors = {(35, 68, 204), (32, 208, 160)}
target_pixels = sum(pixel in target_colors for pixel in left.get_flattened_data())
(lab / 'results').mkdir(exist_ok=True)
(lab / 'results' / 'reference-stability.json').write_text(json.dumps({
'stable': stable, 'diff_box': box, 'target_pixels': target_pixels,
'first_sha256': sha256(run / 'right-reference-a.png'),
'second_sha256': sha256(run / 'right-reference-b.png'),
'first_app_id': 'org.mosaic.StaticReference'}, indent=2) + '\n')
record('reference_stability', stable=stable, box=box, target_pixels=target_pixels)
if not stable or target_pixels:
raise RuntimeError('static H2 reference gate failed before transaction')
def sha256(path):
import hashlib
return hashlib.sha256(path.read_bytes()).hexdigest()
def episode():
global target_source
# WHY: the source pointer belongs to this one episode, not a past ACK.
target_source = source_measure()
if not target_source['left'] < 0 < target_source['right']:
raise RuntimeError('current source does not cross x=0')
reserve(lab, 'episode')
record('episode_start', source=target_source)
client_command('hold_next_configure')
old_held = client_log().count('HELD ns=')
old_timeout = compositor_log().count('timed out')
before_log = len(compositor_log())
command('gaps inner current set 30')
wait(lambda: client_log().count('HELD ns=') > old_held, 3)
held_line = [x for x in client_log().splitlines() if x.startswith('HELD ns=')][-1]
held_ns = int(re.search(r'ns=(\d+)', held_line).group(1))
view = target_source['view']
def target_trace():
text = compositor_log()[before_log:]
trees = [x for x in text.splitlines() if 'SB_TREE_CREATE ' in x and f'view={view}' in x]
saved = [x for x in text.splitlines() if 'TARGET_SAVED ' in x and f'view={view}' in x]
return (trees[-1], saved[-1]) if trees and saved else None
tree_line, saved_line = wait(target_trace, 2)
def field(line, name):
match = re.search(rf'{name}=([^ ]+)', line)
if not match:
raise RuntimeError(f'missing {name}: {line}')
return match.group(1)
saved_node = field(saved_line, 'node')
saved_buffer = field(saved_line, 'buffer')
saved_tree = field(tree_line, 'tree')
x, y = map(int, field(saved_line, 'global').split(','))
sw, sh = map(int, field(saved_line, 'source_size').split(','))
dw, dh = map(int, field(saved_line, 'dest').split(','))
box = [x, y, x + (dw or sw), y + (dh or sh)]
record('saved_target', view=view, tree=saved_tree, node=saved_node,
buffer=saved_buffer, source=target_source, box=box,
tree_trace=tree_line, saved_trace=saved_line)
if (saved_buffer != target_source['source_buffer'] or
field(saved_line, 'global_valid') != '1' or
field(saved_line, 'enabled') != '1' or sw <= 0 or sh <= 0 or
not box[0] < 0 < box[2]):
raise RuntimeError('actual saved target identity/geometry gate failed')
wait(lambda: any('SB_RENDER ' in line and f'node={saved_node}' in line
for line in compositor_log()[before_log:].splitlines()), 1)
if f'view={view}' not in tree_line:
raise RuntimeError('saved target view mismatch')
snapshot('held')
if time.monotonic_ns() - held_ns < 1200000000:
# The H2 move precedes the H1 move so final H2 can be compared whole.
inject('move', 100, 100)
inject('move', max(-700, min(-100, (box[0] + min(box[2], 0)) // 2)),
max(80, min(500, (box[1] + box[3]) // 2)))
record('input_sampled', reference_tail=(run / 'reference.log').read_text().splitlines()[-10:],
target_tail=client_log().splitlines()[-10:])
else:
record('input_not_tested', reason='insufficient natural timeout window')
wait(lambda: compositor_log().count('timed out') > old_timeout, 4)
if client.poll() is not None:
raise RuntimeError('target client exited before natural timeout')
held_tail = client_log().split(held_line, 1)[-1]
if 'ACK ns=' in held_tail:
raise RuntimeError('target client ACKed held configure')
record('timeout_observed', held_serial=field(held_line, 'serial'),
client_alive=True, no_ack=True)
wait(lambda: any('SB_TREE_DESTROY ' in line and f'view={view}' in line and
f'tree={saved_tree}' in line for line in
compositor_log()[before_log:].splitlines()), 2)
record('target_tree_destroyed', tree=saved_tree)
time.sleep(.2)
snapshot('timeout-applied')
if client.poll() is not None:
raise RuntimeError('target client exited after timeout')
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)
pointer = spawn([str(lab / 'pointer-daemon'), '1600', '600'],
'pointer.log', stdin=True)
wait(lambda: 'POINTER_READY' in (run / 'pointer.log').read_text())
command('workspace 1')
command('layout mosaic')
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')
reference = spawn([str(lab / 'static-reference')], 'reference.log', stdin=True)
wait(lambda: 'COMMIT ns=' in (run / 'reference.log').read_text())
tree = ipc('-t', 'get_tree')
ref = find(tree, lambda n: n.get('app_id') == 'org.mosaic.StaticReference')
if not ref or ref['rect']['x'] != 0:
raise RuntimeError('static reference not mapped on H2')
record('static_reference', node=ref)
command('workspace 1')
time.sleep(.2)
reference_stability()
preflight_target()
episode()
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,224 @@
#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;
#ifdef STATIC_REFERENCE
/* CONTRACT: H2 uses one opaque SHM color across every real configure. */
color = 0x00303038;
#endif
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);
#ifdef STATIC_REFERENCE
xdg_toplevel_set_title(toplevel, "Mosaic Static Reference");
xdg_toplevel_set_app_id(toplevel, "org.mosaic.StaticReference");
#else
xdg_toplevel_set_title(toplevel, "Mosaic Saved Buffer Probe");
xdg_toplevel_set_app_id(toplevel, "org.mosaic.SavedBufferProbe");
#endif
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,5 @@
/* WHY: a deterministic H2 reference cannot contain Kitty's blinking cursor.
* CONTRACT: use the same real XDG configure/ACK loop with fixed opaque SHM.
* INVARIANT: once configured and stable, this client changes no pixels. */
#define STATIC_REFERENCE 1
#include "saved-client.c"

View file

@ -0,0 +1,39 @@
--- a/sway/input/cursor.c
+++ b/sway/input/cursor.c
@@ -1,5 +1,7 @@
#include <assert.h>
#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
#include <libevdev/libevdev.h>
#include <linux/input-event-codes.h>
#include <errno.h>
@@ -58,6 +60,17 @@
}
if (scene_node) {
+ /* WHY: one sampled motion must distinguish saved node from live surface.
+ * CONTRACT: passive private trace; no seat, scene or hit result changes.
+ * INVARIANT: log borrowed identities only during this lookup. */
+ struct timespec hit_ts;
+ clock_gettime(CLOCK_MONOTONIC, &hit_ts);
+ if (getenv("SAVED_HIT_TRACE")) {
+ fprintf(stderr, "TARGET_HIT ns=%lld point=%.2f,%.2f node=%p saved=%d\n",
+ (long long)hit_ts.tv_sec * 1000000000LL + hit_ts.tv_nsec,
+ lx, ly, (void *)scene_node,
+ scene_descriptor_try_get(scene_node, SWAY_SCENE_DESC_SAVED_BUFFER) != NULL);
+ }
// determine what wlr_surface we clicked on
if (scene_node->type == WLR_SCENE_NODE_BUFFER) {
struct wlr_scene_buffer *scene_buffer =
@@ -68,6 +81,10 @@
if (scene_surface) {
*surface = scene_surface->surface;
}
+ if (getenv("SAVED_HIT_TRACE")) {
+ fprintf(stderr, "TARGET_HIT_SURFACE node=%p surface=%p\n",
+ (void *)scene_node, scene_surface ? (void *)scene_surface->surface : NULL);
+ }
}
// determine what container we clicked on

View file

@ -0,0 +1,43 @@
"""Offline ledger limits and lock contention; 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, 'run')
reserve(lab, 'episode')
reserve(lab, 'camera')
for _ in range(12):
reserve(lab, 'capture')
assert json.loads((lab / 'timeout-budget.json').read_text()) == {
'run': 1, 'episode': 1, 'camera': 1, 'capture': 12}
for resource in ('run', 'episode', 'camera', 'capture'):
try:
reserve(lab, resource)
raise AssertionError(f'{resource} oversubscription accepted')
except RuntimeError:
pass
print('PASS one run/episode/camera and twelve captures')
def contender(path, queue):
try:
reserve(Path(path), 'run')
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')