swayfx-mosaic/docs/design/STEP20C_ROTATED_OUTPUT_PROBE.md

27 KiB
Raw Permalink Blame History

STEP20C rotated output viewport and input probe

EXPERIMENTAL evidence; PARTIALLY_FEASIBLE. This probe follows the published dynamic-output evidence at 2882f7415952d7f141da85f90c0fd113a17c1665. STEP20C remains NEXT and production multi-output NOT YET SUPPORTED. The source fixture is here. No production source, installed library, package, desktop session or live IPC was changed. The separate partial-popup experiment remains a later gate.

Question and prior evidence

The fractional and dynamic-owner probes sampled negative origins, scale 1.25, nested valid XDG grabs, logical resize and workspace migration, but every dynamic state used transform=normal. The dynamic probe was PARTIALLY_FEASIBLE: two standard D5 reference calibrations failed, the maximum camera clamp and migrated clipped-popup portion were not exercised. This probe asks whether an output rotated 90° keeps the same logical Mosaic viewport, clipping and input while pixels and damage use the changed physical buffer coordinates. A failure is a hypothesis until measured against pristine and private-standard controls.

Pinned inputs and exact source path

SwayFX control fd71a6bdc061bd633b488ae7b83e8a6981d22f86, Mosaic base cd550382b311efdbfe3ed892f4e1d281d852e754, SceneFX 37ccd723bef49e6891156ffafce8f549f01446cc, wlroots 0.20.2 source d783533489e1f75d6886c2ab5c5960090ef268f8. Preserve prior patches unchanged. Fresh private sources/builds, sockets and logs live under one LAB.

Source audit before fixture implementation:

  • SwayFX output_cmd_transform accepts 90 and 270, then swaps their WL_OUTPUT_TRANSFORM_* values: Sway command directions are clockwise while Wayland transform enum directions are anticlockwise. SwayFX's IPC encoder converts the internal enum back to the user-facing direction. The first pristine R90 action measured IPC transform="90", physical 750×1000 and logical 800×600. The initial fixture erroneously expected IPC "270" and stopped before its R90 PNG; that action is consumed and is not replayed. The applied IPC value, not the command string alone, decides the gate. apply_output_configs prepares pending mode, transform, then scale (rounded to 1/120); scene output build/backend commit precedes final output-layout update, arrange_root and transaction_commit_dirty. It dirties optimized blur after output changes. Thus IPC acceptance does not establish one atomic render/input generation.
  • wlroots wlr_output_transformed_resolution swaps width/height for odd transforms; wlr_output_effective_resolution divides those dimensions by scale. wlr_output_transform_invert exchanges unflipped 90/270; wlr_output_transform_compose combines transform values. wlr_box_transform and wlr_region_transform map rectangles differently at 90/270, with width and height arguments from the source coordinate space. wlr_output_commit_state applies state before events.commit; events.present is a separate callback.
  • SceneFX scene_node_intersect_clip/scene_node_visibility operate on logical scene regions. scene_entry_render calls logical_to_buffer_coords for regions and transform_output_box for boxes; both scale first, then apply the inverse output transform with transformed physical dimensions. scale_region expands fractional-scale coverage by one pixel when rounding up. output_to_buffer_coords converts output damage with the inverse transform. scene_output_damage stores buffer-local physical extents; wlr_scene_output_build_state computes render damage and output commit state. Shadow/blur rendering has distinct paths, so effect visibility alone cannot validate optimized-cache invalidation or off-viewport sampling.
  • Mosaic workspace_tiling_scene_x subtracts logical scroll_x from the applied tiled-scene position; the private tiled and root-popup clips are applied logical scene clips. transaction_apply copies pending state into current state, then scene arrangement and cursor rebase follow. node_at_coords and seat_pointer_notify_button use global logical pointer coordinates; wlroots wlr_cursor_absolute_to_layout_coords maps normalized pointer coordinates into the layout's logical bounding box. No physical scale or output rotation is applied to injected global logical points.

The source audit identifies paths, not proof that the measured buffer, PNG and client events agree. It must be confronted with captures and event logs.

Geometry and coordinate predictions

