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:
parent
b47c17075e
commit
93ea6c9158
1 changed files with 229 additions and 0 deletions
229
HANDOFF-2026-08-16-gesture-and-transport.md
Normal file
229
HANDOFF-2026-08-16-gesture-and-transport.md
Normal 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).
|
||||
Loading…
Reference in a new issue