docs(mosaic): preserve fractional scaling probe evidence

This commit is contained in:
fy59 2026-10-12 14:10:06 +02:00
parent aefee44c63
commit 8345aff0f9
11 changed files with 974 additions and 0 deletions

View file

@ -4,6 +4,8 @@
### STEP20C experimental evidence
- Preserved fractional-scale probe: actual 1.25 uniform/mixed and 1.5 mixed outputs, calibrated logical input, sampled root/nested popup and effects clipping, two exact camera returns and physical settled-damage coverage. PARTIALLY_FEASIBLE; opaque tiled controls, interior shadow, restored Mosaic hit and broader gates remain unqualified. No production/package change.
- 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.

View file

@ -234,3 +234,16 @@ 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.
The preserved [fractional scaling probe](design/STEP20C_FRACTIONAL_SCALING_PROBE.md)
is **PARTIALLY_FEASIBLE** for static 1.25 uniform/mixed and 1.5 mixed headless
outputs. Actual modes/PNGs and logical pointer motion were calibrated. Sampled
root/nested popup and shadow spill disappeared on the neighboring output;
visible blur, terminal popup columns, two settled camera returns and available
physical damage were consistent. Opaque tiled-overflow controls, permitted
interior shadow, restored Mosaic hit, caches and intermediate frames remain
unqualified. Ten failed early calibrations and their source-provenance limit
are retained. The two-pair global budget was respected. The next distinct gate
should test resize/migration of owner/output clips; rotation, saved buffers and
ABI/package remain separate. This is evidence only: STEP20C stays **NEXT**,
production multi-output **NOT YET SUPPORTED**, STEP20D–G **PLANNED**.

View file

@ -173,3 +173,16 @@ The [isolated feasibility probe](design/STEP20C_FEASIBILITY_PROBE.md) is **PARTI
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**.
These limits are reflected in [UX](UX.md), [architecture](ARCHITECTURE.md), and [roadmap](ROADMAP.md). A future step must update those contracts before claiming support.
The preserved [fractional scaling probe](design/STEP20C_FRACTIONAL_SCALING_PROBE.md)
is **PARTIALLY_FEASIBLE** for static 1.25 uniform/mixed and 1.5 mixed headless
outputs. Actual modes/PNGs and logical pointer motion were calibrated. Sampled
root/nested popup and shadow spill disappeared on the neighboring output;
visible blur, terminal popup columns, two settled camera returns and available
physical damage were consistent. Opaque tiled-overflow controls, permitted
interior shadow, restored Mosaic hit, caches and intermediate frames remain
unqualified. Ten failed early calibrations and their source-provenance limit
are retained. The two-pair global budget was respected. The next distinct gate
should test resize/migration of owner/output clips; rotation, saved buffers and
ABI/package remain separate. This is evidence only: STEP20C stays **NEXT**,
production multi-output **NOT YET SUPPORTED**, STEP20D–G **PLANNED**.

View file

@ -100,3 +100,8 @@ A proposal does not supersede accepted canonical contracts or assert implementat
| [STEP20B](STEP20B.md) | Historical | First scrollable-row implementation evidence |
STEP reports record what was observed and decided at the time. They are **not** the canonical source for current behavior. When a STEP report conflicts with current canonical documentation, the canonical documentation wins. The original French reports are preserved as historical artifacts; their opening notices point here.
Preserved experimental evidence: [fractional scaling probe](design/STEP20C_FRACTIONAL_SCALING_PROBE.md)
and [source fixture](../tooling/probes/step20c-fractional-scale/README.md).
PARTIALLY_FEASIBLE for the sampled static scale matrix, independently replayed
without new camera pairs; no production qualification.

View file

@ -177,3 +177,16 @@ 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.
The preserved [fractional scaling probe](design/STEP20C_FRACTIONAL_SCALING_PROBE.md)
is **PARTIALLY_FEASIBLE** for static 1.25 uniform/mixed and 1.5 mixed headless
outputs. Actual modes/PNGs and logical pointer motion were calibrated. Sampled
root/nested popup and shadow spill disappeared on the neighboring output;
visible blur, terminal popup columns, two settled camera returns and available
physical damage were consistent. Opaque tiled-overflow controls, permitted
interior shadow, restored Mosaic hit, caches and intermediate frames remain
unqualified. Ten failed early calibrations and their source-provenance limit
are retained. The two-pair global budget was respected. The next distinct gate
should test resize/migration of owner/output clips; rotation, saved buffers and
ABI/package remain separate. This is evidence only: STEP20C stays **NEXT**,
production multi-output **NOT YET SUPPORTED**, STEP20D–G **PLANNED**.

View file