State H1 physical mode Command transform Expected applied logical H1 H2
R0 1000×750 normal (-800,0,800,600), scale 1.25 800×600 at (0,0), normal, scale 1
R90 750×1000 90 clockwise (-800,0,800,600), scale 1.25 unchanged
R270 optional 750×1000 270 clockwise (-800,0,800,600), scale 1.25 unchanged

R0→R90 changes both mode dimensions and transform: no single-variable causal claim is possible from that comparison alone. The target logical viewport is 800×600 in both states; H1's physical buffer is expected to use 1000×750 then 750×1000. Actual mode, transform, scale, global rectangle, scene owner and capture dimensions are gates read from get_outputs, trace and PNG. If the command is refused or remains normal, R90 is INCONCLUSIVE.

Logical global H1 (x,y) becomes output-local (x+800,y). The inverse wlroots transform maps a scaled logical box into the physical buffer. For a clockwise command 90, the parser's applied enum is 270; IPC reports the user-facing 90, while the inverse used by SceneFX is 90. Distinguish the two names and use the internal enum and actual buffer dimensions in wlr_box_transform/wlr_region_transform, including half-open rectangle bounds and fractional rounding. A simple hand-swapped axis is insufficient near boundaries. Record both expected and measured logical→buffer→PNG coordinates for every marker/ROI.

Virtual-pointer normalization is provisionally (sent_x,sent_y)=(global_x+800, global_y) over a 1600×600 logical union. Empirically calibrate on each output with an ordinary surface, (+40,+20) local motion, left/right buttons and crossing x=0. Relocate both ordinary references before scoring either; confirm actual client enter/motion and non-overlap. A prior D5 reference occlusion makes frame-only geometry an insufficient calibration.

Capture orientation gate

grim screencopy may expose transformed pixels in a different orientation from SceneFX's raw framebuffer. Before any graphics assertion, capture a deliberately asymmetric field with five distinct markers: upper-left red square, upper-right green triangle, lower-left blue circle, lower-right yellow stripe and purple center cross. For each, store logical point/box, predicted physical box using observed transform, PNG box, dimensions, delta and rounding rule. Also record capture window CLOCK_MONOTONIC timestamps, mode, scale, transform, physical/logical dimensions and file hash. Do not select ROIs after seeing anomalous pixels. If marker orientation is not identifiable, dependent pixel, popup and damage claims are INCONCLUSIVE. The marker source and distribution mechanism must be documented before use.

Comparative scenarios and stop gates

Use identical deterministic SHM toplevel, valid-parent root A and child B, virtual pointer and ordinary references in: A pristine splith, B private splith, C private mosaic. Preserve TOPLEVEL→A→B; each grab serial must come from its own real button press, with configure/ack/map and legal anchors. Do not infer clipping from no spill unless standard controls visibly spill.

  1. T0: R0 geometry, references, scene/clip, PNG markers and input calibration.
  2. T1: one reserved R0→R90 per mode, then observed mode/transform/scale, logical rectangle, identities, scene/clip, marker/PNG and input gates.
  3. T2/T3: A and B border-spanning cases, a logical x=-3..3 grid at preselected popup y values, full adjacent physical strips, permitted and forbidden pixels, scene hits and separate client delivery/dismissal.
  4. T4: one global Mosaic camera hide/return pair only; same XDG object identity and a new B button after return if no click has dismissed the stack.
  5. T5: one reserved R90→R0 per mode; check restored mode, transform, scene, camera, clip, PNG and input. A clamp may alter camera; its measured value governs expected geometry.
  6. T6: short shadow/blur FX0/FX2 contrasts only after geometry and PNG gates; optimized cache and outside-source sampling remain NOT TESTED otherwise.
  7. T7: measured physical damage extents versus changed physical pixels, including exposed regions on both outputs. Do not double-scale physical extents or infer every intermediate frame from settled comparisons.
  8. T8: private Mosaic layout splith, then no residual clip, ordinary popup, focus, effects and input before an exterior dismissal click.

R270 is optional, static at startup, at most one run per mode and only after R90 capture/pointer gates pass. Transforms 180/flipped, hotplug, all saved buffers, complete cache invalidation, all frames, ABI/package and daily-driver support remain NOT TESTED unless directly measured.

Strict budget, isolation and evidence

