Watch
1
0
Fork
You've already forked souveraine
0

saf: write down how the surface moves

The motion system had no page anywhere — one clock and two strategies,
the card rect as one number, the spring, and the rule that the shell
names a destination and never transforms a window.

Carries the measured numbers: 0.47ms socket, 52 dropped queues in a
boot, 275px of real travel, and the curve bank three surfaces call.
This commit is contained in:
Fimeg 2026-08-16 14:04:47 -04:00
commit a3e0420f5a
2 changed files with 66 additions and 0 deletions

View file

@ -19,6 +19,9 @@ How Souveraine is built, and why. For whoever reads it, human or agent. When thi
- [compaction](memory/01-compaction.md). Five ways to make room.
- [replay](memory/02-replay.md). One projection of what she keeps onto what the model sees.
**surface**: how the body she lives in moves
- [motion](surface/01-motion.md). One clock, two strategies, and why the shell never transforms a window.
Coming: the rest of identity (the memory filesystem, the ledgers), the nervous system (schedules, the event log), the consciousness cycle, the runtime, federation, a glossary.
Work and status live in `docs/tasks/`, not here. The old single-file version sits in `archive/`.

63
saf/surface/01-motion.md Normal file
View file

@ -0,0 +1,63 @@
# Motion
How the shell moves, and who is allowed to move a window. The code is `surfaces/quickshell/services/ZoneTransition.qml`, `modules/souveraine/navigation/`, and `crates/compositor/src/transition.rs` in viewtop.
## One clock, two strategies
The rail measures a thumb and reports one number: how far it has climbed. It derives nothing. `ZoneTransition.pullTo(travel, detent)` takes it, and `clock` is `travel / detent` — 0 at rest, 1 at the multitasking detent, and it keeps counting past that, because the climb toward home is more of the same motion and not a second gesture.
Everything visible is a curve over that one number, and every curve lives in that one file.
`shift` is what the compositor carries the window on. `presence` is how present the destination is. They are genuinely different: the window keeps travelling as the climb continues toward home, while the destination has to recede, or a release would arrive somewhere it was never going to take you. Two curves is correct. Two *places* is the fault — a scale on the rail and a scale on the card, tuned by hand to agree, which is how the original was built and what TASK-60 was written to remove.
## The card rect is one number, not two that agree
A card is the usable zone drawn small. One scale factor, origin included, because `Viewtop::carried` takes its scale from the width alone and applies it uniformly — a destination of any other aspect would letterbox the live window inside the card it is supposed to *be*.
`ZoneTransition` owns the rect. `ZoneOverview` lays its card out at `cardInset*`; the carry names `cardX`/`cardY` as the destination. They are the same rectangle in two coordinate spaces, not two rectangles kept in agreement.
`contentTop` comes off a window's `at.y` before scaling. The card is a picture of the usable zone, not of the whole output, and a window tiled below the bar's reservation starts at the card's top edge. Miss the subtraction in one of the two places and the carried window lands a bar's height off its own picture.
## Releasing is arriving
Every swipe resolves to exactly one end target: `home`, `overview`, `last_zone`, or a named zone. The transform is released by `commit` or `cancel` inside the compositor and there is no third way for a carry to end, so a window cannot outlive the gesture that carried it.
That is structural, not careful. The old shape was a transform one surface applied and a *different* surface had to remember to undo — so tapping a card, which never touches the rail, left a window shrunk with no way back.
`last_zone` is a destination with a name, not an abandon branch, for the same reason.
## The settle is a spring, not a duration
A lift at 60% of the climb used to commit *at* 60%: the window jumped from where the thumb left it to its final state. Two motions with a cut between them.
A duration and an easing curve cannot carry release velocity, so a flick and a drift settled identically. The settle is a critically damped spring integrated on `FrameAnimation` — the render clock, so the carry advances once per painted frame — seeded with the velocity the rail measures over the last 100 ms of the gesture. Critically damped and never underdamped: a navigation surface that rings reads as a toy.
Hard-capped at 600 ms. The commit is what releases the carry, so a settle that never converged has to arrive anyway.
Known and not yet fixed: on the `last_zone` path `travel` clamps at 0 while the exit test still waits for `|velocity| < 0.02`, so the commit can lag the visual arrival by up to that cap on the commonest gesture there is.
## The transport is fast and lossy, and only the second one matters
Measured on blueline, 2026-08-16, 200 samples, full connect → write → event-loop hop → reply → close: `workspaces` 0.47 ms median, 1.04 ms worst. Under 3% of a frame. A quantiser protecting against that cost is protecting against nothing, and `_lastShift`'s 0.01 bought a visible staircase — one step per two logical pixels across 275 px of travel. It is 0.002 now.
What the transport *is* is lossy: 52 failures and 113 `PeerClosedError`s in one 90-minute boot, each of which used to execute `_queue = []`. A lost `overview_commit` leaves the compositor carrying windows the shell believes it released and nothing in the system will ever put them back. `overview_commit`, `overview_cancel` and `workspace` are idempotent and are redelivered once. `overview_progress` is a level and still drops.
Rate is bounded by the render clock, not by a quantiser. `WindowResize` pumps its `place` the same way.
## The curve bank
`Appearance.animationCurves` is Material 3 Expressive and it has been in the tree since the `ii` days. Every spatial curve in it overshoots — `expressiveFastSpatial` peaks at 1.67, `expressiveDefaultSpatial` at 1.21 — and there is a `clickBounce` beside them.
Audited 2026-08-16: twelve souveraine surfaces, 31 hand-typed animations, three calls into that bank from two files. Every navigation surface was animating on a flat linear ramp with a hand-typed duration. That was the whole of "the reference compositors have more whoosh."
Use the bank. A hand-typed `NumberAnimation { duration: N }` in a new surface is a defect unless it says in a comment why the bank is wrong for it.
## The rule under all of it
The shell does not transform a window. It names a destination and the compositor carries it there.
Any shell-side scale over a window's geometry is a second writer, and a transform one surface applies and another must remember to undo always has a path that forgets. Where a card *must* have motion of its own — depth, the entry rise — it is gated on `ZoneTransition.inFlight` so that during a carry every card sits at its true rect and the motion eases in on the commit frame.
## Not yet true
`transition.rs:79` clamps `shift_to` to `0..=1`, so the resistance past the detent is written, shipped and inert. Raising it to `maxOvershoot` (1.22) and updating `shift_is_clamped` is a compositor change. Safe to ship the shell ahead of it: an old compositor clamps and the gesture behaves exactly as it did.