docs(mosaic): preserve SceneFX effects and damage probe evidence

This commit is contained in:
fy59 2026-10-12 12:32:06 +02:00
parent 542d9b9c94
commit aefee44c63
12 changed files with 1033 additions and 6 deletions

View file

@ -4,6 +4,8 @@
### STEP20C experimental evidence
- Added a source-only private effects/damage probe with a real translucent SHM buffer and striped background. Both standard controls show 27,594 shadow-overflow pixels on the neighboring output; private Mosaic excludes that sampled spill, renders a visibly filtered blur within its viewport and restores exact settled pixels after three camera pairs. Sampled private damage covers changed pixels, while interior shadow, optimized cache, popup effects and saved buffers remain unqualified. The result is PARTIALLY_FEASIBLE; no production or package files changed.
- Recorded an effects-probe campaign deviation: replay/trace variants ran 15 camera pairs against the requested maximum of three. All raw runs are retained; the final runner now reserves a finite campaign budget before launch and rejects excess repetitions.
- Preserved and independently replayed a bounded event-loop pending-input probe. The client flush and server application were observed after decoupled request submission, but no input mutation fell between a matching native headless commit and present in five attempts per private mode. The temporal case remains INCONCLUSIVE / NOT EXERCISED; this research line is closed for the tested backend and geometry. No production or package files changed.
- Preserved and independently replayed a commit/present/input ordering probe. A passive private top-level listener and native headless traces found 20 applied input-region mutations but no natural mutation between an accepted output commit and its matching present; sampled hit and client delivery followed effective input. The temporal verdict is INCONCLUSIVE and the generation-contract decision remains UNDECIDED. A complete outside-Git raw evidence manifest now covers all 94 run/result files. No production or package files changed.
- Preserved a reproducible mutation-coverage fixture and source audit. A fresh-clone replay rebuilt private SceneFX/Mosaic and verified pinned wlroots sources in its own lab. Bounded trace-only runs separated camera/clip, popup lifecycle, same-size client content and input-region hits; the latter changed hit testing at an unchanged partial epoch. A real private output-destroy path released one held reference, while backend rejected-commit cleanup remains untested. The architectural generation decision is UNDECIDED; STEP20C stays NEXT.

View file

@ -195,6 +195,21 @@ buffer and later hit remain separate identities. The temporal line is closed
for this backend/matrix after five attempts per mode, while the overall
cross-time architectural question remains **UNDECIDED**.
## STEP20C effects and damage evidence
The [STEP20C effects/damage probe](design/STEP20C_EFFECTS_DAMAGE_PROBE.md)
used a private inherited SceneFX clip with genuinely visible translucent
blur and an ordinary shadow crossing the neighboring output. The sampled
Mosaic blur stayed on its output; the ordinary 27,594-pixel shadow spill
was absent there under Mosaic. Three settled hide/return pairs reproduced
the original pixels, and sampled private damage covered changed pixels.
The Mosaic interior shadow was not independently visible, while optimized
blur cache correctness, out-of-viewport source sampling, popup effects and
saved buffers remain unqualified. The private result is
**PARTIALLY_FEASIBLE** and does not establish a production rendering or
transaction contract. STEP20C remains **NEXT**; multi-output Mosaic remains
**NOT YET SUPPORTED**.
## IPC
On a Mosaic workspace, `get_tree` reports `layout: "mosaic"` and adds a `mosaic` object with `active_row` and `rows`. Each row entry contains ordered child node IDs, `scroll_x`, and focused node ID or null. Existing Sway fields retain their shapes; normal workspace/window events and `get_workspaces` paths remain in use. This is an extension to the Sway layout vocabulary: strict IPC consumers with a closed layout enum must recognize `mosaic`. Additional rows in the model are not a promise of usable multi-row IPC behavior.
@ -212,3 +227,10 @@ Session V2 restoration is **PLANNED**. It must restore Mosaic activation, ordere
- Simulating tiling with floating windows or a repositioning daemon.
- Embedding personal dotfiles or keyboard bindings in the package.
- Maintaining a permanently diverged SwayFX fork instead of a reviewable delta.
Effects freeze audit: the historical campaign exceeded its three-pair budget
(15 pairs in five Mosaic runs); all evidence is retained. A separate fresh
rebuild reproduced the main counts with one pair under the corrected
campaign guard. The traced reference had a different effective size, so
instrumentation neutrality and its cause remain unqualified. See the
[effects report](design/STEP20C_EFFECTS_DAMAGE_PROBE.md). This adds no contract.

View file

@ -150,6 +150,24 @@ interleavings. The sought same-interval hit and grabbed-popup variant are
repeat attempts on the same headless matrix are closed. Effects/damage,
scaling and resize/migration remain open STEP20C gates.
The [effects/damage follow-up](design/STEP20C_EFFECTS_DAMAGE_PROBE.md) is
**PARTIALLY_FEASIBLE** only for the sampled negative-origin private matrix.
Both standard controls showed a genuine shadow overflow into 46 neighboring
columns; the private Mosaic output showed none. A real translucent blur
changed patterned pixels inside Mosaic without changing the neighboring
output, and three settled camera returns were pixel-identical to baseline.
Sampled private damage extents covered capture-to-capture changes. The
Mosaic interior shadow was not independently visible. Optimized blur cache
correctness, source sampling outside the viewport, popup effects, saved
buffers, intermediate frames and other renderers remain **NOT TESTED**.
The optional trace changed the neighboring reference rendering in Mosaic,
so visual claims use the untraced runs. Production multi-output Mosaic
remains **NOT YET SUPPORTED**.
The final audit also found that reproduction/trace replays executed 15
camera pairs instead of the requested campaign maximum of three. This
protocol deviation is retained with all raw runs and prevents claiming
full campaign compliance; the final fixture now enforces the finite budget.
The [isolated feasibility probe](design/STEP20C_FEASIBILITY_PROBE.md) is **PARTIALLY FEASIBLE**, not a release qualification. On two horizontal headless outputs, real Mosaic content leaked visually into an empty adjacent output; that earlier disposable candidate also delivered pointer enter from a partly visible client outside its viewport. Negative-origin, scaled and rotated arrangements showed visual leakage in that candidate. Popups, effects, saved buffers and complete damage correctness remain unqualified across the full matrix. The later probes above narrow individual cases without qualifying multi-output Mosaic, which remains **NOT YET SUPPORTED**.
The later [SceneFX subtree clipping spike](design/STEP20C_SCENEFX_CLIPPING_SPIKE.md) is also **PARTIALLY FEASIBLE**. Its private inherited tree clip stopped tested tiled pixels and pointer hits from crossing the output boundary, including a fully hidden cell, but a real GTK XDG popup rendered and received input on the neighboring output because its scene tree lives in the global popup layer. Dedicated subsurface, saved-buffer, nested-popup and complete effect/damage validation remain open. The spike changed no production library or compositor source; multi-output Mosaic remains **NOT YET SUPPORTED**.

View file

