docs(mosaic): preserve SceneFX ABI and packaging feasibility evidence

This commit is contained in:
fy59 2026-10-13 06:22:28 +02:00
parent 7c22b4020e
commit bb3a112550
12 changed files with 1040 additions and 0 deletions

View file

@ -4,6 +4,8 @@
### STEP20C experimental evidence
- Preserved SceneFX/wlroots ABI/package audit: current stock build and exact-one-file staging PASS; experimental shared clip functions absent from stock despite common SONAME. Private static build/loader passes, but wlroots interposition, notices, security and version-maintenance gates remain. PARTIALLY_FEASIBLE; architectural decision required. No integration or system change; the historical ABI evidence is published separately from any later distribution-contract investigation.
- Preserved optimized-edge gate: four completed controls reproduce the same boundary differences in pristine SceneFX, private splith and Mosaic without OPT traces. Same-binary Mosaic trace-off/on and pristine/private standard match full RGB at six states; central freshness, H2 stability and sampled clipping hold. PARTIALLY_FEASIBLE, architecture UNDECIDED: reduced-domain FBO history has source/model support but no native causal observation. Separate ABI/package audit justified; optional bounded GPU study remains unexecuted. No production change; historical graphical campaign closed.
- Preserved optimized blur cache probe: private splith/Mosaic used a real background source and output-scoped GPU cache, naturally reconstructed A/B/A and restored exact central filtered pixels with stable whole H2 RGB. Boundary equivalence still differs by up to six RGB levels in both private modes. Pristine C0 failed before captures; its duplicate initial image was charged after observation, with no retry or numerical budget overrun. PARTIALLY_FEASIBLE, architecture UNDECIDED; neutrality and whole-cache correctness unqualified. No production change; the historical campaign is closed.

View file

@ -342,3 +342,14 @@ extents cover stabilized differences only. No clipping regression or required
invariant violation is established. A separate SceneFX ABI/package audit is
now justified, retaining this shared rendering limitation; optional selective
GPU observation requires a new scoped ticket. This evidence is preserved without a production change. STEP20C stays NEXT, production multi-output NOT YET SUPPORTED.
The preserved [SceneFX ABI/package audit](design/STEP20C_SCENEFX_ABI_PACKAGE_FEASIBILITY.md)
is **PARTIALLY_FEASIBLE**, architecture
**SCENEFX_ABI_PACKAGE_REQUIRES_ARCHITECTURAL_DECISION**. The unchanged current
package builds with stock SceneFX and stages exactly /usr/bin/swayfx-mosaic.
The shared experimental binary needs two clip exports absent from stock
despite the same SONAME. Static embedding builds and resolves without a
SceneFX shared dependency, but shared wlroots interposition, updates, security
maintenance and license notices remain gates. A distinct private shared library
would require a new file-ownership decision; upstream API acceptance is unknown.
No integration or packaging contract is approved. The historical audit is frozen as evidence; STEP20C stays NEXT, production multi-output NOT YET SUPPORTED.

View file

@ -283,3 +283,14 @@ extents cover stabilized differences only. No clipping regression or required
invariant violation is established. A separate SceneFX ABI/package audit is
now justified, retaining this shared rendering limitation; optional selective
GPU observation requires a new scoped ticket. This evidence is preserved without a production change. STEP20C stays NEXT, production multi-output NOT YET SUPPORTED.
The preserved [SceneFX ABI/package audit](design/STEP20C_SCENEFX_ABI_PACKAGE_FEASIBILITY.md)
is **PARTIALLY_FEASIBLE**, architecture
**SCENEFX_ABI_PACKAGE_REQUIRES_ARCHITECTURAL_DECISION**. The unchanged current
package builds with stock SceneFX and stages exactly /usr/bin/swayfx-mosaic.
The shared experimental binary needs two clip exports absent from stock
despite the same SONAME. Static embedding builds and resolves without a
SceneFX shared dependency, but shared wlroots interposition, updates, security
maintenance and license notices remain gates. A distinct private shared library
would require a new file-ownership decision; upstream API acceptance is unknown.
No integration or packaging contract is approved. The historical audit is frozen as evidence; STEP20C stays NEXT, production multi-output NOT YET SUPPORTED.

View file

@ -41,3 +41,14 @@ The repository contains full source for development. The exporter constructs the
6. If Mosaic fails, use the normal SwayFX session. Do not overwrite `/usr/bin/sway` to recover.
See [upstream maintenance](UPSTREAM.md) and [development](DEVELOPMENT.md) for the operational procedure.
## Preserved experimental ABI/package audit
The [STEP20C audit](design/STEP20C_SCENEFX_ABI_PACKAGE_FEASIBILITY.md) separately
validates the unchanged current package's stock build and exact-one-file stage.
The experimental shared binary cannot resolve its two private clip functions
with stock SceneFX. A private static build removes SceneFX DT_NEEDED, but does
not establish shared-wlroots update safety or a one-file notice policy.
A distinct private dynamic library would add an owned file and require a new
explicit packaging decision. No option is adopted; the contract above remains
authoritative and unchanged. The historical evidence is frozen, PARTIALLY_FEASIBLE.

View file

@ -195,3 +195,17 @@ GPU observation requires a new scoped ticket. This evidence is preserved without
The [preserved edge source fixture](../tooling/probes/step20c-optimized-blur-edge/README.md)
records pinned reconstruction, offline checks and the exhausted four-slot ledger.
The preserved [SceneFX ABI/package audit](design/STEP20C_SCENEFX_ABI_PACKAGE_FEASIBILITY.md)
is **PARTIALLY_FEASIBLE**, architecture
**SCENEFX_ABI_PACKAGE_REQUIRES_ARCHITECTURAL_DECISION**. The unchanged current
package builds with stock SceneFX and stages exactly /usr/bin/swayfx-mosaic.
The shared experimental binary needs two clip exports absent from stock
despite the same SONAME. Static embedding builds and resolves without a
SceneFX shared dependency, but shared wlroots interposition, updates, security
maintenance and license notices remain gates. A distinct private shared library
would require a new file-ownership decision; upstream API acceptance is unknown.
No integration or packaging contract is approved. The historical audit is frozen as evidence; STEP20C stays NEXT, production multi-output NOT YET SUPPORTED.
The [ABI/package source fixture](../tooling/probes/step20c-scenefx-abi-package/README.md)
records build-only reconstruction and non-graphical diagnostic commands.

View file

@ -287,3 +287,14 @@ extents cover stabilized differences only. No clipping regression or required
invariant violation is established. A separate SceneFX ABI/package audit is
now justified, retaining this shared rendering limitation; optional selective
GPU observation requires a new scoped ticket. This evidence is preserved without a production change. STEP20C stays NEXT, production multi-output NOT YET SUPPORTED.
The preserved [SceneFX ABI/package audit](design/STEP20C_SCENEFX_ABI_PACKAGE_FEASIBILITY.md)
is **PARTIALLY_FEASIBLE**, architecture
**SCENEFX_ABI_PACKAGE_REQUIRES_ARCHITECTURAL_DECISION**. The unchanged current
package builds with stock SceneFX and stages exactly /usr/bin/swayfx-mosaic.
The shared experimental binary needs two clip exports absent from stock
despite the same SONAME. Static embedding builds and resolves without a
SceneFX shared dependency, but shared wlroots interposition, updates, security
maintenance and license notices remain gates. A distinct private shared library
would require a new file-ownership decision; upstream API acceptance is unknown.
No integration or packaging contract is approved. The historical audit is frozen as evidence; STEP20C stays NEXT, production multi-output NOT YET SUPPORTED.

View file