One persistent locked ledger spans the campaign. Reserve before each mode/transform action, retain failed reservations, forbid repeats and refuse more than two actions per mode / six overall. One global Mosaic camera hide/return pair maximum. R270 adds at most three static starts, no dynamic actions. Test lock, persistence, duplicate/exhaustion and missing-ledger rejection offline before any run. No second lab may bypass consumed actions.

Every run has private 0700 XDG_RUNTIME_DIR, explicit private SWAYSOCK and swaymsg -s, private WAYLAND_DISPLAY, headless backend and scoped child cleanup. Never resolve the live IPC socket implicitly. Preserve all failure exit codes, attempts and raw logs before owned cleanup; never kill by global process name. Use passive patches only when prior traces are insufficient; do not alter clipping, shaders, damage, callbacks, hit testing or ABI.

Retain source/evidence SHA-256 manifests, raw events/PNGs, output/transform, logical-to-buffer, screenshot, owner/clip, popup, pointer/input, pixel, damage, timeline, budget, matrix and summary JSON under LAB. The local fixture keeps only source and passive patch files; no generated binary or capture enters Git.

Verdict: FEASIBLE only if the applied rotation, coordinate/PNG calibration, standard spill controls, Mosaic visible/forbidden pixels and hits, valid grabs, return clips and measured damage all pass in the tested matrix. PARTIALLY_FEASIBLE requires valid principal measurements with named open cells. INCONCLUSIVE covers un-applied transform or failed orientation/fixture gate. BLOCKED requires a reproduced forbidden pixel/hit, wrong transform clip, crash or blocking standard-layout regression. Even FEASIBLE is experimental evidence, never production qualification. Architectural conclusion is separately ROTATED_OUTPUT_FEASIBLE_FOR_TESTED_MATRIX, ROTATED_OUTPUT_REQUIRES_REDESIGN or UNDECIDED according to measured support.

Measured private campaign — 2026-10-12

EXPERIMENTAL / PARTIALLY_FEASIBLE overall; architectural conclusion UNDECIDED. The new laboratory is /tmp/swayfx-mosaic-nested-grab-TJ3AhbCE. A fresh source-only preparer built private SceneFX/Mosaic and pristine SwayFX from the same pins and compiled the XRGB client and virtual pointer under C17 -Werror. It never used the historical dynamic campaign's consumed actions. All three modes were attempted once under one persistent locked rotation ledger. There are 111 retained PNGs: 16 pristine, 40 private standard and 52 private Mosaic, plus the per-run marker source PNGs. No B source, report or result was committed/pushed.

Fixture incident and applied geometry

The pristine splith run completed R0, two-output pointer calibration and a two-level grabbed popup chain. It reserved and launched R90. get_outputs then showed H1 physical 750×1000, scale 1.25, IPC transform 90, and logical (-800,0,800,600). The initial fixture asserted IPC 270 by mistaking the internal wlroots enum for SwayFX's user-facing IPC string, so it failed before R90 capture. Its R90 action remains consumed. Pristine R90 PNG, input and return are NOT TESTED; this is a fixture error, not a demonstrated compositor rotation defect. The source assertion was corrected before the two private runs; no pristine retry occurred.

Both private modes completed exactly one R0→R90 and one R90→R0. H2 stayed physical/logical 800×600, scale 1, normal at (0,0). H1 remained logical (-800,0,800,600) in all measured states while changing as follows:

State Physical mode IPC transform Internal enum trace Scale H1 PNG
R0 1000×750 normal 0 1.25 1000×750
R90 750×1000 90 3 (WL_OUTPUT_TRANSFORM_270) 1.25 1000×750
Return R0 1000×750 normal 0 1.25 1000×750

The mode's orientation and the transform both changed; the equal logical viewport does not isolate their separate causal effects. An IPC success value alone was never used as the applied-geometry proof.

PNG and pointer gates