@ -39,7 +39,9 @@ The [root README](../README.md) is the public summary. For a specific topic, use
[Ordering source fixture](../tooling/probes/step20c-commit-present-input/README.md) rebuilds the passive private trace.
22. [STEP20C event-loop pending-input probe](design/STEP20C_EVENT_LOOP_PENDING_INPUT_PROBE.md): bounded asynchronous client submission and native callback order; no natural commit-to-present input mutation, INCONCLUSIVE.
[Pending-input source fixture](../tooling/probes/step20c-event-loop-pending-input/README.md) replays the disposable matrix.
23. [STEP20A](STEP20A.md) and [STEP20B](STEP20B.md): historical evidence.
23. [STEP20C effects/damage probe](design/STEP20C_EFFECTS_DAMAGE_PROBE.md): real translucent blur and shadow-overflow pixels compared across standard and private Mosaic layouts; sampled damage and camera returns PARTIALLY_FEASIBLE, optimized blur cache unqualified.
[Effects/damage source fixture](../tooling/probes/step20c-effects-damage/README.md) rebuilds the disposable matrix.
24. [STEP20A](STEP20A.md) and [STEP20B](STEP20B.md): historical evidence.
## Project
@ -85,6 +87,7 @@ The [root README](../README.md) is the public summary. For a specific topic, use
| [STEP20C mutation coverage probe](design/STEP20C_MUTATION_COVERAGE_PROBE.md) | Experimental evidence | PARTIALLY_FEASIBLE overall; an input-region change alters the hit at unchanged partial epoch, client recolor changes pixels without geometry, and a held output reference is released on sampled output destruction; cross-time generation contract remains UNDECIDED |
| [STEP20C commit/present/input ordering probe](design/STEP20C_COMMIT_PRESENT_INPUT_ORDERING_PROBE.md) | Experimental, independently replayed evidence | INCONCLUSIVE for the discriminating temporal window; headless input-region commits and hits agree in observed states, but none of 20 applied mutations occurred between a native output commit and its present |
| [STEP20C event-loop pending-input probe](design/STEP20C_EVENT_LOOP_PENDING_INPUT_PROBE.md) | Experimental, independently replayed evidence | INCONCLUSIVE / NOT EXERCISED for natural commit→input mutation→present despite decoupled client submission; the temporal line is closed for this backend/matrix |
| [STEP20C effects/damage probe](design/STEP20C_EFFECTS_DAMAGE_PROBE.md) | Experimental, independent private replay | PARTIALLY_FEASIBLE for visible blur, excluded sampled shadow overflow, settled camera restoration and sampled damage; shadow interior, optimized cache, popup effects and saved buffers remain unqualified |
A proposal does not supersede accepted canonical contracts or assert implementation status. Its open feasibility items must be resolved before implementation.

View file

@ -128,11 +128,10 @@ tested that gate without changing backend scheduling. Twenty real input
region mutations applied in two private modes, but none occurred between
an accepted output commit and its matching headless present; P2 is
**INCONCLUSIVE**. The sampled hit and client delivery matched effective
input state, and no distinct Mosaic clipping defect was reproduced. Before
deciding whether an extra generation contract is necessary, the smallest
next gate is passive event-loop phase tracing with a client request already
pending during ordinary repaint, then a same-interval hit if naturally
observable. STEP20C remains **NEXT** and multi-output **NOT YET SUPPORTED**.
input state, and no distinct Mosaic clipping defect was reproduced. The
subsequent pending-input event-loop follow-up below addressed that bounded
timing question without observing the target interval. STEP20C remains
**NEXT** and multi-output **NOT YET SUPPORTED**.
The bounded [event-loop pending-input probe](design/STEP20C_EVENT_LOOP_PENDING_INPUT_PROBE.md)
decoupled normal client submission from IPC/client acknowledgements, then
@ -144,6 +143,18 @@ contract or production readiness. The next separate STEP20C gate should
address complete effects/damage, scaling, or resize/migration. STEP20C stays
**NEXT** and multi-output Mosaic **NOT YET SUPPORTED**.
The [effects/damage probe](design/STEP20C_EFFECTS_DAMAGE_PROBE.md) then
demonstrated genuine shadow spill in both standard controls, excluded the
sampled spill under private Mosaic, and measured a visibly active
translucent blur, sampled damage coverage and three exact settled camera
returns. Its result is **PARTIALLY_FEASIBLE**: interior Mosaic shadow,
optimized blur cache correctness, outside-viewport shader sampling, popup
effects and saved buffers remain unqualified. The next distinct gate should
test scaling first for the inherited clip and effect draw regions;
rotation, resize/migration, saved buffers and ABI/package maintenance remain open.
This does not authorize STEP20C production work or STEP20D. STEP20C stays
**NEXT** and production multi-output Mosaic **NOT YET SUPPORTED**.
## STEP20D — multi-row Spatial Canvas · PLANNED
**Goal:** connect rows to runtime tree, transactions, scene, input, and IPC. **Planned deliverables:** active-row navigation, row-local scroll/focus, deterministic insertion/reflow, in-row reorder, and between-row movement. **Exit criteria:** real tiled containers preserve identity and order across row changes; focus and camera restore correctly; headless multi-row tests pass. **Docs first:** settle exact interactions and row invariants in UX and Architecture. Unset key combinations remain TBD.
@ -159,3 +170,10 @@ address complete effects/damage, scaling, or resize/migration. STEP20C stays
## STEP20G — package and daily-use qualification · PLANNED
**Goal:** publish a validated optional session while preserving standard SwayFX fallback. **Planned deliverables:** final Arch package, optional distinct session entry, install/rollback instructions, and a forward port proven against a newer SwayFX release. **Exit criteria:** package file ownership and coexistence verified, release build and headless/live qualification recorded, standard SwayFX updates remain independent. **Docs first:** update Packaging, Upstream, Development, UX, and Known Limitations with the intended release contract.
Effects freeze audit: the historical campaign exceeded its three-pair budget
(15 pairs in five Mosaic runs); all evidence is retained. A separate fresh
rebuild reproduced the main counts with one pair under the corrected
campaign guard. The traced reference had a different effective size, so
instrumentation neutrality and its cause remain unqualified. See the
[effects report](design/STEP20C_EFFECTS_DAMAGE_PROBE.md). This adds no contract.

View file

