diff --git a/CHANGELOG.md b/CHANGELOG.md index 892b335..6f9c5d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 2806b4a..9f955bd 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -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. diff --git a/docs/KNOWN_LIMITATIONS.md b/docs/KNOWN_LIMITATIONS.md index 1275f85..6ab98c9 100644 --- a/docs/KNOWN_LIMITATIONS.md +++ b/docs/KNOWN_LIMITATIONS.md @@ -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**. diff --git a/docs/README.md b/docs/README.md index e7bc52c..14eb9fc 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index a9d9245..77aaede 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -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. diff --git a/docs/design/STEP20C_EFFECTS_DAMAGE_PROBE.md b/docs/design/STEP20C_EFFECTS_DAMAGE_PROBE.md new file mode 100644 index 0000000..4de9142 --- /dev/null +++ b/docs/design/STEP20C_EFFECTS_DAMAGE_PROBE.md @@ -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. diff --git a/tooling/probes/step20c-effects-damage/README.md b/tooling/probes/step20c-effects-damage/README.md new file mode 100644 index 0000000..d3c2fcb --- /dev/null +++ b/tooling/probes/step20c-effects-damage/README.md @@ -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. diff --git a/tooling/probes/step20c-effects-damage/analyze.py b/tooling/probes/step20c-effects-damage/analyze.py new file mode 100644 index 0000000..0295cba --- /dev/null +++ b/tooling/probes/step20c-effects-damage/analyze.py @@ -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() diff --git a/tooling/probes/step20c-effects-damage/effect-glass.patch b/tooling/probes/step20c-effects-damage/effect-glass.patch new file mode 100644 index 0000000..886122b --- /dev/null +++ b/tooling/probes/step20c-effects-damage/effect-glass.patch @@ -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; icolor; ++ /* 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 == ⊤ ++ for (size_t i=0; icolor; + 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); diff --git a/tooling/probes/step20c-effects-damage/effect-trace.patch b/tooling/probes/step20c-effects-damage/effect-trace.patch new file mode 100644 index 0000000..ebdcb98 --- /dev/null +++ b/tooling/probes/step20c-effects-damage/effect-trace.patch @@ -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); diff --git a/tooling/probes/step20c-effects-damage/prepare.sh b/tooling/probes/step20c-effects-damage/prepare.sh new file mode 100644 index 0000000..ead4d1d --- /dev/null +++ b/tooling/probes/step20c-effects-damage/prepare.sh @@ -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" diff --git a/tooling/probes/step20c-effects-damage/run.py b/tooling/probes/step20c-effects-damage/run.py new file mode 100644 index 0000000..ae49104 --- /dev/null +++ b/tooling/probes/step20c-effects-damage/run.py @@ -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']))