Watch
1
0
Fork
You've already forked SouveraineOS
0

docs: the day viewtop got its package, its titles and its borders

STATE.md was five days stale. It now carries what is on the phone
(viewtop r76, souveraine r333) and the four defects fixed on the glass:
per-window capture was unreachable because quickshell holds wlr handles and
the ext capture source is named by an ext handle with nothing correlating
them; titles were never republished on commit; the border was a filled slab
that relied on the client covering its middle; capture came back flipped.

Also the packaging hole, which was the important one — viewtop's PKGBUILD
existed and appeared in none of packages.yml's build lists, so nothing ever
built it. Half closed: it builds now, but a viewtop commit still does not
trigger a rebuild.

TASK-15 question 1 is answered and closed: dropping the accelerometer claim
took iio-sensor-proxy from 15.3% to 1.1%, sensors overall 17.2% to 2.6% of
a core. Consequence noted against 08(g) — confidence loses accel's +0.3.

New: 54, 55, 56, and 57/58 (Pi dock head, SMB2 OTG boost) with the USB
notes they point at.
This commit is contained in:
Fimeg 2026-08-04 18:18:17 -04:00
commit 18c58fc079
10 changed files with 985 additions and 2 deletions

View file

@ -18,8 +18,87 @@ stale the same way for the same reason. The packages are now cured
archive, so `pacman -Sl souveraine-aarch64` is the manifest. The surface gap
(a souveraine overlay host for the subconscious views) is still open.
Last updated: 2026-07-30 (every device package now publishes via CI; phone
updates via pacman; flashing retired).
Last updated: 2026-08-04 (viewtop now packages and publishes like everything
else; window capture, titles and borders fixed; the accelerometer claim is
gone).
## 2026-08-04 — on the phone, verified
Installed and running: `souveraine-viewtop 0.1.0.r76.gf25c2b2`,
`souveraine 0.1.r333.g6b67512`.
**viewtop's packaging hole is closed, and it was the important one.**
`pkgs/souveraine-viewtop/PKGBUILD` had existed since the hand-scp era and
appeared in **none** of `packages.yml`'s build lists, so nothing ever built it
— the device ran a hand-made package while `main` moved ahead. viewtop's own
CI (`ci.yml`) has one job, `check`, and never publishes, so a green run there
still says nothing about what shipped. That is the same ambiguity the retired
`public` branch had in `souveraine`, one repo over, and it is only half closed:
`packages.yml` now builds it, but a viewtop commit still does not trigger a
rebuild. Someone must dispatch `packages.yml` or touch `pkgs/**`.
Also: this Gitea requires sign-in instance-wide, so *no* anonymous clone of a
first-party repo works, public or not. viewtop is the only PKGBUILD whose
`source=` points at our own Gitea — every other one fetches from github,
gitlab, codeberg or freedesktop — which is why nothing had hit this before.
**Four defects fixed, each measured on the glass:**
- **Per-window capture was unreachable.** viewtop served the standard
`ext-image-copy-capture` toplevel path and quickshell never used it:
quickshell holds **wlr** toplevel handles (the protocol with `activate` and
`close`, the only one that can carry a dock), an ext capture source is named
by an `ext_foreign_toplevel_handle_v1`, and nothing correlates them. It
refused client-side — `Capture source set to non captureable object` — and
the compositor was never asked. Every overview card and dock preview was a
transparent box. Fixed by serving `hyprland_toplevel_export_v1`, which takes
the wlr handle directly; given both, quickshell picks it. Both paths end in
the same `copy_window_into`. See `crates/compositor/src/toplevel_export.rs`
for the expiry condition — if quickshell grows the standard path, the global
drops with no renderer change.
- **Titles were empty.** `publish_toplevel` ran at map, on focus change and on
maximize/fullscreen but **never on commit**, and clients set their title
after mapping. Everything read "Window", the QML fallback.
- **Borders differed between two tiled windows** from one width and one code
path: the frame was a filled slab drawn *behind* the window, relying on the
client to cover its middle. Now four edge rects.
- **Screen capture came back vertically flipped** — a top-anchored bar landed
on the bottom edge. `GlesMapping::flipped()` really is hardcoded `true`, so
the old row-reversal looked right; it was one inversion too many.
**The accelerometer claim is dropped** (TASK-15 question 1, answered and
closed). Measured, idle, one flag apart: `iio-sensor-proxy` **15.3% → 1.1%**,
total sensor cost **17.2% → 2.6%** of a core, 47.9 → 45.9 °C. Claiming the
accelerometer is what makes iio-sensor-proxy poll continuously; `monitor-sensor`
itself was 0.8% and sensord 0.1% — the subprocess was never the cost. This is
not "accel is unwanted": a subprocess holding a continuous claim is the wrong
way to get it, and TASK-36's SLPI batching is the right one. Restore the source
only together with that. Consequence for **TASK-08(g)**: `observed_confidence`
loses accel's +0.3 once the source ages out, so there is one fewer input for
confidence to gate.
## Open, and the next session should start here
- **The shell is still keyed to Hyprland's instance signature.**
`Persistent.qml` reads `HYPRLAND_INSTANCE_SIGNATURE`, which is unset under
viewtop, so `isNewHyprlandInstance` compares `""` to `""` and is
**permanently false**. It gates `LockScreen.qml:145`
(`lock.launchOnStartup` — so lock-on-startup never fires), `LockScreen.qml:289`,
`services/Idle.qml:59` and `ii-base/services/Idle.qml:21`. viewtop's
single-instance mechanic is the control socket's `clear_stale`, but that is a
*mutex*, not an identity — there is no per-start value to compare. Fix is
two-sided: viewtop exposes an instance id on `{"op":"state"}`, the shell reads
that instead of the env var.
- **Captured card content is streaky / wrong-surface.** Frames arrive
(`hasContent=true`, correct size) but the contents are not right.
- **viewtop security audit not started.** `SECURITY-AUDIT.md` already carries
the standing principles P1P7, the watch list, "Three ungated boundaries in
the compositor, found 2026-08-03", and the long-arc process-isolation goal —
note it never uses the word "viewtop", it says "the compositor", so absence of
the word is not absence of coverage. Scope, deliverable, and what "RedFlag"
refers to are still unanswered.
- SouveraineOS working tree is dirty: 08, 15 and README modified, 54/55/56
untracked. TASK-15's accel measurement is written in but uncommitted.
## DEPLOYMENT — the phone updates with pacman. Do not flash.

178
USB/README.md Normal file
View file