@ -0,0 +1,446 @@
# STEP20C SceneFX effects and damage probe
**Status: EXPERIMENTAL / PARTIALLY_FEASIBLE for the sampled matrix.** STEP20C stays
**NEXT**, STEP20B **EXPERIMENTAL**, and production multi-output Mosaic
**NOT YET SUPPORTED**. This is a bounded private feasibility measurement,
not a UX, ABI, package or production decision.
## Question and current evidence
The private inherited clip and owner-derived popup guard passed sampled
tiled, nested popup, grab and negative-origin cases. The earlier SceneFX
spike accepted `shadows enable` and `blur enable` and showed a sampled shadow
boundary, but did not prove a distinct translucent blur, optimized blur
cache correctness or effect-specific damage. This probe requires an effect
to be **created and enabled, geometrically placed and measurably visible**
in both standard controls before its Mosaic clipping can pass.
The viewport is HEADLESS-1 `(-800,0,800,600)` beside HEADLESS-2
`(0,0,800,600)`, both scale 1 and normal transform. Mosaic's applied Row 0
clip should prevent tiled-owner content, including its shadow and blur draw
regions, from writing outside the workspace viewport. An effect must remain
visible in its permitted interior. The ordinary client on HEADLESS-2 must
retain its own pixels. A blur shader may sample outside its draw region;
source sampling, visible influence and a contract violation are distinct
claims. No policy forbidding a particular shader source sample has yet been
accepted, so T3 cannot be called a leak on sample reach alone.
## Source audit and working hypotheses
Pinned inputs: private Mosaic base
`cd550382b311efdbfe3ed892f4e1d281d852e754`, SceneFX
`37ccd723bef49e6891156ffafce8f549f01446cc`, wlroots 0.20.2.
The preserved private clip patch is layered on these bases. In SceneFX
`types/scene/wlr_scene.c`, `scene_node_intersect_clip` intersects ancestor
tree clips for each leaf. `scene_node_update_iterator` intersects
`node->visible`; `scene_node_bounds` intersects new bounds; and
`scene_node_update` damages old and new visibility. The render-list
constructor rejects empty visible intersections. `scene_entry_render`
passes `node->visible ∩ output damage` as the draw clip for buffer, rect,
shadow and blur entries. This demonstrates a route for all four node types
through the inherited **draw** clip, not proven shader sampling bounds.
`scene_output_damage` maps logical damage into the output buffer and queues
repaint. Its extents can exceed changed pixels and cannot prove every
intermediate frame or optimality.
SwayFX creates a shadow and blur under each container `scene_tree`; their
geometry and enabled state are updated in `sway/desktop/transaction.c`;
`output_configure_scene` additionally configures blur selection, strength
and corner radii using the closest container. A tiled container should inherit its tiling
tree clip. Layer-shell surfaces have their own effect nodes, with different
ownership. Root XDG popups are globally parented and receive the separate
owner-derived private guard; no popup effect is assumed without observed
node creation. Each output creates an `optimized_blur` node under staging,
outside the tiled owner's tree. `mark_dirty` sets its dirty flag only while
enabled; scene rendering clears it after a successful optimized blur pass.
SwayFX marks it dirty on selected blur/layer changes. Camera movement alone
is not yet proven to invalidate all relevant optimized caches. Thus the
ordinary inherited clip does **not** directly own the output optimized node.
The optimized cache pass itself uses its output-sized destination box;
it does not receive the tiled descendant's `render_region` draw clip.
The final ordinary blur texture draw does receive that clip. These two
operations must be audited separately.
The box-shadow node can extend by its configured sigma and offset beyond
the view's rectangle. `scene_entry_render` supplies a clipped draw region,
while shader/filter sampling may use a wider source footprint. The
`apply_blur_region` path expands damage for blur sampling; actual output
pixels and damage must be measured. Saved transaction buffers are distinct
from client SHM buffers, output buffers and optimized blur caches. No saved
buffer claim is valid without an observed controlled creation/use path.
These source answers are hypotheses until real effects are activated. A
successful IPC command, compiled shader, enabled flag or nonempty render
list alone is insufficient visual evidence. The renderer, EGL/GLES version,
headless backend, buffer formats and linked SceneFX/wlroots versions must be
recorded for each private run; `WLR_RENDERER=pixman` is a request, not proof
of the renderer actually used.
## Fixture and comparisons
Rebuild from a full Git clone into a private temporary lab. Preserve the
same deterministic SHM clients and effect settings in three modes:
unmodified SwayFX `splith`, private `splith`, private `mosaic`. Use explicit
private runtime and IPC socket, bounded process groups, timeouts and
captures; never address the daily session. The visual client will supply a
documented high-contrast striped/damier background, a translucent surface,
an opaque surface, and a stable ordinary reference on HEADLESS-2. Record
all SHM sizes, colors, alpha values, geometry and mapping. Disable dynamic
content and animations where the normal compositor allows it. Fix pixel
regions of interest before inspecting anomalies: interior effect, boundary
band on each output, unchanged reference and exposed/old-owner regions.
PNG coordinates are output-local; Wayland/Mosaic global coordinates are
signed.
Separate **FX0** (shadow/blur off), **FX1** (shadow only), **FX2** (blur on
translucent patterned background), **FX3** (optimized blur only if a real
enabled node and dirty/cache transition can be observed), and **FX4**
(combined only after independent FX1/FX2 proof). For each effect require:
accepted configuration, observed enabled node and size/position, a
preselected pixel ROI changing against FX0, and both standard controls
visibly exercising it. An unavailable effect is `NOT TESTED`; visible but
inseparable effects are `INCONCLUSIVE`.
## Bounded scenarios
* **T1 shadow boundary:** standard controls visibly draw a shadow across
global `x=0`; Mosaic retains its permitted interior and excludes its
forbidden right-output pixels. If controls do not overflow, no clipping
claim follows.
* **T2 translucent blur boundary:** demonstrate blur distinct from alpha
compositing with effect off/on ROIs, then compare the three modes.
* **T3 hidden sampling influence:** keep inside-viewport sources fixed,
alter only an outside pattern, and test visible blurred pixels. Record
draw clip and sampling evidence separately; classify contract legitimacy
conservatively.
* **T4/T5 hidden owner and return:** at most three `scroll_x 400→1280→400`
pairs, with right-output reference, enabled nodes, cache state, settled
pixels and old/new damage checked. Do not accidentally dismiss popups.
* **T6 effect toggle and close:** disable/re-enable by normal commands,
then close the relevant surface and compare discovered regions.
* **T7 standard-layout return:** switch to `splith` while surfaces remain
open; ordinary effects must return without Mosaic clip leakage.
Only after main cases are valid, consider one bounded real-serial nested
popup hierarchy. First audit whether that hierarchy owns any applicable
effect node. No positive popup effect is invented. A horizontal regression
is allowed only if it distinguishes an observed anomaly. No scale, rotation,
migration or saved-buffer animation is silently added.
## Pixel, damage and classification rules
Record per capture output, dimensions, phase, monotonic capture window,
SHA-256, fixed ROIs, reference colors, pixel difference count and bounding
box, plus color-distance summaries for blur. Use lossless PNG. Compare FX1
and FX2 separately against FX0 with identical clients/geometry. Never mask
an anomalous area after seeing it. Verify actual enabled nodes and renderer
before interpreting equal pixels.
The fixed output-local ROIs for the 800×600 images are: left effect interior
`(300,100,700,500)`, left boundary `(720,50,800,550)`, right boundary
`(0,50,80,550)`, and right reference `(250,250,550,500)`. Whole-image
differences are retained as well. The striped wallpaper alternates
`#f5f5f5` and `#0f192d` every 8 pixels horizontally; the test client's
translucent top buffer is premultiplied ARGB `0x88442010`. The first FX1
capture showed no pixel difference from FX0, so shadow visibility must be
established by an amended, documented fixture before T1 can pass.
Retain `SPIKE_DAMAGE`, `TEMPORAL_DAMAGE`, accepted output commits and native
headless presents when present in the private chain. For each transition,
compare changed-pixel boxes with the union of observed damage extents on
the same output, and inspect restored/old regions. Damage coverage is a
bounded observation: it is not minimum damage, every-frame correctness, or
submitted-buffer identity. Mark cache dirty and pass events with passive
private traces only if existing logs are insufficient. Never alter the
clip algorithm, shaders, public ABI or backend scheduler to obtain a pass.
**FEASIBLE** requires visibly active relevant effects in both standards,
permitted interior pixels in Mosaic, no prohibited adjacent-output writes,
correct settled restoration and damage evidence supporting each claim.
**PARTIALLY_FEASIBLE** means some effects pass but other effect/cache/damage
paths remain unproven. **INCONCLUSIVE** means visibility or origin cannot be
established. **BLOCKED** requires a reproducible pixel leak, stale cache,
crash or standard-layout regression, with a minimal retained reproducer.
Stop the affected scenario on a concrete leak; do not implement an
algorithmic repair in this ticket. Even FEASIBLE here does not qualify all
renderers, scaling, rotation, migration, saved buffers or SceneFX packaging.
Keep source patches and fixture code under
`tooling/probes/step20c-effects-damage/`; keep binaries, builds, logs, PNGs,
protocol-generated files and sockets outside Git. Source and complete raw
evidence manifests must be checked separately. Synchronize canonical
Architecture, Known Limitations, Roadmap, index and Changelog to observed
results, without changing UX. This protocol is updated with actual evidence
before the final local handoff; phase B is never committed in this ticket.
## Executed matrix and evidence
Final verification amendment: reuse the preserved
`step20c-presentation-identity/presentation-events.patch` in the private
Mosaic copy to retain native commit/present identities. The neighboring
`wev` reference keeps its natural floating size rather than receiving a
late resize request; its position stays `(250,250)`. Run the supplemental
effect/native-event control with **zero camera pairs**. The three already
measured camera pairs remain the restoration evidence; do not increase
that repetition count to obtain a preferred timing result.
**Protocol deviation discovered at final audit:** the default runner
repeated three camera pairs in each exploratory/reproduction/trace Mosaic
run. Across five such runs, **15 pairs actually ran**, exceeding the
ticket's global maximum of three. The final native supplement used zero
pairs, but that does not repair the earlier excess. All 15 retained runs and 342
PNGs are retained and included in the final raw manifest. Numerical pixel
claims use named comparable runs; selecting them does not make the whole
campaign compliant. The final runner locks a lab campaign and reserves a
persisted three-pair budget before launch, including failed attempts. Its
budget rejection was verified without creating another run. No further
compositor experiment was executed after discovery of the deviation.
The independent full clone at
`/tmp/step20c-effects-clone` contains the frozen repository commit
`542d9b9c945b22cf9134a4a643c8dc763b34607d` plus the local source
fixture. The private build and all selected evidence are in
`/tmp/swayfx-mosaic-nested-grab-OIWBj9xM/`. Its headless backend actually
loaded the SceneFX FX renderer with OpenGL ES 3.2 Mesa 26.2.4-arch1.1 on
AMD Radeon 780M (radeonsi); output GBM buffers were XR24 and XB24.
`WLR_RENDERER=pixman` did not imply Pixman rendering. Output IPC confirms
HEADLESS-1 `(-800,0)` and HEADLESS-2 `(0,0)`, each 800×600, scale 1,
normal transform. The translucent client supplied a real ARGB buffer;
the wallpaper used the documented eight-pixel alternating stripes.
The comparable, **untraced** three-run matrix finished without fixture
failure: ordinary `splith` 18 captures/3.822 s, private `splith` 18
captures/3.800 s, and private Mosaic 32 captures/5.762 s. The Mosaic run
included exactly three hide/return pairs. `effects-damage-results/
pixel-analysis.json` retains every capture hash, monotonic capture window,
whole-image/ROI difference and sampled transition damage comparison.
| Mode | FX1 shadow pixels on right output | FX2 blur pixels on left output | FX2 pixels on right output |
| --- | ---: | ---: | ---: |
| Ordinary `splith` | 27,594, columns 0–45 | 112,308 | 0 |
| Private `splith` | 27,594, columns 0–45 | 112,308 | 0 |
| Private Mosaic | 0 | 319,734 | 0 |
The ordinary and private-standard shadow screenshots and differential
pixels show a genuine spill across global `x=0`. The corresponding Mosaic
right screenshot remains byte-identical to FX0, so the tested overflow is
excluded. Private passive trace later observed an enabled shadow render
entry on HEADLESS-1 with a draw region clipped at local `x=800`. However,
Mosaic FX1 changed **zero sampled pixels on HEADLESS-1** even after
`gaps inner 24`; a visible authorized interior shadow was not established.
T1 therefore proves exclusion of the sampled right-output spill but does
not fully qualify shadow rendering inside the clip.
FX2 is a real filtered blur, not merely alpha compositing. At `y=300`,
the standard owner's stripe luminance standard deviation fell from about
50.32 (FX0) to 1.37 (FX2), and its mean adjacent-column contrast from
12.37 to 0.88. Mosaic's corresponding preselected strip fell from about
50.33 to 0.69 and 12.61 to 0.74. Enabled regular-blur render entries were
also observed in the private trace. The right output had zero FX2 changes
in all three modes. T2 is **FEASIBLE** for these sampled draw boundaries.
T3 source-sampling influence is **NOT TESTED**: no outside-only pattern
mutation with an unchanged interior was executed, and the viewport contract
does not yet prohibit a shader reading neighbors outside its draw clip.
For T4/T5, the three hidden captures were mutually pixel-identical; each
return was pixel-identical to the pre-hide FX4 reference on both outputs.
Thus no residual pixel was measured at the settled return. There is no
matched effects-off hidden-state reference, so absence of every effect
pixel in the hidden state is not established by repeat equality. T6 normal
effect disable/re-enable changed 112,308 left-output pixels after layout
return, then the owner closed and the exposed area was repainted. T7
`layout splith` succeeded while the owner remained mapped; its tree
geometry reverted to standard tiling and the subsequent enabled/disabled
effect toggles remained observable. Closure changes the exposed pixels,
but a fresh, identically arranged owner-absent reference was not captured;
complete left-output ghost exclusion after closure remains unqualified.
No grabbed XDG popup was opened, so
popup-effect ownership and dismissal remain **NOT TESTED**.
Private `TEMPORAL_DAMAGE` supplied 681 events in standard layout and 715
in Mosaic. For the sampled settled capture transitions, no changed pixel
fell outside the union of same-output damage extents between capture
windows; the uninstrumented ordinary control supplies no native damage
claim. These extents are often broad. This measurement does not prove
minimum damage, exact intermediate frames or output-buffer identity.
The initial matrix observed private output commits. A final supplemental
zero-camera-pair matrix reused the preserved native presentation listener:
private standard observed **82 commits / 80 presentations**, private Mosaic
**95 / 93**, with matching `(output, commit_seq)` identities. The two
initial commits of sequence 2 per private mode remain
**UNEXPLAINED / NO PRESENT OBSERVED**. This listener also confirms applied
tiled clip/scene geometry and removal on standard-layout return. The
supplement uses the natural-size neighboring reference and reproduces
the same shadow/blur difference counts as the original controls.
The optional `effect-trace.patch` is a passive private SceneFX patch,
base `37ccd723bef49e6891156ffafce8f549f01446cc` plus the preserved
historical patches. It records enabled shadow (type 3), optimized-blur
(type 4), and regular-blur (type 5) render entries, draw regions and the
optimized dirty flag. SHA-256:
`32056bd03177ad9b73e1f821bacd121dd39c3b8c078d96a8367f3b1ecc90bc01`.
That compiled variant is retained outside Git in
`effects-preparation-logs/effect-trace-compiled.patch`. The preserved
fixture adds WHY/CONTRACT/INVARIANT comments only, with final SHA-256
`6a89142afe2b019262033c7397d4c4593e1b7d0e63b0d620a546a2aa8ee29df7`.
The optimized entries included dirty=1 and dirty=0, but FX3 PNGs exactly
equaled FX2. Source inspection explains a limit of this comparison:
`output_configure_scene` already selects bottom-layer optimized blur for
tiled, non-overlapping containers, even with `blur_xray` disabled. The
regular blur node passes that selection through `use_optimized_blur`.
Therefore FX2 cannot be labeled a guaranteed non-optimized control, and
FX3 equality is not evidence that optimized blur was unavailable. Node
presence and dirty states were observed, but independent optimized-cache
invalidation correctness and a stale-cache test remain **NOT TESTED**.
The trace had a measurable fixture limitation. All 18 private-standard
PNGs matched the untraced run; 17/32 private-Mosaic PNGs matched. The
remaining 15 differed only in the right reference rectangle, where the
traced run's `wev` tree node was mapped but its checker did not appear.
Durations were 3.825 versus 3.800 seconds and 5.772 versus 5.762 seconds;
output commit counts were 80 versus 78 and 113 versus 112. Thus perfect
instrumentation neutrality is **not claimed**. The untraced matrix is
authoritative for pixel conclusions; the trace establishes node-path
presence only. No rendering algorithm, shader, scheduler or ABI changed.
The final raw manifest covers **890 files / 342 PNGs**, totaling
26,817,110 bytes: all contents of the **15 retained runs**, including
exploratory and excluded reference variants (configs, IPC, client/compositor logs,
captures, output states, exits and events), the wallpaper, generated runner,
preparation logs, compiled trace patch, source manifest, library metadata
and structured pixel analysis. Its SHA-256 is
`5fbe12b0312f2d82b3c5414251ae57345db8d25a83cfc018947308d40e752b42`;
all 890 entries verified. The structured analysis measures 174 PNGs in
eight named comparable/control runs; metadata identifies each role. The
separate 29-entry source manifest, including
the preparer chain, pinned patches, protocol inputs, clients and private
SceneFX/Mosaic sources, has SHA-256
`8fbd0cf927e3e2690a77aeaf2288072c183ef1dbe8f856cb63e0442454614d95`;
all 29 entries verified. The client patch SHA-256 is
`49070bb13ffc96a58e35693b34f1d25c60f824aba69507061211a44ecaeb8087`.
Exploratory runs remain excluded from selected numerical claims while
included in the complete raw manifest and the campaign repetition count.
**Verdict: PARTIALLY_FEASIBLE.** The tested inherited draw clip excludes
the demonstrated ordinary shadow spill, and the genuinely visible blur
stays within the sampled Mosaic output with exact settled camera returns.
Authorized interior shadow pixels, optimized cache behavior, outside-only
shader sampling, popup effects, saved buffers and intermediate frames are
unqualified. No concrete leak or standard-layout regression requires a
SceneFX redesign on these observations. The next independent STEP20C gate
should test scaling first, since this entire effects matrix uses scale 1
and normal transform; rotation, resize/migration, saved buffers and ABI/package
maintenance remain subsequent gates. STEP20C stays **NEXT** and production
multi-output Mosaic stays **NOT YET SUPPORTED**.
## Final technical validation and retained incidents
Shell syntax, Python AST, C17 client compilation, patch applicability,
private SceneFX/Mosaic builds, the Meson `mosaic-model` test (1/1), local
Markdown links and `git diff --check` passed. Native presentation callbacks
were later than their matching commit callbacks and reported presented
buffers; no relation is assigned to the two unmatched startup commits.
The actual private library linkage resolves `libscenefx-0.5.so` inside the
lab, wlroots 0.20.2 and Wayland 1.26.0 system libraries without replacement.
The preparer creates source hashes automatically; the analyzer creates the
complete chosen-run evidence manifest. No build/binary/capture is tracked.
Retained fixture incidents: the initial hand-written client patch failed
applicability before any experiment and the preparation trap removed only
its own failed lab; the corrected patch subsequently applied and compiled.
The late-resize neighboring-reference variant differed between untraced
and traced Mosaic and is excluded from perfect-neutrality claims. A source
manifest made during edits became stale when the analyzer was updated;
the final manifest was regenerated and every entry now passes. None is
silently converted into a compositor defect. The private build also retains
the existing upstream libinput enumeration warning; it compiled successfully.
All 15 run-specific runtimes created in this lab were removed. No lab
process or socket remained at validation. Production source, historical
patches, cumulative patch, `PKGBUILD` and UX are unchanged; phase B remains
local and uncommitted. The overall architectural conclusion is **UNDECIDED**:
the tested blur draw boundary is viable, but unexercised cache/sampling,
interior-shadow and buffer paths prevent a complete effects contract.
## Effects freeze V1 audit plan (2026-10-12)
Reverify the historical 890-entry evidence and 29-entry source manifests
without rewriting them. Audit the eight budget cases offline, including
launch failure, concurrency and interruption. Rebuild in a new full clone
with the pinned inputs and replay the three controls: zero camera pairs in
ordinary/private standard, one pair in Mosaic. The new reproduction has a
single three-pair campaign ceiling; no second lab is used to reset it.
Keep its manifest and results separate from the nonconforming historical
campaign. Audit mapped reference geometry and client events before assigning
any cause to the traced `wev` anomaly. No production or UX change is authorized.
### Independent audit of the historical reference incident
The retained FX0 IPC trees show `wev` as `floating_con`, visible=true,
at global (250,250) in each selected run. The untraced Mosaic reference
is actually 640×480; the traced Mosaic reference is 300×250. Both event
streams include floating enable, resize 300×250 and move 250,250, but the
client logs differ: only the traced variant receives the later 300×250
configure (serials 59/60); the untraced variant retains 640×480 (57/58).
The common runner maps `wev` on workspace 2 before mapping the owner on
workspace 1, then applies the floating/resize adaptation. `FX_TRACE` is
present in the traced compositor log. Mapping/IPC visibility is insufficient
to prove the checker was rendered. The anomalous 15 captures remain in the
historical manifest; no screenshot was removed. Geometry/configure timing
is a demonstrated confounder, not a proven cause. Logging causality and
perfect instrumentation neutrality remain unqualified. Main pixel claims
retain the untraced matrix as their reference.
The reservation block passed isolated executable tests of reservation
before launch, nonblocking exclusive flock, a launch returning failure,
SIGINT interruption, refusal after exhaustion, zero-pair operation with an
exhausted budget, and historical retained captures exceeding the limit.
The JSON reservation survives failure/interruption. Existing capture counts
are combined with the persisted count using max, not reset. These are local
process interruption tests, not a power-loss durability guarantee. An initial
audit harness failed before executing the reservation block because its
synthetic script path had insufficient parent components; the corrected
harness passed. No compositor or camera pair ran in that harness.
### Freeze V1 independent replay result
New full clone: `/tmp/step20c-effects-freeze-v1-clone`, HEAD
`542d9b9c945b22cf9134a4a643c8dc763b34607d`, with the source-only fixture
copied in before preparation. New lab:
`/tmp/swayfx-mosaic-nested-grab-xbAPurbe`. Preparation reconstructed pinned
SceneFX/Mosaic, checked each historical patch before application, rebuilt
private libraries/compositor and compiled the adapted C17 client. No
historical lab or binary was used as the reproduction source. The ordinary
control uses installed unmodified `/usr/bin/sway`, as the historical
fixture does; exact package-source byte identity is not claimed.
The three runs used `--camera-pairs 0`, `0`, `1` respectively; 18/18/24
captures, failure=null, compositor exit 0 and client exit 0. Helpers were
terminated by the runner (SIGTERM where recorded). FX1 right-output counts
are again 27,594 / 27,594 / 0; FX2 left counts 112,308 / 112,308 / 319,734,
with zero right changes in all modes. Mosaic's FX1 left remains zero;
interior shadow is still INCONCLUSIVE. The single camera return is
pixel-identical to FX4 on both outputs. Available sampled private damage
contains no changed pixel outside the recorded union; ordinary control
has no such native damage claim. Renderer again reports SceneFX FX,
GLES 3.2 Mesa 26.2.4-arch1.1, AMD Radeon 780M/radeonsi, wlroots 0.20.2.
The separate new 164-entry evidence manifest SHA-256 is
`46ac42d414a63ccf5f6f02e6d616fadb72b5533fa8a1b8f56687fdb9600e36c6`;
new 29-entry build-source manifest:
`3693b3be887a8781b3010f330ddeed2c1b69228228f0d579b13b847ccded0bde`.
Every entry verifies. The persisted budget is consumed=1; actual captured
pairs=1. Historical manifests separately verify 890/890 and 29/29 at their
original announced hashes. Historical patches, including the distinct
compiled trace provenance, are unchanged. The new source manifest records
the build inputs before subsequent documentation amendments; it is not
represented as a final-publication source manifest.
Shell syntax, Python AST, controlled patch application, C17 compilation,
private builds, model test 1/1, local links, unchanged production paths
and `git diff --check` pass. A Pillow getdata deprecation warning is
retained; it did not invalidate current analysis. No runtime was left by
the three runs. Verdict remains PARTIALLY_FEASIBLE. The historical 15
pairs remain supplementary measurements outside the authorized original
protocol; this new conforming reproduction does not retroactively authorize
them or erase the instrumentation-neutrality limitation.

