Watch
1
0
Fork
You've already forked SouveraineOS
0

handoff: the boot failure, the lossy socket, the gesture

RefuseManualStart killed every session start since 08-15. The control socket
answers in 0.47ms median and drops whole queues — 52 in one boot — which is what
strands a window. Detent, velocity and settle now come from the destination.
Six items left open, including Bitwarden, where the position theory is exhausted.
This commit is contained in:
Fimeg 2026-08-16 10:41:01 -04:00
commit 93ea6c9158

View file

@ -0,0 +1,229 @@
# HANDOFF — 2026-08-16, the gesture and the transport
Session ran from a black screen on boot to the multitasking tray. Everything
below is on blueline and measured there unless it says otherwise.
**Nothing is pushed.** `souveraine` is ahead 4 on `primary`; `souveraine-viewtop`
is ahead 1 on `main`. Pushing publishes into `edge`; that was left as Casey's
call.
---
## 1. The boot failure, and why it will come back
`/usr/bin/souveraine-session-viewtop` (viewtop r117) ran:
```
systemctl --user start graphical-session-pre.target
systemctl --user start graphical-session.target
```
Both are stock systemd targets carrying `RefuseManualStart=yes`. Both calls
failed **every boot since 2026-08-15**, with the refusal sitting in
`$XDG_RUNTIME_DIR/viewtop-session.log` the whole time:
```
Failed to start graphical-session-pre.target: Operation refused, unit
graphical-session-pre.target may be requested by dependency only.
```
So sessiond never started, viewtop logged *"sessiond unreachable; starting
locked"*, the shell never started, and a compositor holding a lock with no lock
surface composites nothing. Black panel, `graphical.target` green, zero failed
units.
TASK-76 diagnosed the right cause — *nobody was lighting the target* — and
reached for the one verb systemd forbids. `6a86b51` recorded it as landed. It
was never watched come up on glass.
**Fixed** (`souveraine-viewtop 7883f7d`): two wrapper targets that `BindsTo=`
the stock ones. A dependency may pull a refused target in where a direct request
cannot. Teardown rides the stock targets' own `StopWhenUnneeded=yes`. Verified
on blueline — both went `inactive``active` through the wrappers with the
session already up, no disruption.
**Not packaged.** Splint copies live in `~/.config/systemd/user/` so the next
cold boot survives. They outrank `/usr/lib/systemd/user/` and **must be deleted
the day the package ships**, or they shadow it permanently — the exact disease
TASK-76 is about.
**One consequence.** With the target finally lighting,
`graphical-session.target.wants/` runs for the first time, and it contained
`hypridle.service` — a second idle authority beside sessiond. Stopped and
disabled on blueline 2026-08-16. The laptop was left alone; it is still a
Hyprland session.
---
## 2. The transport was lossy, not slow
The session opened on the theory that a socket round-trip per motion event was
what made the pill feel worse than Android's. **Measured, and it is not.** 200
samples on blueline, full connect → write → event-loop hop → reply → close:
| op | median | p90 | worst |
|---|---|---|---|
| `workspaces` | 0.47 ms | 0.67 ms | 1.04 ms |
| `state` | 0.78 ms | 1.07 ms | 1.99 ms |
| `describe` | 1.34 ms | 2.61 ms | 3.49 ms |
Under 3% of a 16.7 ms frame. `ZoneTransition._lastShift`'s 0.01 quantiser
existed to protect against a cost that is not there, and bought a visible
staircase — 100 steps across 275 px is one step per 2 logical pixels on a
scaling window. Now `shiftEpsilon = 0.002`.
**What the transport actually was is lossy.** One boot, ~90 minutes of ordinary
use, before any change: **52** `[viewtop-control]` transport failures and **113**
`PeerClosedError`s. Every one of the 52 executed `root._queue = []`.
If an `overview_commit` is in that queue, the compositor keeps carrying windows
the shell believes it released, and **nothing in the system will ever put them
back**. Casey, 2026-08-16: *"once I put a window in multitasking sometimes it
glitches and then I have to drag it back down manually… once I reopen it"* —
reopening begins a fresh carry, and that carry's commit frees the stranded one.
Two causes, both addressed:
- The phone was running a **pre-2026-08-13 `ViewtopControl.qml`** — the copy
that writes into a socket viewtop has already closed. The repo's fixed version
never reached it, because QML travels by hand-copy. Deployed.
- `_queue = []` was unconditional. `overview_commit`, `overview_cancel` and
`workspace` are idempotent and their loss strands state, so they are
redelivered once (`souveraine 2b4c827`). `overview_progress` is a level and
still drops.
The old error message named neither the verb nor the count, which is why this
hid for three days. It does now.
---
## 3. The gesture (`souveraine 46d2601`)
Read against `~/Downloads/lineage-trebuchet/quickstep/`, in full, not summarised.
- **The detent is the card's distance.** `missionAt` was `screen.height * 0.18`
= 194 px. The card's bottom sits at **805 of 1080 logical**, so the real travel
is **275 px** — every release committed with the window ~40% short of being its
card, and the hand-off covered the rest in one frame. That is the *"the swipe
to this swap is awkward"* seam, reported six times. quickstep returns the
destination rect and the drag length from one call
(`getSwipeUpDestinationAndLength`); `ZoneTransition.dragLength` is that.
Latched at the press, because the overview surface reports its geometry from
inside a gated Loader and would otherwise change the detent mid-drag.
- **Release velocity is measured.** Was `travel / (whole gesture duration)` — a
slow climb ending in a flick read as slow. Now a 100 ms tail window, which is
what `AbsSwipeUpHandler.onGestureEnded(endVelocityPxPerMs, …)` is handed by a
VelocityTracker.
- **The settle is a spring.** A duration and an easing curve cannot carry
release velocity; Android's home path is `RectFSpringAnim`. Critically damped,
integrated on `FrameAnimation`, seeded with the measured velocity, hard-capped
at 600 ms so a commit can never fail to arrive.
- **Resistance past the detent** replaces the hard clamp, on Android's
`DECELERATE` (`1-(1-t)²`) bounded at `maxOvershoot = 1.22`. **Inert today**
`transition.rs:79` clamps `shift_to` to `0..=1`. Safe to ship ahead of the
compositor: an old one clamps it and the gesture behaves as before.
---
## 4. The overview (`souveraine f06bb83`)
- **The card is the usable zone, not the panel.** A tiled window's `at.y` starts
below the bar's 40 px reservation, so a card drawn as the whole panel carried
`40 × cardScale` ≈ 28 px of dead space above every window. `contentTop` is
subtracted in `ZoneTransition.targetFor` **and** in `ZoneOverview`'s pane
layout. Miss it in one and the carried window lands a bar-height off its own
picture.
- **Opaque.** `colLayer1` was letting the blurred wallpaper through, so each card
was a smear of the backdrop. Wallpaper and blur stay as the room; cards sit on
it.
- **Smaller and centred.** `cardFill 0.84`, `cardGutter 16`, delegate = card +
gutter with a centred highlight range instead of a full-width delegate pinned
at x=0.
- **The strip always came to rest on Home.** `zones` rebuilds into a new array on
every window change, a ListView resets `currentIndex` to 0 on a model change,
and `onOpenChanged` asks for a refresh — so the reset was guaranteed and
`Component.onCompleted` could never survive it. Re-seated on every model
change.
- **The dock is gone from multitasking**, gated above the home check because the
gesture is usually made *from* home, where `activeZone` is still 0 and every
rung below returned PINNED.
- **The tray.** Float/Unfloat, mint a zone, close all, and a line she can write
to (`qs -c souveraine ipc call overviewNote say "…"`), defaulting to locally
computed facts so it needs no model. Minting needed no new verb:
`Request::Workspace` already pushes a zone when asked for one past the end.
Measured geometry after the change, against the reported surface (540 × 811.2,
top 40):
```
cardScale 0.6084 card 328.5 × 632.7 neighbour peek 85.7 px each side
dragLength 335.1 px = 31.0% of the panel (was 275 px = 25.5%)
```
**Open decision.** A smaller card is a further card, so the home threshold moved
25.5% → 31%. Casey's original spec was *"beyond the bottom 15-20%"*. The two are
coupled today; quickstep decouples them with `MIN_PROGRESS_FOR_OVERVIEW = 0.7`,
a release committing at seven tenths of the drag length. `cardFill` is the one
knob meanwhile.
---
## 5. The sheet (`souveraine 75cb392`)
`pose` has been in the wire and the compositor since 2026-08-05 — it genuinely
scales content — and **no caller had ever used it**. `poseActiveZone` was the
only one and TASK-60 deleted it as the shell's half of the two-writer bug, which
took the verb's last user with it. Casey, 2026-08-16: *"I can move windows but
the zooming them in and out didn't work yet."* It did not work because nobody
asked.
Zoom composes pixels the client already drew; Resize `place`s a different
rectangle and the client relays out inside it. Both now on the sheet, with
Float/Unfloat beside them.
---
## 6. Open, and where to start
1. **Compositor batch, needs archdev → CI → pacman.**
- `transition.rs:79` clamps `shift_to` to `0..=1`; raise to `maxOvershoot`
(1.22) and update `shift_is_clamped`. Until then §3's resistance is inert.
- `control.rs` `handle()` serves one request per connection and returns.
Loop until EOF so a client can hold the connection. Hoist the `BufReader`
out of `read_request` first — a fresh one per call drops whatever it read
past the newline.
2. **Firefox draws 504 when told 540.** The compositor's own instrument says so:
`told size 540×1040`, `drawn at (24,21) size 504×1040 bbox 552×1088`. kitty
obeys exactly. Height honoured, width not. This is the dark strip down the
right of the panel with the zone behind showing through. Unexplained.
3. **Bitwarden / Firefox overlay menus still take no taps.** `a073ea5` **is** on
`main` and **is** in the running r117 — not a shipping problem. Render
(`wayland.rs:4539`) and hit test (`:5302`) compute the identical
`at = loc + offset popup.geometry().loc`, and that is correct against
smithay's real definitions: `PopupKind::location()` is the positioner's
committed `geometry.loc`, and `iter_popups` accumulates it from `(0,0)`, so
the offset is the popup's geometry origin relative to the parent's window
geometry. **The position theory is exhausted.** Next step is an instrumented
build logging what `client_surface_under` picks for a tap on that menu. Do
not theorise further without it.
4. **`in_layer` cannot fall through.** `client_surface_under` does
`map.layer_under(which, point)?` — the topmost layer whose *bounding box*
holds the point — then `layer.surface_under(...)?`. If that layer's input
region rejects, it returns `None` **without trying the layer beneath**. A
transparent Overlay surface can eat a tap meant for one below it. Found while
chasing 3; not that bug, still live.
5. **The shell is owned by no package.** `pacman -Qo` errors on every QML file
under `~/souveraine-surfaces`, and on blueline that directory **is not a git
checkout**. `souveraine-shell.service` is packaged and runs
`qs --no-duplicate -c souveraine`, which resolves entirely into unowned
home-directory files. TASK-76 item 2 owns this.
6. **Delete the splint** in `~/.config/systemd/user/souveraine-session*.target`
when viewtop's package ships them.
## Connects to
TASK-60 (the multitasking view — §3 and §4 are its remaining acceptance),
TASK-76 (provisioning by package — §1 corrects it and §5 is its item 2),
TASK-52 (atmosphere; one clock, effects as strategies over it), TASK-70 (the
island — §4's tray band is where it lands), START-HERE §5 (how anything reaches
the phone, and why the QML row is the one that bit).