@ -0,0 +1,473 @@
# STEP20C fractional scaling and viewport probe
**Status: EXPERIMENTAL / PARTIALLY_FEASIBLE for the sampled static matrix.**
Source-only evidence preserved for the fractional freeze. STEP20A DONE, STEP20B EXPERIMENTAL, STEP20C NEXT,
STEP20D–G PLANNED; production multi-output NOT YET SUPPORTED. No UX contract
or production change is authorized. The temporal headless line stays closed
at INCONCLUSIVE; no additional transaction contract is introduced.
## Scientific question and historical baseline
Scale-1 private inherited clips excluded sampled tiled/popups pixels and hits
at negative origin. The effects report demonstrates neighboring shadow
exclusion and visible blur, but no permitted interior Mosaic shadow,
optimized-cache invalidation, saved-buffer or every-frame qualification.
Fractional conversion can expose rounding, missing boundary pixels, wrong
output damage or disagreement with logical hit testing. These are hypotheses,
not established defects. Instrumentation is passive; no algorithm, shader,
damage decision, callback order, transaction or public ABI may be changed.
## Requested geometry and acceptance gate
| Geometry | HEADLESS-1 mode/scale/origin | HEADLESS-2 mode/scale/origin |
| --- | --- | --- |
| G0 | 800×600 / 1 / (-800,0) | 800×600 / 1 / (0,0) |
| G1 | 1000×750 / 1.25 / (-800,0) | 1000×750 / 1.25 / (0,0) |
| G2 | 1000×750 / 1.25 / (-800,0) | 800×600 / 1 / (0,0) |
| G3 optional | 1200×900 / 1.5 / (-800,0) | 800×600 / 1 / (0,0) |
Expected logical rectangles are 800×600. Record actual active/name, global
rect, current physical mode, scale and transform from get_outputs, plus real
PNG dimensions. Rejected/normalized geometry is INCONCLUSIVE, not PASS or
BLOCKED. G3 is allowed only after G1/G2 calibration succeeds. G0 is a small
calibration control, not a new historical campaign.
## Coordinate contracts to audit before adapting the fixture
Global Wayland, Mosaic geometry, camera scroll and virtual-pointer points are
logical. Output-local logical = global minus output origin exactly once.
SceneFX scales logical boxes/regions into output-buffer physical coordinates;
transform is normal in this matrix. Audit actual floor/ceil/round rules,
pixman intersections, fractional conservative expansion, texture filtering,
source/destination boxes and damage conversions. Do not use arbitrary integer
truncation as the expected physical clipping rule. Never multiply scroll_x
by scale. At expected geometry, pointer injection remains sent_x=global_x+800
with logical extent 1600×600. Verify enter/motion coordinates and left/right
buttons on both outputs before scoring clipping/input.
Captures must record their real dimensions and applied output scale/origin,
logical and physical resolutions, timestamps and hashes. Predeclare logical
ROIs: left interior (300,100,700,500), left boundary (720,50,800,550), right
boundary (0,50,80,550), reference (250,250,550,500). Physical ROIs conservatively
cover floor(lower*scale)..ceil(upper*scale); retain full-image differences.
Boundary bands and adjacent columns are never removed after anomalies.
Compare equivalent logical regions, not historical pixel counts.
## Comparisons, scenarios and finite budget
Each qualified geometry compares ordinary SwayFX splith, private splith and
private Mosaic with identical deterministic clients. Controls must demonstrate
actual overflow before a clipping claim. Reuse preserved valid-anchor nested
SHM client, opaque client, deterministic background, reference and virtual
pointer. C adaptations, if needed, are private patches with pinned base/hash.
S0: IPC/mode/PNG/scene/pointer calibration gates all later input claims.
S1: inspect owner/tiled boundary pixels and logical hits.
S2: map TOPLEVEL→POPUP_A→POPUP_B using genuine button serials; retain
configure/ack/map, parent identities and anchor validity; compare visible
and clipped hits. Exterior clicks which dismiss the chain need separate runs.
S3: predeclared grid around x=0 and complete physical boundary bands.
S4: FX0/FX1/FX2, visible control shadow and filtered blur; distinguish draw
clip from sampling and cache semantics. Interior shadow remains unqualified
unless new pixels demonstrate it.
S5: one 400→1280→400 pair per G1/G2 Mosaic run with A/B mapped; no hidden
click, preserve objects and test returned hits.
S6: observed same-output damage extents compared to settled changed pixels,
with explicit physical coordinate provenance and fractional expansion.
S7: layout splith with objects open, clear clips and preserve ordinary hits.
**Global B budget: two pairs total, across all attempts and geometries.**
Reserve before launching under a campaign lock; failure consumes reservation.
G0/G3 and all standards use zero pairs. One persistent campaign ledger belongs
to the single new lab, never reset or bypass through another clone. Untestable
variants remain NOT TESTED. Preserve every attempted run and real exit code.
## Criteria and limits
FEASIBLE requires applied geometry, calibrated logical input and physical
captures, actual control overflow, excluded sampled forbidden pixels/hits,
correct visible interactions, active effects and observed restoration/damage.
PARTIALLY_FEASIBLE leaves effects/rounding/transitions/damage partly qualified.
INCONCLUSIVE means geometry/control/fixture cannot establish conversions.
BLOCKED requires reproducible forbidden pixels/hits, scene corruption or
blocking regression; preserve a minimal reproducer and do not repair algorithm.
Rotation, migration/resize/hotplug, full animations, all optimized caches,
saved buffers, other renderers and production ABI/package are NOT TESTED.
Sources/fixtures are local in tooling/probes/step20c-fractional-scale;
all builds, logs, PNGs, generated protocols, sockets and manifests stay outside
Git. Final analysis must answer Q1–Q11 with these evidence limits.
## Source audit before fixture adaptation
Pinned Mosaic `cd550382b311efdbfe3ed892f4e1d281d852e754`, SceneFX
`37ccd723bef49e6891156ffafce8f549f01446cc`; locally retained wlroots source
describes exactly tag 0.20.2 (the installed linked version also reports 0.20.2).
Sway output configuration sets round(scale*120)/120 after the physical mode;
1.25 and 1.5 are exactly representable protocol steps. output_update_position
copies the logical layout box. wlroots effective_resolution divides transformed
physical integer dimensions by scale with integer assignment; these test modes
yield exactly 800×600 without fractional logical extent.
SceneFX scale_box rounds both edges with round(), computing width from the
rounded far edge minus rounded origin. wlr_region_scale uses floor lower / ceil
upper. scale_region(...,true) additionally expands by one physical pixel for
fractional scales. scene_damage_outputs subtracts scene-output logical origin,
scales/expands and transforms, then scene_output_damage clips to physical
output dimensions. TEMPORAL_DAMAGE is emitted there: **already physical
buffer-local extents**, never multiply those traces by scale again.
Opaque background culling also expands one pixel at fractional scale and
reintersects damage. Buffer damage may expand for linear filtering; ordinary
buffer filter mode and bilinear blur sampling differ. Draw clips pass through
logical_to_buffer_coords; texture boxes and pixman regions need not share
identical integer bounds. An integral boundary x=0 does not exercise every
possible fractional edge offset.
The private inherited clip intersects ancestor logical rectangles; popup clips
are converted from the applied global viewport into root-popup local coordinates.
node_at uses floor(global point) for its logical query and checks inherited
point_inside_clip before surface input. Shadows/blur nodes do not accept input.
wlr_cursor_absolute_to_layout_coords uses mapping.width*normalized_x+mapping.x,
with the combined output-layout box if no explicit device mapping. The private
pointer normalizes sent integers by logical extent, so +800 is origin removal,
not physical scaling. No pointer C adaptation is needed for these 800×600
logical outputs. The runner must empirically verify both clients' local points.
## Retained fixture incidents
First G0 calibration attempt (zero camera pairs) placed the left ordinary
floating reference at x=-1550: `move position -750 300` was output-relative,
adding the -800 output origin. The bounds check rejected injection before
input scoring. The fixture now uses `move absolute position -750 300` and
continues to derive points from actual IPC content coordinates. This is a
fixture command error, not a Mosaic scaling defect. The failed run is retained.
The next zero-pair standard calibration attempts retained a 25-logical-pixel
vertical discrepancy between the floating reference's IPC-derived point and
its client-local enter/motion. The intended local (60,50) was reported (60,75).
Mosaic's same calibration passed, so no scale-specific defect is inferred.
To avoid assigning frame-decoration geometry to the test surface, both ordinary
references now explicitly use `border none`; local coordinates are still checked
against the client log, not accepted from IPC alone. All failed runs remain in
the lab manifest. This fixture change is documented before further experiments.
The ordinary control is now also rebuilt from pristine pinned SwayFX 0.6,
`fd71a6bdc061bd633b488ae7b83e8a6981d22f86`, in standard-build and uses the
installed unmodified SceneFX. Preliminary installed-Sway calibrations remain
retained attempts, not the final comparative standard control.
Border none alone did not remove the standard reference's stale IPC frame
geometry (rect y=325 while surface origin is y=300). Final calibration uses
explicit absolute placement origins (-750,300) and (250,250), then requires
client enter=(20,30), motion=(60,50), and both button deliveries. IPC frame
and window geometry are recorded separately. This checks known placement
against real client surface coordinates; the IPC decoration discrepancy is
retained rather than treated as the surface-coordinate authority.
The stricter nominal-placement calibration then failed in Mosaic, where the
surface retained a different vertical placement from standard. Final S0
therefore distinguishes requested placement from measured surface origin:
first enter determines the actual surface origin, a second independent global
move of (+40,+20) must produce the identical logical local delta, and both
buttons must arrive. Initial x=20 and an interior y are asserted. Record both
origins and the IPC frame separately. This is an empirical coordinate
calibration, not a promise that a pending move command defines applied geometry.
No clipping/input scenario is scored before this two-point calibration passes.
The b_cross main runs succeeded in G1/G2, including one camera pair per
Mosaic run with A/B mapped. Their visible references and returns are identical
on both outputs. To distinguish root from nested boundary coverage, add a
zero-camera `both_cross` comparison in each of the three modes for G1/G2,
with the retained client's valid anchors A=(350,80) in TOPLEVEL 420×320 and
B=(160,60) in A 350×170. Exterior clicks stay in these distinct runs. This
uses an existing client variant, not a C or clipping algorithm adaptation.
Add two final zero-camera root-input controls: in both_cross Mosaic G1/G2,
click (20,140), the forbidden root-A portion above B's top edge. Keep them
separate from the (20,190) nested-B dismissal controls. The existing standard
runs already prove real root-A delivery at their configured visible points.
Do not spend another camera pair to qualify restored Mosaic hit testing:
the two existing S5 runs captured visual restoration, but did not inject a
new visible B button before switching to standard layout. That assertion
remains NOT TESTED under the exhausted budget.
## SWAYFX_MOSAIC_STEP20C_EFFECTS_FREEZE_AND_FRACTIONAL_SCALING_PROBE_V1
### Phase A freeze gate
Initial main/local remote references were `542d9b9c945b22cf9134a4a643c8dc763b34607d`,
with only the expected effects fixture/report and five documentation files dirty.
The historical 890-entry evidence and 29-entry source manifests verified at
`5fbe12b0312f2d82b3c5414251ae57345db8d25a83cfc018947308d40e752b42` and
`8fbd0cf927e3e2690a77aeaf2288072c183ef1dbe8f856cb63e0442454614d95`.
Historical client/trace patches retain their announced hashes, including the
separate compiled trace provenance. The report preserves 15 historical camera
pairs against a maximum of three; default repetition across five Mosaic runs
caused the excess. Selecting three pairs does not authorize the other twelve.
A fresh independent clone rebuilt the private chain; the new three-control
replay consumed one pair total and reproduced 27,594 standard shadow-spill
pixels / zero Mosaic, real filtered blur and one exact settled camera return.
New evidence/source hashes are listed in the effects report. The reservation
block passed locking, failure/interruption, exhaustion, zero-pair and retained-run
checks. Historical reference geometry differed (640×480 untraced versus 300×250
traced); missing checker causality and logging neutrality remain unqualified.
Interior shadow, optimized caches, source sampling, popups, saved buffers and
intermediate frames remain limited as documented.
The unique authorized commit is
`aefee44c636a1529eca9a082658fd80b4d17cf5f`, title
`docs(mosaic): preserve SceneFX effects and damage probe evidence`.
Normal pushes to origin/main and github/main succeeded; direct ls-remote checks
returned that SHA for both. Worktree was clean at the A→B gate.
Validation qualification: ordinary `git diff --check` passed before adding new
files, but `git diff --cached --check` flagged historical trace-patch **context**
lines (space+tab and the blank context marker). Those bytes are valid unified
patch syntax and belong to the mandated preserved SHA; they were not rewritten.
The commit/push proceeded with this explicitly classified patch-format exception.
Builds, controlled patch application, C17, model 1/1 and link checks passed.
Do not describe the unfiltered indexed whitespace check as PASS.
`SWAYFX_MOSAIC_EFFECTS_DAMAGE_FREEZE=PASS`
### Phase B laboratory and actual geometry
Independent full clone `/tmp/step20c-fractional-v1-clone` at the A commit;
private lab `/tmp/swayfx-mosaic-nested-grab-0DfvGCxn`. Private SceneFX/Mosaic
were rebuilt from the pinned preparer chain, with the historical clip, hidden
owner guard, damage and passive presentation patches. No algorithm changed.
Pristine upstream SwayFX 0.6 was separately archived and built in standard-build;
final ordinary controls use it, linked to installed unmodified SceneFX. Private
controls link only their process to the private library. wlroots 0.20.2 source
HEAD `d783533489e1f75d6886c2ab5c5960090ef268f8` is clean at tag 0.20.2.
| Geometry | Actual H1 mode / scale / logical rect | Actual H2 mode / scale / logical rect | Scope |
| --- | --- | --- | --- |
| G0 | 800×600 / 1 / (-800,0,800,600) | 800×600 / 1 / (0,0,800,600) | Small calibration only |
| G1 | 1000×750 / 1.25 / (-800,0,800,600) | 1000×750 / 1.25 / (0,0,800,600) | Three modes, b_cross + both_cross |
| G2 | 1000×750 / 1.25 / (-800,0,800,600) | 800×600 / 1 / (0,0,800,600) | Three modes, b_cross + both_cross |
| G3 | 1200×900 / 1.5 / (-800,0,800,600) | 800×600 / 1 / (0,0,800,600) | Optional three-mode b_cross, no camera |
All active, normal transform, physical modes and scales actually accepted;
PNGs match each target output's physical mode. All logical viewports remain
800×600. No requested geometry is mislabeled from a command's success alone.
SceneFX FX reports GLES 3.2 Mesa 26.2.4-arch1.1 / Radeon 780M radeonsi.
### Calibration, geometry and input
Qualified final runs pass ordinary-reference enter on each output, independent
(+40,+20) local motion, both buttons, crossing and return. Signed normalization
is sent_x=global_x+800, extent 1600×600; physical scales never enter injection.
Requested reference placement, actual measured surface origin, IPC frame and
surface-local coordinates are recorded separately. Earlier placement, frame-offset
and already-entered-pointer assumptions caused ten retained calibration failures,
all with zero reserved camera pairs. One additional calibration adapter attempt
had an escaped regular-expression defect; its failed log is among those ten.
No failed calibration is counted as clipping evidence. A forced move outside
before enter removes the already-entered-pointer ambiguity.
Both XDG grabs use actual received left-button serials unchanged; parent identities,
configure/ack/map and logs are retained. All observed grabs validate. b_cross
anchors A=(200,80) in TOPLEVEL 420×320 and B=(220,60) in A 300×170 are valid;
both_cross anchors are also inside their actual parent geometry. No invalid-anchor
campaign is reused. Visible B right buttons arrive in each main run, including
after standard-layout return; there is no popup_done in those main runs.
For b_cross Mosaic, B's applied configured global box is (-138,165,300,130).
On H1 at 1.25, local x=662 maps to rounded destination x=828; the visible
physical box ends at x=1000, y=206..369. Its 172×163 exact-color pixels fill
that retained rectangle. At 1.5, the corresponding box starts x=993 and ends
at x=1200, y=248..443 (195 rows). Conservative region floor/ceil and extra
physical expansion differ from rounded destination edges; those differences
are not automatically classified as leaks.
The both_cross grid records B motion x=46/47,y=25 at global x=-2/-1,y=190,
then B leave at x=0, with no new popup events at x=1,2,20. Separate right clicks
at (20,190) produce no popup button under Mosaic and dismiss both grabs.
Separate root-A clicks at (20,140), above B, likewise produce no popup button
and dismiss the chain in G1/G2 Mosaic. This is grab dismissal, not universal
click-through. The b_cross standard controls actually receive a popup button at
(20,190), without dismissal. The both_cross standard (20,190) point is outside
its differently placed popup; its dismissal is ordinary behavior, not a clip
comparison. Their configured visible A/B points still demonstrate real hits.
### Pixels, effects and rounding
| Geometry/mode | FX1 changed H2 pixels | FX2 changed H1 pixels | FX2 changed H2 pixels |
| --- | ---: | ---: | ---: |
| G1 ordinary / private splith | 43,500 each, columns 0–57 | 175,665 each | 0 |
| G1 Mosaic | 0 | 499,749 | 0 |
| G2 ordinary / private splith | 27,594 each, columns 0–45 | 175,665 each | 0 |
| G2 Mosaic | 0 | 499,749 | 0 |
| G3 ordinary / private splith | 27,594 each, columns 0–45 | 252,546 each | 0 |
| G3 Mosaic | 0 | 718,983 | 0 |
In the predeclared Mosaic interior ROI at 1.25, luminance deviation falls
51.499588→0.782573 and adjacent contrast 13.004008→0.747014. At 1.5 they fall
51.498856→0.711986 and 12.896494→0.689552. Standard 1.25 ROI contrast falls
3.350701→0.316020 (the ROI also contains non-owner pixels). The visible image
and quantitative smoothing establish filtering beyond fixed alpha compositing.
FX1 changes zero left Mosaic pixels: authorized interior shadow is still
INCONCLUSIVE, not PASS. Optimized cache and outside-only sampling remain NOT TESTED.
Main b_cross standard right images gain 127,200 popup pixels in G1 and
81,200 in G2/G3; private standards agree. Mosaic gains **zero pixels anywhere
on H2** versus its effects-off reference, not just zero exact popup colors.
both_cross standard right differences are 121,575 in G1 and 77,600 in G2;
Mosaic is again unchanged. Both root and nested permitted pixels remain visible.
Complete last four H1 columns retain B counts 163 per column at 1.25, 195 at
1.5. both_cross additionally retains 75 A pixels per last column at 1.25 above
B. First four H2 columns contain no A/B pixels under Mosaic. All predeclared
boundary ROIs and whole neighboring images remain in analysis; no anomalous
border is discarded. No forbidden antialias/filter contribution is observed.
This establishes sampled edge retention, not a universal absence of lost visible
pixels for every fractional subpixel phase.
The named historical capture `S1-opaque-reference` is actually an effects-off
reference with the ARGB owner; it is not proof of an opaque tiled spill.
Future source fixture uses `S1-effects-off-reference`; the analyzer preserves
compatibility with the old filenames. The opaque kitty is present, but the
standard tiled content itself does not demonstrate a crossing visual rectangle.
S1's independent opaque tiled-overflow prerequisite is **INCONCLUSIVE**; shadow
and popup overflow must not be substituted for that specific assertion.
### Camera, damage and standard return
Exactly two pairs ran: one in G1 Mosaic, one in G2 Mosaic, all successful.
Ledger reservations=2, completed pair events=2. All other attempts reserve zero.
No later run resets/bypasses the ledger. A third requested pair is refused with
exit 2 before creating a run. Offline real-reservation tests pass lock,
interruption, launch failure, exhaustion, zero and missing-ledger rejection.
Each main hidden state contains zero exact A/B pixels on both outputs, with a
recorded empty owner intersection and root hidden guard. Objects stay mapped,
no popup_done occurs during no-click hiding, and return PNGs exactly match the
pre-hide chain reference on both outputs. The return's Mosaic hit/button before
switching layouts is **NOT TESTED**; the budget is not increased to repair that
omission. S7 subsequently clears clips, restores standard geometry, preserves
A/B and delivers a second B right button without unexpected dismissal.
Main private damage events: G1 standard 981 / Mosaic 936; G2 924 / 818;
G3 918 / 761. Available physical buffer-local extents cover every changed pixel
between the analyzed settled capture windows; no uncovered changed pixel is
measured. Ordinary controls have no native damage instrumentation. Extents
can be broad; fractional expansions are source-audited but their minimum extent
and every intermediate frame are not qualified. Native commit/present logs are
retained as passive context, not a reopened temporal campaign or atomic input
identity proof.
### Reproducibility, validation and remaining limits
42 attempted runs / 32 without fixture failure / 10 failed early calibrations;
680 PNGs. All retained in the complete manifest, including excluded attempts.
Early reusable generated calibration filenames were overwritten; exact generated
source for every failed early attempt is not available. Their logs/configs/PNGs
and documented defects remain. Final attempts save unique generated source and
per-run source copies; main sources are retained. This provenance limitation is
explicit and does not make the final reproducer depend on a historical lab.
Evidence manifest (1,986 entries) and separate source manifest (39 entries)
verify; exact final hashes are recorded below after final synchronization.
The manifests include the final analysis, metadata, budget, retained run artifacts,
private source inputs and fixture sources. Builds/binaries/logs/images/manifests
remain outside Git. Final fixture contains prepare.sh, run.py, analyze.py,
test-budget.py and README.md; no new C adaptation or algorithmic patch was added.
Shell syntax, Python AST including generated runners, standalone C17 clients
with warnings as errors, pinned controlled patch application, fresh private
SceneFX/Mosaic and pristine-standard builds, model 1/1, actual scale/PNG checks,
pointer calibration, grabs, clip traces, pixel/damage analysis and manifest
verification pass. Final production paths, historical patches, cumulative patch,
PKGBUILD and UX are unchanged against initial HEAD. Each runtime is removed;
no lab process or private socket remains. No installation, live IPC, daily-session
operation or STEP20D work was performed. Phase B remains uncommitted/unpublished.
Remaining NOT TESTED or limited: rotation, resize/migration/hotplug, full animations,
all optimized caches, saved buffers, other renderers, source-sampling influence,
intermediate frames, all subpixel edge phases and production ABI/package. No new
reproducible clipping leak or scene corruption was demonstrated.
### Architectural answers
| Question | Evidence-backed answer |
| --- | --- |
| Q1 viewport coherence | Yes in sampled applied static G1/G2/G3: 800×600 logical viewport, independent physical scaling, camera remains logical. |
| Q2 forbidden rounding pixels | None observed in whole H2 popup/effect comparisons and retained boundary bands. |
| Q3 missing visible pixels | None in sampled final columns/known popup boxes; all fractional phases and opaque tiled boundary are not qualified. |
| Q4 hit/render agreement | Yes at calibrated points and x=-2,-1,0,1,2,20 grid; no same-frame atomic identity claim. |
| Q5 nested XDG clipping | Root and child both_cross/b_cross clips exclude sampled H2 pixels and forbidden buttons while preserving visible interactions. |
| Q6 true grabs | Both real serials unchanged, configure/ack/map and visible buttons succeed; outside dismissal remains normal. |
| Q7 shadow/blur | Shadow-spill exclusion and real blur persist; interior shadow/cache/sampling limits persist. |
| Q8 uniform versus mixed | No clipping/input divergence found; physical counts differ as expected with each output's scale. |
| Q9 damage conversion | Source-audited physical extents cover settled measured changes; no minimal/every-frame proof. |
| Q10 redesign required | No reproduced defect here forces a private algorithm change; production gates remain open. |
| Q11 next gate | Resize/migration: these results use fixed logical boxes, while the calibration exposed frame/applied-placement distinctions; verify owner/output/clip updates as geometry actually changes. Rotation, saved buffers and ABI/package remain separate subsequent gates. |
`SWAYFX_MOSAIC_FRACTIONAL_SCALING_PROBE=PARTIALLY_FEASIBLE`
Overall architectural conclusion: **UNDECIDED**. Observed static fractional
clip behavior is viable, but S1 prerequisite, interior shadow and restored
Mosaic hit are insufficient for complete requested-matrix qualification.
No production qualification or new transaction/UX contract follows.
`SWAYFX_MOSAIC_STEP20C_EFFECTS_FREEZE_AND_FRACTIONAL_SCALING_PROBE_V1=PARTIALLY_FEASIBLE`
Final source manifest SHA-256 (39 entries): `a553a1106297ae21819a6f00ffea681cba88996135e28feea393b1b37a1f1c5d`.
Final evidence manifest SHA-256 (1,986 entries): `b47ddab0be8f60b1fd43576fcdc2ca0dbcc5fee19b4c8faf343b648c3ed36fdd`.
Both verified entry by entry after final fixture synchronization. The final
ordinary/private standard FX0/FX1/FX2/popup PNGs are pixel-identical in G1/G2/G3.
## Fractional evidence freeze — 2026-10-12
The initial main/local origin/main/github/main references all matched
`aefee44c636a1529eca9a082658fd80b4d17cf5f`; only this fixture/report and the
five expected documentation files were dirty. No unrelated changes were found.
Both historical manifests matched their announced hashes and every one of
39 source / 1,986 evidence entries verified before documentation edits.
The 42 attempts, 32 successful fixtures, ten calibration failures and 680 PNGs
were independently counted. All observed grab serials matched their received
buttons. Both historical camera returns changed zero pixels on both outputs;
the ledger consumed exactly two pairs. The ten exclusions remain fixture
failures, with the overwritten early-source limitation retained above.
A new full clone `/tmp/step20c-fractional-freeze-clone-v1` reconstructed private
SceneFX/Mosaic, pristine pinned SwayFX and the C17 clients. Its disposable lab
is `/tmp/swayfx-mosaic-nested-grab-UZe5sYFr`. Seven runs passed: G1 ordinary
calibration, then b_cross outside scenarios in all three modes for G1 and G2.
Each run calibrated both logical references; all twelve popup grabs used real
unchanged serials and mapped. Ordinary/private splith added 127,200 H2 popup
pixels in G1 and 81,200 in G2. Mosaic added zero H2 pixels, retained visible
popup pixels, and delivered no popup button for the exterior click; both
popup_done events followed the exterior grab dismissal. Real modes, normal
transforms, scales and per-output physical PNG dimensions matched G1/G2.
Boundary hover records and passive clip logs are retained. The actual FX
renderer reports GLES 3.2 Mesa 26.2.4-arch1.1 / Radeon 780M radeonsi.
Available private physical damage extents covered all measured settled changed
pixels; ordinary SwayFX has no such native instrumentation.
The replay reserved **zero new camera pairs**, retaining the historical 2/2
budget as the sole camera evidence. No restored-Mosaic hit assertion was added.
Shell syntax, fixture/generated Python AST, C17 -Werror clients, controlled
patch application and all three builds passed; Meson model passed 1/1.
Offline reservation tests passed. Documentation links and git diff --check
passed. Production sources, historical patches, cumulative patch, PKGBUILD
and UX remained unchanged. Per-run runtime cleanup removed private sockets
and terminated owned children. No system installation or live IPC was used.
New source manifest: 974 entries, SHA-256
`c527db9988223fc51d056fdfe8d155ea293c25d1f7361b0aaf93b72be1f06869`.
New complete freeze evidence manifest: 443 entries, SHA-256
`54968808f6df7d23b142d3d9decf975eb2378c1c3e07649332120f66fcfd2722`.
Both live under the new lab's fractional-results directory; historical
manifests were not rewritten. Sources in the new manifest reflect the exact
pre-publication replay fixture, including its then-local documentation.
Verdict remains **PARTIALLY_FEASIBLE**, architectural conclusion **UNDECIDED**.
Opaque tiled overflow remains INCONCLUSIVE, permitted interior shadow remains
INCONCLUSIVE, and a new Mosaic hit after historical camera return remains
NOT TESTED. Rotation, dynamic geometry/ownership, optimized caches, sampling,
saved buffers, intermediate frames, subpixel phases and ABI/package remain
open gates. This freeze adds source-only evidence, not a production contract.

