Watch
1
0
Fork
You've already forked SouveraineOS
0
SouveraineOS/docs/tasks/43-viewtop-compositor.md
2026-08-13 17:46:49 -04:00

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).

Terminology — 2026-08-13

Membrane is this component's architectural name: the device-local touch and display boundary at the glass. It composes the scene, routes physical input, and applies sessiond's disclosure facts. The Sensorium remains the much larger field of Souveraine's sensing and action across tools and surfaces; sessiond still decides session and lock truth.

The repository, package, binary, socket, and wire names remain viewtop until the compatibility migration is deliberately staged. Do not turn this note into a partial search-and-replace.

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. Plus register_shell(), which holds the heartbeat whose EOF is shell death, and reads {"directive":"lock"} back down it. Heartbeat is #[must_use]: dropping it locks the phone.
  • compositor/lock.rsext-session-lock-v1 served, sessiond still deciding. Models the real two-client handoff (below).
  • compositor/gate.rs — one predicate for input routing, disclosure, and composition. Replaces denial's secure_session_locked().
  • compositor/layers.rs — furniture stacking as a derived total order. pill_outranks_dock is 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. Origin carries 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 generic handle_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

  1. 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 needs misc:allow_session_lock_restore). The compositor cannot tell a release from a crash and must not try. TASK-03: one lock surface, ever.
  2. Do not build a fallback presenter. sessiond already is the availability layer — heartbeat EOF, retake, its own draw.rs surface (TASK-02). A second one breaks the criterion above. This was designed wrong once already.
  3. The engine runs in-process and that is settled on the embedder contract (fbo_with_frame_info_callback + buffer age + vsync), not on preference. wire is 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:

  1. 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.
  2. 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, ImageCopyCaptureHandler at wayland.rs:1816, helpers capture_peer/copy_output_into/render_capture around 676-850). gate.rs's Act::Capture half 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.
  3. furniture_at still returns None, so Route::Shell is unreachable.
  4. Nothing dispatches ToCompositor — but control.rs is now the socket it lands on, which was the missing half.

The original plan, for context

  1. Smithay frontend. Done. wl_compositor, xdg-shell, layer-shell, seat with keyboard/pointer/touch, primary selection + wlr data-control. gate.rs is 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).
  2. ext-session-lock-v1 server. Done and proven end to end, including the release-without-unlock handoff and adoption of an abandoned lock.
  3. 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.
  4. External texture → GBM atlas, then the Dart shell. shell-host is 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_authority has no caller: sessiond has no step-up verb, so Reveal(Credential) is refused outright. Wire it when the verb lands.

  • may_take_lock is a uid check. TASK-41's attestation lands there and nowhere else; it is named partial in the code as that task asks.

  • LockEffect::Report produces trail entries nothing writes yet — §11's forensic.jsonl is where they go.

  • shell-host classifies (Chrome) but hosts no engine. Thirteen lines.

  • Viewtop::furniture_at() returns None unconditionally, so Route::Shell is 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 dispatches ToCompositor. Landed 2026-08-02 (276bb3e). control.rs carries wire::ToCompositor verbatim under a scene verb, and a test walks verbs::INTENTS asserting 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 passing 1.0 into.

    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/LockPresented are 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 in navigation.rs, no bridge) and synthesized touchinput::Origin::Agent and the disjoint slot namespace are built and nothing feeds them, so she still reaches touch only through uinput, which is the thing input.rs exists to replace. That is the highest-value remaining bridge.

  • One refusal arm in input::Router::resolve is unreachable, pinned by the_lock_claims_a_furniture_hit_before_the_reveal_check_can. It goes live the moment any furniture is Credential or a step-up reveals chrome on a locked glass. Same discipline as StepUp::from_authority.

Do not

  • Port authentication.rs. PAM is sessiond's.
  • Let locked become 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 the vanguard entry in ~/.ssh/config on the phone has the wrong user (vanguard@); archdev answers as casey@10.10.20.123. ssh vanguard fails 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.