grim -s 1.25 exposed a logical-oriented 1000×750 PNG even with the 750×1000 physical R90 mode. All five exact-color marker centers were identical in private R0 and R90 screenshots: TL (37,37), TR (962,37), BL (37,712), BR (962,712), center (499.5,374.5). The pristine R0 marker matched. This identifies the PNG repère for this screencopy path, not the raw SceneFX framebuffer. For the internal 270 transform, SceneFX uses inverse 90 on scaled logical rectangles. A pixel center (x,y) in this logical PNG maps by source formula to physical buffer (749-y,x): TL approximately (712,37), TR (712,962), BL (37,37), BR (37,962), center approximately (375,500). These buffer coordinates are calculated, not directly read back. The exact marker equality has zero measured PNG-center deviation; raw-buffer deviation is NOT TESTED. Fractional edges use SceneFX's region expansion and box rounding, so the center mapping is not a claim about every filtered edge. The actual wallpaper used five distinct exact RGB colors and five asymmetric positions, but drew every mark as a square. This does not meet the protocol's distinct-shape preference; the unique color/position pairs made orientation identifiable in the retained images, and no shape-based assertion is claimed.

On each private mode, independent ordinary references received pointer enter/motion, logical (+40,+20) local deltas and left/right buttons on H1/H2 at R0 and R90. Their first local point was (20,30), second (60,50). The injector used the observed logical union min_x=-800, extent 1600×600, sent_x=global_x+800, sent_y=global_y, without multiplying by 1.25 or rotating injection coordinates. It traversed x=-3..3 at y=550; those moves were not scored against popup client events and do not qualify clipped popup boundary hit testing. Both references were relocated before either calibration, avoiding the earlier D5 overlap incident.

Popups, clips and pixels

All measured A/B grabs used real received button serials. Pristine R0 had two valid grabs; each private mode had four valid grabs across its first and reopened chains. The client retained TOPLEVEL→A→B parentage, configure, ack, map and distinct new role lifetimes after closure. A's anchor (200,80) lies within the 420×320 parent buffer; B's (220,60) lies within A's 300×170 buffer. No protocol error was logged. One A/B popup_done pair appeared in each private run during the explicit close/reopen sequence; the chain present during R90 was captured before closure. This does not prove universal XDG persistence through arbitrary transforms.

Mosaic retained workspace ID 6, tiled owner ID 8, order [7,8,9,10] and camera 640 through R0, R90 and return. DO_STATE records transform 0→3→0. Pre-capture applied traces give tiled scene origin (-1440,0), local clip (640,0,800,600), derived viewport (-800,0,800,600) at R0/R90/return. Owner-derived root-popup clip from global root (-798,25) and local (-2,-25,800,600) yields the same viewport. Nested popup inheritance is visible in scene traces. These are sampled applied states, not every frame or a shared render/input transaction generation.

The private-standard R90 screenshot has 15,860 exact B-color pixels on H2, including 130/390/910 pixels in its first 1/3/7 physical columns. Pristine R0 showed the same 15,860 H2 B pixels. Private Mosaic R90 has zero A/B-color pixels on H2 and permitted A/B colors on H1. However, its B-color box in the logical PNG is x=[528,903), y=[206,369), wholly inside the H1 viewport x=[0,1000) in PNG coordinates. Standard B reaches x=1000 and continues on H2. The different standard/Mosaic layout positions mean this campaign did not challenge a partially clipped Mosaic popup at R90. The zero H2 count is an observed no-spill state, not proof that a forbidden popup portion was clipped. A crossing and simultaneous A/B crossing are also NOT EXERCISED. The fixed x=-3..3/y=550 grid did not lie in a scored popup region; invisible hit tests there remain NOT TESTED.

The single reserved Mosaic camera hide/return pair moved scroll 640→1280→640 under R90. At 1280, owner 8's applied scene x=-1440 with width 640 ends at the viewport's x=-800, yielding an empty intersection. Its root popup clip became (0,0,0,0); H1 captured zero owner/A/B-color pixels. The return restored the same scene/clip and exact R90 image hash, and a new right button on B at global (-343,195) reached that client after return. The second camera reservation is the return half of this one pair, not a second pair. The second chain's sampled popup roles (A 13, B 16 in this run) remained mapped through that pair, without a new popup_done; their IDs are run observations, not fixture constants. B also received a right button while visible under R90 before the hide.

R90→R0 restored mode, transform and clip in both private runs. Mosaic's settled R0 popup screenshots were byte-identical to its R90 logical-oriented popup screenshots and to its earlier R0 state. layout splith subsequently removed the Mosaic clips; its left/right PNG hashes equaled the private standard splith result, including the ordinary 15,860-pixel B overflow on H2. This samples return and ordinary layout behavior, without proving all transition frames or all focus/popup event orderings.