View file

@ -0,0 +1,92 @@
# Fractional-scale source fixture
**EXPERIMENTAL / preserved source-only evidence, PARTIALLY_FEASIBLE.** Read the
[protocol](../../../docs/design/STEP20C_FRACTIONAL_SCALING_PROBE.md) first.
Reconstruct private pinned sources through the effects preparer in a new full
clone. The scaling runner adapts only fixture Python; no render,
clip, damage, shader, input decision or ABI changes. Artifacts stay in the
single temporary lab. All camera runs reserve from a two-pair global ledger.
## Reproduction from sources
Use a full clone containing base commit
`aefee44c636a1529eca9a082658fd80b4d17cf5f` and the preserved fractional
fixture. No historical lab is required. `prepare.sh` rebuilds pinned
private SceneFX/Mosaic and C17 clients through the preserved preparer chain,
then builds a pristine SwayFX 0.6 standard control. It prints a fresh LAB path.
All generated protocols, libraries and binaries stay there; nothing is installed.
```sh
sh tooling/probes/step20c-fractional-scale/prepare.sh
# Set LAB to the exact printed path; use this same lab for the entire campaign.
python tooling/probes/step20c-fractional-scale/test-budget.py
python tooling/probes/step20c-fractional-scale/run.py "$LAB" control G0 --scenario calibration
python tooling/probes/step20c-fractional-scale/run.py "$LAB" control G1
python tooling/probes/step20c-fractional-scale/run.py "$LAB" private_standard G1
python tooling/probes/step20c-fractional-scale/run.py "$LAB" private_mosaic G1 --camera-pairs 1
python tooling/probes/step20c-fractional-scale/run.py "$LAB" control G2
python tooling/probes/step20c-fractional-scale/run.py "$LAB" private_standard G2
python tooling/probes/step20c-fractional-scale/run.py "$LAB" private_mosaic G2 --camera-pairs 1
python tooling/probes/step20c-fractional-scale/analyze.py "$LAB"
```
Every run first calibrates both ordinary references. Failed runs are retained
and consume any reserved pair. A lab-wide flock serializes runners, persisted
reservations survive launch failure/interruption, and a missing ledger in an
existing campaign is rejected. Camera default is zero; after two reservations,
only zero-pair variants are accepted. `test-budget.py` executes the actual
reservation block with the launch tail replaced; it never launches a compositor.
Do not use another lab or delete the ledger to bypass exhaustion.
For G1/G2 root/nested comparisons, use `--scenario outside --placement
both_cross` in each mode, with zero pairs. The existing b_cross variant is the
default; its separate outside click is at logical (20,190). Root-A forbidden
input uses a separate Mosaic both_cross run with `--outside-y 140`. Optional
G3 main uses zero pairs in each mode after G1/G2 calibration succeeds.
The fully qualified matrix is explicitly listed in the report; absence of a
scenario in this short command example is not a PASS.
## Coordinate and artifact contract
The runner sets requested physical modes and scales in private configs, checks
get_outputs logical 800×600 rectangles and normal transforms, and gives grim
an explicit per-output scale. It checks actual PNG sizes against physical modes.
Global pointer points remain logical: sent_x=global_x+800, extent 1600×600.
Calibration records requested placement, measured surface origin, independent
local-motion delta and left/right delivery. IPC frame coordinates are separate.
The analyzer retains full-image differences, predeclared logical/physical ROIs,
complete edge columns, exact popup colors, configured popup-box edge conversions,
contrast/luminance metrics and settled damage coverage. Damage trace extents are
already physical. Equality of full neighboring PNGs also detects non-exact-color
filtering pixels. Exact-color counts alone are not treated as a leak detector.
Captured images and input logs are related observations, not atomic frame identity.
All attempted runs, including ten failed early calibration fixtures, remain in
`/tmp/swayfx-mosaic-nested-grab-0DfvGCxn` for this execution. The final adapter
saves a uniquely named generated source per attempt and a source copy per run.
Earlier reusable calibration source names were overwritten: exact generated
sources for every early failed attempt are not all retained. Their logs,
configurations, captures and documented fixture defects are retained. This
limits forensic replay of those failures, not the final source-only reproduction.
The final source/evidence manifests live outside Git in fractional-results.
## Observed qualification
G1/G2/G3 requested modes and scales were applied. Both standard controls prove
popup and shadow overflow; private Mosaic's neighboring popup/FX1/FX2 images
remain unchanged, while visible blur and permitted popup pixels persist.
Two G1/G2 camera pairs restore exact popup PNGs. Result is PARTIALLY_FEASIBLE:
opaque tiled overflow controls, interior Mosaic shadow, optimized caches,
restored Mosaic hit before layout return and intermediate frames are unqualified.
No production correction is implemented. STEP20C stays NEXT, multi-output
NOT YET SUPPORTED. The original fractional campaign was local during the effects freeze; its
source-only evidence is now preserved separately.
## Freeze replay: no new camera pair
For the fractional freeze, run G1 calibration and G1/G2 three-mode
`--scenario outside --placement b_cross --camera-pairs 0`. Do not run the
camera commands in the historical campaign example above during this freeze.
The historical two-pair ledger is exhausted and remains authoritative for
its camera evidence; a fresh rebuild supplies no additional camera evidence.