View file

@ -0,0 +1,128 @@
# STEP20C effects and damage source fixture
**EXPERIMENTAL / PARTIALLY_FEASIBLE for the measured matrix.** This fixture
only builds and runs disposable private headless copies. It does not install
or modify SwayFX, SceneFX, wlroots, the daily IPC socket, the cumulative
Mosaic production patch or `PKGBUILD`.
## Pinned inputs and private adaptation
Start from a full clone of this repository containing commit
`542d9b9c945b22cf9134a4a643c8dc763b34607d`. `prepare.sh` delegates
to the preserved temporal-damage preparer, which pins private Mosaic
`cd550382b311efdbfe3ed892f4e1d281d852e754`, SceneFX
`37ccd723bef49e6891156ffafce8f549f01446cc` and wlroots `0.20.2`.
The preserved presentation-identity patch then observes native output
commit/present callbacks in the private Mosaic copy.
It then applies `effect-glass.patch` **only to the lab copy** of the
pre-existing nested Wayland SHM client and compiles it as C17. That patch
changes the top buffer to premultiplied ARGB `0x88442010` so the underlying
64×64 wallpaper's 8-pixel `#f5f5f5` / `#0f192d` stripes remain visible.
The opaque buffer path stays unchanged. The patch does not touch compositor
rendering or scheduling. The normal private clip and damage patches are
those preserved by the earlier fixtures.
For a separate passive-node trace, set `STEP20C_EFFECT_TRACE=1` when calling
`prepare.sh`. This applies `effect-trace.patch` only to the private SceneFX
copy and recompiles it. Its `FX_TRACE` records node type, enabled flag,
geometry, draw region and optimized dirty flag. Compare its outputs with
the default untraced run before using it as visual evidence.
## Reproduction
From the full clone, with user-local build dependencies already available:
```sh
sh tooling/probes/step20c-effects-damage/prepare.sh
python tooling/probes/step20c-effects-damage/run.py "$LAB" control --camera-pairs 0
python tooling/probes/step20c-effects-damage/run.py "$LAB" private_standard --camera-pairs 0
python tooling/probes/step20c-effects-damage/run.py "$LAB" private_mosaic --camera-pairs 1
python tooling/probes/step20c-effects-damage/analyze.py "$LAB" \
"$LAB"/runs/control-effects-* \
"$LAB"/runs/private_standard-effects-* \
"$LAB"/runs/private_mosaic-effects-*
```
Read the exact `LAB=` path printed by `prepare.sh`; the code block uses it
as a shell variable for brevity. Pass only the chosen three run directories
to the analyzer when exploratory runs also exist. The runner uses the lab's
private runtime, private discovered `SWAYSOCK`, 120-second deadline,
40-capture and 35-MB limits. It uses ordinary IPC commands; it never
addresses the desktop session. `run.py` builds a local adaptation of the
preserved applied-state runner inside the lab; the generated Python file is
an artifact, not a tracked source.
The geometry is HEADLESS-1 `(-800,0)`, HEADLESS-2 `(0,0)`, both 800×600,
scale 1, normal transform. HEADLESS-2 has a floated `wev` reference. The
owner is the same nested SHM client in all three modes. `gaps inner 24`
is applied in all three modes. The standard layouts move the owner to the
right edge, so ordinary shadow overflow enters HEADLESS-2. Mosaic uses
`scroll_x 400`, then at most three `400→1280→400` camera pairs. Captures
are lossless PNG and all coordinates in them are output-local.
The reference keeps its natural floating size at `(250,250)`; an earlier
late-resize variant produced inconsistent reference rendering and is
retained as a fixture limitation. `--camera-pairs 0` allows native-event
or instrumentation controls without repeating the three camera pairs.
`--reference-resize` reproduces that retained variant, including its
known limitation. The original camera campaign used it; the final native
event supplement kept the natural reference size and used zero pairs.
The final runner counts camera pairs across the lab's retained runs and
locks the campaign against concurrent runners. A fresh lab permits at most
three pairs in total; subsequent trace/native controls must use
`--camera-pairs 0`. The executed ticket predates this guard and actually
ran 15 pairs over reproduction/trace variants, exceeding the requested
three-pair bound. All such runs are retained and manifested; this protocol
deviation limits the campaign qualification and is not hidden by selecting
a three-pair subset.
FX0 disables shadow and blur. FX1 enables shadow with `shadow_blur_radius
16`, `shadow_offset 30 0`, and `shadow_color #000000A0`. FX2 disables the
shadow and enables blur with radius 5 and three passes. FX3 additionally
enables `blur_xray`; this is **only an optimized-blur candidate** until a
dirty/cache transition is observed. FX4 enables both effects. T6 toggles
and closes the ordinary owner; T7 exits Mosaic through `layout splith`.
The analyzer records per-capture SHA-256, 800×600 dimensions, monotonic
capture windows, output-local fixed ROIs, whole-image and ROI differences,
per-transition pixel/damage comparison, and a complete raw-evidence SHA-256
manifest in `$LAB/effects-damage-results/`. A control without private
damage instrumentation records damage coverage as unknown. Its manifest
covers all files in the selected run directories plus the wallpaper PNG;
source hashes are recorded separately in the report. Old exploratory runs
are not silently substituted for selected comparable controls.
`--include-all-raw-runs` includes every retained run in the evidence
manifest while measuring only the explicitly chosen comparable runs.
## Measured result and limits
In the selected three-run matrix, both standard controls show a 27,594-pixel
shadow overflow in the first 46 columns of HEADLESS-2. Mosaic changes zero
pixels there. FX1 is invisible on the sampled Mosaic interior, even after
adding gaps, so its permitted interior rendering is **INCONCLUSIVE**.
FX2 produces 112,308 changed pixels on the standard HEADLESS-1 images and
319,734 on Mosaic; contrast of the eight-pixel stripe edges falls markedly,
establishing a real blur beyond alpha compositing. HEADLESS-2 stays
unchanged by that Mosaic blur. Three camera returns exactly reproduce the
pre-hide FX4 PNGs. Private damage extents cover the pixels changed between
selected settled captures, but are not proof of minimum damage or every
intermediate frame. The tiled FX2 path can already select optimized blur
when containers cannot overlap; FX3's `blur_xray` therefore does not
necessarily choose a different path. Optimized nodes and dirty=1/0 entries
were observed, but independent cache invalidation correctness, T3
out-of-viewport shader sampling, popup effects and saved buffers remain
**NOT TESTED**. The report
also retains an instrumentation-neutrality limitation: enabled shadow
(type 3), optimized blur (type 4) and regular blur (type 5) entries were
traced. Standard-layout PNGs matched the untraced run, but traced Mosaic
omitted the visible `wev` reference despite its mapped IPC tree entry.
The primary pixel claims use the untraced matrix. The report contains the
numerical evidence and the exact lab and manifest hashes.
## Freeze V1 independent replay
The 2026-10-12 audit rebuilt from `/tmp/step20c-effects-freeze-v1-clone`
at the pinned repository HEAD into `/tmp/swayfx-mosaic-nested-grab-xbAPurbe`.
The commands above consumed one pair total, with 18/18/24 captures and no
fixture failure. Historical evidence remains separate and nonconforming.
The reference-geometry audit found 640×480 versus 300×250 in the selected
untraced/traced Mosaic runs; instrumentation causality remains unknown.
See the report for verified historical and new manifest hashes and limits.

