Watch
1
0
Fork
You've already forked SouveraineOS
0
SouveraineOS/docs/tasks/43-viewtop-compositor.md
2026-08-24 23:24:55 -04:00

346 lines
18 KiB
Markdown

# TASK 43 — viewtop: the Souveraine compositor
**Status: daily session compositor on the Pixel 3; package-selected for the
next x86 login.** On the phone, greetd launches ViewTop, Hyprland is absent,
the shell's whole surface tree composites through it, and Firefox has rendered
and accepted touch. On 2026-08-24, signed x86 package r127 (`58b09e6`) installed
the **ViewTop on x86** SDDM entry and SDDM selected it for the next login. The
currently running laptop session began before that package and remains
Hyprland; x86 login/runtime proof is still open. **Size:** several sessions,
one lane each. **Repo:** `~/Projects/souveraine-viewtop` (internal Gitea
`Fimeg/souveraine-viewtop`, branch `main`).
**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.rs``ext-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:
```text
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).
```json
{"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.
```json
{"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 touch**
`input::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.