@ -0,0 +1,441 @@
# STEP20C SceneFX / wlroots ABI and package feasibility
**EXPERIMENTAL — PRESERVED HISTORICAL EVIDENCE.**
Protocol recorded before builds. A freeze gate passed at
`7c22b4020edc2d9ab6779d789159937523aae75a` on local, Forgejo and GitHub,
with a clean worktree. STEP20A DONE, STEP20B EXPERIMENTAL, STEP20C NEXT,
STEP20D–G PLANNED; production multi-output NOT YET SUPPORTED.
## Authority and problem
[Packaging](../PACKAGING.md) remains authoritative: exactly one installed
file `/usr/bin/swayfx-mosaic`, no Sway replacement, library overwrite,
conflicts/provides/replaces, personal configuration or service.
The current production patch has no STEP20C clip dependency. The private
SceneFX adds `wlr_scene_tree_set_clip` and `wlr_scene_tree_get_clip` while
retaining the stock SONAME. A shared SONAME does not supply missing symbols.
No strategy is preapproved; conclusions are evidence, not integration authority.
## Pinned sources and comparison
SwayFX 0.6 `fd71a6bdc061bd633b488ae7b83e8a6981d22f86`; private Mosaic
`cd550382b311efdbfe3ed892f4e1d281d852e754`; SceneFX 0.5
`37ccd723bef49e6891156ffafce8f549f01446cc`; wlroots 0.20.2
`d783533489e1f75d6886c2ab5c5960090ef268f8`.
Compare installed SceneFX, rebuilt pristine/private SceneFX, retained pristine
SwayFX, freshly reconstructed production Mosaic and rebuilt private Mosaic.
Preserve ELF headers/dynamic tags, defined/undefined/versioned symbols,
pkg-config flags/features, actual compiler headers and loader resolution.
Compare all public SceneFX headers and public struct size/alignment/offsets
where relevant. Audit the sidecar lifetime and wlroots shared structures.
Installed package versions alone never prove exact source provenance or ABI.
## Budget and isolation
One stock production reconstruction, one DESTDIR staging, one private
SceneFX/Mosaic shared build chain. One optional static SceneFX/Mosaic build
only after source audit establishes a bounded Meson method. No compositor
launch, graphics, system install, package creation, algorithm or UX change.
All source/build/stage/raw evidence stays in a home-directory LAB outside Git.
No inherited session sockets, global library path or loader configuration.
Loader diagnostics use `ld-linux --list` or `ldd -r` on trusted private
artifacts only; these are not compositor execution or graphics tests.
Stop implementing any option needing a public ABI, package-contract change,
new package or architecture migration; record the required future decision.
## Scenarios and decision criteria
S0: apply the unchanged cumulative patch on pinned pristine, prove source
equivalence and PKGBUILD checksums, compile with stock dependencies, run only
Meson model tests, stage once and inspect the complete manifest.
S1: private binary with private versus stock SceneFX; inspect required clip
symbols, same SONAME, RUNPATH/RPATH, transitive needs and missing relocations.
S2: compare upstream API, private static embedding, isolated distinct-SONAME
shared library, and an architecture without a new SceneFX API. Prioritize
standard SwayFX coexistence, independent updates, explicit ownership, security
maintenance, symbol interposition, licenses and versioned wlroots constraints.
FEASIBLE requires S0 staging plus characterized incompatibilities and a
sufficiently demonstrated candidate without identified ABI risk in the matrix.
PARTIALLY_FEASIBLE retains technical/contract/decision gates. INCONCLUSIVE
means insufficient reliable comparison. BLOCKED requires an established
coexistence violation, not the expected missing private functions in stock.
Architecture conclusion remains UNDECIDED until evidence is assessed.
## Limits
No graphical, GPU lifetime/cache, all-frame, rotated/scaled/migrated output,
installation, update or rollback test. Those are NOT TESTED. The optimized
edge property remains a separate shared rendering limitation; native FBO
causality remains unproved. No stock-package provenance assertion beyond
available metadata/source comparison. All optional strategies require a new
explicit integration decision before production work.
## Completed non-graphical audit (2026-10-13)
`SWAYFX_MOSAIC_SCENEFX_ABI_PACKAGE_PROBE=PARTIALLY_FEASIBLE`
Architecture: **SCENEFX_ABI_PACKAGE_REQUIRES_ARCHITECTURAL_DECISION**.
No production strategy is selected. The current package passes its separate
stock build/stage gate. The naive experimental/stock-library combination is
INCOMPATIBLE_WITH_STOCK_SCENEFX. Static embedding is technically demonstrated
but retains interposition, shared-wlroots, security and notice gates.
LAB: `/home/fy59/swayfx-mosaic-abi-package-tp22xfwm/`.
Raw commands/stdout/stderr/return codes are in `raw/`; derived evidence is
in `results/`. All build artifacts remain external to Git. Sources are fresh
archives of verified fixed Git objects, not copies of dirty historical source
trees. SceneFX and wlroots object stores were read-only inputs. The pristine
SwayFX executable is explicitly reused from the sealed A laboratory, with
SHA-256 checked against A's identity; it was not rebuilt or launched.
## Installed dependencies and source identity
Installed package metadata: SceneFX `0.5-1`, wlroots0.20 `0.20.2-1`, SwayFX
`0.6-1`. pkg-config reports SceneFX **0.5.0** as `scenefx-0.5`, wlroots
**0.20.2** as `wlroots-0.20`; generated installed headers and a tiny observer
calling native `wlr_version_get_*` agree on runtime wlroots 0.20.2.
The `.pc` files reside in `/usr/lib/pkgconfig`, with include roots
`/usr/include/scenefx-0.5` and `/usr/include/wlroots-0.20`.
All ten queried wlroots build features are true: DRM, X11, libinput, Xwayland,
GLES2, Vulkan, GBM, udmabuf, session and color management.
The 121 non-generated installed wlroots headers exactly match the pinned
0.20.2 source archive. Installed generated config/version headers are retained
separately. No wlroots source was patched. Header equality and runtime version
do not recover the complete distro binary build provenance, patch set or flags;
that exact provenance remains **NOT ESTABLISHED**. Other 0.20 builds and future
updates are **NOT TESTED**. Compiler dependency output and Meson compile
commands/dependencies record the actual headers and library paths used, rather
than relying only on declared package versions.
## S0: unchanged current production package
`PKGBUILD` remains byte-identical to A. Version `0.6+mosaic.1-1`, x86_64,
MIT, SwayFX fixed Git pin, no conflicts/provides/replaces.
Depends: cairo, gdk-pixbuf2, libevdev, libinput, json-c, systemd-libs, wayland,
libxcb, libxkbcommon, pango, pcre2, scenefx, ttf-font, wlroots0.20.
Makedepends: git, meson, ninja, pkgconf, python, wayland-protocols, libdrm, pixman.
Checkdepends: kitty, grim. The declared graphical check was deliberately not
executed; this ticket forbids compositor launches. `makepkg --printsrcinfo`
passed; no package build/install command or dependency installation was used.
Checksums recomputed without changing the recipe:
| Input | Expected and actual SHA-256 | Result |
| --- | --- | --- |
| cumulative production patch | `a04cd06a6ef4fc58e3b40333a53226849e82d9a4757b9ccee58ad7b59a23ff4b` | PASS |
| headless IPC source | `0a16c887ca6a87f0392c2eef498b4fad78b9f32afaf7d2a6c0e5e0e37c3c1744` | PASS |
| upstream Git source | recipe SKIP plus exact commit check/archive | PASS pinned source; no archive checksum invented |
The cumulative patch passed forward application on a pristine archive of
`fd71a6bdc061bd633b488ae7b83e8a6981d22f86`. Full source equivalence passed
for include/, sway/, meson.build, meson_options.txt and tests/mosaic-model.c.
The build used the recipe's Meson package flags, prefix `/usr`, bindir `bin`,
stock SceneFX and wlroots, `-j 8`. Meson mosaic-model passed 1/1.
Exactly one `meson install --destdir LAB/stage-current --no-rebuild` ran.
Complete non-directory manifest (including symlinks):
```text
/usr/bin/swayfx-mosaic
```
**CURRENT_PACKAGE_STAGE_PASS**. One regular executable ELF64 x86-64, no
symlinks, second binary, library, config, dotfile, service or session file.
The staged binary has no clip references, no RPATH or RUNPATH; its SceneFX
and wlroots references resolve against `/usr/lib`, with no missing relocations.
This qualifies only S0 in the measured build/loader/stage matrix. It is not
a full Arch release package, graphical test, or STEP20C qualification.
## ELF comparison
All compared objects are ELF64 x86-64. Every ELF header, dynamic table, dynamic
symbol table, version table, nm listing and objdump output is retained.
| Object | SONAME or SceneFX dependency | Clip symbols | RPATH / RUNPATH |
| --- | --- | --- | --- |
| installed SceneFX | `libscenefx-0.5.so` | neither defined | absent |
| rebuilt pristine SceneFX | same | neither defined | absent |
| rebuilt private SceneFX | same | both defined | absent |
| retained pristine SwayFX | DT_NEEDED `libscenefx-0.5.so` | neither required | historical pristine build RUNPATH retained |
| staged current Mosaic | DT_NEEDED `libscenefx-0.5.so` | neither required | absent |
| shared experimental Mosaic | DT_NEEDED `libscenefx-0.5.so` | both strong undefined | new private SceneFX build RUNPATH |
| static experimental Mosaic | no SceneFX DT_NEEDED | both present in ordinary symbol table | absent |
| installed wlroots | `libwlroots-0.20.so` | neither defined | absent |
Exact artifact digests and sizes:
| Object | Bytes | SHA-256 |
| --- | ---: | --- |
| stock-binary-elf | 6312256 | `479c55bdbf2cc10e72f4ed8633fc7287d15358ed3f451dd2e6bb350671985af0` |
| experimental-binary-elf | 6324344 | `058e51366a71f54ff7824f9a166e3d5cd6797c14f39968ba03587c30bb07bb05` |
| static-binary-elf | 6760600 | `ccd47ce5b74c132e19bcd46d022a80e90e8a944acc3db521a89427ee7913c614` |
| system-scenefx-symbols | 182216 | `63586ffbe268c6830b5782d5327e8ca3624dbd9b9c9b0e93e591e94ca28a6e62` |
| pristine-scenefx-symbols | 493216 | `4e342bbd832ae6e6cfb62938af5bf1fb6b46d30ec50c8b4eea50b652892af19e` |
| private-scenefx-symbols | 504792 | `b9582b6c7dd2f903f6de740534f71356261f3e0b84143a24f666f64575b1d91c` |
| system-wlroots-elf | 1378496 | `7edca9a7d65855addda68e29ba5db94d57c803eb5fa7d0f86db3136b90ca9837` |
| pristine-sway-elf | 6297064 | `8ac865481bc0487344210cd0e16d8e6c7655b29db887cd58ddc11b0ca22fe494` |
Installed and rebuilt pristine SceneFX have exactly the same exported symbol
set. Private SceneFX adds precisely `wlr_scene_tree_set_clip` and
`wlr_scene_tree_get_clip`; no stock export is removed. The new functions are
unversioned. ELF version requirements for dependency symbols are retained;
there is no versioned clip API contract. A common SONAME selects a library
identity for DT_NEEDED; it is not evidence that this library implements every
undefined function required by the consumer.
## S1: actual runtime search and relocations
`ld-linux-x86-64.so.2 --list` inspected library selection without running
application main. `ldd -r` on these trusted generated executables checked
relocations; return code alone is insufficient because missing-symbol
diagnostics can be emitted with status zero. The audit explicitly checks
stdout for missing symbols. No command launched the compositor.
| Consumer / provider | Selected library | Result |
| --- | --- | --- |
| current staged Mosaic / default | `/usr/lib/libscenefx-0.5.so` | PASS selection/relocations |
| experimental / default | LAB/scene-private-build/libscenefx-0.5.so via build RUNPATH | PASS selection/relocations only |
| experimental / private process path | same private library | PASS selection/relocations only |
| same experimental / stock process path | `/usr/lib/libscenefx-0.5.so` | two missing strong symbols |
| static experimental / default | no SceneFX loaded; stock wlroots still loaded | PASS selection/relocations only |
The stock counterfactual uses the loader's process-local `--library-path
/usr/lib` and a separately recorded `LD_LIBRARY_PATH=/usr/lib` only for
`ldd -r`. DT_RUNPATH is overridden in this tested setup. No global environment
was modified. Build RUNPATH is an absolute temporary path and is not a
distribution policy. Moving/removing that private library can expose the
stock fallback and its missing functions. No universal loader claim follows
for preload, secure execution, another RUNPATH or a changed library graph.
The documented search semantics are in [ld.so](https://man7.org/linux/man-pages/man8/ld.so.8.html).
## Public headers, structures and lifetime
All seven installed public SceneFX headers match pristine source bytes. The
private difference is only two declarations in types/wlr_scene.h. All
existing typedefs, enums, callbacks, public struct members and ownership
comments in these headers remain byte-identical. Private instrumentation
headers are internal, not installed public API. No wlroots header is changed.
C17 `-Wall -Wextra -Werror` observers report identical size/alignment for 17
SceneFX/wlroots structs in stock/pristine/private configurations, plus six
selected member offsets. Examples: scene_node 112/8, scene_tree 128/8, scene
304/8, scene_buffer 472/8, scene_output 344/8, wlr_output 624/8, output_state
208/8, surface 968/8. Full results are in struct-abi-audit.json. This is a
measured layout subset; complete semantic ABI compatibility is not proven.
The retained working patch stores clips in a file-local linked sidecar list
in types/scene/wlr_scene.c. Each record holds a tree pointer, copied wlr_box
and destroy listener; clearing or node destruction removes the listener and
frees the record. Set copies a tree-local rectangle, NULL clears, nonnegative
dimensions are asserted; get copies or returns false. No clip field is added
to a public struct. Linear lookup, unchecked coordinate addition and silent
allocation failure remain private-prototype production design gates. This
ticket does not repair them or freeze these API semantics.
The historical first discarded candidate added a public tree field and
crashed; the [spike report](STEP20C_SCENEFX_CLIPPING_SPIKE.md) retains that
separate evidence. It must not be conflated with the preserved sidecar patch.
No additional current ABI failure beyond the two absent stock functions was
demonstrated in this non-graphical matrix. That is not proof of absence of
semantic, allocation, concurrency, GPU or future-version defects.
## Transitive dependencies and symbol ownership
SceneFX stock/pristine/private share the same direct dependency list:
wlroots, wayland-server, libm, libdrm, pixman, EGL, GBM, GLESv2, lcms2 and
libc. wlroots additionally depends on Vulkan, libinput, libseat, libudev,
display-info, liftoff, Wayland client and XCB components, among others.
All DT_NEEDED entries and recursive loader paths are retained; 72 resolved
system-library paths map to 50 installed package metadata/license records.
No library package was installed.
SceneFX and wlroots define **85 common dynamic symbols**, including scene,
EGL, matrix and color functions. This overlap already exists with stock
SceneFX and does not by itself demonstrate a new clipping regression.
Controlled `LD_DEBUG=bindings` with relocation tracing shows 34 wlroots
scene bindings in each stock/private/static case: 31 go to SceneFX (or the
static executable), three remain in wlroots. Thus distinct SONAME or static
embedding alone does not isolate the process-wide symbol namespace.
The complete binding records distinguish actual provider from matching names.
A future design must preserve all shared-struct and function semantics across
this graph; hiding/renaming every symbol blindly could break wlroots's scene
helper integration. No symbol renaming or visibility experiment is authorized
or performed here.
## S2: static feasibility and license constraints
The pinned [SceneFX Meson target](https://github.com/wlrfx/scenefx/blob/37ccd723bef49e6891156ffafce8f549f01446cc/meson.build)
is a native library() honoring `default_library=static`. One private static
SceneFX build and one Mosaic link use its uninstalled pkg-config file; no
production Meson edit is needed for this laboratory demonstration.
The static executable is 6,760,600 bytes versus shared experimental 6,324,344
(+436,256 bytes, about 6.90%, both unstripped local builds). It contains both
clip implementations as ordinary `T` symbols, not new dynamic exports.
DT_NEEDED no longer names SceneFX; EGL/GBM/GLESv2/lcms2 are direct dependencies
instead. wlroots remains dynamic, and 85 common exports remain observable.
Model and loader checks pass. There is no second staging, strip, package build
or graphical run; the one-file ELF shape is demonstrated, a static release
package is **NOT TESTED**.
SwayFX, pinned SceneFX and wlroots use MIT notices. The source license texts
and transitive package license metadata are retained. Static copying requires
preservation of copyright/permission notices, per the
[SceneFX license](https://github.com/wlrfx/scenefx/blob/37ccd723bef49e6891156ffafce8f549f01446cc/LICENSE).
A separately installed notice would expand the current exact-one-file manifest.
An embedded notice strategy is possible in principle but **NOT TESTED** and
requires explicit packaging/legal review; this audit asserts no blanket legal
clearance. Embedded SceneFX security fixes require Mosaic rebuilds and an
identified maintenance owner. Static embedding does not freeze shared wlroots
or make future system updates ABI-safe.
## Comparative architecture matrix
| Criterion | A: upstream clipping API | B: static private SceneFX | C: distinct private shared library | D: no new SceneFX API |
| --- | --- | --- | --- | --- |
| Technical evidence | prototype only; acceptance unknown | build/model/relocations PASS | private lookup PASS; distinct identity NOT TESTED | architecture unknown |
| System libraries intact | normal upstream update only | yes | yes if isolated | yes |
| /usr/bin/sway intact | yes | yes | yes | yes |
| Coexistence | supported upstream version required | file ownership preserved; symbol/version qualification pending | extra file and complete lookup/binding contract required | unproved implementation |
| Stable clip API | absent today | private | private | no new API, mechanism unproved |
| ABI | future release unestablished | measured layouts/relocations pass; semantic/future ABI unestablished | same SONAME naive stock incompatible; isolated design unestablished | unestablished |
| Exactly one package file | possible after adoption | ELF shape demonstrated; notices/stage pending | no | possible, not demonstrated |
| Upgrade risk | medium shared-version transition | high until wlroots/security policy | medium/high; shared wlroots persists | unknown |
| SceneFX maintenance | upstream if accepted | Mosaic embedded-code owner needed | private-library owner needed | stock upstream; alternate mechanism owned by Mosaic |
| wlroots maintenance | qualified version pair | shared 0.20 ABI/interposition | shared 0.20 ABI/interposition | stock APIs plus larger Mosaic implementation |
| Reproducibility | fixed prototype inputs only | commands/sources retained, bit identity untested | laboratory lookup only | not tested |
| Human decision | yes, no submission approved | yes, bundling/security/license contract | yes, new file/SONAME/package ownership | yes, new architecture |
A is the conditional long-term maintenance direction: one maintained generic
primitive rather than perpetual private API patches. It needs maintainer
agreement, an ownership/error/bounds contract, version policy and closure of
remaining graphical gates. No upstream contact/submission occurred and no
acceptance is presumed.
B is the most concretely demonstrated candidate that avoids owning a second
SceneFX file and removes dependency on the system SceneFX's clip symbols. It
does not yet establish safe independent wlroots upgrades or a distributable
one-file notice strategy. C separates filesystem ownership and library identity
but requires a packaging decision and still shares a symbol namespace.
D cannot be called viable: surface crops/output scissor alone do not establish
popup-owner rendering, topmost hit traversal, damage, effects, saved buffers,
scale/rotation/migration or applied transaction consistency. No alternate
clipping engine is developed.
## Upgrade and hypothetical rollback
S0 can coexist by file ownership with standard SwayFX. No recipe conflict or
replacement blocks its updates. That does not guarantee an old Mosaic binary
will load after every shared SceneFX/wlroots upgrade. Requalify and rebuild
Mosaic separately; do not pin or replace system libraries to force it to work.
Static SceneFX makes Mosaic independent of system SceneFX updates, but an
embedded security fix requires its own rebuild and shared wlroots remains a
constraint. A private shared library likewise needs its own security/rebase
policy and explicit transitive compatibility.
Hypothetical removal of S0 deletes only /usr/bin/swayfx-mosaic and leaves
/usr/bin/sway, SceneFX and wlroots untouched. C would additionally remove only
its approved private library path. Real install, upgrade, uninstall and
rollback are NOT TESTED and were not attempted.
## Evidence-backed answers Q1–Q18
| Question | Answer |
| --- | --- |
| Q1 current stock build/stage? | Yes: clean pinned source/patch equivalence, stock Meson build and single DESTDIR stage PASS. |
| Q2 exact manifest? | Yes, exactly one regular ELF /usr/bin/swayfx-mosaic, no symlink. |
| Q3 added symbols? | Exactly wlr_scene_tree_set_clip and wlr_scene_tree_get_clip, no removed export. |
| Q4 required? | Both strong undefined in the shared private Mosaic binary; both ordinary definitions in static. |
| Q5 SONAME? | Names a provider, does not certify its export set; stock selection proves the two functions missing. |
| Q6 public layouts? | No member/type/enum/callback change in retained SceneFX headers, no wlroots source change; 17 sizes/alignments and six offsets identical. |
| Q7 further ABI incompatibility? | None newly demonstrated in current non-graphical matrix; 85 overlaps and mixed scene bindings are observed constraints, not proof of a new crash. Historical discarded struct-field candidate did crash separately. |
| Q8 stock with private binary? | No: missing strong clip relocations; unmodified stock cannot satisfy the tested binary. |
| Q9 static? | Build/model/loader shape passes with no SceneFX DT_NEEDED; one-file distribution still needs notice, security and wlroots decisions. |
| Q10 isolated shared? | Separate file/SONAME/RUNPATH could isolate library selection; process-wide symbol interactions remain. Distinct-SONAME build NOT TESTED. |
| Q11 package changes? | A needs approved supported API/version dependency; B approved embedding and notices/security/build policy; C extra file/identity/lookup contract; D architecture decision without necessarily another file. None applied. |
| Q12 update independence? | B removes shared SceneFX dependency and preserves file independence most concretely here; shared wlroots still prevents unconditional update-safety claims. |
| Q13 long-term maintenance? | A conditionally, if officially accepted/maintained; no acceptance evidence. Otherwise B/C need explicit project owners for security and rebases. |
| Q14 upstream preferable? | Conditional maintenance inference from one shared maintained primitive; error/bounds/ABI policy, ownership and graphical gates still missing. No submission authorized. |
| Q15 GPU/caches/version risks? | Not measured here; all-frame damage, references, effects and cache semantics must be separately qualified across actual version pairs. |
| Q16 shared edge phenomenon? | Separate rendering limitation carried across all options; static linkage does not repair it or prove native causality. |
| Q17 choose now? | Evidence supports candidate comparison only; explicit user architecture/packaging decision required before integration. |
| Q18 smallest next work? | Non-graphical contract dossier for static symbol binding/ownership, version qualification, security owner and one-file notices, plus an upstream API proposal dossier without submission. No new renderer or production patch. |
## Budget, validation and limits
PASS: one stock reconstruction, one stock stage, one private shared chain,
one optional static chain, one rebuilt pristine SceneFX reference. All Meson
setup/compile commands returned zero; mosaic-model 1/1 in stock, private
shared and static builds. C17 layout observers pass. AST, shell syntax, patch
application/equivalence, recipe checksums, export/symbol/header/loader and
license inventories are checked independently of graphical evidence.
Zero compositor launches, graphical runs, installations and packages generated.
NOT RUN: graphical headless IPC suite, GPU readback, physical backend/scanout,
new geometry/caches/lifetimes, actual update/rollback, another wlroots binary
version, bit-reproducible builds, static staging, distinct-SONAME build or
upstream submission. Preparation guards were tightened after the single build
chain; LAB/prepare-executed.py preserves the prior executed version and command
logs. No build was repeated for that non-behavioral fixture hardening.
The report and source fixture remain local and uncommitted. No source Mosaic
production, PKGBUILD, cumulative patch, UX, AGENTS, source pin or installed
compositor/library was changed. Final integrity and manifest details follow.
## Final integrity and local handoff
Source inventory: **1,463 entries**, SHA-256
`a27fa33d57d3423e09f02430f04cfed643762d876099b7f88489229115c4a1e5`.
Evidence inventory: **204 entries**, SHA-256
`86f7654c8ad962053644ce2c4c13eb9f026d6618b171e0cc3a8fc3034eb380d5`.
Both were sealed once and every entry independently checked again.
Original A source/evidence manifests remain byte-identical and pass with the
explicit historical README snapshot mapping.
Final normal checks PASS: AST for all four B Python scripts; shell syntax
for the existing exporter and PKGBUILD; C17 observers; all recorded builds and
three model tests; patch application/source equivalence; recipe digests;
Markdown local links; complete tracked/new-file whitespace/source-only audit;
protected production/UX/AGENTS/package/pin paths; installed sway/SceneFX/wlroots
hash equality; absence of LAB processes and sockets. The deep closeout review
was performed by the parent, not by an independent subagent; its findings and
fixture hardening are recorded in results/closeout-review.json.
Local HEAD and directly queried origin/main and github/main remain
`7c22b4020edc2d9ab6779d789159937523aae75a`. A was published; B has no staged
files, commit or push. The final worktree intentionally contains only the B
report/fixture and synchronized Architecture, Known Limitations, Roadmap,
README, Packaging audit section and Changelog. Packaging authority is unchanged.
Complete B diff, including untracked files, is retained as LAB/complete-phase-b.diff.
This report/diff is outside the sealed scientific inventory to avoid digest cycles.
No system install, package distribution, library replacement, live-session IPC,
QuickShell/dotfile/Codex change or STEP20D work occurred. No experimental process
or private socket remains. STEP20C NEXT; production multi-output NOT YET SUPPORTED.
## ABI/package evidence freeze — new ticket phase A
The preceding local handoff records the historical campaign state and its
A/B labels. This freeze publishes those ABI/package findings, without
reclassification, new builds, staging or compositor execution. The new static
distribution-contract phase B is separate and is not part of this commit.
All 1,463 source and 204 evidence entries were verified before changing the
fixture README's publication wording. Historical manifests are immutable.
For later verification, map only the historical fixture README path to:
`/home/fy59/swayfx-mosaic-abi-freeze-i6x1osil/historical-fixture-README.md`
The snapshot retains the hash in the original manifest; all other paths must
be checked directly. The four Python scripts are unchanged. Freeze audit
commands and historical-result verification are retained in `/home/fy59/swayfx-mosaic-abi-freeze-i6x1osil`.
Current package remains CURRENT_PACKAGE_STAGE_PASS; private shared with stock
SceneFX remains INCOMPATIBLE_WITH_STOCK_SCENEFX; static remains build/loader
evidence with no historical static staging or graphical qualification.
`SWAYFX_MOSAIC_SCENEFX_ABI_PACKAGE_PROBE=PARTIALLY_FEASIBLE`
`SCENEFX_ABI_PACKAGE_REQUIRES_ARCHITECTURAL_DECISION`
No distribution architecture or production integration is authorized.

View file

@ -0,0 +1,85 @@
# SceneFX ABI/package audit fixture
**EXPERIMENTAL / PRESERVED EVIDENCE / no production integration.**
Read the [protocol](../../../docs/design/STEP20C_SCENEFX_ABI_PACKAGE_FEASIBILITY.md).
Private sources/builds/results must stay outside Git. No script may launch a
compositor, use desktop IPC, install system files or change package contracts.
Budgets: one stock reconstruction/stage, one private shared chain, optionally
one static chain after source inspection. ELF checks are diagnostics only.
Generated artifacts and manifests stay in LAB.
## Build-only reproduction
Use a full Mosaic clone containing the fixed commits, installed stock build
prerequisites and local Git object stores containing pinned SceneFX and wlroots.
`prepare.py` does not fetch or install missing dependencies. It archives exact
objects, applies retained private patches in new copies, and logs each bounded
build command. Create a new LAB under your home directory, outside this clone.
A LAB can be prepared only once; a failed preparation consumes that attempt.
Do not bypass the ticket's build budget with another directory.
```sh
python tooling/probes/step20c-scenefx-abi-package/prepare.py LAB \
--scenefx-git /path/to/scenefx-object-store \
--wlroots-git /path/to/wlroots-object-store --static
python tooling/probes/step20c-scenefx-abi-package/audit.py LAB
python tooling/probes/step20c-scenefx-abi-package/summarize.py LAB
```
`--static` is optional and requires the source-audit decision documented in
this ticket. `--pristine-binary` optionally accepts only the A campaign's
SHA-256-qualified retained pristine executable; otherwise its ELF comparison
is NOT TESTED. A newly reproduced baseline binary needs its own provenance
qualification, not substitution under that historical identity.
The retained completed LAB is `/home/fy59/swayfx-mosaic-abi-package-tp22xfwm`.
Its optional static trial was exercised once; no new run/build is authorized
by these instructions for this completed ticket.
`audit.py` checks complete stage entries including symlinks, recipe digests,
ELF headers/symbols/version requirements, loader paths and relocations, public
header differences, C17 struct-layout observers and compiler dependencies.
It runs only diagnostic loader modes and small non-graphical layout/version
observers. It never runs a compositor entry point. `ldd -r` diagnostics are
checked even when its status is zero. Its stock-versus-private process library
paths are temporary and recorded. Required tools: cc, Meson/Ninja, pkg-config,
readelf, nm, objdump, the native x86-64 glibc loader, ldd and read-only Arch
package metadata (`pacman -Qi`; no package installation). Other platforms are
NOT TESTED, rather than silently inferred compatible.
`summarize.py` records measured
S0/S1 results separately from inferred architectural options. Static loader
success does not establish graphics, semantic ABI, future updates or licensing
clearance. All generated JSON/logs/source archives/builds remain in LAB.
## Completed result
Current package: **CURRENT_PACKAGE_STAGE_PASS**, exactly one regular ELF
`/usr/bin/swayfx-mosaic`, stock dynamic dependencies, no clip references.
Shared private binary with stock SceneFX: **INCOMPATIBLE_WITH_STOCK_SCENEFX**,
two missing strong clip symbols despite the common SONAME. Shared private
resolution and static embedding pass non-graphical build/model/loader checks.
Seventeen layouts and six selected offsets match; 85 exports overlap wlroots.
Stock/private/static wlroots scene bindings are retained, including the mix
between SceneFX providers and wlroots helpers. Distinct private SONAME,
future-version safety, GPU lifetimes and graphical behavior are NOT TESTED.
Overall **PARTIALLY_FEASIBLE**; architecture
**SCENEFX_ABI_PACKAGE_REQUIRES_ARCHITECTURAL_DECISION**. No packaging option is
adopted. The historical campaign is closed; STEP20C NEXT and production multi-output
NOT YET SUPPORTED. `seal.py LAB` refuses existing manifests; `seal.py LAB
--verify` verifies all entries without regenerating. Derive all evidence and
finalize fixtures before sealing. Report and complete diff are captured outside
the sealed inventory to avoid self-referential digest dependencies.
## ABI evidence freeze
The October 2026 freeze verifies the original 1,463 source entries and 204
evidence entries without rerunning the campaign. Their manifest hashes remain
`a27fa33d57d3423e09f02430f04cfed643762d876099b7f88489229115c4a1e5` and
`86f7654c8ad962053644ce2c4c13eb9f026d6618b171e0cc3a8fc3034eb380d5`.
This README's publication wording changed after verification. To verify the
sealed historical source manifest subsequently, map only its README entry to
the exact pre-freeze snapshot recorded in the report. Do not regenerate the
manifest or substitute other source files. Python fixture bytes are unchanged.
The old report's A/B labels refer to its earlier ticket, not the new static
distribution-contract phase. That later phase must remain local.

View file

@ -0,0 +1,207 @@
#!/usr/bin/env python3
"""Read-only ELF/header/package audit of prepared trusted private artifacts.
WHY: record missing relocations, symbol overlap and header provenance separately.
CONTRACT: no compositor entry point executes; loader commands only inspect ELF.
INVARIANT: results may be derived again before sealing, never after sealing.
"""
import argparse
import difflib
import hashlib
import json
import os
from pathlib import Path
import re
import shlex
import subprocess
p = argparse.ArgumentParser(description=__doc__)
p.add_argument('lab', type=Path)
a = p.parse_args(); lab = a.lab.resolve(); repo = Path(__file__).resolve().parents[3]
assert lab.is_dir() and lab.is_relative_to(Path.home()) and lab != Path.home()
assert not lab.is_relative_to(repo)
assert not (lab/'results/evidence-sha256.txt').exists(), 'sealed evidence; verify, never rewrite'
results = lab/'results'; raw = lab/'raw'
env = {k: v for k, v in os.environ.items() if k not in (
'SWAYSOCK', 'I3SOCK', 'WAYLAND_DISPLAY', 'DISPLAY', 'XDG_RUNTIME_DIR',
'LD_LIBRARY_PATH', 'LD_PRELOAD', 'PKG_CONFIG_PATH')}
env['LC_ALL'] = 'C'
def sha(path): return hashlib.sha256(path.read_bytes()).hexdigest()
def save(name, value): (results/(name+'.json')).write_text(json.dumps(value, indent=2)+'\n')
def run(label, args, extra=None):
proc = subprocess.run(list(map(str, args)), env={**env, **(extra or {})},
capture_output=True, text=True, timeout=60)
entry = {'args': list(map(str, args)), 'env_override': extra or {},
'returncode': proc.returncode, 'stdout': proc.stdout, 'stderr': proc.stderr}
(raw/(label+'.json')).write_text(json.dumps(entry, indent=2)+'\n')
return entry
def elf(label, path):
path = path.resolve(); parts = {}
assert path.stat().st_size <= 128*1024*1024, 'ELF input exceeds audit bound'
for tag, args in [('header', ['readelf','-hW']), ('dynamic',['readelf','-dW']),
('symbols',['readelf','--dyn-syms','-W']), ('versions',['readelf','-VW']),
('nm',['nm','-D','--format=posix']), ('nm-all',['nm','--format=posix']), ('objdump',['objdump','-p'])]:
parts[tag] = run(label+'-'+tag, [*args, path])
assert parts[tag]['returncode']==0, (label,tag)
defs = []; undefs = []; weak = []
for line in parts['symbols']['stdout'].splitlines():
fields = line.split()
if len(fields) >= 8 and fields[0].rstrip(':').isdigit():
name = fields[7]; base = name.split('@')[0]
if fields[6]=='UND':
(weak if fields[4]=='WEAK' else undefs).append(base)
elif fields[4] in ('GLOBAL','WEAK'): defs.append(base)
d = parts['dynamic']['stdout']
result = {'path':str(path), 'sha256':sha(path), 'bytes':path.stat().st_size,
'class':re.search(r'Class:\s+(.*)',parts['header']['stdout'])[1],
'machine':re.search(r'Machine:\s+(.*)',parts['header']['stdout'])[1],
'SONAME':re.findall(r'\(SONAME\).*?\[(.*?)\]',d),
'DT_NEEDED':re.findall(r'\(NEEDED\).*?\[(.*?)\]',d),
'RPATH':re.findall(r'\(RPATH\).*?\[(.*?)\]',d),
'RUNPATH':re.findall(r'\(RUNPATH\).*?\[(.*?)\]',d),
'defined':sorted(set(defs)), 'undefined':sorted(set(undefs)),
'weak_undefined':sorted(set(weak)),
'clip_defined':[x for x in defs if x in clips],
'clip_undefined':[x for x in undefs if x in clips]}
save(label,result);return result
clips = ['wlr_scene_tree_set_clip','wlr_scene_tree_get_clip']
versions = {}
for pkg in ['scenefx-0.5','wlroots-0.20','wayland-server','libdrm','pixman-1']:
versions[pkg] = {op:run('pkg-'+pkg+'-'+op,['pkg-config','--'+op,pkg])['stdout'].strip()
for op in ('modversion','cflags','libs')}
for pkg in ['scenefx-0.5','wlroots-0.20']:
path=Path('/usr/lib/pkgconfig')/(pkg+'.pc');sh=(raw/(pkg+'.pc'));sh.write_bytes(path.read_bytes())
features = {name:run('feature-'+name,['pkg-config','--variable=have_'+name,'wlroots-0.20'])['stdout'].strip()
for name in ['drm_backend','x11_backend','libinput_backend','xwayland',
'gles2_renderer','vulkan_renderer','gbm_allocator','udmabuf_allocator','session','color_management']}
save('dependency-versions',{'packages':versions,'wlroots_features':features})
# Metadata is evaluated in a non-installing shell: functions are defined but not called.
meta = run('pkgbuild-metadata',['bash','-c','startdir=$2; source "$1"; declare -p pkgname pkgver pkgrel arch license depends makedepends checkdepends source sha256sums _upstream_commit; declare -p conflicts provides replaces 2>/dev/null || true','audit',repo/'PKGBUILD',repo])
assert meta['returncode']==0
save('pkgbuild-metadata',meta)
pkgtext=(repo/'PKGBUILD').read_text()
expected=re.findall(r"'([a-f0-9]{64})'",pkgtext)
paths=[repo/'patches/0001-mosaic-layout-viewport.patch',repo/'tests/headless-ipc.py']
checks=[{'path':str(path),'expected':exp,'actual':sha(path),'PASS':sha(path)==exp} for path,exp in zip(paths,expected)]
assert len(checks)==2 and all(x['PASS'] for x in checks)
save('package-source-checksums',checks)
assert not re.search(r'^\s*(conflicts|provides|replaces)=',pkgtext,re.M)
save('package-contract',{'authority':'docs/PACKAGING.md','expected':['/usr/bin/swayfx-mosaic'],
'conflicts_provides_replaces':False,'stock_scene_dependency':True,'STEP20C':False})
stage=lab/'stage-current'
entries=[]
for path in sorted(stage.rglob('*')):
if path.is_symlink() or not path.is_dir():
entries.append({'path':'/'+str(path.relative_to(stage)), 'symlink':path.is_symlink(),
'executable':os.access(path,os.X_OK),'sha256':sha(path) if path.is_file() else None})
assert len(entries)==1 and entries[0]['path']=='/usr/bin/swayfx-mosaic' and not entries[0]['symlink'] and entries[0]['executable']
save('stock-stage-manifest',{'status':'CURRENT_PACKAGE_STAGE_PASS','entries':entries})
paths={'system-scenefx-symbols':Path('/usr/lib/libscenefx-0.5.so'),
'system-wlroots-elf':Path('/usr/lib/libwlroots-0.20.so'),
'pristine-scenefx-symbols':lab/'scene-pristine-build/libscenefx-0.5.so',
'private-scenefx-symbols':lab/'scene-private-build/libscenefx-0.5.so',
'stock-binary-elf':stage/'usr/bin/swayfx-mosaic',
'experimental-binary-elf':lab/'private-build/sway/swayfx-mosaic'}
if (lab/'retained-pristine-sway').exists():paths['pristine-sway-elf']=lab/'retained-pristine-sway'
if (lab/'static-build/sway/swayfx-mosaic').exists():paths['static-binary-elf']=lab/'static-build/sway/swayfx-mosaic'
elves={label:elf(label,path) for label,path in paths.items()}
stockdefs=set(elves['system-scenefx-symbols']['defined']);privdefs=set(elves['private-scenefx-symbols']['defined'])
wlrootsdefs=set(elves['system-wlroots-elf']['defined'])
save('symbol-comparison',{'private_added':sorted(privdefs-stockdefs),'private_removed':sorted(stockdefs-privdefs),
'scene_wlroots_overlap':sorted(privdefs & wlrootsdefs),
'pristine_system_added':sorted(set(elves['pristine-scenefx-symbols']['defined'])-stockdefs),
'pristine_system_removed':sorted(stockdefs-set(elves['pristine-scenefx-symbols']['defined']))})
assert set(clips)<=privdefs and not set(clips)&stockdefs
assert set(clips)<=set(elves['experimental-binary-elf']['undefined'])
save('unresolved-symbols',{'stock_missing':clips,'experimental_requires':clips,'classification':'INCOMPATIBLE_WITH_STOCK_SCENEFX'})
# ld-linux --list never enters the application. Stock --library-path overrides
# DT_RUNPATH on this artifact; verify the selected absolute library in raw output.
loader='/usr/lib/ld-linux-x86-64.so.2';loads={}
for label,path in [('stock',paths['stock-binary-elf']),('private',paths['experimental-binary-elf'])]:
loads[label+'-default']=run('loader-'+label+'-default',[loader,'--list',path])
for label,directory in [('private',lab/'scene-private-build'),('stock',Path('/usr/lib'))]:
loads['experimental-'+label]=run('loader-experimental-'+label,[loader,'--library-path',directory,'--list',paths['experimental-binary-elf']])
loads['relocations-'+label]=run('relocations-'+label,['ldd','-r',paths['experimental-binary-elf']],{'LD_LIBRARY_PATH':str(directory)})
loads['stock-bindings']=run('stock-bindings',['ldd','-r',paths['stock-binary-elf']],{'LD_DEBUG':'bindings'})
loads['stock-relocations']=run('stock-relocations',['ldd','-r',paths['stock-binary-elf']])
if 'static-binary-elf' in paths:
loads['static-default']=run('loader-static-default',[loader,'--list',paths['static-binary-elf']])
loads['static-relocations']=run('static-relocations',['ldd','-r',paths['static-binary-elf']])
assert '/usr/lib/libscenefx-0.5.so' in loads['experimental-stock']['stdout']
assert all('undefined symbol: '+c in loads['relocations-stock']['stdout'] for c in clips)
assert 'undefined symbol:' not in loads['relocations-private']['stdout']
assert 'undefined symbol:' not in loads['stock-relocations']['stdout']
if 'static-relocations' in loads: assert 'undefined symbol:' not in loads['static-relocations']['stdout']
if 'static-binary-elf' in paths:
loads['static-bindings']=run('static-bindings',['ldd','-r',paths['static-binary-elf']],{'LD_DEBUG':'bindings'})
loads['private-bindings']=run('private-bindings',['ldd','-r',paths['experimental-binary-elf']],{'LD_DEBUG':'bindings','LD_LIBRARY_PATH':str(lab/'scene-private-build')})
run('installed-package-metadata',['pacman','-Qi','scenefx','wlroots0.20','swayfx'])
save('loader-resolution',loads)
bindings={}
for name in ['stock-bindings','private-bindings','static-bindings']:
if name in loads:
lines=[line.strip() for line in loads[name]['stdout'].splitlines()
if 'binding file /usr/lib/libwlroots' in line and 'wlr_scene_' in line]
bindings[name]={'total':len(lines),
'external_provider':sum(' to /usr/lib/libwlroots' not in line for line in lines),
'self_wlroots':sum(' to /usr/lib/libwlroots' in line for line in lines),
'lines':lines}
save('scene-symbol-bindings',bindings)
# All installed public headers are compared; generated config headers are reported
# separately rather than mistaken for source changes.
stockheaders=Path('/usr/include/scenefx-0.5/scenefx')
headeraudit=[]
for installed in sorted(stockheaders.rglob('*.h')):
rel=installed.relative_to(stockheaders);pristine=lab/'scene-pristine/include/scenefx'/rel;private=lab/'scene-private/include/scenefx'/rel
if not pristine.exists():
headeraudit.append({'path':str(rel),'generated_or_missing_source':True,'installed_sha256':sha(installed)});continue
diff=''.join(difflib.unified_diff(pristine.read_text().splitlines(True),private.read_text().splitlines(True),fromfile='pristine/'+str(rel),tofile='private/'+str(rel)))
headeraudit.append({'path':str(rel),'system_pristine_equal':installed.read_bytes()==pristine.read_bytes(),
'pristine_private_equal':pristine.read_bytes()==private.read_bytes(),'diff':diff,
'sha256':{'installed':sha(installed),'pristine':sha(pristine),'private':sha(private)}})
save('public-header-diff',headeraudit)
# Compile tiny C17 layout observers, not scene/compositor instances. Size/offset
# equality is a measured necessary condition, never a full ABI guarantee.
structs=['wlr_scene_node','wlr_scene_tree','wlr_scene','wlr_scene_surface','wlr_scene_rect','wlr_scene_shadow','wlr_scene_blur','wlr_scene_optimized_blur','wlr_scene_buffer','wlr_scene_output','wlr_scene_timer','wlr_scene_layer_surface_v1','wlr_output','wlr_output_state','wlr_surface','wlr_buffer','wlr_renderer']
source='#define WLR_USE_UNSTABLE\n#include <stddef.h>\n#include <stdio.h>\n#include <scenefx/types/wlr_scene.h>\n#include <wlr/types/wlr_output.h>\n#include <wlr/types/wlr_compositor.h>\n#include <wlr/types/wlr_buffer.h>\n#include <wlr/version.h>\nint main(void) {\nprintf("wlroots_runtime %d.%d.%d\\n",wlr_version_get_major(),wlr_version_get_minor(),wlr_version_get_micro());\n'
for struct in structs:source+=f'printf("{struct} %zu %zu\\n", sizeof(struct {struct}), _Alignof(struct {struct}));\n'
for struct,field in [('wlr_scene_tree','children'),('wlr_scene','tree'),('wlr_scene','outputs'),('wlr_scene_node','parent'),('wlr_scene_buffer','buffer'),('wlr_scene_output','output')]:
source+=f'printf("offset:{struct}.{field} %zu\\n", offsetof(struct {struct},{field}));\n'
source+='return 0; }\n'; (lab/'struct-layout.c').write_text(source)
flags=shlex.split(run('layout-pkg-cflags',['pkg-config','--cflags','wlroots-0.20','wayland-server','pixman-1'])['stdout'])
layouts={}
for name,inc in [('system',Path('/usr/include/scenefx-0.5')),('pristine',lab/'scene-pristine/include'),('private',lab/'scene-private/include')]:
exe=lab/('layout-'+name)
build=run('layout-compile-'+name,['cc','-std=c17','-Wall','-Wextra','-Werror','-I'+str(inc),*flags,lab/'struct-layout.c','-o',exe,'-lwlroots-0.20'])
assert build['returncode']==0,build['stderr']
layouts[name]=run('layout-run-'+name,[exe])['stdout']
assert len(set(layouts.values()))==1
save('struct-abi-audit',{'layouts':layouts,'size_alignment_offsets':'IDENTICAL in 17 structs / 6 offsets',
'complete_ABI':'NOT ESTABLISHED; layout equality alone insufficient',
'private_storage':'destroy-listener-owned sidecar; no public field additions'})
wlheaders=[]
for src in sorted((lab/'wlroots-source/include/wlr').rglob('*.h')):
rel=src.relative_to(lab/'wlroots-source/include/wlr');installed=Path('/usr/include/wlroots-0.20/wlr')/rel
wlheaders.append({'path':str(rel),'installed':installed.exists(),'equal':installed.exists() and src.read_bytes()==installed.read_bytes(), 'source_sha256':sha(src),'installed_sha256':sha(installed) if installed.exists() else None})
required={label:sorted(set(info['undefined']) & wlrootsdefs) for label,info in elves.items() if label!='system-wlroots-elf'}
save('wlroots-compatibility',{'pkg_config':'wlroots-0.20','version':'0.20.2','source_pin':'d783533489e1f75d6886c2ab5c5960090ef268f8','headers':wlheaders,'required_symbols_provided_by_installed_wlroots':required,'exact_installed_binary_source_provenance':'NOT ESTABLISHED by version/headers alone','future_0_20_ABI':'NOT ESTABLISHED'})
# Compiler dependency files contain headers actually read, not inferred flags.
for build in ['stock-build','private-build','static-build','scene-private-build']:
if (lab/build).exists():run('compiler-headers-'+build,['ninja','-C',lab/build,'-t','deps'])
static=elves.get('static-binary-elf')
save('static-link-feasibility',{'status':'BUILD_AND_LOADER_PASS' if static else 'NOT TESTED',
'scene_DT_NEEDED_absent':static is not None and 'libscenefx-0.5.so' not in static['DT_NEEDED'],
'wlroots_still_dynamic':static is not None and 'libwlroots-0.20.so' in static['DT_NEEDED'],
'clip_dynamic_defined':static['clip_defined'] if static else [],
'clip_static_symbols':[line for line in run('static-clip-symbols',['nm','--format=posix',paths['static-binary-elf']])['stdout'].splitlines() if line.split()[0] in clips] if static else [],
'overlapping_dynamic_exports':sorted(set(static['defined']) & wlrootsdefs) if static else [],
'unique_file_possible':'ELF shape demonstrated; no second stage performed',
'licenses':'MIT notices must accompany substantial copied code; embedded notice strategy NOT TESTED',
'update_safety':'NOT ESTABLISHED; executable interposes shared wlroots scene symbols'})
save('private-shared-feasibility',{'status':'PROCESS_LOCAL_LOADER_PASS; distinct SONAME packaging NOT TESTED',
'same_SONAME':'unsafe naive substitution; default build RUNPATH is private LAB, not distributable',
'future_requirements':['distinct SONAME and DT_NEEDED','private installation path','controlled RUNPATH','audit all transitive scene consumers and interposition','security/version owner'],
'current_one_file_contract':'would require new decision for additional library artifact'})
print('PASS stock stage, ELF/loader comparisons, missing clip proof, C17 17-struct layouts and header audit')

View file

@ -0,0 +1,134 @@
#!/usr/bin/env python3
"""One-shot private source/build/stage reconstruction; never runs a compositor.
CONTRACT: empty external LAB, explicit local Git object stores at fixed commits.
INVARIANT: every command is logged; no install except stock DESTDIR staging.
WHY: a successful experimental link must never stand in for stock packaging.
"""
import argparse
import hashlib
import json
import os
from pathlib import Path
import shutil
import signal
import subprocess
import tarfile
p = argparse.ArgumentParser(description=__doc__)
p.add_argument('lab', type=Path)
p.add_argument('--scenefx-git', type=Path, required=True)
p.add_argument('--wlroots-git', type=Path, required=True)
p.add_argument('--pristine-binary', type=Path)
p.add_argument('--static', action='store_true')
a = p.parse_args()
lab = a.lab.resolve()
repo = Path(__file__).resolve().parents[3]
assert lab.is_dir() and lab.is_relative_to(Path.home()) and lab != Path.home()
assert not lab.is_relative_to(repo) and not (lab/'stock').exists()
assert not (lab/'prepare-started.json').exists(), 'budget consumed; inspect logs, never restart'
(lab/'prepare-started.json').write_text(json.dumps(vars(a), default=str, indent=2)+'\n')
raw = lab/'raw'; raw.mkdir(exist_ok=True)
results = lab/'results'; results.mkdir(exist_ok=True)
env = {k: v for k, v in os.environ.items() if k not in (
'SWAYSOCK', 'I3SOCK', 'WAYLAND_DISPLAY', 'DISPLAY', 'XDG_RUNTIME_DIR',
'LD_LIBRARY_PATH', 'LD_PRELOAD', 'PKG_CONFIG_PATH', 'OPT_TRACE')}
env['PYTHONDONTWRITEBYTECODE'] = '1'
commands = []
def run(args, cwd=None, extra=None):
i = len(commands)
entry = {'args': list(map(str, args)), 'cwd': str(cwd or repo), 'env_override': extra or {}}
commands.append(entry)
with (raw/f'build-{i:03}.log').open('wb') as out:
proc = subprocess.Popen(entry['args'], cwd=cwd or repo, env={**env, **(extra or {})},
stdout=out, stderr=subprocess.STDOUT, start_new_session=True)
try:
proc.wait(timeout=600)
except subprocess.TimeoutExpired:
# Own only this command's group; compiler children must not outlive
# a failed reproduction. No process-name or desktop-wide cleanup.
os.killpg(proc.pid, signal.SIGTERM)
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
os.killpg(proc.pid, signal.SIGKILL)
proc.wait()
try:
os.killpg(proc.pid, signal.SIGKILL)
except ProcessLookupError:
pass
entry['timed_out'] = True
entry['returncode'] = proc.returncode
(results/'build-commands.json').write_text(json.dumps(commands, indent=2)+'\n')
if proc.returncode:
raise RuntimeError(f'command {i} failed; inspect {raw}/build-{i:03}.log')
def archive(git, rev, name):
target = lab/name
target.mkdir()
arc = lab/(name+'.tar')
with arc.open('wb') as out:
subprocess.run(['git', '-C', str(git), 'archive', rev], stdout=out, check=True, env=env)
with tarfile.open(arc) as tf:
tf.extractall(target, filter='data')
return target
pins = {'SwayFX': 'fd71a6bdc061bd633b488ae7b83e8a6981d22f86',
'Mosaic': 'cd550382b311efdbfe3ed892f4e1d281d852e754',
'SceneFX': '37ccd723bef49e6891156ffafce8f549f01446cc',
'wlroots': 'd783533489e1f75d6886c2ab5c5960090ef268f8'}
stock = archive(repo, pins['SwayFX'], 'stock')
archive(a.scenefx_git, pins['SceneFX'], 'scene-pristine')
archive(a.scenefx_git, pins['SceneFX'], 'scene-private')
archive(a.wlroots_git, pins['wlroots'], 'wlroots-source')
archive(repo, pins['Mosaic'], 'mosaic-private')
patchdir = lab/'patches'; patchdir.mkdir()
patch = patchdir/'production.patch'; shutil.copyfile(repo/'patches/0001-mosaic-layout-viewport.patch', patch)
run(['git', 'apply', '--check', patch], stock); run(['git', 'apply', patch], stock)
for rel in ('include', 'sway', 'meson.build', 'meson_options.txt', 'tests/mosaic-model.c'):
run(['git', 'diff', '--no-index', '--exit-code', stock/rel, repo/rel])
chains = {'scene-private': ['step20c-valid-grab/scenefx-popup-plus-base.patch',
'step20c-temporal-damage/scenefx-timing.patch',
'step20c-optimized-blur-cache/scenefx-cache-trace.patch'],
'mosaic-private': ['step20c-valid-grab/mosaic-popup-plus-base.patch',
'step20c-valid-grab/hidden-guard.patch',
'step20c-temporal-damage/mosaic-output-timing.patch',
'step20c-presentation-identity/presentation-events.patch',
'step20c-optimized-blur-cache/sway-source-trace.patch']}
for tree, chain in chains.items():
for rel in chain:
dst = patchdir/Path(rel).name; shutil.copyfile(repo/'tooling/probes'/rel, dst)
run(['git', 'apply', '--check', dst], lab/tree); run(['git', 'apply', dst], lab/tree)
flags = ['-Dwerror=false', '-Dsd-bus-provider=libsystemd', '-Dmosaic-package=true',
'-Dswaybar=false', '-Dswaynag=false', '-Dman-pages=disabled', '-Ddefault-wallpaper=false']
run(['meson', 'setup', lab/'stock-build', stock, '--prefix=/usr', '--bindir=bin', *flags])
run(['meson', 'compile', '-C', lab/'stock-build', '-j', '8'])
run(['meson', 'test', '-C', lab/'stock-build', 'mosaic-model', '--print-errorlogs'])
run(['meson', 'install', '-C', lab/'stock-build', '--destdir', lab/'stage-current', '--no-rebuild'])
for name in ('pristine', 'private'):
run(['meson', 'setup', lab/('scene-'+name+'-build'), lab/('scene-'+name), '-Dexamples=false', '-Dwerror=false'])
run(['meson', 'compile', '-C', lab/('scene-'+name+'-build'), '-j', '8'])
private_env = {'PKG_CONFIG_PATH': str(lab/'scene-private-build/meson-uninstalled')}
run(['meson', 'setup', lab/'private-build', lab/'mosaic-private', *flags], extra=private_env)
run(['meson', 'compile', '-C', lab/'private-build', '-j', '8'], extra=private_env)
run(['meson', 'test', '-C', lab/'private-build', 'mosaic-model', '--print-errorlogs'])
if a.static:
# Source inspection establishes a native Meson library() target honoring this
# option. Only SceneFX is embedded; wlroots and other dependencies stay shared.
run(['meson', 'setup', lab/'scene-static-build', lab/'scene-private',
'-Ddefault_library=static', '-Dexamples=false', '-Dwerror=false'])
run(['meson', 'compile', '-C', lab/'scene-static-build', '-j', '8'])
static_env = {'PKG_CONFIG_PATH': str(lab/'scene-static-build/meson-uninstalled')}
run(['meson', 'setup', lab/'static-build', lab/'mosaic-private', *flags], extra=static_env)
run(['meson', 'compile', '-C', lab/'static-build', '-j', '8'], extra=static_env)
run(['meson', 'test', '-C', lab/'static-build', 'mosaic-model', '--print-errorlogs'])
if a.pristine_binary:
# Retained pristine binary is tied to the sealed A campaign identity.
assert hashlib.sha256(a.pristine_binary.read_bytes()).hexdigest() == '8ac865481bc0487344210cd0e16d8e6c7655b29db887cd58ddc11b0ca22fe494'
# Copy/hash only, never launch it.
shutil.copyfile(a.pristine_binary, lab/'retained-pristine-sway')
pins['pristine_binary_origin'] = str(a.pristine_binary.resolve())
pins['pristine_binary_sha256'] = hashlib.sha256(a.pristine_binary.read_bytes()).hexdigest()
(results/'source-pins.json').write_text(json.dumps(pins, indent=2)+'\n')
print('PASS private builds/model tests and single stock staging; zero compositor launches')

View file

@ -0,0 +1,36 @@
#!/usr/bin/env python3
"""Seal completed source/evidence inventories once, or verify existing bytes.
WHY: derived reports cannot silently overwrite the evidence they cite.
CONTRACT: fixtures finalized first; report/diff are captured outside inventory.
INVARIANT: no manifest replacement, no binaries/build trees in source inventory.
"""
import argparse
import hashlib
from pathlib import Path
p=argparse.ArgumentParser(description=__doc__);p.add_argument('lab',type=Path)
p.add_argument('--verify',action='store_true');a=p.parse_args();lab=a.lab.resolve()
fixture=Path(__file__).resolve().parent;repo=fixture.parents[2]
assert lab.is_relative_to(Path.home()) and lab!=Path.home() and not lab.is_relative_to(repo)
r=lab/'results'
def sha(path):return hashlib.sha256(path.read_bytes()).hexdigest()
if not a.verify:
assert not any((r/(name+'-sha256.txt')).exists() for name in ['source','evidence'])
source=set(p for p in fixture.iterdir() if p.is_file())
for tree in ['stock','scene-pristine','scene-private','wlroots-source','mosaic-private']:
source.update(p for p in (lab/tree).rglob('*') if p.is_file() and 'subprojects' not in p.relative_to(lab/tree).parts)
source.update(p for p in (lab/'patches').iterdir() if p.is_file())
source.update(p for p in lab.iterdir() if p.is_file() and p.suffix in ['.py','.c','.md'])
evidence=set(p for p in (lab/'raw').iterdir() if p.is_file())
evidence.update(p for p in r.iterdir() if p.is_file() and not p.name.endswith('-sha256.txt'))
for name,files in [('source',source),('evidence',evidence)]:
manifest=r/(name+'-sha256.txt')
# Exclusive creation, never truncate a prior result on interrupted runs.
with manifest.open('x') as out:
for path in sorted(files):out.write(f'{sha(path)} {path}\n')
for name in ['source','evidence']:
path=r/(name+'-sha256.txt');lines=path.read_text().splitlines()
for line in lines:
expected,filename=line.split(' ',1);assert sha(Path(filename))==expected,filename
print(name,len(lines),sha(path),'PASS')

View file

@ -0,0 +1,77 @@
#!/usr/bin/env python3
"""Summarize measured evidence without turning a candidate into an approval.
CONTRACT: successful S0 and expected incompatible naive S1 are distinct.
INVARIANT: no ABI inference from SONAME, compile success, or struct size alone.
"""
import argparse
import json
from pathlib import Path
p=argparse.ArgumentParser(description=__doc__);p.add_argument('lab',type=Path)
a=p.parse_args();lab=a.lab.resolve();r=lab/'results'
assert not (r/'evidence-sha256.txt').exists(), 'already sealed'
def read(name):return json.loads((r/(name+'.json')).read_text())
def save(name,value):(r/(name+'.json')).write_text(json.dumps(value,indent=2)+'\n')
assert read('stock-stage-manifest')['status']=='CURRENT_PACKAGE_STAGE_PASS'
assert read('unresolved-symbols')['classification']=='INCOMPATIBLE_WITH_STOCK_SCENEFX'
options={
'A_upstream':{'technical':'UNKNOWN acceptance; API source prototype demonstrated',
'system_library_intact':'normal maintained upstream upgrade required; no private overwrite',
'sway_intact':True,'coexistence':'conditional on supported upstream API/version',
'stable_API':'NOT AVAILABLE in pinned stock','ABI':'NOT ESTABLISHED for future upstream release',
'one_file':'possible after adoption, NOT TESTED','upgrade_risk':'medium; shared dependency transition',
'SceneFX_maintenance':'upstream if accepted; no commitment obtained','wlroots':'version-paired qualification',
'reproducibility':'private prototype only','human_decision':True,
'remaining':['accepted ownership/error/bounds contract','upstream maintainer agreement','public ABI/version policy','graphical feasibility gates']},
'B_static':{'technical':'shared-source static build and loader PASS',
'system_library_intact':True,'sway_intact':True,'coexistence':'conditional; no filesystem overlap',
'stable_API':'private','ABI':'17-struct layout and current relocations PASS; future safety NOT ESTABLISHED',
'one_file':'binary dependency shape demonstrated, static stage NOT TESTED',
'upgrade_risk':'high until explicit wlroots interposition/update policy',
'SceneFX_maintenance':'Mosaic must own all embedded security fixes; no owner committed',
'wlroots':'still shared libwlroots-0.20.so; overlapping exports must be qualified',
'reproducibility':'fixed sources/patches/commands retained; bit reproducibility NOT TESTED',
'human_decision':True,'remaining':['symbol binding contract','wlroots version matrix','MIT notices in one-file distribution','security/rebase owner','static package contract acceptance','graphical tests separately authorized']},
'C_private_shared':{'technical':'same-SONAME private process resolution PASS; distinct-SONAME candidate NOT TESTED',
'system_library_intact':True,'sway_intact':True,'coexistence':'conditional on separate file/SONAME and complete binding audit',
'stable_API':'private','ABI':'future isolated distribution NOT ESTABLISHED',
'one_file':False,'upgrade_risk':'medium/high; shared wlroots still imposes constraints',
'SceneFX_maintenance':'Mosaic-private library owner required','wlroots':'shared consumers/interposition remain',
'reproducibility':'laboratory lookup only','human_decision':True,
'remaining':['new owned library file and packaging decision','distinct SONAME/DT_NEEDED','private RUNPATH/transitive lookup','no stock/private duplication within process','security owner']},
'D_no_new_API':{'technical':'UNKNOWN; no implementation authorized',
'system_library_intact':True,'sway_intact':True,'coexistence':'conditional on valid compositor architecture',
'stable_API':'existing unstable scene API only','ABI':'new dependency avoided, architecture unproved',
'one_file':'possible, NOT TESTED','upgrade_risk':'unknown, broad renderer maintenance possible',
'SceneFX_maintenance':'normal upstream for stock; Mosaic owns alternate mechanism',
'wlroots':'same version-paired graphical contracts','reproducibility':'NOT TESTED','human_decision':True,
'remaining':['popup/global stacking','hit traversal','damage/visibility','effects and saved buffers','rotation/scaling/migration','transaction state consistency']}}
save('architectural-options',options)
save('coexistence-matrix',{'S0':'no filesystem conflict; stock dynamic dependency',
'S1_naive_stock':'missing two symbols; cannot be distributed as stock-compatible',
'S2_static':'no SceneFX file, 85 overlapping exports with shared wlroots',
'S2_shared':'extra private library file requires new package ownership contract',
'standard_sway':'no modification in any experiment'})
save('upgrade-rollback-risks',{'current_upgrade':'no conflicts/provides/replaces prevents package-manager block; shared SONAME/API changes can still break Mosaic and require separate rebuild',
'static_upgrade':'SceneFX system upgrade does not update embedded code; shared wlroots remains an ABI constraint',
'private_shared_upgrade':'private file protects library ownership; shared wlroots plus symbol semantics still require qualification',
'rollback':'hypothetical remove only /usr/bin/swayfx-mosaic in S0; stock sway/libraries untouched; actual removal NOT TESTED',
'forbidden':'pinning shared system dependencies to block stock SwayFX updates'})
save('decision-gates',{'public_API':'new decision before implementation',
'package_contract':'unchanged; static bundling or extra private library requires explicit decision',
'GPU_lifetime_and_cache':'NOT TESTED here; separate from linker evidence',
'recommended_next':'bounded non-graphical design of symbol/ownership/update and license contracts for static candidate; prepare upstream API dossier without submission',
'integration_authorized':False})
save('matrix',{'S0_build':'PASS','S0_model':'PASS','S0_stage':'CURRENT_PACKAGE_STAGE_PASS',
'S1_private_loader':'PASS relocations only','S1_stock_loader':'INCOMPATIBLE_WITH_STOCK_SCENEFX',
'S2_static_build':'PASS','S2_static_loader':'PASS relocations only','distinct_SONAME_build':'NOT TESTED',
'graphics':'NOT RUN / FORBIDDEN','system_install':'NOT RUN / FORBIDDEN','bit_reproducibility':'NOT TESTED'})
save('summary',{'verdict':'PARTIALLY_FEASIBLE','architecture':'SCENEFX_ABI_PACKAGE_REQUIRES_ARCHITECTURAL_DECISION',
'current_package':'CURRENT_PACKAGE_STAGE_PASS','experimental_stock':'INCOMPATIBLE_WITH_STOCK_SCENEFX',
'candidate':'static embedding demonstrated technically; unresolved maintenance, symbol and license gates',
'long_term_direction':'upstream maintained API preferred conditionally, no acceptance presumed',
'budget':{'stock_rebuild':1,'stock_stage':1,'private_shared_chain':1,'optional_static_chain':1,
'pristine_SceneFX_reference_build':1,'compositor_launches':0,'graphical_runs':0,'installs':0},
'phase_B_commit_push':False})
print(json.dumps(read('summary'),indent=2))