Damage, effects and budget

SceneFX's traced TEMPORAL_DAMAGE extents are clipped to physical buffer-local output->width/height after output_to_buffer_coords applies the inverse transform. For equal-transform settled R90 Mosaic pairs, map changed PNG pixels (x,y) to physical (749-y,x) before comparing extents. The R90 visible→fully-hidden and hidden→restored H1 pairs each changed 569,789 logical-PNG pixels; 27 and 18 physical extents respectively covered all changed pixels, zero uncovered. H2 changed zero in that pair. The R0 Mosaic→standard return changed 442,712 H1 pixels; 89 available normal extents covered them, zero uncovered. Full-output extents occurred, so neither minimal damage nor complete intermediate-frame coverage is established. Mode/transform transitions were not compared as equal physical buffers. Commit/present callbacks were retained but no screenshot was tied to a native buffer identity or atomic input generation. Private Mosaic logged 221 native commit and 215 present callbacks; private standard logged 188/182. The pristine control has no passive native trace. These count differences include startup/teardown and are not themselves a frame-loss finding.

Shadow and blur were disabled in this campaign. T6, optimized cache invalidation, off-viewport blur source sampling and physical raw-buffer readback are NOT TESTED. R270 was not run: the main negative partial-popup challenge is already absent, so another static direction would not close it.

The durable ledger retains one failed control R90 action, private-standard R90/R0 and private-Mosaic R90/R0: 5/6 mode/transform actions. Mosaic used 1/1 camera hide/return pair; R270 static runs 0/3. No reservation was refunded or replayed, and no second lab was created to evade the budget.

Structured evidence and integrity

The authoritative final analysis is LAB/results-final/; the prior sealed LAB/results/ and LAB/results-corrected/ remain byte-for-byte preserved. The initial analysis counted zero Mosaic spill but did not check the Mosaic B bounding box, so its architectural conclusion was too strong. The final results include outputs, transforms, logical-to-buffer mapping, marker coordinates, workspace owner, scene clips, popup geometry, pointer/input, pixel/damage, timeline, budget, matrix and summary JSON. All are recalculable from raw events, logs and 111 retained run PNGs.

The corrected v2 1,015-entry source manifest SHA-256 was 8fe5bdec378ca200a94c589db867fc21c80ee9fe37762c38077746e4eb2d9ce6; its 282-entry evidence manifest was 77e3d60621f14e46655779d09ce53cc92e48cc50b8aa2d72c5faa64432fdba51. The final v3 manifests and entry counts are recorded after sealing below. Final v3 1,015-entry source manifest SHA-256: f17ee5999edec84885997ea5dfb193859f7c1d278bf4dda22f8ab20b1c5e0b8f. Final v3 284-entry evidence manifest SHA-256: 261ff4cb12d7be3494d43f29ce1d24f980d8f19abdc558fef42599d50070747d. Both verify entry by entry. The final B box is derived from pixels as [528,206,903,369] against PNG width 1000; owner ID is derived from each run's tree state. Raw captures and private logs are unchanged across analyses. The v1 evidence manifest 02d7f47e1be9e3fb8d867e9a2d0d8d27ee358af43d96c9b217ab5435c7e5c4b8 also still verifies; each later evidence manifest includes the prior manifest files. The v1 source manifest recorded earlier fixture bytes and is historical after the analysis correction; no raw run file was modified.

An additive offline LAB/results-roi/ annotation records fixed logical/PNG/physical half-open boundary ROIs, exact A/B counts, comparable previous-capture pixel changes, mode/scale/transform, hashes and monotonic capture windows for all 108 run captures (the three marker-source PNGs are not run captures). At R90 the H1 PNG strip [995,1000) maps to physical-buffer y=[995,1000), x=[0,750), while H2 uses its first four physical columns. The private-standard R90 strip has 815 B pixels on H1 and 520 on H2; Mosaic has zero in both strips because its B box ends at PNG x=903. The 115-entry addendum manifest SHA-256 is 426f40078ad6388b34219b09f576d833d99a8dec76ea286235bc34fa324f8f63 and passed entry-by-entry verification. It chains the final analysis manifests without changing any earlier sealed file. These strip counts are not popup hit-delivery measurements.