View file

@ -0,0 +1,143 @@
"""Physical PNG analysis with predeclared logical ROIs; no border exclusions."""
import argparse
import hashlib
import json
import math
from pathlib import Path
import re
from PIL import Image, ImageChops, ImageDraw, ImageStat
ROIS = {'left': {'interior': (300,100,700,500), 'boundary': (720,50,800,550)},
'right': {'boundary': (0,50,80,550), 'reference': (250,250,550,500)}}
COLORS = {'A': (224,192,32), 'B': (32,208,160)}
def sha(p):
return hashlib.sha256(p.read_bytes()).hexdigest()
def nonzero(d):
r,g,b=d.split()
return ImageChops.lighter(ImageChops.lighter(r,g),b)
def diff(a,b):
d=ImageChops.difference(a,b)
return {'compared_pixels': a.width*a.height,
'modified_pixels': a.width*a.height-nonzero(d).histogram()[0],
'bbox': d.getbbox()}
def physical(box,scale):
# CONTRACT: conservative ROI containment; not a substituted render-box rule.
return [math.floor(box[0]*scale), math.floor(box[1]*scale),
math.ceil(box[2]*scale), math.ceil(box[3]*scale)]
def analyze(run):
events=[json.loads(x) for x in (run/'events.jsonl').read_text().splitlines()]
geoms={(e['tag'],e['output']):e for e in events if e['kind']=='capture_geometry'}
log=(run/'compositor.log').read_text(errors='replace')
# WHY: source audit establishes these extents are already buffer-local.
# INVARIANT: no second scale conversion of TEMPORAL_DAMAGE.
damages=[(int(ns),out,list(map(int,box))) for ns,out,*box in
re.findall(r'TEMPORAL_DAMAGE ordinal=\d+ ns=(\d+) output=(\S+) extent=(-?\d+),(-?\d+)\.\.(-?\d+),(-?\d+)',log)]
captures=[];previous={};images={};transitions=[]
for e in events:
if e['kind']!='capture':continue
path=run/e['path'];g=geoms[(e['tag'],e['output'])]
side='left' if e['output']=='HEADLESS-1' else 'right'
im=Image.open(path).convert('RGB');images[(side,e['tag'])]=im
assert list(im.size)==g['dimensions']==[g['mode']['width'],g['mode']['height']]
obs={'file':path.name,'sha256':sha(path),'geometry':g,'before_ns':e['before_ns'],
'after_ns':e['after_ns'],'coordinate_source':'get_outputs + explicit per-output grim scale',
'roi':{},'client_pixels':{},'edge_columns':{},
'hit_evidence':{'client_log_sha256':sha(run/'client.log') if (run/'client.log').exists() else None,
'pointer_log_sha256':sha(run/'pointer.log') if (run/'pointer.log').exists() else None,
'scope':'ordered run events; no atomic capture/input identity'}}
for name,box in ROIS[side].items():
pb=physical(box,g['scale']);roi=im.crop(pb)
obs['roi'][name]={'logical':box,'physical':pb,'pixels':roi.width*roi.height}
for name,col in COLORS.items():
delta=ImageChops.difference(im,Image.new('RGB',im.size,col))
mask=nonzero(delta).point(lambda v: 255 if v == 0 else 0)
obs['client_pixels'][name]={'exact_pixels':mask.histogram()[255], 'bbox':mask.getbbox()}
# Entire predeclared columns, not selected after inspecting anomalies.
cols=list(range(0,min(4,im.width))) if side=='right' else list(range(im.width-4,im.width))
for x in cols:
obs['edge_columns'][str(x)]={name:sum(im.getpixel((x,y))==c for y in range(im.height)) for name,c in COLORS.items()}
if side in previous:
older,oldim=previous[side]
intervals=[box for ns,out,box in damages if out==e['output'] and older['after_ns']<ns<=e['after_ns']]
mask=Image.new('1',im.size,0);draw=ImageDraw.Draw(mask)
for box in intervals:draw.rectangle((box[0],box[1],box[2]-1,box[3]-1),fill=1)
delta=ImageChops.difference(oldim,im)
changed=nonzero(delta).point(lambda v: 255 if v else 0)
uncovered=ImageChops.subtract(changed,mask.convert('L'))
outside=uncovered.histogram()[255]
transitions.append({'side':side,'from':older['tag'],'to':e['tag'],**diff(oldim,im),
'damage_coordinate_source':'TEMPORAL_DAMAGE physical buffer-local',
'damage_extents':intervals,'changed_outside_damage':outside if damages else None})
previous[side]=(e,im);captures.append(obs)
comparisons=[]
for side in ROIS:
base=images.get((side,'FX0-off'))
if base:
scale=next(g['scale'] for (tag,out),g in geoms.items() if out==('HEADLESS-1' if side=='left' else 'HEADLESS-2'))
for tag in ('FX1-shadow','FX2-blur','FX4-combined'):
if (side,tag) not in images:continue
im=images[(side,tag)]
entry={'side':side,'phase':tag,'whole':diff(base,im),'rois':{}}
for name,box in ROIS[side].items():
pb=physical(box,scale);aa=base.crop(pb);bb=im.crop(pb)
entry['rois'][name]=diff(aa,bb)
# Luminance and contrast measured in the predeclared ROI.
metrics=[]
for img in (aa,bb):
gray=img.convert('L')
horizontal=ImageChops.difference(gray.crop((1,0,img.width,img.height)),gray.crop((0,0,img.width-1,img.height)))
metrics.append({'luminance_stddev':ImageStat.Stat(gray).stddev[0],
'mean_adjacent_contrast':ImageStat.Stat(horizontal).mean[0]})
entry['rois'][name]['filter_metrics_fx0_effect']=metrics
comparisons.append(entry)
for side in ROIS:
ref_tag='S1-effects-off-reference' if (side,'S1-effects-off-reference') in images else 'S1-opaque-reference'
if (side,ref_tag) in images and (side,'S2-chain-stable') in images:
first=images[(side,ref_tag)];second=images[(side,'S2-chain-stable')]
scale=next(g['scale'] for (tag,out),g in geoms.items() if out==('HEADLESS-1' if side=='left' else 'HEADLESS-2'))
comparisons.append({'side':side,'phase':'S2-popup-vs-reference','whole':diff(first,second),
'rois':{name:diff(first.crop(physical(box,scale)),second.crop(physical(box,scale))) for name,box in ROIS[side].items()}})
boxes=[]
for chain in (e for e in events if e['kind']=='chain'):
for name,point,offset,cfg in [('A',chain['a_global'],(40,35),chain['a_configure']),('B',chain['b_global'],(35,30),chain['b_configure'])]:
gx,gy=point[0]-offset[0],point[1]-offset[1]
for output,ox in [('HEADLESS-1',-800),('HEADLESS-2',0)]:
g=next(v for (tag,out),v in geoms.items() if out==output);sc=g['scale']
def rounded(v):return math.floor(v+0.5) if v>=0 else math.ceil(v-0.5)
edges=[rounded((gx-ox)*sc),rounded(gy*sc),rounded((gx-ox+cfg[2])*sc),rounded((gy+cfg[3])*sc)]
boxes.append({'popup':name,'output':output,'global_logical':[gx,gy,cfg[2],cfg[3]],
'rounded_destination_edges':edges,'source':'configured geometry and successfully targeted local offsets; SceneFX scale_box edge rule',
'viewport_physical':[0,0,g['mode']['width'],g['mode']['height']]})
returns=[]
for side in ROIS:
if (side,'S5-return') in images:
returns.append({'side':side,**diff(images[(side,'S2-chain-stable')],images[(side,'S5-return')])})
client=(run/'client.log').read_text(errors='replace') if (run/'client.log').exists() else ''
grabs=[]
for name,target,serial,used in re.findall(r'(POPUP_[AB]) GRAB_REQUEST origin=wl_pointer.button target=(\S+) serial=(\d+) used=(\d+)',client):
valid=serial==used and bool(re.search(rf'POINTER_BUTTON target={target} serial={serial} button=272 state=1',client))
grabs.append({'popup':name,'parent':target,'received_serial':int(serial),'used_serial':int(used),'valid':valid,
'mapped':f'{name} MAP' in client})
return {'run':run.name,'exit_status':json.loads((run/'exit-status.json').read_text()),
'captures':captures,'comparisons':comparisons,'transitions':transitions,'returns':returns,
'grabs':grabs,'physical_popup_boxes':boxes,'client_events':re.findall(r'^(?:POINTER_|POPUP_)[^\n]*',client,re.M),
'calibration':[e for e in events if e['kind']=='pointer_calibration'],
'scenario_events':[e for e in events if e['kind'] in ('chain','exterior_click','camera_pair_complete','standard_return_hit','boundary_hover')],
'clip_trace':[line for line in log.splitlines() if 'PROBE_POPUP_' in line or any(t in line for t in ('PI_SCENE ', 'PI_OWNER ', 'PI_POPUP '))],
'damage_events':len(damages)}
p=argparse.ArgumentParser();p.add_argument('lab',type=Path);args=p.parse_args();lab=args.lab.resolve()
out=lab/'fractional-results';out.mkdir(exist_ok=True)
results=[analyze(r) for r in sorted((lab/'runs').iterdir()) if (r/'exit-status.json').exists()]
(out/'analysis.json').write_text(json.dumps(results,indent=2)+'\n')
proofs=[f for r in (lab/'runs').iterdir() for f in r.rglob('*') if f.is_file()]
proofs += [f for f in lab.glob('fractional-*') if f.is_file() and f.suffix!='.lock']
proofs += [f for f in out.iterdir() if f.is_file() and f.name != 'evidence-sha256.txt']
proofs += [lab/'fx-checker.png',out/'analysis.json',lab/'effects-source-sha256.txt',lab/'effects-build-log.txt']
(out/'evidence-sha256.txt').write_text(''.join(f'{sha(f)} {f}\n' for f in sorted(set(proofs))))
print(json.dumps({'runs':len(results),'manifest_entries':len(set(proofs)),'manifest_sha256':sha(out/'evidence-sha256.txt')}))