View file

@ -0,0 +1,163 @@
#!/usr/bin/env python3
"""Measure fixed output-local PNG regions and hash every retained raw proof."""
import argparse
import hashlib
import json
import re
from pathlib import Path
from PIL import Image, ImageChops, ImageDraw
ROIS = {
"left": {"interior": (300, 100, 700, 500),
"boundary": (720, 50, 800, 550)},
"right": {"boundary": (0, 50, 80, 550),
"reference": (250, 250, 550, 500)},
}
PHASES = ("FX1-shadow", "FX2-blur", "FX3-optimized-candidate",
"FX4-combined", "T4-hidden-1", "T5-return-1", "T4-hidden-2",
"T5-return-2", "T4-hidden-3", "T5-return-3",
"T7-standard-return", "T6-disabled", "T6-restored", "T6-closed")
def sha(path):
h = hashlib.sha256()
with path.open("rb") as source:
for block in iter(lambda: source.read(1024 * 1024), b""):
h.update(block)
return h.hexdigest()
def native_events(log):
commits = [{"output": m[0], "commit_seq": int(m[1]), "ns": int(m[2])}
for m in re.findall(r"PI_OUTPUT_COMMIT output=(\S+) commit_seq=(\d+) ns=(\d+)", log)]
presents = [{"output": m[0], "commit_seq": int(m[1]), "ns": int(m[2]),
"presented": bool(int(m[3]))}
for m in re.findall(r"PI_OUTPUT_PRESENT output=(\S+) commit_seq=(\d+) ns=(\d+).*?presented=(\d+)", log)]
identities = {(p["output"], p["commit_seq"]) for p in presents}
return {"instrumented": bool(commits), "commits": commits, "presentations": presents,
"unpresented": [c for c in commits if (c["output"], c["commit_seq"]) not in identities]}
def difference(first, second, box=None):
if box:
first, second = first.crop(box), second.crop(box)
delta = ImageChops.difference(first, second)
bbox = delta.getbbox()
channels = list(delta.getdata())
pixels = sum(channel != (0, 0, 0) for channel in channels)
return {"pixels": pixels, "bbox": bbox,
"mean_rgb_distance": round(sum(sum(channel) for channel in channels) /
(3 * delta.width * delta.height), 4)}
def analyze_run(run):
result = {"run": run.name, "captures": [], "comparisons": [],
"damage": {"events": 0, "outputs": {}}, "failure": None}
status = run / "exit-status.json"
if status.exists():
result["exit_status"] = json.loads(status.read_text())
events = []
event_file = run / "events.jsonl"
if event_file.exists():
events = [json.loads(line) for line in event_file.read_text().splitlines()]
captures = {(item["output"], item["tag"]): item for item in events
if item.get("kind") == "capture"}
damage_events = []
log = run / "compositor.log"
if log.exists():
log_text = log.read_text(errors="replace")
result["native_events"] = native_events(log_text)
result["renderer"] = {key: re.findall(pattern, log_text)[:1]
for key, pattern in {
"gles": r"Using (OpenGL ES[^\n]+)",
"gpu": r"GL renderer: ([^\n]+)",
"wlroots": r"wlroots version ([^\n]+)",
"scenefx": r"scenefx version ([^\n]+)",
}.items()}
damage = re.compile(r"TEMPORAL_DAMAGE ordinal=\d+ ns=(\d+) output=(\S+) extent=(-?\d+),(-?\d+)\.\.(-?\d+),(-?\d+)")
for match in damage.finditer(log.read_text(errors="replace")):
damage_events.append((int(match.group(1)), match.group(2),
tuple(int(match.group(i)) for i in range(3, 7))))
result["damage"]["events"] += 1
output = match.group(2)
result["damage"]["outputs"][output] = result["damage"]["outputs"].get(output, 0) + 1
for side in ROIS:
baseline = run / f"{side}-FX0-off.png"
if not baseline.exists():
continue
with Image.open(baseline) as img:
base = img.convert("RGB")
previous = None
ordered = [(item["after_ns"], run / item["path"], item)
for item in captures.values() if item["output"] ==
("HEADLESS-1" if side == "left" else "HEADLESS-2")]
for _, path, capture in sorted(ordered):
with Image.open(path) as img:
image = img.convert("RGB")
if image.size != (800, 600):
result["failure"] = f"unexpected dimensions: {path.name} {image.size}"
result["captures"].append({"file": path.name, "side": side,
"phase": path.stem[len(side)+1:], "dimensions": image.size,
"sha256": sha(path), "before_ns": capture["before_ns"],
"after_ns": capture["after_ns"]})
if previous is not None:
older, old_image = previous
output = capture["output"]
interval = [box for ns, name, box in damage_events
if name == output and older["after_ns"] < ns <= capture["after_ns"]]
mask = Image.new("1", image.size, 0)
painter = ImageDraw.Draw(mask)
for box in interval:
painter.rectangle((box[0], box[1], box[2]-1, box[3]-1), fill=1)
changed = ImageChops.difference(old_image, image).convert("RGB")
outside = sum(px != (0, 0, 0) and not cover
for px, cover in zip(changed.getdata(), mask.getdata()))
result.setdefault("transitions", []).append({"side": side,
"from": older["tag"], "to": capture["tag"],
"changed_pixels": difference(old_image, image)["pixels"],
"damage_events": len(interval) if damage_events else None,
"changed_outside_damage": outside if damage_events else None})
previous = capture, image
if path != baseline:
result["comparisons"].append({"side": side,
"phase": path.stem[len(side)+1:],
"baseline": baseline.name,
"whole": difference(base, image),
"rois": {name: difference(base, image, box)
for name, box in ROIS[side].items()}})
return result
def main():
parser = argparse.ArgumentParser()
parser.add_argument("lab", type=Path)
parser.add_argument("runs", nargs="+", type=Path)
parser.add_argument("--include-all-raw-runs", action="store_true",
help="manifest all retained runs, including excluded fixture variants")
args = parser.parse_args()
lab = args.lab.resolve()
out = lab / "effects-damage-results"
out.mkdir(mode=0o700, exist_ok=True)
results = [analyze_run(run.resolve()) for run in args.runs]
(out / "pixel-analysis.json").write_text(json.dumps(results, indent=2) + "\n")
raw_runs = list((lab / "runs").iterdir()) if args.include_all_raw_runs else args.runs
proofs = [path for run in raw_runs for path in run.resolve().rglob("*") if path.is_file()]
proofs += [lab / "fx-checker.png"]
proofs += [path for path in (lab / "effects-damage-run-generated.py",
lab / "effects-build-log.txt",
lab / "effects-source-sha256.txt",
out / "metadata.json",
out / "pixel-analysis.json") if path.exists()]
proofs += [path for path in (lab / "effects-preparation-logs").rglob("*") if path.is_file()]
manifest = out / "raw-evidence-sha256.txt"
manifest.write_text("".join(f"{sha(path)} {path.relative_to(lab)}\n"
for path in sorted(set(proofs))))
print(json.dumps({"runs": len(results), "captures": sum(len(r["captures"]) for r in results),
"raw_proofs": len(proofs), "manifest_sha256": sha(manifest),
"results": str(out)}, indent=2))
if __name__ == "__main__":
main()