@ -0,0 +1,178 @@
# USB — the Pixel 3 as a KVM device
**Exploration opened 2026-08-04** from Casey: "start exploring the notion of
KVM from this thing." This folder holds USB-related design and measurement;
the KVM exploration starts here.
## What exists, measured on the phone (2026-08-04)
| Piece | State |
|---|---|
| Kernel gadget framework | `CONFIG_USB_GADGET=y`, `CONFIG_USB_CONFIGFS=y` — built in, no modules |
| **HID gadget** | **`CONFIG_USB_CONFIGFS_F_HID=y` — the keyboard/mouse half of KVM already ships** |
| FunctionFS | `CONFIG_USB_CONFIGFS_F_FS=y` (flexible userspace gadgets, MTP path) |
| **UVC gadget** | **`CONFIG_USB_CONFIGFS_F_UVC` not set — the video half is the one missing config line** |
| Other functions | NCM, ECM, RNDIS, EEM, ACM, serial, OBEX, mass storage; no UAC1/2, no MIDI |
| Live configfs | Two gadgets bound: `dbg` (acm.GS0, acm.GS1, ncm.usb0) and `usb-signaller-developer` (ncm.usb0) on UDC `a600000.usb` (dwc3, **super-speed capable**, currently high-speed) |
| Mode switching | `usb-signaller` 0.3.1-3 active (dev mode at boot via `usb-signaller-default-mode`); `blueline-usb-gadget` 1.0.0-2; dev mode = NCM usb0 (172.16.42.1) + ACM |
| Video hardware | Venus **encoder** `/dev/video12` + **decoder** `/dev/video13`; CAMSS sensors `/dev/video0-11`; no v4l2loopback |
| Other packages | v4l-utils, libusb, libgusb, libusbmuxd |
## Host mode — the dock question (2026-08-04, Casey: "if I plug a USB-C dock into the phone right now…")
DeX-style: the phone as the computer, a dock supplying monitors + keyboard
+ mouse. Measured against the running kernel *and* Google's own vendor DT
(`sdm845-b1c1-usb.dtsi`, LineageOS kernel lineage-22.2):
| Ask | Verdict | Evidence |
|---|---|---|
| **Multi-monitor (DP alt mode)** | **Unwired in every tree that exists.** Google's board DT wires the port as **DRP + USB-PD** (`pmi8998_pdphy`, source 5V@0.9A, sink 5V/9V@3A, try-sink) with **no DP alt-mode path** — no redriver, no typec mux, no altmode node; the only "mux" is test GPIO104 for HS enumeration. Matches Google's official spec (USB 3.1 Gen 1, no video out). The *silicon* is capable — mainline `sdm845.dtsi` fully wires `mdss_dp` → QMP USB3-DP PHY (`usb_1_qmpphy_dp_in`) — but no sdm845 board in mainline wires it end-to-end either | vendor DT (fetched), mainline dtsi (local tree) |
| **USB keyboard/mouse** | **Yes — this is what LineageOS actually gives you.** DRP port + PD = real host mode. Our tree pins `dr_mode = "peripheral"` and usb-signaller holds the port as a gadget, so *we* don't do it today — but the hardware does | vendor DT, our board DT |
| **Via a plain OTG adapter** | **Works** — classic USB-A keyboard/flash-drive case | standard OTG |
| **Via a Dell dock (WD19…)** | **Host mode: works on LineageOS, one role-switch away on ours.** Monitors over DP alt mode: no. Monitors over **DisplayLink** (software display over USB host): only with a DisplayLink driver | per-dock, plug it in |
| **Charging from the dock** | Yes (PD sink 5V/9V@3A wired in the vendor DT) | vendor DT |
**The correction (2026-08-04):** an earlier draft called the DP gap a
"connector mux on the board — hardware." That was asserted from memory and
is wrong as stated. The evidence: Google wired the port for data+PD and
never enabled DP alt mode in any tree, and the *hardware question that
remains* — whether the PCB even routes DP lanes from the QMP PHY to the
connector — is **unproven in both directions**. If LineageOS drove real
monitors over the dock, the lanes exist (via DisplayLink, not alt mode).
Decisive physical test: a USB-C→HDMI **alt-mode** adapter (not DisplayLink)
with the port in host mode; if the board routes DP, a wired-up kernel has
something to talk to.
**The daily-driver catch:** the USB-C port is the management port — tethering
and dev mode (SSH over usb0) die while it is a host. Deliberate, documented
mode switch only; cold boots stay Casey's.
## Host mode — the live test (recipe, 2026-08-04)
Confirmed on the device: the role switch is real (`/sys/class/usb_role/
a600000.usb-role-switch`, role currently `device`), passwordless sudo works,
and usb-signaller owns `com.meego.usb_moded` on the system bus (its modes are
all device-side today — Developer/Tethering/MTP — so host mode is a direct
sysfs switch, and usb-signaller's restart is the way *back*).
```
# 1. give up the gadget first — a bound gadget + host role breaks
echo "" | sudo tee /sys/kernel/config/usb_gadget/usb-signaller-developer/UDC
# 2. switch the port to host
echo host | sudo tee /sys/class/usb_role/a600000.usb-role-switch/role
# 3. plug the dock, then observe:
lsusb
dmesg | tail -30
# 4. back — WiFi SSH survives the whole trip, so this always works:
echo device | sudo tee /sys/class/usb_role/a600000.usb-role-switch/role
sudo systemctl restart usb-signaller # re-provisions the developer gadget
cat /sys/class/usb_role/a600000.usb-role-switch/role
```
Gotchas: the dock is self-powered (Dell docks carry their own PSU), so its
hub can enumerate even if the phone's VBUS sourcing is weak or absent — the
dwc3 node has **no `vbus-supply`**, so bus-powered OTG peripherals (plain
keyboards) may not power, which is itself a measurement. If the dock's hub
enumerates, its keyboard/mouse are just kernel HID input — the shell gets a
real keyboard for free. If the dock's *monitors* ever light up, the DP lanes
exist and we go hunt the mux wiring; with no typec/altmode wired, expect
nothing from them.
## The KVM notion
Plug the phone into another machine's USB port. The phone controls that
machine and sees its screen — keyboard, mouse, monitor, in one cable. Two
halves, with very different states of readiness:
**Input half — HID gadget: already built.** The kernel handles the USB HID
descriptors; userspace writes 8-byte keyboard reports to `/dev/hidg0` and
4-byte mouse reports to `/dev/hidg1`. This is the half that reaches
bootloaders/BIOS (the pelya project proved it) — no video ever reaches a
BIOS, so a BIOS-stage KVM is HID plus whatever screen the machine itself has.
**Display half — two routes:**
- **Route A (works today, zero kernel change):** the live NCM link. The
controlled machine serves its display over the USB network (VNC/waypipe);
the phone shows it in a surface. Touches become HID reports. Worst part is
latency, and the link is super-speed-capable.
- **Route B (one config line away):** UVC gadget. The phone presents as a
UVC camera; the controlled machine runs a "screen-as-webcam" tool (OBS
virtual cam and friends) which is the stream source; the phone decodes
(Venus decoder ✓) and shows it. The reverse direction also exists: Venus
encoder + UVC = the PC sees the phone's *own* screen as a webcam — an
in-the-cable scrcpy.
## Prior art (verified)
- **tejado/android-usb-gadget** — USB Gadget Tool (F-Droid): configfs roles
for keyboard/mouse, FIDO CTAP, CCID, UVC camera; activates gadgets at boot.
- **Android 14 "Device as Webcam"**`CONFIG_USB_CONFIGFS_F_UVC` + configfs
recipe (MJPEG 1080p30, `dwMaxVideoFrameBufferSize` etc.); enabled by
default in GKI since android12-5.10. The configfs UVC setup pattern is the
one to copy.
- **pelya/android-keyboard-gadget** — the classic: `/dev/hidg0` keyboard,
`/dev/hidg1` mouse, `hid-gadget-test`; works into BIOS/bootloader.
- **Kernel docs**`usb/gadget_hid.rst` (hid_gadget_test) and
`usb/gadget_uvc.html`; the UVC reference app is
`gitlab.freedesktop.org/camera/uvc-gadget` — "queue image buffers from a
variety of sources", i.e. the phone-side userspace we would adapt to feed
viewtop/compositor frames into the gadget.
## Questions to settle, in order
1. **Directions:** KVM-into-another-machine only, or also the reverse
(phone screen out as webcam)? Both share the UVC config; the userspace
differs.
2. **Route A or B first?** A proves the whole loop today with no rebuild; B
is the "real peripheral" feel but needs a kernel rebuild + cold boot —
and that is Casey's call, on the daily driver.
3. **Touch → HID mapping:** a KVM mode turns the phone's screen into a
touchpad/screen for the target machine. The machinery is the mirror of
TASK-50's touch intent — same input-routing problem, other direction.
4. **Where does the mode live?** usb-signaller gains a "kvm" mode (profile
+ service), or sessiond owns it as a mode with a verb. The agent angle
is real: Ani gets a keyboard/mouse that reaches any host — doctrine §13
says she should have the verb, not a config file.
5. **The kernel path:** enabling F_UVC is one config line, but the kernel
package still publishes unsigned raw `.pkg.tar.zst` (TASK-25's
`repo-add --sign` is unbuilt) and installs are cold-boot events.
## Next steps (ordered, each measurable)
1. **Prove HID today** — provision a HID keyboard function into a scratch
gadget on the phone (configfs, no reboot), bind it, write
`hid-gadget-test` key reports, watch the laptop enumerate a second
keyboard. One session, no rebuild.
2. **Route A latency test** — laptop serves its screen (VNC/waypipe) over
the live 172.16.42.1 link; phone displays it; measure.
3. **Route B** — config one-liner + rebuild + cold boot (Casey); configfs
UVC setup per the AOSP recipe; `uvc-gadget` userspace fed by a capture
surface.
4. **usb-signaller "kvm" mode** once the pieces exist, with clean
teardown/provision against the dev mode.
## Acceptance (measured)
- Phone plugged into the laptop enumerates as keyboard+mouse; typing from
the phone lands in the laptop's focused app.
- Route A: the laptop's screen renders on the phone with usable latency.
- Route B: the laptop's virtual-cam tool lists the phone as a camera; the
phone shows the stream.
- Switching modes never strands the daily driver's tethering/dev mode.
## Connects to
TASK-25 (kernel package signing — the rebuild path), TASK-50 (her hand on
the glass — the touch→HID bridge is the mirror of the touch intent),
doctrine §13 (the KVM mode as a verb), `usb-signaller` (the mode host),
souveraine-viewtop (the display surface and capture path).
## The buildable work lives here
TASK-57 (the Pi dock head — phone as gadget, Pi as display/input head over
NCM) and TASK-58 (host power — the SMB2 OTG boost, one kernel change that
unlocks host-mode power, power sharing, and phone→Pi power).

View file

@ -114,6 +114,51 @@ is no longer "what should confidence gate" but "is anything left for it to
gate". The honest candidate is the doze promotion rate and possibly nothing
else. Answer that, then wire it or delete the table and record why.
## (h) Thermal policy — the zones are there and the policy is empty (2026-08-04)
Measured on the device, so this is inventory rather than plan:
- **23 thermal zones**, all on the **`step_wise`** governor. `power_allocator`
(IPA — the PID that allocates a power budget across cooling devices) is
*available* on every zone and used by none.
- **Cooling devices:** `cpufreq-cpu0` (17 states), `cpufreq-cpu4` (23),
`devfreq-5000000.gpu` (6), plus `cx` and `ebi`.
- **Trip points:** passive at CPU 90/95 °C, GPU 85 °C, PMIC 105 °C; `hot` 90,
`critical` 110. **Nothing below 85 °C.** So nothing throttles until the die is
already hot, and there is no gentle first response at all.
- **cpuidle:** two states (WFI, little-rail power collapse), governor `menu`,
and `menu` is the only one built. Android downstream had far deeper cluster
and rail collapse; upstream `teo` is not available to compare against.
- **GPU:** `simple_ondemand` devfreq, idling at 257 MHz.
- Battery reads 41 °C at 22% while idle — and note the existing warning above
about `temp` = 570 while charging. Verify the sensor before building policy on
it; a skin model fed a lying thermistor is worse than no model.
**The Android prior art, and which parts transfer.** The HAL types zones
(`CPU/GPU/SKIN/BATTERY/USB_PORT/POWER_AMPLIFIER`) and maps each to a severity
ladder — `NONE → LIGHT → MODERATE → SEVERE → CRITICAL → EMERGENCY → SHUTDOWN`.
That ladder is the interesting part and it is *ours to define*, because it is
policy, not hardware. `PowerManager.getThermalHeadroom()` is its app-facing half:
a normalized 01 forecast some seconds ahead, so software degrades itself before
the kernel throttles it. **Skin temperature is a virtual sensor** — a weighted
model over die zones, because there is no thermistor on the glass; on SDM845
those coefficients lived in Qualcomm's per-device `thermal-engine.conf`, which
is the one piece of blueline-specific prior art worth going and reading.
**How it lands here, and the trap to avoid.** §12's rule is not optional and an
outside adviser will propose breaking it: **a thermal daemon that reads a zone
and calls a governor is the eighth blind actor.** Temperature enters through
`sensor_input` like proximity and lux; the response leaves as an `Action` from
`tick()`, through the executor table that already owns `Dim`/`Restore`/`Blank`.
One writer, one trail. The severity ladder is then a `DeviceStatePolicy` field
the agent can read and rewrite, which is what makes "the phone is getting warm"
something she can act on instead of something that happens to her.
**Do the cheap thing first.** TASK-15's 2026-08-04 measurement found ~36% of a
core burning at idle in the sensor stack, with the SoC nowhere near any trip.
No thermal policy would have helped that. Fix the spinning before modelling the
heat.
## Sub-tasks (do separately)
1. State machine + pub/sub (mce/datapipe shape), device-flag gating.

View file

@ -15,6 +15,70 @@ notifications") plus the measured idle burn below.
A phone that is "off" should approach zero. Nothing on the device today
even attempts that.
## Re-measured 2026-08-04 — the biggest spinner is now OURS, and it is the sensors
41 minutes uptime, locked, screen off, viewtop session:
```
23.3% iio-sensor-proxy 9m35s of CPU in 41m of uptime
9.3% monitor-sensor --proximity --light --accel (parent: souveraine-sensord)
3-4% dbus-broker carrying the traffic between the two
```
~36% of one core, permanently, on a phone doing nothing. Dies 4448 °C, battery
41 °C, and **the SoC is nowhere near a trip point** — the shallowest passive
trip on this device is the GPU's at 85 °C. So this heat is not a thermal-policy
problem, it is a spinning userspace, and no governor will touch it.
**The cause is architectural, not a bug in either program.** `souveraine-sensord`
spawns `monitor-sensor --proximity --light --accel` and holds it open forever.
Claiming the **accelerometer** is what makes iio-sensor-proxy poll continuously —
proximity and light are cheap and event-shaped, accel is polled to derive
orientation. And per `DEVICE-STATE-MACHINE.md` §4, `accel.moving` is evidence
the machine deliberately **stopped branching on**: "motion cannot tell a pocket
from an ear from a hand". We are burning a third of a core to keep producing a
reading no rule consumes.
**The prior art is the reason the SLPI exists.** Android's sensor HAL batches:
the sensor DSP accumulates samples into a hardware FIFO and wakes the AP once
per batch (`batch(sensor, sampling_period, max_report_latency)`), with wake-up
and non-wake-up variants so a screen-off phone is not woken per sample. Polling
from userspace on the application processor, through D-Bus, defeats the entire
point of having a sensor DSP — and this device *has* one (TASK-36, TASK-34).
Three questions to settle, in this order, before any freeze work:
1. **Does anything still need the accelerometer claimed?** — **ANSWERED AND
DONE 2026-08-04** (`6b67512`). Measured on the phone, idle, screen on, same
session, one flag apart:
| `monitor-sensor` flags | iio-sensor-proxy |
|---|---|
| `--proximity --light --accel` | **15.3%** |
| `--proximity --light` | **1.1%** |
Fourteen points of a core, continuously, for the reading §4 weights least
(+0.3) and §9 calls a weak signal. The claim is what makes iio-sensor-proxy
poll; nothing else here does. `monitor-sensor` itself measured 0.8% and
sensord 0.1% — the subprocess was never the cost, the *claim* was.
The accelerometer source is dropped from `SOURCES` in
`src/bin/souveraine-sensord.rs`, along with `parse_accel`, `accel_moving`
and `MOTION_WINDOW` — all three existed only to turn orientation into a
motion edge. The 2026-07-27 stuck-`Moving(true)` finding is kept in comment
because whatever reports motion next owes a decay too.
This is not "accel is unwanted": it is that a subprocess holding a
continuous claim is the wrong way to get it. See (2).
2. **If it is wanted, does it go through the SLPI with batching** rather than
iio-sensor-proxy polling? That is TASK-36's host, and it turns a continuous
poll into an interrupt per batch.
3. **Is `monitor-sensor` the right transport at all**, or does sensord read the
IIO character device with a buffer and a trigger directly? A subprocess and a
D-Bus round trip per reading is three context switches for one number.
This is the *before* number for this task now: 36% of a core and 45 °C at idle.
## Shape of the thing
One coordinator (substrate side, likely a sessiond sibling or sessiond

View file

@ -0,0 +1,144 @@
# TASK 54 — the compositor stops drawing what nobody can see
**Status: open.** Created 2026-08-04, from Casey's question: *"could viewtop
correctly stop drawing elements in a way that makes us not have to edit every
drawing app?"* **Repo:** `souveraine-viewtop`.
The answer is yes, the mechanism is already in the protocol, and one half of it
is already built and working. This task is the other half, plus the leak that
exists today.
## The seam is the frame callback, and that is why no app needs editing
A Wayland client does not draw when it likes. It commits a buffer, requests
`wl_surface.frame`, and waits — and it renders the next frame only when the
compositor fires that callback. **Withholding the callback stops the client
rendering, from the compositor, with no cooperation from the client, no protocol
extension, and no per-app change.** Every toolkit already honours it, because
honouring it is how you get a frame at all.
That is the whole answer to "without editing every drawing app". It is also why
this belongs in the compositor and nowhere else: no daemon can do it, and a
policy that asked apps nicely would only bind the apps we wrote.
`send_frame_callbacks` states the invariant in its own header:
> *"a surface that is composited but never called back freezes on its first
> frame, and one that is called back without being drawn spins rendering frames
> nobody sees"*
Both halves are real. The first is why you cannot just stop calling back
anything; the second is the waste this task is about.
## It already works, and the lock screen is the proof
While the session is locked, `frame_elements` composites the lock client **and
nothing else**, and `send_frame_callbacks` sends to the lock surfaces and then
`continue`s. So every session client freezes on its last frame and the lock
client keeps animating. That is exactly the shape wanted — *everything stops
except the glance* — and it has been running on the phone for weeks.
**So the "different layer" for lock-screen elements that keep drawing does not
need building.** It is `ext-session-lock`, the lock client is quickshell's `Lock`
surface, and `LockMediaCard` / `LockAgentCard` / `LockNotifyCard` are already
its content. A now-playing card in that surface keeps its callbacks while the
rest of the device is frozen, and costs nothing at all while the panel is dark,
because a dark panel produces no vblank and `on_vblank` returns before
`send_frame_callbacks` (see `kick_render`'s power guard).
## What is broken today, measured
`frame_elements` skips a window when:
- its workspace is not visible (`workspaces.is_visible(ws)` — a float, so during
a strip settle *both* neighbours are visible and neither may be skipped), or
- its exposure resolves to `alpha <= 0.0` (`ExposureLevel::Obscured`).
`send_frame_callbacks` applies **neither filter**. It walks
`self.space.elements()` unconditionally. So a window one workspace away is not
drawn and *is* called back: it re-renders at panel rate, forever, into a buffer
that is discarded. The render grew the workspace filter on 2026-08-03 and the
callback path did not follow — the same "documented guarantee the code stopped
keeping" shape as every finding in TASK-53.
This is the cheapest thermal win on the device: a frame not requested is CPU,
GPU and memory bandwidth not spent, and it needs no thermal policy, no governor
change and no new trip points.
## The pieces, in order
1. **Callbacks follow the render, exactly.** One predicate — "is this surface in
the scene this frame" — asked once and used by both loops. Not two
predicates that agree today: `SESSION-AUTHORITY-DOCTRINE.md` §11's
second-decider shape is precisely what TASK-53 §1 just removed from the hit
test, and re-introducing it here would be the same defect one loop over.
The honest implementation is for `frame_elements` to record what it drew and
for the callback pass to read that, rather than re-deriving it.
2. **Occlusion.** A window fully covered by an opaque window above it is
composited every frame today (TASK-51 §1 names this). Skipping it saves the
compositing *and*, with (1), stops the client rendering at all.
**Fail open:** occlusion may only be claimed from `wl_surface.set_opaque_region`
— a client that declares nothing is not proven to cover anything, and
treating unknown as opaque would blank a window behind a translucent one.
Alpha in a *pose* is the same question: a posed window at reduced scale does
not cover what it used to.
3. **What must keep drawing even when off-screen.** Two cases, both real:
- **A live capture session.** `ext-image-copy-capture` renders a window on
demand from its last committed buffer. A client that stopped drawing hands
the capture a stale frame — an overview card of a window that is quietly
frozen in the past. A window with an open capture session keeps its
callbacks, and that is not an exception to the rule so much as the rule
applied correctly: something *is* looking at it.
- **A window the agent has posed into a visible card.** Doctrine §13 makes
composition hers; a scaled live window in an overview is on the glass, so
it is visible by the same predicate as anything else. This falls out of
(1) for free if the predicate is "in the scene", and breaks immediately if
the predicate is "on the active workspace".
4. **The idle scene already costs little, and say so.** `DrmCompositor` reports
`frame.is_empty` when nothing changed and no flip is queued, so a still scene
already stops flipping. The waste is not the compositor's compositing — it is
the clients rendering into buffers that are thrown away. Do not "optimise"
the render loop looking for it.
## Design questions to settle before code
- **Subsurfaces and popups.** `Window::send_frame` walks the toplevel's
subsurfaces and popups, which is why popups are not iterated separately. The
filter has to be applied at the window, not per surface, or a popup of a
hidden window keeps its parent alive.
- **How long may a frozen client hold its buffer?** Forever is correct for
correctness (it is what makes coming back a repaint rather than a relaunch —
the argument `set_visible` already makes) and is memory the phone may want
back. TASK-15's freeze tier is the other lever; decide which one owns it
rather than having both act.
- **Does an idle-notify inhibitor imply "keep drawing"?** No — a client holding
an idle inhibitor is asking the *session* not to idle out, not asking to be
composited while invisible. Worth writing down because it will be proposed.
## Acceptance
Measured, on the phone, with an unmodified client:
- open a client on workspace 0, swipe to workspace 1, and its process CPU falls
to approximately zero within a second — then swipe back and it repaints
without a relaunch and without a visible stall;
- with two windows tiled, cover one completely (a fullscreen over a tiled pair)
and the covered client's CPU falls the same way;
- an overview card of an off-workspace window still shows live content, because
its capture session keeps its callbacks;
- the lock glance still animates while every session client is frozen — the
behaviour that already works must not regress;
- and the idle die temperature falls. Today, locked with the screen off, the
dies sit at 4448 °C and the battery at 41 °C. That number is the before.
## Connects to
TASK-51 §1 (occlusion is named there; this owns it now), TASK-53 §1 (one
predicate, not two — the same lesson), TASK-15 (the freeze/doze tier: it stops
*processes*, this stops *rendering*, and they must not both decide the same
thing), TASK-08 (governor and thermal policy — none of which this needs),
TASK-52 (Atmosphere: an animator that runs while invisible is this bug with a
nicer name).

View file

@ -0,0 +1,164 @@
# TASK 55 — the hand's verbs: a window action sheet
**Status: open — design questions only, nothing decided.** Created 2026-08-04,
from Casey: *"I asked for a Three Finger Tap overlay — a couple of buttons:
Move/Resize, Split, Close. I got three finger tap drag stuff. And I don't have a
good way to close a running app."* **Repo:** `souveraine-viewtop` (compositor +
`wire`) and `souveraine` (shell surfaces).
The gesture recognition is built, the binding exists, and it points at the wrong
thing. What is missing is the surface the recogniser's own header promised: *"the
compositor recognises, the shell draws"* (`gesture.rs`). The shell was never
given the job.
## What exists, measured
| Piece | Where | State |
|---|---|---|
| Tap/drag recognition | `viewtop/crates/compositor/src/gesture.rs` | Solid, tested; tap carries the centroid "so the caller can name which window was under them" |
| Tap forwarding | `wayland.rs:3160``(3,"tap")` to sessiond | Works |
| The binding | `sessiond/device_state.rs:1717` — 3-finger tap → `Action::Overview``qs … ipc call overview toggle` (`server.rs:375`) | Works — and this is the wrong target: the rail's swipe-up already reaches Mission Control, which opens the *same* `WindowOverview` surface (`WindowOverview.qml:64` reads `overviewOpen \|\| missionControlOpen`). The tap duplicates a gesture the thumb already has |
| Three-finger drag | `wayland.rs:3175` — workspace strip sideways, drag-window up/down | Works. This is what shipped instead of the overlay |
| Close | `ToCompositor::Close``toplevel.send_close()` (`wire` lib.rs:347, `wayland.rs:2532`) | Agent verb only. **No hand path exists** |
| Split | `placement.rs` — automatic top/bottom tiling of two windows | No verb in `wire`; a button has nothing to say |
| The dial | `DialHost.qml` — hand-written entries incl. "Kill window" via `hyprctl dispatch` | Hyprland-bound — dead under viewtop, and the third capability enumeration (TASK-31) |
| Rail Home | `SystemGestureRail.qml:233` — tap = Home via `hyprctl dispatch hl.dsp.focus workspace 1` | Hyprland-bound — the "back to square one" path dies under viewtop |
| Selection chip | `SelectionHost.qml` / `SelectionChip.qml` | The closest existing floating-action precedent — and **broken**: "tiny, illegible, not working properly at all" (Casey 2026-08-04). It has already had one untappable incarnation on device ("completely untappable" — `SelectionHost.qml` header) |
## The questions to settle — in order, before code
### Q1. What is the three-finger tap FOR now?
The tap currently raises a surface the rail already reaches. Either the tap
becomes the action sheet (what Casey asked for) and the overview is left to the
rail, or the tap keeps the overview and the sheet lives elsewhere. Decide the
*vocabulary* first: on this device a gesture that duplicates another gesture is
a defect by construction (TASK-53 §1's one-decider lesson, one layer down).
### Q2. One surface or two: the sheet and the dial
The dial already carries window verbs (Kill window) and phone verbs (Lock,
Screenshot, Keyboard) in one ring. The sheet wants Move/Resize, Split, Close.
These must "jive" — the real question is whether they are one surface with
context-dependent entries (a window focused/under-fingertip → window verbs
appear) or two surfaces. The wrong answer is a second hand-written list in a
second file: TASK-30's verb tables exist to be the single source, and TASK-31
already owes the dial's entries to them. The sheet must read the same table the
dial will read, whichever surface it lives on.
### Q3. What does Split mean when tiling is automatic?
`placement.rs` tiles without being asked. A Split button is therefore either:
pin this window to one half and let the next open window take the other (the
`place`-then-tile shape), force the split axis/order, or refuse. This decides a
new `wire` intent (`Split`?) and its legality rules — the compositor owns
whether a placement is legal (`place`'s own doc). Not answerable in one
sentence; design the gesture-of-intent before the button.
### Q4. Close: polite, or kill, or both?
`send_close` is a request; a client may refuse. The dial's "Kill window" was a
Hyprland close dispatch (same polite mechanism, via Lua). Under viewtop a
refusing client leaves the hand with nothing — and "no way to close an app" is
the complaint that started this task. If a kill verb exists, who may reach it —
the hand, or step-up only? (§13's columns apply: killing is in the user's
column on the phone; on the laptop the agent owns composition. The two seats
may legitimately differ.)
### Q5. Back to square one, safely
Android and HarmonyOS: swipe up = Home, swipe up-and-hold = Recents; the home
screen is the always-reachable safe state. iOS: swipe up = Home. Our rail is
already this shape (tap = Home, swipe = dock, further = Mission Control) — but
Home is `hyprctl` under the hood, so under viewtop the safe state is
unreachable. Questions: what IS square one on viewtop — an empty workspace, the
dock, nothing focused? What must Home safely *leave* — the sheet open, a drag
latched, a window posed or placed (does Home un-place?), the OSK up? `goHome()`
today clears mission control/overview/dock/OSK; under viewtop it must be a
compositor verb (`ToCompositor::Home`?) with defined effects on windows, not
`hyprctl`. "Safely" means: no state the gesture leaves behind can strand the
user (the cancelled-drag latch that shipped once is the precedent).
### Q6. Why is the selection chip broken — and is the sheet going to inherit it?
Symptoms: tiny, illegible, not working. The chip is 44 px tall with
`font.pixelSize: smaller` and 1820 px icons, anchored to a pointer hint
(`SelectionHost.qml`) — and it has already had an untappable iteration. Before
the sheet reuses that machinery: is it scale (2x panel, logical/physical mix),
font, target size, or anchoring? **The sheet must not ship the chip's root
cause.** Fix or name it first — the sheet is the second surface built on the
same foundation.
### Q7. The sheet's input and dismissal
Precedent conflict in-house: the dial uses OnDemand focus and open/close
signals; the selection host originally used a fullscreen mask and abandoned it
for a card-sized window. The sheet floats over an app the user just touched —
its dismissal rules (tap outside, gesture cancel, window change) are part of
its correctness, and the recogniser already has the right shape (`cancel`
emits nothing — the sheet must hear a cancel).
### Q8. Naming the window
The tap carries the centroid precisely so the caller can name the window under
the fingers. A tap on the wallpaper is a different event entirely (nothing to
act on — overview? nothing?). Decide the no-window case before it becomes a
"why did a blank sheet appear" bug on device.
## Prior art on the table
- **Android / HarmonyOS gesture navigation** — swipe-up = Home, swipe-up-hold =
Recents, predictive back; the home screen is the guaranteed exit. Our rail
already implements this shape; what's missing is the viewtop-backed Home
(Q5) and the recents/overview not being duplicated (Q1).
- **iOS** — swipe-up = Home; action sheets present at the gesture origin, and
dismissal by outside-tap is load-bearing.
- **denial** (`Pixel3Arch/references/shells/denial`, assessment in
`VIEWTOP-AND-DENIAL.md`) — the shell is an in-process Flutter scene; its
surface would be drawn by the compositor's own shell, not a separate client.
The in-process-vs-client fork is still open in that doc; the action sheet is
the first surface that will be shaped by the answer.
- **The references shells** (`~/Projects/references/`) — the Flutter reference
overview (cards, flick to dismiss, one `progress` driving everything —
already adopted in `WindowOverview.qml`), Phosh's `use_top_layer = !locked`
state-gates-visibility model, and Launcher3's AllAppsEduView discovery nudge
(already adopted in the rail).
- **Our own SelectionHost** — the closest precedent and the caution: the
floating-action surface has failed twice on device.
## Shape of the deliverable (once the questions are answered)
1. A `wire` intent or two (`Split` if Q3 lands it; `Home` for Q5) + their
legality rules in the compositor.
2. The surface: entries from the same verb-table source the dial is owed
(TASK-30/31), anchored and dismissed per Q6/Q7, sized and scaled per Q6.
3. The binding table change in sessiond (tap → sheet/overview per Q1).
4. The rail's Home repointed from `hyprctl` to the compositor (Q5).
## Acceptance (measured, on the phone)
- Three-finger tap on a window presents the sheet for **that** window; the
same tap on wallpaper does the decided no-window thing.
- Move/Resize pins the window (the `place` path); Close closes it, and a
refusing client produces a visible refusal rather than silence.
- Split produces the second tile per Q3's decision.
- The sheet dismisses on outside tap and on a cancelled gesture, without
leaving a latch behind.
- Rail Home returns to square one under viewtop with no hyprctl anywhere in
the path, and no state stranded.
- The selection chip row is either fixed or its root cause is named and the
sheet demonstrably does not share it.
- The sheet's entries are read from the same source the dial's will be — no
third hand-written list.
## Connects to
TASK-30 (verb tables — the sheet's entries), TASK-31 (dial entries — same
source question), TASK-18 (the selection menu precedent and its failures),
TASK-50 (her hand on the glass — the agent's touch path is the mirror image of
this), TASK-43 (the compositor's surface model), TASK-53 (one decider; a
gesture duplicating another gesture), TASK-14/51 (the overview), TASK-19
(settings — the sheet may open settings surfaces from its entries),
`VIEWTOP-AND-DENIAL.md` (in-process vs client shell), doctrine §13 (whose verbs
are these; the user's 40 vs the agent's 60), §12 (the sheet is a surface, not a
daemon — it must read a binding table, never decide).

View file

@ -0,0 +1,125 @@
# TASK 56 — the island, and the bar's honest face
**Status: open — Part A is design questions only; Part B is small and buildable.
Created 2026-08-04, from Casey: the top bar is "static only due to a lack of
creativity and ingenuity" — the bar should clear a region ("an island") where a
notification can sprawl banner text; laptop/phone merge is the ultimate goal,
and "the agent is supposed to have full control on all the things soon".**
**Repos:** `souveraine` (shell surfaces, `ii-phone` bar), sessiond for the
island's decision side.
## What exists, measured
| Piece | Where | State |
|---|---|---|
| The phone bar | `surfaces/quickshell/ii-phone/modules/ii/bar/BarContent.qml` | Three anchored regions: left (sidebar button + carrier + active window), middle (clock), right (pill: wifi/mute/mic/notify-unread reveals, battery, resources, systray, weather). Floating rounded rect with border |
| The island's seed | `BarContent.qml:268-306``Revealer` + `Layout.rightMargin` Behaviors | The right pill already animates contents *apart* to make room; the island is this same primitive with a real budget |
| `bar.bottom` | `BarContent.qml:338` | Exists — an island always grows away from the screen edge, so a top bar and a bottom bar are one layout mirrored |
| Banners today | `ii-base/modules/ii/notificationPopup/NotificationPopup.qml` (phone shares the desktop one) | Corner-stacked, entirely separate from the bar — pure client noise; the bar's only hook is `NotificationUnreadCount` |
| Carrier | `surfaces/quickshell/services/Cellular.qml` — mmcli + gdbus `PropertiesChanged`, signal-driven; `operatorName`, `accessTech` (5G/LTE/H+/H/3G/E/G), `hasService`, `materialSymbol` | Bar shows `"<operator> <tech>"` e.g. "Fido LTE" + signal bars (`BarContent.qml:95-114`) |
| Network face | `services/Network.qml``materialSymbol` is already honest about wifi (connected → strength bars, disconnected → `wifi_find`, disabled → off) | The pill's always-visible face (`BarContent.qml:312-318`) — but it never shows cellular: wifi disconnected shows a scan glyph even while LTE is the live connection |
| Tap conflict | `BarContent.qml:50-68` | The whole left region is one `FocusedScrollMouseArea` (press = left sidebar toggle) — a carrier tap needs its own MouseArea ahead of the parent |
## Part A — the island
### A1. What is the island, exactly?
iPhone: the bar recedes around a pill that expands. Android: heads-up banners
drop below the status bar. Our phone is 540×1080 portrait — an island cannot
sprawl sideways; it grows *downward* into the screen. On the laptop it grows
sideways from the center. Same surface, two geometries — that is the
laptop/phone merge in miniature. Decide what class of content it carries:
notifications only, or the agent's general attention surface (turn activity,
timer runs, device-state changes)?
### A2. What happens to the displaced sections?
Compress or hide? Hide is where the desktop bar went wrong — modules vanish and
the user loses their place. The right pill's Revealers already compress; the
island is that mechanism given a budget. On the phone the left section
(sidebar button + carrier) and the clock must survive at reduced width or move
out of the island's sprawl path.
### A3. Who decides what lands in the island?
Today banners are client-driven noise (freedesktop notifications → corner
popup). For the island to be the agent's surface, the display decisions are
sessiond's — the surface reads a binding table, never decides (doctrine §12,
§13): island-worthy vs quiet (a policy), steal-vs-queue when a second banner
lands mid-sprawl (preemption is a decision), linger duration, whether the
screen dims during it (touches sessiond's dim/lock contract). Shape: an intent
reachable the way `overview toggle` is (`qs -c souveraine ipc call …`), plus a
routing policy over the existing notifications service. "A verb she cannot
reach is a defect."
### A4. Dismissal and touch
Tap = open the notification's app or the sidebar surface; swipe = dismiss.
These are gesture bindings — they come from sessiond's binding table, not
hardcoded QML. Also: does the bar remain interactive while the island is
sprawled (the three MouseAreas)?
### A5. Does it replace the corner popup on the phone?
The ii-base `NotificationPopup` is the current home of banner text, and the
phone still uses the desktop one. The island replaces it for island-worthy
classes on the phone; toasts / OSK / IME are separate concerns. Decide
coexistence before building.
## Part B — the bar's honest face (buildable now)
### B1. Carrier name, tap-to-cycle
Drop the tech suffix: the bar shows `operatorName` only — "Fido", no "LTE".
Tapping the name cycles **Fido → Casey → Fimeg** — a display alias, "just for
fun": the SIM still reports the real operator; the alias list is local.
Questions: persist (an existing settings key?) or per-session? Where does the
tap live — the carrier row needs its own MouseArea ahead of the left region's
sidebar-toggle press.
### B2. A live network-type indicator
"LTE/Wifi should be the indicator on what type of network we have running
live." The pill's wifi glyph never reflects cellular: wifi disconnected shows
a scan glyph while LTE is the live connection. The indicator should pick the
*active* network: wifi connected → the wifi strength glyph (existing
`Network.materialSymbol`); wifi not connected but cellular has service → the
tech (LTE/H/3G/5G via `Cellular.accessTech`); neither → the disconnected/off
glyph. The tech label thus moves out of the carrier name and into the
indicator — one source of truth, live.
## Acceptance (measured, on the phone)
Part B (buildable now):
- The bar shows "Fido" — no "LTE" suffix.
- Tapping the carrier cycles Fido → Casey → Fimeg, wraps, and the alias
survives per the persistence decision.
- The network indicator shows the live type: wifi glyph on wifi, tech label
(LTE/…) when wifi is down but cellular is up, off-glyph when neither — and
it flips on connection change without a shell restart.
- The left-region sidebar toggle still works on its own area.
Part A (once the questions are answered):
- A banner arrives and the bar clears space for it — sections compress,
nothing hides.
- Sessiond can put content in the island through a reachable intent — no
hardcoded QML text paths.
- Dismissal and open behave per the binding table; a second banner
mid-sprawl does the decided thing.
- The phone island grows downward, the laptop island sideways from the
center — one surface.
- No hyprctl anywhere in the path.
## Connects to
TASK-55 (the same bar region; gesture bindings and the sheet's dismissal
rules), TASK-30/31 (verb tables as the single source of what the agent can
drive — the island intent joins them), TASK-50 (the agent's touch path is the
mirror image of the island's), TASK-53 (one decider — a banner the agent
cannot influence is the same defect class), TASK-14/51 (the overview and the
bar's windows), doctrine §12 (the island is a surface, not a daemon — it reads
a binding table, never decides), §13 (the agent owns the device; "a verb she
cannot reach is a defect"), `VIEWTOP-AND-DENIAL.md` (in-process vs client
shell shapes where the island is drawn).

View file

@ -0,0 +1,99 @@
# TASK 57 — the Pi dock head
**Status: open.** Created 2026-08-04, from Casey: *"I need a 'I plugged in
this dock and my pixel 3 screens lit up' system"* — the Pi 4 becomes the
phone's display and input head. The phone stays a gadget (device mode —
charging and tethering keep working), the Pi is the host that drives the
monitor and the dock's peripherals, and the phone's desktop streams to it
over the existing NCM link. Rejected paths, already argued: scrcpy (Android
only — no ADB on mainline), waypipe (forwards *apps*, not a shell; our shell
is a compositor, not forwardable), and DP alt-mode (unwired/unknown on
blueline — the Pi bridge works today).
**Repos:** the phone side is `souveraine-viewtop` (capture) + the phone
kernel/userspace; the Pi side is its own OS (bring-up state unknown — "major
malfunctions" TBD, find them first).
## The topology
```
[Dell dock: keyboard/mouse + monitor] [Pixel 3, device mode]
│ HDMI / USB-A │
▼ USB-C→A
[Pi 4] ◄──────────────────────────────────────────────┘
│ NCM link (usb0, 172.16.42.x) — phone compositor
│ capture → Venus H.264 → out over the cable
└── always-on: udev sees the phone's NIC → viewer fullscreen
```
The phone's port stays in the one mode that already works. The Pi's USB-A
port is a host: it sources VBUS (charges the phone, ~1.2 A shared), and the
phone's default developer gadget (NCM + ACM via usb-signaller) comes up the
moment the cable lands — no configuration on either side.
## What exists, measured
| Piece | Where | State |
|---|---|---|
| NCM gadget | usb-signaller default developer mode, `usb0` 172.16.42.1 | Live — WiFi SSH (`10.10.20.234` via wlan0) survives everything USB, so the management path is never at risk |
| Venus H.264 encoder | `/dev/video12`, decoder `/dev/video13` (v4l-utils 1.32.0-2) | Present; encoder idle and unproven at 1080p — load it first |
| Compositor capture | `souveraine-viewtop` — window capture landed (`86d0f93`) | Written and merged; **whether `zwlr_screencopy` exists on the wire is unverified** — if not, expose capture through our own IPC |
| Pi 4 | Casey's, bring-up TBD | Pi state unknown; needs OS + network check before anything |
| Link headroom | USB 2.0 HS, 480 Mbps vs H.264 1080p30 at ~510 Mbps | ~50× — bandwidth is not the constraint, latency is |
| Phone screen | 540×1080 portrait | The Pi must scale/rotate the stream to the monitor — free the Pi to do the transform (hardware scaler on the Pi 4) |
## Phases
1. **Pi bring-up** — what's on it, what's broken, network reach. Then plug
the phone into the Pi's USB-A: confirm the Pi sees a new NIC
(`172.16.42.1` pingable), the phone charges, tethering still works.
2. **Static chain** — phone: test pattern (or any source) → Venus H.264 →
NCM; Pi: decode (hardware) → fullscreen on HDMI. Proves the transport
before the capture exists. Latency budget: <100 ms is a usable desk;
measure, don't assume.
3. **Real capture** — phone: compositor capture → Venus encode → stream.
Trigger: the phone starts streaming when `usb0` has carrier (not the typec
udev event — blueline mainline has **no typec driver**, so usb-signaller's
cable monitor never fires; the NIC is the honest signal). Pi: udev rule on
the phone's gadget NIC → launch viewer.
4. **Input** — dock keyboard/mouse → Pi evdev → over the same cable → inject
into the phone's compositor via its IPC (both ends are ours). This is the
step that makes it a *dock* rather than a monitor.
5. **Parked — the car version.** A car head unit is the same problem with a
dashboard for a far end; the same bridge applies over NCM or WiFi.
"Android Auto" against *stock* head units is a separate universe — the AA
transport is closed Android proprietary userspace (the `f_accessory`/AOA
family lives in Android's kernel forks, not mainline), no open phone-side
implementation, zero kernel work involved. Not a task until Casey wants it
to be.
## Open questions
- Screencopy or custom capture IPC? (Determines the phone-side build.)
- Does the Venus encoder take the compositor's buffer format directly, or is
there a packing/stride hop (TASK-46 hit this with CAMSS)?
- Portrait 540×1080 vs monitor: stream native and let the Pi scale, or
rotate on the phone? (Native stream + Pi transform is simpler.)
- Latency target per use: desk cursor (<100 ms) vs video (<200 ms).
## Acceptance (measured)
- Plug the phone into the Pi's USB-A: the monitor lights up with the phone's
desktop within seconds, no interaction on either side, phone keeps
charging.
- The phone's own screen can be off/dimmed while the Pi shows the desktop.
- The laptop's shell and the phone's wifi networking are untouched by a dock
session.
- Dock keyboard/mouse drive the phone's desktop (Phase 4) with no visible
double-handling of input.
- A Pi that loses the phone exits cleanly and waits (no zombie viewer).
## Connects to
TASK-58 (host power — when the SMB2 boost lands, the phone can power the Pi
through the same cable, killing the Pi's wall wart), TASK-51 (the capture
punch list — the same capture feeds both UI verification and this stream),
TASK-54 (the frame-callback culling — the phone-side stream must not fight
the compositor's render budget), `USB/README.md` (the exploration record and
the rejected-paths argument), usb-signaller (the gadget side is already
built and default).

View file

@ -0,0 +1,81 @@
# TASK 58 — host power: the SMB2 OTG boost
**Status: open — investigation first, patch second.** Created 2026-08-04,
from Casey: *"host mode still — what about the phone sharing its power to
another device?"* One kernel change unlocks host-mode *power*, power sharing
(power-bank mode), and docks without a Pi in between. **Repos:** `Pixel3Arch`
(kernel build/packaging), the running kernel tree (identify first).
## The measured wall
| Fact | Evidence |
|---|---|
| Host *data* works | Role flip to `host` via `/sys/class/usb_role/a600000.usb-role-switch`; xHCI registered, SuperSpeed, uptime clean — proven live |
| The missing half is power | Plugged a bus-powered NVMe: no LED, zero enumeration (only root hubs on the bus). The phone does not source VBUS |
| Charger is mainline | `pmi8998-charger` bound by **`qcom-smbx-charger`** (`qcom,pmi8998-charger` @ `spmi c440000 pmic@2 charger@1000`) |
| Boost not exposed | 41 regulators in `/sys/class/regulator`, zero boost/otg/vbus — the driver never registers it |
| No typec | `/sys/class/typec` empty — no tcpm, no Rp on CC, so no C-to-C sink/source detection |
| dwc3 wires no power | `usb_1_dwc3` has no `vbus-supply`; the only VBUS model in mainline is db845c's GPIO fixed regulator with a *"FIXME: this regulator is responsible for VBUS on the left USB port"* |
| The hardware exists | Google's vendor DT drives the SMB2's 5 V boost device-side (`ext_5v_boost` + `usb0 { google,switch-vbus = <250>; }`, `pmi8998_pdphy` as `TYPEC_PORT_DRP` default `TYPEC_SINK`) — the boost is in silicon, hidden behind the SLPI-era vendor stack |
| Ceiling | 5 V/~1.5 A out. USB-PD at 12/20 V lives in the SLPI firmware, which mainline does not run — that wall stands; nothing this task wants needs it |
Dylan Van Assche's usb-signaller (the mode host) never touches VBUS either —
his design contract is *"the kernel owns power"*: his platforms' kernels
auto-switch role on cable plug and source VBUS. The role half of that
contract works on blueline; the power half is exactly what this task is.
## Phase 1 — investigate (do this before any patch)
1. **Find the running kernel's source**`uname -r` on the phone, match the
tree that built it. The rebuild/packaging path is the known sore spot:
`kernel.yml` has never gone green and flashing is retired (cold boot only
— a warm reboot poisons TZ/remoteproc).
2. **Read `qcom-smbx-charger`** — mainline file (likely
`drivers/power/supply/qcom_smb2.c`): does it touch the SMB2 boost/OTG
registers at all? What does the vendor `switch-vbus` actually drive?
Pull Google's `android_kernel_google_msm-4.9` charger source (the fetched
vendor DT already points at `pmi8998_gpios 8` for extcon VBUS — the GPIO
*detects*, the boost is PMIC-internal).
3. **`.config` on the phone** — `/proc/config.gz` (or the build config):
`CONFIG_TYPEC`, `CONFIG_TYPEC_TCPM`, `CONFIG_TYPEC_QCOM_PMIC` presence.
The pmi8998 typec block's mainline driver is
`drivers/usb/typec/tcpm/qcom_pmic_typec.c` — confirm whether it binds
pmi8998 or only newer PMICs.
4. **SMB2 register docs** — the boost enable is in the charger's register
map; find it in the vendor kernel (that `250` in `switch-vbus` is a
register-relative offset hint).
## Phase 2 — the patch
1. **smb2: expose the OTG boost as a vbus regulator** — the silicon is in
the SMB2 charger; mainline's driver just never registers it.
2. **blueline DT: `vbus-supply = <&boost>` on `usb_1_dwc3`** — role `host`
then sources 5 V the way every other mainline board sources it.
3. **TYPEC/TCPM + `qcom_pmic_typec`** (or the minimal path if it doesn't
bind) — Rp on CC so C-to-C *sinks* (the Dell dock's cable, USB-C
peripherals) see a source. Without this, only legacy USB-A targets via
adapter can be powered; with it, proper OTG.
Then the phone in `role=host`: powered *and* enumerating. The same lock also
lets the phone power the Pi in TASK-57's topology — one cable, no wall wart.
## Acceptance (measured on the phone)
- `role=host` + a bus-powered device on a USB-A adapter: device lights up
*and* enumerates (`/sys/bus/usb/devices`), where today it does nothing.
- The first-test NVMe lights up.
- `/sys/class/regulator` shows the vbus/boost regulator and it toggles with
the role.
- Device mode untouched: plugged into a charger, `pmi8998-charger` still
reports `online=1` charging, tethering/dev gadget still provision.
- Role flips stay safe under the WiFi SSH path; usb-signaller restart
re-provisions after any manual flip.
## Connects to
TASK-57 (the Pi dock head — this unlocks phone→Pi power over the same
cable), `USB/README.md` (the exploration record: evidence table, correction,
live role-switch recipe), `NET_KERNEL_GAPS.md` (the kernel rebuild/reflash
path — tier 3's blocker is the same gating), TASK-25 (kernel package
signing), usb-signaller (the userspace that will switch roles against the
kernel's power contract).

View file

@ -59,6 +59,10 @@ Four that are cheap relative to what they unblock:
| # | Task | What's left |
|---|------|-------------|
| 58 | [Host power: the SMB2 OTG boost](58-host-power-smb2-boost.md) | Host data works; the boost isn't. The SMB2 5 V boost exists in silicon and mainline's `qcom-smbx-charger` never registers it. Investigation first (which tree builds the running kernel, does smb2 touch the boost, does `qcom_pmic_typec` bind pmi8998), then vbus-supply + regulator + typec. Unlocks host-mode power, power-bank mode, and the Pi without a wall wart. |
| 57 | [The Pi dock head](57-pi-dock-head.md) | Phone stays a gadget; the Pi 4 drives the monitor + dock. NCM link is default, Venus encoder is present, compositor capture landed — the unproven seams are `zwlr_screencopy` vs our own capture IPC, and the Pi's own bring-up ("major malfunctions TBD"). Four phases: Pi bring-up, static chain, real capture + udev trigger, input injection. |
| 56 | [The island, and the bar's honest face](56-island-and-the-bars-honest-face.md) | Bar clears a region for sprawling notification banners, agent-controlled (Part A: design questions). Part B is buildable now: drop the LTE suffix, tap-to-cycle the carrier name (Fido → Casey → Fimeg), and a network-type indicator that shows the live connection instead of a wifi glyph that never reflects cellular. |
| 55 | [The hand's verbs: a window action sheet](55-hand-verbs-on-the-glass.md) | Design questions only. Three-finger tap currently opens the same overview the rail reaches; the sheet needs Move/Resize/Split/Close with real verbs, and Home must become a compositor verb — the rail's is `hyprctl` and dies under viewtop. |
| 52 | [Atmosphere](52-atmosphere.md) | Attention as a compositor capability — the fifteen-technique vocabulary. Two are **already landed** (`expose` is negative space, `pose` is gravity wells), and `compile_custom_pixel_shader` + `PixelShaderElement` are already in our smithay, so most of the rest needs no new machinery. Build the attention model and one clock **first**; fifteen uncoordinated animators is §1's blind actors in the render path. |
| 50 | [Her hand on the glass](50-agent-hand-on-the-glass.md) | Every scene intent reaches the compositor now **except touch** — so she can move, scale, dim and close a window and cannot press anything inside one. The receiving end is built (`Origin::Agent`, the disjoint slot namespace, the evidence predicate); nothing calls it, so she still reaches touch through uinput, which is the exact conflation `input.rs` exists to prevent. Also owns the inverse mapping `pose` owes the hit test. |
| 51 | [viewtop to daily driver](51-viewtop-to-daily-driver.md) | The punch list between *runs* and *stable*, all of it found by using the phone: windows stack with nothing to tell them apart, screen capture is written and unmerged (so no UI change can be verified), volume buttons are recognised and dropped, and five named holes that are one decision each. |