346 lines
18 KiB
Markdown
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.
|