View file

@ -0,0 +1,22 @@
--- a/nested-client.c
+++ b/nested-client.c
@@ -45,10 +45,17 @@
unlink(path);
uint32_t *pixels = mmap(NULL, bytes, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (pixels == MAP_FAILED) { perror("mmap"); exit(1); }
- for (size_t i=0; i<bytes/4; ++i) pixels[i] = s->color;
+ /* WHY: a translucent mapped SHM surface exposes actual blur of the
+ * checkerboard below, instead of merely accepting the blur command.
+ * CONTRACT: ARGB channels are premultiplied and the buffer size is fixed. */
+ bool glass = s == &top;
+ for (size_t i=0; i<bytes/4; ++i) pixels[i] = glass ? 0x88442010 : s->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, s->width, s->height,
- s->width*4, WL_SHM_FORMAT_XRGB8888);
+ s->width*4, glass ? WL_SHM_FORMAT_ARGB8888 : WL_SHM_FORMAT_XRGB8888);
+ fprintf(stderr,"EFFECT_BUFFER wl=%u format=%s width=%d height=%d argb=%08x\n",
+ wl_proxy_get_id((struct wl_proxy *)s->wl), glass ? "ARGB8888" : "XRGB8888",
+ s->width, s->height, glass ? 0x88442010 : s->color);
wl_surface_attach(s->wl, buffer, 0, 0);
wl_surface_damage_buffer(s->wl, 0, 0, s->width, s->height);
wl_surface_commit(s->wl);