Validation and isolation closeout

The new ledger passed offline lock, durable reservations, duplicate, exhaustion and missing-ledger tests without a compositor launch. The five fixture modules and three generated runners passed Python AST; the inherited preparer's shell syntax passed. The private SceneFX, Mosaic and pristine builds completed; the Mosaic Meson model test passed 1/1. Both experimental C17 clients compiled with -Wall -Wextra -Werror. The two dynamic-fixture patches retained their published SHA-256 and reverse-applicability checks in the fresh private tree. The forward patch sequence was exercised by the fresh preparer. A standalone reverse check of the older SceneFX effect-trace.patch against the fully stacked private tree failed at wlr_scene.c:2117; later overlapping patches make that isolated reverse check inapplicable. No historical patch was edited; the complete private SceneFX build passed.

Markdown links and the complete local diff, including new files through a temporary Git index, passed checks. The real Git index remains empty. Production Mosaic sources, the cumulative patch, PKGBUILD, installed SceneFX/wlroots, /usr/bin/sway, QuickShell and dotfiles were untouched. Every experimental IPC call used an explicit private socket; no live SwayFX IPC was accessed. All three per-run 0700 runtime directories were removed, with no retained private socket or experimental process. No install, session restart or STEP20D work occurred. This historical laboratory and its consumed budget are closed; the separate static-R90 partial-popup campaign requires a new ledger and private runtime.

Architectural answers and verdict

Question Evidence-backed answer
Q1 viewport coherence Sampled applied R90 tiled/root-popup clips still derive (-800,0,800,600); logical owner/camera stable.
Q2 SceneFX physical conversion Source scales then inverse-transforms boxes/regions. Physical damage coverage is compatible at two settled camera transitions; raw framebuffer pixels were not read, so direct physical-render proof is partial.
Q3 PNG frame Yes for this grim -s 1.25 path: 1000×750 logical orientation with all five marker centers unchanged under R90.
Q4 visible and forbidden pixels Permitted Mosaic owner/A/B pixels survive R90; zero exact A/B colors on H2. Partial Mosaic popup overflow was not created, so forbidden-pixel clipping is NOT EXERCISED.
Q5 root/child clips Root clip and nested inheritance trace the owner viewport; fully hidden owner reaches empty clip. Partial border challenge NOT EXERCISED.
Q6 grabs Two genuine grabs per completed chain, valid serial/configure/ack/map and no protocol error.
Q7 pointer Two-output logical calibration passes R0/R90 in both private modes, without scale/rotation injection; popup border hit grid unscored.
Q8 damage Available physical extents cover 569,789 changed pixels on each R90 camera leg, zero uncovered; no all-frame/minimal guarantee.
Q9 R0→R90 stale clip/cache No stale clip in sampled traces/screenshots; effects disabled, optimized cache NOT TESTED.
Q10 R90→R0 Private modes restore physical/logical geometry and normal transform; Mosaic restores sampled scene/clip and identical logical PNG. Pristine return NOT TESTED.
Q11 effects Shadow/blur disabled; NOT TESTED under rotation.
Q12 algorithm redesign No reproducible leak, invisible hit or crash demonstrated. Missing challenge does not justify production or private algorithm changes.
Q13 applied geometry contract A future STEP20C contract must bind applied mode, transform, scale, logical viewport and input/damage generation explicitly. This probe proposes that requirement; it does not freeze a new contract.
Q14 open cases Pristine R90 PNG/input/return; partial A/B Mosaic crossing; scored popup-border hits; effects/cache; raw physical readback; saved buffers; intermediate frames; R270; ABI/package.
Q15 next gate Another rotated geometry probe with a Mosaic popup demonstrably crossing x=0 and legal grabs, plus a completed pristine R90 control, is the smallest decisive gate. Saved buffers, optimized cache and ABI/package remain later independent gates.

SWAYFX_MOSAIC_ROTATED_OUTPUT_PROBE=PARTIALLY_FEASIBLE

Architectural conclusion: UNDECIDED for partial popup clipping under physical rotation. The tested logical geometry, PNG/pointer calibration, fully-hidden guard, damage coverage and return are useful evidence, but the central negative clipping comparison is absent. STEP20C stays NEXT; multi-output Mosaic stays NOT YET SUPPORTED.