3431223 supplies tiled edges and closes the 504-of-540 draw; 41f07b1 removes 3,034 false touch-frame warnings.\n\nBoth are on ViewTop main. Package and glass proof remain separate.
293 lines
16 KiB
Markdown
293 lines
16 KiB
Markdown
# TASK 53 — what an audit of the 2026-08-03 compositor work left open
|
|
|
|
**Status: the ten findings are fixed, pushed, and on the phone as a package**
|
|
(`souveraine-viewtop 0.1.0.r74.g6c9614c20471-1` = viewtop `6c9614c`, built on
|
|
archdev from `main`, `pacman -U`'d 2026-08-04, `pacman -Qkk` clean). It has
|
|
since **survived a cold boot** and `phone-triage.sh` is green on every check.
|
|
Created 2026-08-04 from an adversarial read of
|
|
`ba4bd03..c98f4c8` and `souveraine 266af98..914e48f`. **Repo:**
|
|
`souveraine-viewtop` unless noted.
|
|
|
|
## Follow-up faults measured 2026-08-16
|
|
|
|
The original ten findings below remain closed. Hardware use exposed the next
|
|
compositor batch, and this task owns the defects so the dated handoff can return
|
|
to archive:
|
|
|
|
- `transition.rs` clamps `shift_to` at 1.0, leaving the shell's resistance past
|
|
the overview detent inert. Raise the compositor ceiling to the agreed 1.22
|
|
and keep its clamp test honest.
|
|
- `control.rs` serves one request and closes. A persistent client needs a loop
|
|
to EOF and one `BufReader`; constructing a reader per line can discard bytes
|
|
it already read past the newline.
|
|
- **Source-fixed and pushed in `3431223`, delivery proof pending:** Firefox was
|
|
configured at 540 logical pixels and drew 504 because the compositor supplied
|
|
size without tiled-edge state, so Firefox retained its CSD shadow. Viewtop now
|
|
publishes all four tiled edges for tiled windows and clears them on `place`;
|
|
that commit also retiles the remaining split when one window floats.
|
|
- Firefox and Bitwarden popup menus render but take no taps. Position arithmetic
|
|
has been checked against smithay; the next evidence is an instrumented build
|
|
logging which surface `client_surface_under` selects.
|
|
- `in_layer` stops at the top layer whose bounding box contains the point. If
|
|
that surface's input region rejects, it returns `None` instead of trying the
|
|
next layer down. A transparent overlay can therefore consume a tap without
|
|
receiving it.
|
|
|
|
Also source-fixed and pushed: `41f07b1` sends `wl_touch.frame` only when that
|
|
contact group reached a client. Shell-consumed contacts had produced 3,034
|
|
false warnings in one session and buried useful evidence beneath them.
|
|
|
|
The package-owned shell and wrapper-target cleanup remain TASK-76. Motion
|
|
architecture and the measured lossy transport live in
|
|
`souveraine/saf/surface/01-motion.md`.
|
|
|
|
**Three more defects the hardware found that four gates did not**, which is the
|
|
whole argument for this section existing. The third was found by re-checking
|
|
after the work was already called done:
|
|
|
|
- **A missing sessiond was a verdict, not a wait — and it cost the phone its
|
|
power button.** `Sessiond::discover()` is a single `path.exists()`, and both
|
|
compositor callers (`evidence::Reporter::spawn`, `spawn_heartbeat`) took
|
|
`NotFound` as final and returned. On the cold boot at 10:00:51 viewtop looked
|
|
**0.46 s** into its life and lost a sub-second race with sessiond's user unit.
|
|
For the next half hour every button edge, gesture, wake, panel report and
|
|
touch report went into a channel nobody read, and `observe_authority` was
|
|
never fed — doctrine §4's frozen-first-answer, which the 2026-08-02 session
|
|
fixed and this re-entered by the front door. Ground truth: sessiond's own
|
|
`sensor_health.touch: unknown`. Fixed in `6c9614c` — poll every 500 ms until
|
|
the socket appears, loud on the way in and out; `NoRuntimeDir` still fails at
|
|
once, because that is not a late daemon. `touch: live` now.
|
|
|
|
- **`delivered` was a lie.** `touch_down` on a *locked* session answered
|
|
`{"delivered": true}` while `resolve` had withheld the contact and nothing had
|
|
been sent to anyone. The field was `is_down`, and a withheld contact is still
|
|
tracked on purpose — evidence is about the finger, not about where the event
|
|
ended up. Holding a slot and reaching a client are two questions; `delivers()`
|
|
is the second one. Fixed in `bed1ed6`, re-measured `delivered: false`.
|
|
- **A drift warning that fired on the normal path.** `prune_workspaces` derives
|
|
membership from the surface table, so it dropped the closing window's own
|
|
entry and reported "membership outlived its windows" on *every* window close.
|
|
A signal that fires when nothing is wrong is not a signal — §4's rule pointed
|
|
at itself. The ordinary close removes its own entry now and the sweep warns
|
|
only on real drift (`6446386`).
|
|
|
|
Six findings from that audit were fixed the same day (the strip not moving,
|
|
agent touch reaching the lock surface, the stale settle stranding the strip, the
|
|
shared wake latch, workspace-blind focus, the missing workspace verb, per-window
|
|
capture returning nothing). These were the rest.
|
|
|
|
The audit's own method note is the thing to keep: **every one of these passed
|
|
all four gates.** A wedged compositor reports `shell=active` with zero errors,
|
|
so "green" proves nothing about behaviour. The fixes below passed four gates and
|
|
264 tests too. Launch an app and read the log.
|
|
|
|
## 1. Two hit tests that disagree — FIXED
|
|
|
|
`toggle_float` and `drag_window` used `self.space.element_under(point)`;
|
|
`client_surface_under` undoes the strip shift *and* the pose. So a three-finger
|
|
carry picked a different window than a touch did, whenever anything was posed or
|
|
the strip was off an integer — §11's second-decider shape in miniature, getting
|
|
louder as `pose` gets used.
|
|
|
|
Now `windows_under()` is the one hit test: it walks the space topmost-first,
|
|
applies `window_origin` (the strip) and `unpose` (the pose), and yields every
|
|
window under the point with the point in that window's own coordinates. An
|
|
iterator rather than one answer, because the two callers want different amounts
|
|
of it — the touch path walks past a window whose input region misses, the carry
|
|
takes the first.
|
|
|
|
`window_origin` is also what `frame_elements` reads, so the picture and the
|
|
touch cannot drift.
|
|
|
|
## 2. Agent contacts are unbounded and never cleaned up — FIXED
|
|
|
|
`SLOTS_PER_ORIGIN = 64`. `Origin::slot()` is fallible now and refuses anything
|
|
past it, so `Router::active` is bounded by construction rather than by a client
|
|
lifetime the control socket does not have (it is a line per connection). The
|
|
refusal surfaces at `serve_intent` as `invalid_argument` naming the bound.
|
|
|
|
That also kills the sharper half: `Origin::Agent.slot()` was
|
|
`saturating_add(1 << 16)`, so `u32::MAX` stayed `u32::MAX` and reached
|
|
`wl_touch`'s `int` id as `-1` — the no-slot sentinel. Wrapping instead of
|
|
saturating would have been worse: her slot 64 would alias slot 0 and retarget a
|
|
contact somebody else holds.
|
|
|
|
Three tests pin it, including that the cap stays smaller than the gap between
|
|
the two namespaces.
|
|
|
|
## 3. `ext_foreign_toplevel_list_v1` had no bind filter, and its handles were empty — FIXED
|
|
|
|
Both halves, and they had to land together: filling the titles is what would
|
|
have made the missing filter matter, since the list was mitigated only
|
|
accidentally — by the titles never being set.
|
|
|
|
- **`Privileged::WindowList`**, applied to *both* window lists: the standard one
|
|
through smithay's `new_with_filter`, the wlr one through `GlobalDispatch::
|
|
can_view` with the seat uid carried in the global's data (`ListGate`), because
|
|
`can_view` is an associated function with no `self` and no closure to capture.
|
|
Same ceiling as the other four — any process of the session's user still binds
|
|
— and TASK-41's grant is what narrows it.
|
|
- **Titles and app_ids on the standard handles**, edge-triggered. Note the
|
|
earlier attempt (`0eda51e`) was reverted for a protocol event storm:
|
|
`publish_toplevel` runs on every focus change, and while smithay's
|
|
`send_title`/`send_app_id` drop an unchanged value themselves, `send_done`
|
|
does not — and `done` is what a client relayouts on. So the change is detected
|
|
first and `done` is sent only when something moved.
|
|
|
|
**Still owed and NOT this task's:** titles cross while the session is locked.
|
|
That is a disclosure gate, asked at a different moment than a bind filter, and
|
|
it is TASK-51 §5's.
|
|
|
|
## 4. Errors that are indistinguishable from nothing happening — FIXED
|
|
|
|
`DEVICE-STATE-MACHINE.md` §10's rule. The protocol's failure vocabulary is only
|
|
three words wide (`Unknown`, `BufferConstraints`, `Stopped`), so what the client
|
|
is told cannot always be specific — but what the log says can be.
|
|
|
|
- `capture_window_for`: a window that closed is `Stopped`, which is what it is.
|
|
The other two are compositor bugs and warn with what did not match.
|
|
- `copy_window_into` / `copy_output_into`: every exit logs, at the severity it
|
|
deserves — no renderer and no output are `error!`, a window that has not
|
|
committed a buffer yet is `debug!` and not a fault.
|
|
- `drag_window`: three logged returns, and the axis latch (§7) is what stops a
|
|
failed pick from letting the same gesture flip to strip mode later.
|
|
- `fullscreen_request`: refuses out loud, and only where it can (§6).
|
|
- `toplevel_destroyed`: see §5 and §10 below.
|
|
- `border`: asks `Act::Compose` like every other composition verb, and `state`
|
|
reports `borders` so a chain can read back what is in force.
|
|
|
|
## 5. `focus_border` ignored `pose`, and its fallback was the whole panel — FIXED
|
|
|
|
The border now follows the posed rectangle — the same arithmetic `pose_origin`
|
|
does, kept in logical pixels because the zone it clamps against is logical.
|
|
|
|
The fallback was the worse half. `registry.output()` is not a conservative
|
|
default, it is the largest possible wrong answer, and `toplevel_destroyed`
|
|
reached it every time: `registry.remove` then `retile` → `kick_render` →
|
|
`frame_elements`, all before `space.refresh()` had dropped the element. One
|
|
frame of full-zone grey slab, and a full-screen hit box on a dead window in the
|
|
same frame. Now `window_extent()` falls back to the window's own geometry, and
|
|
`toplevel_destroyed` unmaps the element first so "destroyed" is true for every
|
|
reader at once.
|
|
|
|
## 6. `fullscreen_request` arranged the scene on a client's say-so — FIXED, and the gate question was the wrong one
|
|
|
|
Both doors go through `take_fullscreen_room` / `give_fullscreen_room_back` now.
|
|
The disagreement the audit found was real and is closed: `Ask::Fullscreen` from
|
|
the dock minted no workspace, so whether your video got its own room depended on
|
|
which thing you tapped.
|
|
|
|
**It does not ask `client_may_arrange`, and that is deliberate.** That predicate
|
|
answers "may a client dictate its own geometry" — what `move_request` and
|
|
`resize_request` are — and it always says no. Fullscreen is not that: it is a
|
|
state the protocol defines and the compositor implements however it likes, and
|
|
refusing it would leave the client waiting on a configure that never comes. What
|
|
*was* genuinely missing is the lock: minting a room moves the view, which
|
|
changes what the operator finds when they authenticate, and a client does not
|
|
get to decide that from behind a lock surface. So `disclosure_locked()` refuses
|
|
it and says so.
|
|
|
|
## 7. Touch-cancel and a fourth finger left the drag latched — FIXED
|
|
|
|
`abandon_drag()` clears `dragging`, `drag_axis`, `strip_drag` and
|
|
`strip_samples`, and **releases** the strip at zero velocity rather than
|
|
snapping it — a cancel means the hand is gone, and the spring should take it
|
|
from where the hand left it. `touch_cancel` calls it: the recogniser forgetting
|
|
its contacts never moved the strip it was dragging, and those are two different
|
|
pieces of state.
|
|
|
|
The palm case is closed by matching `fingers >= 3` on `Drag` and any count on
|
|
`DragEnd`. `peak_fingers` is max-concurrent, so a fourth contact renamed every
|
|
later event and neither arm matched — the strip stayed latched half-way with
|
|
nothing in flight to walk it home. A drag still only *begins* at exactly three.
|
|
|
|
And the axis is latched on **both** outcomes. It used to be inferred from
|
|
whether `strip_drag` was set, so a vertical drag that began over the wallpaper
|
|
latched nothing and a later event could flip it into scrolling the world. A
|
|
latch set on only one of two outcomes is not a latch. `drag_axis_of()` is a free
|
|
function with four tests; ties go to the window, as the more recoverable answer.
|
|
|
|
## 8. Doc comments that asserted the opposite of the code — FIXED
|
|
|
|
The three displaced by insertions (`copy_output_into`'s doc on
|
|
`session_is_toplevel`, `frame_elements`' on `focus_border`, and the duplicated
|
|
`#[cfg]` in `Viewtop::new`) are back where they belong. The four that said false
|
|
things — the registry carrying "size, not position", the hit test "owing" the
|
|
inverse mapping, `publish_toplevel` not carrying the standard handles' strings
|
|
(now it does), and `foreign_toplevel.rs`'s claim that the standard list is
|
|
deliberately not served — now describe the code.
|
|
|
|
`WindowOverview.qml`'s header (souveraine) said *"viewtop has no workspaces at
|
|
all"*, written the same night `workspace.rs` landed. Corrected, including what
|
|
is now true and undrawn: the strip is readable over the control socket and this
|
|
overview does not show it.
|
|
|
|
## 9. A floated window could not rejoin the layout — FIXED
|
|
|
|
`unplace` on the wire, in the verb table, and in `serve_intent`. `place()` still
|
|
pins a window out of the tiling permanently and on purpose — dragging a window
|
|
somewhere is saying where it should be — but the one-way door has a return now.
|
|
It answers `was_floating`, because idempotent success and a window that was
|
|
never floated are the same call and different facts.
|
|
|
|
It belongs on the wire rather than back on the gesture: it is the inverse of a
|
|
verb she already has (§13 makes geometry hers), and the binding that used to
|
|
carry `toggle_float` is sessiond's now.
|
|
|
|
## 10. Render churn — FIXED
|
|
|
|
`kick_render` moved out of `retile_workspace` and into `retile`, once. A strip
|
|
drag lays out two or three workspaces per motion event and each used to queue
|
|
its own frame.
|
|
|
|
Also here rather than in a section of its own: `prune_workspaces()` derives
|
|
membership from the surface table instead of maintaining it beside one. A
|
|
destroy that could not resolve an id used to leave the `on_workspace` entry
|
|
behind, and an entry behind is a room that can **never** look empty again —
|
|
one in the strip forever, holding nothing, costing a swipe every time you cross
|
|
it.
|
|
|
|
## Acceptance
|
|
|
|
**Verified on the phone 2026-08-04**, session locked, over the control socket
|
|
and with real clients:
|
|
|
|
- the session comes up and locks — lock granted and attested to `peer:7
|
|
(uid 1000)`, every layer surface mapped, and **no refusal from the new
|
|
`Privileged::WindowList` filter**. That was the deployment risk worth naming:
|
|
2026-08-03 closing five bind filters left the phone unable to lock.
|
|
- `state` reports `borders` — the read-back §4 asked for.
|
|
- a slot outside the namespace: `invalid_argument`, *"slot 4294967295 is outside
|
|
the agent's namespace of 64"*.
|
|
- `unplace` end to end on a live window: `was_floating: false` → `place` →
|
|
`was_floating: true`. §9's one-way door has a return.
|
|
- two windows tile on one workspace, closing each leaves the strip at 1 with no
|
|
phantom room, and the log is clean — zero warnings that are not smithay's own
|
|
two (`drm master`, `mode property blob`).
|
|
- and it survived a **cold boot**: `phone-triage.sh` green on all six sections,
|
|
`Class=user`, `LockedHint=yes`, lock-then-blank holding.
|
|
|
|
Method note for the next session: all of that was measured **while locked**,
|
|
over the control socket, with clients launched as
|
|
`XDG_RUNTIME_DIR=/run/user/1000 WAYLAND_DISPLAY=wayland-1 setsid kitty &`. They
|
|
map and tile behind the lock surface, disclosing nothing, because the scene
|
|
stays lock-only. `socat` is not installed on the phone; a four-line python
|
|
`AF_UNIX` client is the way in.
|
|
|
|
**Still owed, and it needs the PIN** — the 40 is the user's, so an agent cannot
|
|
open the session to check the rest:
|
|
|
|
- three-finger tap → **the window action sheet** through sessiond (rebound from
|
|
the overview 2026-08-05, TASK-55 — it now carries the tapped window), and a
|
|
three-finger carry;
|
|
- a window posed to 0.5 with the border around the *window*;
|
|
- fullscreen from the app and from the dock landing on the same workspace;
|
|
- a palm down mid-drag, and confirming the strip still settles.
|
|
|
|
## Connects to
|
|
|
|
TASK-51 (viewtop → daily driver; §5's disclosure gate on the window list is the
|
|
half deliberately left there), TASK-50 (done; §1 and §5 here were its
|
|
consequences), TASK-41 (§3's bind filter is uid-only until the attested grant),
|
|
`DEVICE-STATE-MACHINE.md` §10 (§4 here is that rule),
|
|
`SESSION-AUTHORITY-DOCTRINE.md` §11 (§1 and §6).
|