Card-composition architecture contract
Decision
The selected route is a source-built, opt-in patch to pinned Sway 1.12.
It keeps Sway as the sole DRM/KMS owner and continues to use the current Pixman
renderer. The normal shell service and its unpatched Sway package remain the
default and recovery path. This decision follows the source audit in
card-composition-source-audit.md: the
existing launcher client has no access to another client’s live scene surface
or to touch before Sway dispatches it, while pinned Sway owns views, scene
trees, touch seat operations, focus, and close dispatch.
This is a selected experiment boundary, not card-feature delivery. The
route now has board-injected capability proof in
card-composition-board/README.md:
live parent/subsurface content, drag, expand, close/refusal, keyboard focus and
normal-shell restoration. Real-finger and optical proof remain open; product
card UX and cost acceptance are separate requirements.
Route comparison
| Boundary | Existing protocol client | Opt-in Sway patch |
|---|---|---|
| Live app surface | Sway exposes foreign-toplevel and image-copy/capture protocols, but capture creates copied images from a separate capture scene rather than sharing a live scene subtree. | sway_view owns scene_tree/content_tree; patch stays at that owner. |
| Touch before app dispatch | No: a layer-shell client receives events after surface selection. | Yes: Sway seatop_impl receives down/motion/up/cancel before dispatch. |
| Focus and keyboard return | Client may ask Sway over IPC, but cannot authoritatively restore seat state. | Sway uses seat_set_focus_container; patch saves and restores its prior focus. |
| Dismissal | No ownership of another client close request. | Existing view_close is the request boundary; actual exit/refusal remains observable. |
| DRM/renderer | Does not provide composition ownership. | Existing sole owner, Pixman, and output pipeline remain in Sway. |
Contract for the selected probe and consuming product change
The patch adds no external compositor and opens no DRM node. It works only
inside an explicitly started opt-in Sway session. A root-coordinated board
session stops the normal shell service first, starts that package as the one
DRM owner, and always restores the known normal service. This planned stop and
start must not be represented as unchanged process invocation IDs.
Eligible surfaces and identity
The capability probe allowlists exactly two launched XDG toplevel test apps.
For each it records a card ID owned by the Sway patch and the paired
sway_container/sway_view; it never gives a client process a wlr_surface,
buffer, DRM FD, or capture of a non-allowlisted app. The card uses the existing
view scene subtree. The probe logs map, unmap, destroy, output-leave, and any
format/stride observed by Sway.
Lifecycle and surface ownership
normal -> deck-entry -> attached -> dragging -> selected -> expanded is the
selection path. dragging -> dismissal-requested -> refused -> restored and
dragging -> dismissal-requested -> app-exit -> destroyed are distinct paths.
An unmap, destroy, output leave, unsupported format, allocation failure, or
session abort cancels the gesture, removes/disables the patch-owned card node,
returns focus to a still-live container or workspace, and returns to normal
Sway. A close request is not success: it becomes refused unless the observed
app exit/destroy completes it.
The probe may position, clip, and order Sway’s existing scene nodes. wlroots
has only per-buffer wlr_scene_buffer_set_dest_size, not a whole-XDG-subtree
scale. It must apply the same scale and translated coordinates to every live
content/subsurface buffer on every commit and show that full app content changes
while shrunken, rather than a crop or saved static clone. If it cannot, this
selected route is blocked for product work. It may not copy a client buffer into
a client-owned thumbnail, promise DMA-BUF/zero-copy, or retain a buffer after
Sway’s map/unmap/destroy boundary. It must log the observed Pixman format and
stride instead of assuming RGB565 texture support.
Touch, focus, and keyboard
While a single accepted finger is in dragging, the Sway seat operation owns
continuous down/motion/up/cancel routing and updates the card position on each
motion. A second contact, session lock, or loss of the selected surface cancels
the card gesture and restores ordinary Sway routing. During deck interaction,
keyboard focus stays with the saved live container; after expand, refusal,
exit, or abort, the patch calls Sway’s focus path to restore that saved
container or a valid workspace. The probe must show there is no focus orphan.
Completion and measurements
For each run, evidence separates scene damage/commit, client frame-done, and presentation feedback. A frame callback is not panel presentation. It records renderer, observed format/stride, buffer-lifetime events, process CPU, RSS, and timestamps for the two-app drag. No GPU claim follows from this route.
Required proof before product work
The selected package must provide card-composition-probe, remain outside the
default image, and pass host lifecycle fixtures. The board coordinator then
needs an opt-in session proving two live app surfaces, shrink, continuous drag,
adjacent expand, dismissal request plus refusal or exit, keyboard restoration,
and restored normal Pixman shell. It must additionally show a content update
while shrunken, proving live per-buffer scaling. Failure records the failed
state and keeps this proposal open; it cannot be relabelled as metadata cards
or as completed visual-card UX.
Implemented probe boundary (2026-09-23)
The opt-in patch mirrors each enabled content/subsurface buffer with
wlr_scene_surface_create, rather than a raw saved buffer. Pinned wlroots
types/scene/surface.c owns independent commit handling, client-buffer locks,
acquire/release synchronization, output sampling and presentation feedback.
Sway’s existing frame iterator recognizes these mirror scene surfaces, so
hidden originals do not starve the clients. The mirror geometry pass copies
source cropping, transform, opacity and color representation, applies one scale
and coordinate translation to every descendant, and conservatively clears the
opaque region. A before-render pass prevents an unscaled commit from being
presented. A 16ms discovery timer finds new subsurfaces whose disabled original
scene might not schedule a frame. That timer and conservative damage can cost
CPU; neither is a performance claim.
Only exact SWAY_K230_CARD_COMPOSITION_PROBE=1 enables registration, and entry
also verifies the renderer is Pixman. Only app IDs k230.card.one and
k230.card.two are eligible; duplicate IDs, extra outputs and another workspace
are outside this bounded experiment. Supported probe buffers are SHM XRGB8888,
ARGB8888 and RGB565; other buffers abort. Both complete mirrors must exist before
either original is hidden. Source-node/surface destruction removes listeners
and releases mirror references; unmap, seat/output loss, lock and allocation
failure return through the same cleanup path. The original Sway scene restores
its previous enabled state and focus returns through the normal Sway seat API.
After both apps map on the active workspace, touch the bottom 48 logical pixels
to enter. The two live cards occupy separate vertical slots. Tap selects and
expands; upward motion beyond 120 logical pixels requests close. A still-mapped
client after 1500ms is classified close-refused (a timeout observation, not a
protocol-level refusal reply). An observed unmap after the request is logged
app-exit; client process exit must additionally be checked in client/session
logs. A second contact aborts and consumes remaining contacts through release.
k230_card_probe enter|down ID X Y|motion ID X Y|up ID|cancel is an explicit,
opt-in IPC test interface using the same handlers and logging input=injected.
fail-mirror N injects allocation failure after N new mirror allocations to
exercise partial-setup rollback. Neither interface is evidence of physical
finger interaction. The normal shell package contains none of these changes.