View file

@ -0,0 +1,17 @@
#!/bin/sh
# EXPERIMENTAL: rebuild pinned inputs in one private lab; no installation.
set -eu
repo=$(CDPATH= cd -- "$(dirname -- "$0")/../../.." && pwd)
log=$(mktemp /tmp/fractional-prepare-XXXXXXXX)
sh "$repo/tooling/probes/step20c-effects-damage/prepare.sh" > "$log" 2>&1
lab=$(sed -n 's/^LAB=//p' "$log")
test -n "$lab" && test -d "$lab"
# CONTRACT: the baseline is pristine upstream, never a Mosaic-modified tree.
mkdir "$lab/standard"
git -C "$repo" archive fd71a6bdc061bd633b488ae7b83e8a6981d22f86 | tar -xf - -C "$lab/standard"
meson setup "$lab/standard-build" "$lab/standard" -Dwerror=false \
-Dsd-bus-provider=libsystemd -Dswaybar=false -Dswaynag=false \
-Dman-pages=disabled -Ddefault-wallpaper=false >> "$log" 2>&1
meson compile -C "$lab/standard-build" -j 8 >> "$log" 2>&1
cp "$log" "$lab/fractional-build-log.txt"
printf 'LAB=%s\nBUILD_LOG=%s\n' "$lab" "$log"