View file

@ -0,0 +1,24 @@
--- a/types/scene/wlr_scene.c
+++ b/types/scene/wlr_scene.c
@@ -2117,6 +2117,21 @@
};
scene_node_get_size(node, &dst_box.width, &dst_box.height);
transform_output_box(&dst_box, data);
+
+ // WHY: prove real effect-node presence separately from accepted IPC.
+ // CONTRACT: trace only a private copy; identities are process-local.
+ // INVARIANT: read-only logging changes no render or cache decision.
+ if (node->type == WLR_SCENE_NODE_SHADOW || node->type == WLR_SCENE_NODE_BLUR
+ || node->type == WLR_SCENE_NODE_OPTIMIZED_BLUR) {
+ const pixman_box32_t *box = pixman_region32_extents(&render_region);
+ int dirty = node->type == WLR_SCENE_NODE_OPTIMIZED_BLUR
+ ? wlr_scene_optimized_blur_from_node(node)->dirty : -1;
+ wlr_log(WLR_INFO, "FX_TRACE output=%s type=%d node=%p enabled=%d "
+ "dst=%d,%d %dx%d draw=%d,%d..%d,%d dirty=%d",
+ data->output->output->name, node->type, (void *)node, node->enabled,
+ dst_box.x, dst_box.y, dst_box.width, dst_box.height,
+ box->x1, box->y1, box->x2, box->y2, dirty);
+ }
pixman_region32_t opaque;
pixman_region32_init(&opaque);

View file

