18 KiB
TASK 43 — viewtop: the Souveraine compositor
Status: it is the session compositor, 2026-08-02. greetd launches it,
Hyprland is not running, the shell's whole surface tree composites on it, and
Firefox ran well enough to drive a Claude session from. Size: several
sessions, one lane each. Repo: ~/Projects/souveraine-viewtop
(10.10.20.120:4455/Fimeg/souveraine-viewtop), head bceb0ac = origin/main
2026-08-02.
Read docs/SESSION-2026-08-02.md in that repo first. It is the record of
the day this became real: five bugs only hardware could find, two upstream
smithay patches, and the exact things still missing. This file is the task; that
file is why the task looks the way it does now.
Goal
Replace Hyprland + quickshell with our own compositor, so the surfaces whose guarantees are currently documented-but-unenforced become structural, and so the agent composes over the scene instead of asking a window manager for permission (doctrine §13).
What exists after 2026-08-02
7918 lines, 150 tests, CI green on all six gates — including the two build
steps that had been lying: the workspace cross-build skipped the binary, and
the KMS half compiled for no target at all until scripts/build-cross.sh.
sessiond-client— the twelve-verb table,describe-driven. Plusregister_shell(), which holds the heartbeat whose EOF is shell death, and reads{"directive":"lock"}back down it.Heartbeatis#[must_use]: dropping it locks the phone.compositor/lock.rs—ext-session-lock-v1served, sessiond still deciding. Models the real two-client handoff (below).compositor/gate.rs— one predicate for input routing, disclosure, and composition. Replaces denial'ssecure_session_locked().compositor/layers.rs— furniture stacking as a derived total order.pill_outranks_dockis a test, not a comment in another repo's Lua.compositor/navigation.rs— TASK-37's back verb, both tiers, predictive drag.compositor/registry.rs— surfaces, and what the shell is shown of them.compositor/input.rs— touch, and who is touching.Origincarries provenance from the event source so a finger and an agent tap reach the seat identically while only the finger is evidence. See below.compositor/evidence.rs— ships that evidence to sessiond off the input thread, bounded, dropping rather than stalling a finger.compositor/wayland.rs— seat now has touch; one generichandle_input<B: InputBackend>serves libinput and winit both.wire/verbs.rs— the intent table, with examples enforced to parse.docs/SHELL-BOUNDARY.md,docs/LOCK-AUTHORITY.md,docs/TOUCH-AND-EVIDENCE.md— the three settled forks.
Touch, settled 2026-08-02
Full argument in souveraine-viewtop/docs/TOUCH-AND-EVIDENCE.md. The short of
it: §4 makes touch a sensor (if touch.active: confidence += 0.1), and
sessiond keeps two verbs for it — input ("real user input happened") and
sensor_input ("evidence; never an authority") — which both claim a person is
at the glass.
§13 makes tapping the screen Annie's verb, so her touch takes the identical
path to the seat and no client can tell the difference. It reports neither
sessiond verb. Otherwise observed_confidence climbs in an empty room and the
idle budget never expires: the agent lying to her own proprioception, through
the machinery built to give her one.
This is a reason viewtop exists. The uinput bridge under Hyprland launders her intent into a physical touch inside the kernel; nothing above it can undo that. Provenance has to start at the source.
Before this, smithay/backend_libinput was enabled in the kms feature and
had never been used, and the seat had no touch capability at all — on a device
whose only input is the panel.
The three things that will bite a fresh session
- The lock handoff is the normal path. sessiond's glance and the shell's
lockscreen pass the lock without it ever unlocking
(
SessionOutcome::Released; Hyprland needsmisc:allow_session_lock_restore). The compositor cannot tell a release from a crash and must not try. TASK-03: one lock surface, ever. - Do not build a fallback presenter. sessiond already is the availability
layer — heartbeat EOF, retake, its own
draw.rssurface (TASK-02). A second one breaks the criterion above. This was designed wrong once already. - The engine runs in-process and that is settled on the embedder contract
(
fbo_with_frame_info_callback+ buffer age + vsync), not on preference.wireis a vocabulary over a platform channel, not IPC.
What to build, in order — refreshed 2026-08-02
Steps 1 and 2 below are done: the Smithay frontend serves 23 globals, the gate
is wired at input and disclosure, and ext-session-lock-v1 has carried the real
two-client handoff on hardware including adoption. Step 3's DRM/KMS half is
done and running; the arm64 engine is not. Step 4 is untouched.
The live list is in souveraine-viewtop/docs/SESSION-2026-08-02.md §"What is
missing" — read its second-pass section, which is what changed after bb09206.
Done since: scaling (scale 2, 540x1080 logical, derived from the panel's own
62x124 mm), the layer arrange() that never ran on commit (centred sidebars),
toplevels sized to the non-exclusive zone rather than the whole panel, hardware
buttons reaching sessiond's button verb, keyboard reaching the seat, panel
DPMS with the EBUSY deferral (power.rs), a control socket (control.rs), and
zwlr_foreign_toplevel_management_v1 so the dock and overview can finally
enumerate windows.
Shortest form of what is left, by what blocks daily use:
- Windows pile on each other. Every toplevel is configured to the same rectangle with nothing to tell them apart. "Multiple apps at once" needs placement and a focus border, not more protocol — the enumeration half is done now.
- Screen capture is still unmerged, and it is worth doing next because
without it nothing visual can be verified: no screenshots means every UI
change ships unverified. It is not lost — it is a whole viewtop tree on
archdev at
~/vt-capture(ext-image-copy-capture-v1+ext-output-image-capture-source-manager-v1,ImageCopyCaptureHandleratwayland.rs:1816, helperscapture_peer/copy_output_into/render_capturearound676-850).gate.rs'sAct::Capturehalf is already merged into the current tree, so the port is the handler and its helpers only. It renders the scene a second time off-screen rather than reading the scanout buffer, and the reason is worth keeping: handing a client a mapping of the swapchain also hands it whatever the next frame reuses. furniture_atstill returnsNone, soRoute::Shellis unreachable.- Nothing dispatches
ToCompositor— butcontrol.rsis now the socket it lands on, which was the missing half.
The original plan, for context
Smithay frontend.Done.wl_compositor, xdg-shell, layer-shell, seat with keyboard/pointer/touch, primary selection + wlr data-control.gate.rsis wired at input; clipboard, capture and Xwayland placements are still owed (denial's map:input.rs:599,768,1045,1704,clipboard.rs:526,handlers.rs:933,978,1010).Done and proven end to end, including the release-without-unlock handoff and adoption of an abandoned lock.ext-session-lock-v1server.- DRM/KMS on device. ← the live lane. The backend exists, takes the panel, has a render pass, and cross-builds to a real aarch64 binary. It has never been run on the phone.
- External texture → GBM atlas, then the Dart shell.
shell-hostis thirteen lines; this is the long pole and the reason nothing is painted.
Resuming: KMS on device
Everything below is verified as of 458a354 unless it says otherwise.
Build. scripts/build-cross.sh on archdev produces
target/aarch64-unknown-linux-gnu/debug/viewtop with the kms feature and
libinput linked against the sysroot. scripts/run-nested-phone.sh is the
nested pipeline and assumes it is driven from the laptop — it relays
archdev → laptop → phone because archdev holds no phone key.
What KMS mode will actually show: whatever Wayland clients connect, on black. There is no shell, so no pill, no dock, no wallpaper. A run with no client is a correctly-working black screen. Plan a client into the test or the result is unreadable.
The cheap proof that needs no pixels. On the phone right now:
sensor_health.touch : unknown
evidence_fresh.touch : false
Nothing has ever reported touch on this device. A finger on a viewtop surface
flips those to live / true, and sessiond device_state is the whole
assertion — it validates libinput → router → reporter → sessiond with no
client, no shell and nothing drawn.
No touch-capable client is installed. kitty is touch-blind (zero
wl_touch symbols in its vendored GLFW — this is why kitty-touch-bridge
exists). Install extra/weston for weston-simple-touch (a dot per slot:
proves multi-contact and the agent slot namespace in one picture) and
extra/wev (raw wl_touch events: proves the numbering).
Unverified: whether winit forwards touch through Hyprland in nested mode. If it does not, that is nested-only — KMS reads libinput directly.
Risk. KMS takes DRM master on the daily driver. Kms::new refuses to start
while another compositor holds the seat rather than racing it, which is the
right failure, but a successful start means Hyprland is not on the panel. Have
a TTY or serial console reachable first.
Scene composition & the selection surface — settled 2026-07-30
Doctrine §13 set the goal: client windows become textures the agent addresses,
not rectangles a window manager owns. This settles what that costs in wire,
and where the wall sits. Built for Annie and the substrate before the user — so
the verbs optimize for her control and fit the table grammar (describe-driven,
refusal-typed, test-enforced), not for a human picking from a menu.
The wall: texture-level vs content-level
A surface arrives as a TextureId — a GPU buffer the shell paints. Annie can
transform the whole of it (it is hers to compose); she cannot reach inside
it. Selection rectangles, caret positions, handle geometry all live in opaque
client pixels, and no Wayland protocol exposes them to the compositor. viewtop
keeps this wall on purpose: it is the contract that lets Firefox, GTK, Qt run
unmodified.
- Whole-texture ops reach every app, including Firefox:
pose,place,expose. This is the ambient merge. - Content-level reach does not, for opaque clients. Selection-handle
geometry inside a third-party surface is unknowable. True rect-anchoring is
available only for first-party surfaces, where we own both ends and can
carry geometry through
wire.
Two new verbs, split by who owns them
The 60/40 decides where each property lives, and therefore which table it is in.
pose — geometry is Annie's (the 60). Rotation, scale, anchor: how the
texture is painted. One atomic verb, struct-extensible (skew, pivot,
parallax later, without new verbs or new tests). The compositor reads it for
inverse input-mapping — the real cost of "spin and twirl" is not the paint
but mapping a touch on a rotated surface back into client coordinates. Refused
only when remap is impossible (mid predictive-back drag, exempt surfaces).
{"intent":"pose","id":1,"rotation":30.0,"scale":0.9,"anchor":{"x":0.5,"y":0.5}}
expose — legibility straddles the line. Annie requests a level; the gate
may clamp it down (veil) per the surface's Sensitivity and device state
(Observed, Locked). She never unilaterally de-obscures a Credential
surface — that is a step-up, the 40. Levels climb obscured → dimmed → ambient → clear; policy is a floor the aesthetic composes on top of in the render.
{"intent":"expose","id":1,"level":"ambient"}
place stays as-is (translate + size, input geometry). place + pose are the
geometry family; the compositor composes both for hit-testing. The ambient merge
is a chain — {pose, expose} applied atomically by the shell's render —
which is the unit Annie already plans in, so atomicity is expressed in the
existing grammar, not invented.
The selection surface is a scene element, not a layer surface
On Hyprland the selection chip is a WlrLayer.Overlay client fighting z-order —
the entire tap / ExclusionMode / keyboardFocus bug class (TASK-17,
TASK-18's broken state) is a layer-shell artifact. On viewtop the selection
surface is another node in the one Flutter scene Annie composes. No layer shell,
no z-order war. The bug dissolves by construction.
Selection source: primary-selection + data-control — wired 2026-07-30
The shell needs "what is selected." viewtop now serves zwp_primary_selection_v1
and zwlr_data_control_manager_v1 (the latter created against the
primary-selection state, so a data-control client observes primary selections
without focus) — the combination wl-paste --primary --watch relies on
(234a631). Geometry stays unavailable for opaque clients (the wall), so
third-party anchoring remains pointer-hint-based, while first-party surfaces can
carry true rects.
Owed at this boundary: ext_data_control (the standardized successor; its
same-named DataControlState collides with wlr's, so it is its own pass), and a
disclosure gate that withholds selection content while locked and narrows
observation to the shell/agent client. data_device_state is wired the same
plain way today, so the gate is owed across the whole frontend, not just here.
What is portable from the Hyprland chip, what is terminal
The selection backends are surface-agnostic and worth finishing now:
Speech.speak for Read Aloud, the Ai.qml side-shoot / primary-conversation
seams (TASK-18 §2), the reference lookups. The Hyprland surface layer (the
WlrLayer.Overlay chip, its anchoring, its z-order) is terminal — do not polish
it. See TASK-18.
Owed, named rather than hidden
-
StepUp::from_authorityhas no caller: sessiond has no step-up verb, soReveal(Credential)is refused outright. Wire it when the verb lands. -
may_take_lockis a uid check. TASK-41's attestation lands there and nowhere else; it is named partial in the code as that task asks. -
LockEffect::Reportproduces trail entries nothing writes yet — §11'sforensic.jsonlis where they go. -
shell-hostclassifies (Chrome) but hosts no engine. Thirteen lines. -
Viewtop::furniture_at()returnsNoneunconditionally, soRoute::Shellis unreachable. The engine owns shell geometry and publishes a region layout; inventing rectangles before it does would be a second, drifting idea of where the pill is. Wire it with the region layout, not before. -
Nothing dispatchesLanded 2026-08-02 (ToCompositor.276bb3e).control.rscarrieswire::ToCompositorverbatim under asceneverb, and a test walksverbs::INTENTSasserting every advertised intent parses through the socket — an intent the table advertises and the door cannot take is a verb she cannot reach.Real:
focus,close,place,pose,raise,lower,set_visible,expose. Pose and exposure are not settings nothing draws — the render applies scale about the pivot (a shrink happens in place instead of collapsing toward the top-left) and exposure as alpha, which the render path was already passing1.0into.The authority split lives in the dispatch rather than in a comment: geometry is honoured with no clamping (a window half off the screen is composing, not erring); exposure is clamped down by the gate and the reply carries what was granted, so a caller cannot paint against a level it did not get;
RequestLock/LockPresentedare refused explicitly so the refusal reads as a decision rather than an oversight.Still refused with
unavailable, and saying what is missing: navigation (policy built innavigation.rs, no bridge) and synthesized touch —input::Origin::Agentand the disjoint slot namespace are built and nothing feeds them, so she still reaches touch only through uinput, which is the thinginput.rsexists to replace. That is the highest-value remaining bridge. -
One refusal arm in
input::Router::resolveis unreachable, pinned bythe_lock_claims_a_furniture_hit_before_the_reveal_check_can. It goes live the moment any furniture isCredentialor a step-up reveals chrome on a locked glass. Same discipline asStepUp::from_authority.
Do not
- Port
authentication.rs. PAM is sessiond's. - Let
lockedbecome a bool the compositor owns. - Add furniture without placing it in
layers::ALL— the test will catch it, which is the point. - Build on the phone or the laptop. archdev,
-i ~/.ssh/ani, absolute paths. Note thevanguardentry in~/.ssh/configon the phone has the wrong user (vanguard@); archdev answers ascasey@10.10.20.123.ssh vanguardfails with publickey and looks like a key problem.
Connects to
VIEWTOP-AND-DENIAL.md (what is liftable), TASK-02/03 (the lock surfaces and
the boot timeline it must not regress), TASK-30 (the intent table is one of the
surfaces that owed one), TASK-31/37 (the dial and back share the table),
TASK-18 (the selection surface is a scene element here, not a Hyprland layer
surface; its backends are portable), TASK-17 (the OSK z-order precedent the
selection surface inherits), TASK-41 (attestation), TASK-25/27 (viewtop is not
packaged), doctrine §4/§13.