View file

@ -0,0 +1,176 @@
"""EXPERIMENTAL scaling adapter; only private fixture code is generated."""
import argparse
import fcntl
import json
import os
from pathlib import Path
import subprocess
import sys
from PIL import Image
p = argparse.ArgumentParser()
p.add_argument('lab', type=Path)
p.add_argument('mode', choices=('control', 'private_standard', 'private_mosaic'))
p.add_argument('geometry', choices=('G0', 'G1', 'G2', 'G3'))
p.add_argument('--camera-pairs', type=int, choices=(0, 1), default=0)
p.add_argument('--outside-y', type=int, choices=(140,190), default=190)
p.add_argument('--placement', choices=('b_cross', 'both_cross'), default='b_cross')
p.add_argument('--scenario', choices=('main', 'outside', 'calibration'), default='main')
a = p.parse_args()
lab = a.lab.resolve()
repo = Path(__file__).resolve().parents[3]
if a.camera_pairs and (a.mode != 'private_mosaic' or a.geometry not in ('G1', 'G2') or a.scenario != 'main'):
p.error('pairs only in G1/G2 main Mosaic runs')
# WHY: repeat attempts must not create a new scientific budget.
# CONTRACT: single lab-wide ledger; failure/interruption consumes reservation.
# INVARIANT: hold exclusive lock through launch and child cleanup, never reset.
lock = (lab / 'fractional-campaign.lock').open('a')
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
ledger = lab / 'fractional-camera-budget.json'
if not ledger.exists() and any((lab / 'runs').glob('*-fractional-*')):
p.error('existing campaign has no ledger; refusing silent budget reset')
state = json.loads(ledger.read_text()) if ledger.exists() else {'consumed': 0, 'attempts': []}
if type(state['consumed']) is not int or not 0 <= state['consumed'] <= 2:
p.error('invalid persisted budget')
if state['consumed'] != sum(t['reserved'] for t in state['attempts']):
p.error('inconsistent persisted reservations')
if state['consumed'] + a.camera_pairs > 2:
p.error('global two-pair budget exhausted')
state['consumed'] += a.camera_pairs
state['attempts'].append({'geometry': a.geometry, 'mode': a.mode, 'scenario': a.scenario, 'placement': a.placement, 'outside_y': a.outside_y, 'reserved': a.camera_pairs})
tmp = ledger.with_suffix('.tmp')
with tmp.open('w') as f:
f.write(json.dumps(state, indent=2) + '\n'); f.flush(); os.fsync(f.fileno())
tmp.replace(ledger)
# CONTRACT: record all attempts including zero-pair controls before launch.
scales = {'G0': (1, 1), 'G1': (1.25, 1.25), 'G2': (1.25, 1), 'G3': (1.5, 1)}[a.geometry]
modes = [(int(800*s), int(600*s)) for s in scales]
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')
s = (repo / 'tooling/probes/step20c-applied-state-input/run.py').read_text()
s = s.replace("'hidden_click', 'toggle'))", "'hidden_click', 'toggle', 'scale'))")
s = s.replace("f'{args.mode}-{args.kind}-asi{args.asi_mode}-{time.time_ns()}'", "f'{args.mode}-fractional-"+a.geometry+'-'+a.scenario+"-{time.time_ns()}'")
for i in (0,1):
old = f'output HEADLESS-{i+1} resolution 800x600 position {(-800 if i==0 else 0)} 0'
new = f'output HEADLESS-{i+1} resolution {modes[i][0]}x{modes[i][1]} scale {scales[i]} position {(-800 if i==0 else 0)} 0'
assert old in s;s=s.replace(old,new)
s = s.replace("output.get('scale') != 1.0", "output.get('scale') != EXPECTED_SCALES[output['name']]")
s = s.replace("['grim', '-o', output, str(path)]", "['grim', '-s', str(EXPECTED_SCALES[output]), '-o', output, str(path)]")
# WHY: grim's default multi-output scale can differ from the target output.
# CONTRACT: request that output's scale, then assert PNG dimensions against mode.
s = s.replace("record('capture', tag=tag", """with Image.open(path) as png:
dimensions = list(png.size)
out = next(o for o in outputs if o['name'] == output)
mode = out['current_mode']
if dimensions != [mode['width'], mode['height']]:
raise RuntimeError(f'capture/mode mismatch {dimensions} {mode}')
record('capture_geometry', tag=tag, output=output, dimensions=dimensions,
scale=out['scale'], transform=out['transform'], rect=out['rect'], mode=mode)
record('capture', tag=tag""")
s = s.replace("'nested-client'), 'b_cross'", "'nested-client'), " + repr(a.placement))
s = s.replace("Path('/usr/bin/sway')", "lab / 'standard-build/sway/sway'")
s = s.replace("from pathlib import Path", "from pathlib import Path\nfrom PIL import Image\nEXPECTED_SCALES = " + repr(dict(zip(('HEADLESS-1','HEADLESS-2'),scales))))
scenario = r''' if args.kind == 'scale':
def wev_calibrate(proc, logname, node):
# CONTRACT: logical enter infers origin; independent motion checks the delta.
text = lambda: (run / logname).read_text()
r, w = node['rect'], node['window_rect']
origin = [r['x'], 300 if logname == 'wev-left.log' else 250]
x, y = origin[0] + 20, origin[1] + 30
inject('move', 790, 10)
time.sleep(.05)
before = len(text())
inject('move', x, y)
wait(lambda: re.search(r'enter:[^\n]*x, y: ([\d.]+), ([\d.]+)', text()[before:]))
enter = re.findall(r'enter:[^\n]*x, y: ([\d.]+), ([\d.]+)', text()[before:])[-1]
ex, ey = map(float, enter)
if ex != 20 or not 0 <= ey < 100:
raise RuntimeError(f'calibration enter out of expected interior: {enter}')
applied_origin = [x-ex, y-ey]
before = len(text())
inject('move', x+40, y+20)
wait(lambda: f'x, y: {ex+40:.6f}, {ey+20:.6f}' in text()[before:])
for button in ('left', 'right'):
inject(button, x+40, y+20)
wait(lambda: 'button: 272' in text() and 'button: 273' in text())
record('pointer_calibration', global_point=[x+40,y+20], local_expected=[ex+40,ey+20],
rect=r, content=w, requested_origin=origin, surface_origin=applied_origin, coordinate_source='client enter origin and independent motion delta', log=logname, passed=True)
command('[app_id="wev"] floating enable')
command('[app_id="wev"] border none')
command('[app_id="wev"] move position 250 250')
time.sleep(.3)
ref = find(tree(), lambda n: n.get('app_id') == 'wev')
wev_calibrate(None, 'wev.log', ref)
command('workspace 1')
cal = spawn(['stdbuf','-oL','wev'], 'wev-left.log')
wait(lambda: find(tree(), lambda n: n.get('app_id') == 'wev' and n['id'] != ref['id']))
left = find(tree(), lambda n: n.get('app_id') == 'wev' and n['id'] != ref['id'])
command(f"[con_id={left['id']}] floating enable")
command(f"[con_id={left['id']}] border none")
command(f"[con_id={left['id']}] move absolute position -750 300")
time.sleep(.3)
left = find(tree(), lambda n: n.get('id') == left['id'])
wev_calibrate(cal, 'wev-left.log', left)
# Cross the output boundary and return with both targets still alive.
inject('move', 310,300); inject('move', -690,350)
snapshot('S0-calibrated')
command(f"[con_id={left['id']}] kill")
cal.wait(timeout=8)
command('workspace 1')
command('[app_id="org.mosaic.NestedGrabProbe"] focus')
if args.mode == 'private_mosaic': scroll(400)
if SCENARIO != 'calibration':
spawn(['swaybg','-i',str(lab/'fx-checker.png'),'-m','tile'], 'swaybg.log')
command('gaps inner 24')
if args.mode != 'private_mosaic':
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')
time.sleep(.3);snapshot('FX0-off')
command('shadows enable');time.sleep(.25);snapshot('FX1-shadow')
command('shadows disable');command('blur enable')
time.sleep(.25);snapshot('FX2-blur')
command('shadows enable');time.sleep(.25);snapshot('FX4-combined')
command('shadows disable');command('blur disable')
time.sleep(.25);snapshot('S1-effects-off-reference')
open_chain('scale')
time.sleep(.25);snapshot('S2-chain-stable')
# Predeclared logical edge grid: hover does not dismiss valid grabs.
for x in (-2,-1,0,1,2,20):
before = len(client_text())
inject('move',x,190);time.sleep(.08)
record('boundary_hover',global_point=[x,190],client_events=client_text()[before:])
snapshot('S3-boundary')
if SCENARIO == 'outside':
before = len(re.findall(r'POINTER_BUTTON target=POPUP_[AB][^\n]*state=1',client_text()))
inject('right',20,OUTSIDE_Y);time.sleep(.25)
after = len(re.findall(r'POINTER_BUTTON target=POPUP_[AB][^\n]*state=1',client_text()))
record('exterior_click',popup_buttons_before=before,popup_buttons_after=after,
done_a=count('POPUP_A POPUP_DONE'),done_b=count('POPUP_B POPUP_DONE'))
snapshot('S2-exterior-click')
else:
for pair in range(CAMERA_PAIRS):
scroll(1280);time.sleep(.25);snapshot('S5-hidden')
inject('move',-700,190);time.sleep(.1)
scroll(400);time.sleep(.25);snapshot('S5-return')
record('camera_pair_complete',pair=pair+1)
command('layout splith');time.sleep(.3);snapshot('S7-standard')
o=owner();ac,bc=configured('POPUP_A'),configured('POPUP_B')
bx=o['rect']['x']+o['window_rect']['x']+ac[0]+bc[0]+35
by=o['rect']['y']+o['window_rect']['y']+ac[1]+bc[1]+30
inject('move',bx,by);inject('right',bx,by);time.sleep(.1)
record('standard_return_hit',global_point=[bx,by])
snapshot('S7-standard-hit')
elif args.kind == 'cycles':
'''
scenario=scenario.replace('OUTSIDE_Y',str(a.outside_y))
scenario=scenario.replace('SCENARIO',repr(a.scenario)).replace('CAMERA_PAIRS',str(a.camera_pairs))
assert s.count(" if args.kind == 'cycles':\n") == 1
s=s.replace(" if args.kind == 'cycles':\n",scenario)
s = s.replace("run.mkdir(parents=True, mode=0o700)", "run.mkdir(parents=True, mode=0o700)\n(run / 'fixture-source.py').write_text(Path(__file__).read_text())")
generated=lab/f'fractional-{a.geometry}-{a.mode}-{a.scenario}-attempt{len(state["attempts"])}.py';generated.write_text(s)
raise SystemExit(subprocess.call([sys.executable,str(generated),str(lab),a.mode,'scale','--max-seconds','120','--max-captures','50','--max-bytes','35000000']))