@ -0,0 +1,56 @@
#!/bin/sh
# EXPERIMENTAL: only private copied sources and a bounded SHM client.
set -eu
repo=$(CDPATH= cd -- "$(dirname -- "$0")/../../.." && pwd)
fixture="$repo/tooling/probes/step20c-effects-damage"
log=$(mktemp /tmp/fxd-prepare-XXXXXXXX)
if ! sh "$repo/tooling/probes/step20c-temporal-damage/prepare.sh" > "$log" 2>&1; then
printf 'PRIVATE_BUILD_FAILED log=%s\n' "$log" >&2
tail -40 "$log" >&2
exit 1
fi
lab=$(sed -n 's/^LAB=//p' "$log")
test -n "$lab" && test -d "$lab"
trap 'rm -rf -- "$lab"' 0
# CONTRACT: native identities come from existing output callbacks; the
# preserved listener changes no commit/present or scene decision.
presentation="$repo/tooling/probes/step20c-presentation-identity/presentation-events.patch"
git -C "$lab/mosaic" apply --check "$presentation"
git -C "$lab/mosaic" apply "$presentation"
meson compile -C "$lab/mosaic-build" -j 8 >> "$log" 2>&1
# CONTRACT: optional trace is passive and exists only in this private copy.
if [ "${STEP20C_EFFECT_TRACE:-0}" = 1 ]; then
git -C "$lab/scenefx" apply --check "$fixture/effect-trace.patch"
git -C "$lab/scenefx" apply "$fixture/effect-trace.patch"
meson compile -C "$lab/scenefx-build" -j 8 >> "$log" 2>&1
fi
# CONTRACT: only the disposable top-level SHM client becomes translucent.
(cd "$lab" && git apply --check "$fixture/effect-glass.patch" &&
git apply "$fixture/effect-glass.patch")
cc -std=c17 -Wall -Wextra -Werror -Wno-unused-parameter -I"$lab" \
"$lab/nested-client.c" "$lab/xdg-shell-protocol.c" \
-o "$lab/nested-client" $(pkg-config --cflags --libs wayland-client) >> "$log" 2>&1
sha256sum "$fixture"/*.patch "$fixture"/*.py "$fixture/prepare.sh" \
"$fixture/README.md" "$presentation" \
"$repo/tooling/probes/step20c-applied-state-input/run.py" \
"$repo/tooling/probes/step20c-nested-grab/prepare.sh" \
"$repo/tooling/probes/step20c-nested-grab/nested-client.c" \
"$repo/tooling/probes/step20c-negative-origin/prepare.sh" \
"$repo/tooling/probes/step20c-vertical-grab/pointer-extents.patch" \
"$repo/tooling/probes/step20c-valid-grab/scenefx-popup-plus-base.patch" \
"$repo/tooling/probes/step20c-valid-grab/mosaic-popup-plus-base.patch" \
"$repo/tooling/probes/step20c-valid-grab/hidden-guard.patch" \
"$repo/tooling/probes/step20c-valid-grab/pointer-daemon.c" \
"$repo/tooling/probes/step20c-valid-grab/wlr-virtual-pointer-unstable-v1.xml" \
"$repo/tooling/probes/step20c-temporal-damage/prepare.sh" \
"$repo/tooling/probes/step20c-temporal-damage/scenefx-timing.patch" \
"$repo/tooling/probes/step20c-temporal-damage/mosaic-output-timing.patch" \
"$repo/tooling/probes/step20c-temporal-damage/client-cycle.patch" \
"$lab/nested-client.c" "$lab/pointer-daemon.c" \
"$lab/xdg-shell-protocol.c" "$lab/xdg-shell-client.h" \
"$lab/virtual-pointer-protocol.c" "$lab/virtual-pointer-client.h" \
"$lab/scenefx/types/scene/wlr_scene.c" "$lab/mosaic/sway/desktop/output.c" \
> "$lab/effects-source-sha256.txt"
cp "$log" "$lab/effects-build-log.txt"
trap - 0
printf 'LAB=%s\nBUILD_LOG=%s\n' "$lab" "$log"

View file

@ -0,0 +1,125 @@
"""Bounded headless FX0–FX4 and camera-state source fixture."""
import argparse
import fcntl
import json
from pathlib import Path
import subprocess
import sys
from PIL import Image
ap = argparse.ArgumentParser()
ap.add_argument('lab', type=Path)
ap.add_argument('mode', choices=('control', 'private_standard', 'private_mosaic'))
ap.add_argument('--camera-pairs', type=int, choices=range(4), default=3)
ap.add_argument('--reference-resize', action='store_true',
help='replay the retained late-resize reference variant')
args = ap.parse_args()
lab = args.lab.resolve()
repo = Path(__file__).resolve().parents[3]
# WHY: reproduction and trace controls must not multiply camera repetitions.
# CONTRACT: one active runner per lab; camera pairs are counted across runs.
# INVARIANT: at most three new hide/return pairs in a fresh lab campaign.
campaign_lock = (lab / 'effects-campaign.lock').open('a')
fcntl.flock(campaign_lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
prior_pairs = sum(len(list(run.glob('left-T4-hidden-*.png')))
for run in (lab / 'runs').glob('private_mosaic-effects-*'))
budget_file = lab / 'effects-camera-budget.json'
if budget_file.exists():
consumed = json.loads(budget_file.read_text())['consumed']
if type(consumed) is not int or consumed < 0:
ap.error('invalid persisted camera budget')
prior_pairs = max(prior_pairs, consumed)
if args.mode == 'private_mosaic' and args.camera_pairs and prior_pairs + args.camera_pairs > 3:
ap.error(f'campaign camera budget exceeded: {prior_pairs} already recorded; use --camera-pairs 0')
if args.mode == 'private_mosaic' and args.camera_pairs:
# Reserve before launch: failed attempts also consume the finite budget.
temporary_budget = budget_file.with_suffix('.tmp')
temporary_budget.write_text(json.dumps({'consumed': prior_pairs + args.camera_pairs}) + '\n')
temporary_budget.replace(budget_file)
pattern = Image.new('RGB', (64, 64))
for y in range(64):
for x in range(64):
pattern.putpixel((x, y), (245, 245, 245) if (x // 8) % 2 == 0
else (15, 25, 45))
pattern.save(lab / 'fx-checker.png')
source = (repo / 'tooling/probes/step20c-applied-state-input/run.py').read_text()
old = "choices=('cycles', 'buffer_cycles', 'identity', 'outside', 'hidden_click', 'toggle')"
assert source.count(old) == 1
adapted = source.replace(old, old[:-1] + ", 'effects')")
anchor = " if args.kind == 'cycles':\n"
assert adapted.count(anchor) == 1
scenario = ''' if args.kind == 'effects':
# CONTRACT: ordinary IPC creates effects; snapshots follow settled
# states. No compositor scheduler or rendering implementation changes.
spawn(['swaybg', '-i', str(lab / 'fx-checker.png'), '-m', 'tile'],
'swaybg.log')
time.sleep(.3)
command('[app_id="wev"] floating enable')
if EFFECT_REFERENCE_RESIZE:
command('[app_id="wev"] resize set width 300 height 250')
command('[app_id="wev"] move position 250 250')
command('gaps inner 24')
if args.mode != 'private_mosaic':
command('[app_id="org.mosaic.NestedGrabProbe"] focus')
command('move right')
command('move right')
command('shadows disable')
command('blur disable')
command('shadow_blur_radius 16')
command('shadow_offset 30 0')
command('shadow_color #000000A0')
command('blur_radius 5')
command('blur_passes 3')
if args.mode == 'private_mosaic': scroll(400)
record('effect_owner', owner=owner()['rect'], mode=args.mode)
time.sleep(.2)
snapshot('FX0-off')
command('shadows enable')
time.sleep(.2)
snapshot('FX1-shadow')
command('shadows disable')
command('blur enable')
time.sleep(.2)
snapshot('FX2-blur')
command('blur_xray enable')
time.sleep(.2)
snapshot('FX3-optimized-candidate')
command('shadows enable')
time.sleep(.2)
snapshot('FX4-combined')
if args.mode == 'private_mosaic':
for pair in range(1, EFFECT_CAMERA_PAIRS + 1):
scroll(1280)
time.sleep(.15)
snapshot(f'T4-hidden-{pair}')
scroll(400)
time.sleep(.15)
snapshot(f'T5-return-{pair}')
command('layout splith')
time.sleep(.2)
snapshot('T7-standard-return')
command('blur disable')
command('shadows disable')
time.sleep(.2)
snapshot('T6-disabled')
command('blur enable')
command('shadows enable')
time.sleep(.2)
snapshot('T6-restored')
client.stdin.write('close_owner\\n')
client.stdin.flush()
client.wait(timeout=8)
record('owner_closed', exit_code=client.returncode)
time.sleep(.2)
snapshot('T6-closed')
elif args.kind == 'cycles':
'''
adapted = adapted.replace(anchor, scenario).replace('EFFECT_CAMERA_PAIRS', str(args.camera_pairs))
adapted = adapted.replace('EFFECT_REFERENCE_RESIZE', str(args.reference_resize))
generated = lab / 'effects-damage-run-generated.py'
generated.write_text(adapted)
raise SystemExit(subprocess.call([sys.executable, str(generated), str(lab),
args.mode, 'effects', '--asi-mode', '0', '--max-seconds', '120',
'--max-captures', '40', '--max-bytes', '35000000']))