docs(mosaic): preserve saved target timeout and input evidence
This commit is contained in:
parent
75f057d471
commit
6fd8945d10
16 changed files with 1264 additions and 0 deletions
1
.gitattributes
vendored
1
.gitattributes
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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**.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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**.
|
||||
|
|
|
|||
230
docs/design/STEP20C_SAVED_TARGET_TIMEOUT_INPUT_PROBE.md
Normal file
230
docs/design/STEP20C_SAVED_TARGET_TIMEOUT_INPUT_PROBE.md
Normal 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.
|
||||
28
tooling/probes/step20c-saved-target-timeout-input/README.md
Normal file
28
tooling/probes/step20c-saved-target-timeout-input/README.md
Normal 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`.
|
||||
203
tooling/probes/step20c-saved-target-timeout-input/analyze.py
Normal file
203
tooling/probes/step20c-saved-target-timeout-input/analyze.py
Normal 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)}')
|
||||
30
tooling/probes/step20c-saved-target-timeout-input/budget.py
Normal file
30
tooling/probes/step20c-saved-target-timeout-input/budget.py
Normal 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
|
||||
23
tooling/probes/step20c-saved-target-timeout-input/prepare.sh
Normal file
23
tooling/probes/step20c-saved-target-timeout-input/prepare.sh
Normal 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"
|
||||
391
tooling/probes/step20c-saved-target-timeout-input/run.py
Normal file
391
tooling/probes/step20c-saved-target-timeout-input/run.py
Normal 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)
|
||||
224
tooling/probes/step20c-saved-target-timeout-input/saved-client.c
Normal file
224
tooling/probes/step20c-saved-target-timeout-input/saved-client.c
Normal 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, ®istry_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;
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
|
|
@ -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')
|
||||
Loading…
Reference in a new issue