View file

@ -0,0 +1,27 @@
"""Execute the real reservation block without starting a compositor or camera."""
from pathlib import Path
import json
import signal
import subprocess
import tempfile
runner=Path(__file__).with_name('run.py').resolve()
source=runner.read_text().split("scales = ")[0]
# CONTRACT: only the launch tail is replaced; production reservation executes.
root=Path(tempfile.mkdtemp(prefix='fractional-budget-test-'))
script=root/'reservation.py'
script.write_text('__file__='+repr(str(runner))+'\n'+source+"\nprint('RESERVED',flush=True)\nsys.stdin.readline()\nraise SystemExit(7)\n")
def launch(lab,pairs):
return subprocess.Popen(['python',str(script),str(lab),'private_mosaic','G1','--camera-pairs',str(pairs)],stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE,text=True)
lab=root/'lab';lab.mkdir()
p=launch(lab,1);assert p.stdout.readline().strip()=='RESERVED'
q=launch(lab,1);out,err=q.communicate();assert q.returncode!=0 and 'BlockingIOError' in err
p.send_signal(signal.SIGINT);p.communicate(timeout=5)
assert json.loads((lab/'fractional-camera-budget.json').read_text())['consumed']==1
p=launch(lab,1);assert p.stdout.readline().strip()=='RESERVED';p.communicate();assert p.returncode==7
p=launch(lab,1);out,err=p.communicate();assert p.returncode==2 and 'exhausted' in err
p=launch(lab,0);assert p.stdout.readline().strip()=='RESERVED';p.communicate()
assert json.loads((lab/'fractional-camera-budget.json').read_text())['consumed']==2
old=root/'old';(old/'runs/control-fractional-G1-old').mkdir(parents=True)
p=launch(old,0);out,err=p.communicate();assert p.returncode==2 and 'silent budget reset' in err
print('PASS reserve/lock/interruption/failure/exhaustion/zero/missing-ledger; no compositor launched:',root)