compositor pushes the strip; float, gamma
This commit is contained in:
parent
ec552a82a2
commit
1aa61030dd
5 changed files with 479 additions and 3 deletions
122
HANDOFF-2026-08-05-navigation.md
Normal file
122
HANDOFF-2026-08-05-navigation.md
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
# Handoff — 2026-08-05, navigation & the window sheet
|
||||
|
||||
**Your job first: audit what this session did against the docs, then judge
|
||||
whether it actually delivered Casey's ask.** Do not start new work until you
|
||||
have. Several things were built fast and one was built wrong and rewritten.
|
||||
|
||||
## Read these FULLY. Do not grep them.
|
||||
|
||||
Grepping these produces confident, wrong architecture — it is the single most
|
||||
reliable failure in this project, and it is written down in three places.
|
||||
|
||||
1. `SouveraineOS/START-HERE.md`
|
||||
2. `SouveraineOS/docs/tasks/55-hand-verbs-on-the-glass.md` — the sheet; most of
|
||||
this session is its "what landed" section
|
||||
3. `SouveraineOS/docs/tasks/60-the-multitasking-view.md` — written after the
|
||||
multitasking view was built wrong and Casey said so. **Uncommitted.**
|
||||
4. `SouveraineOS/docs/SESSION-AUTHORITY-DOCTRINE.md` — §13 first, then §4, §9-11
|
||||
5. `SouveraineOS/docs/DEVICE-STATE-MACHINE.md` — §1, §4, §12
|
||||
6. `SouveraineOS/docs/LOCK-DPMS-LESSONS.md` — §1 is the lock-then-blank invariant
|
||||
7. `souveraine-viewtop/docs/SHELL-BOUNDARY.md` — the engine runs in-process
|
||||
8. `souveraine-viewtop/crates/compositor/src/workspace.rs` — header only; the
|
||||
zone model, and why a zone exists because something is on it
|
||||
9. `souveraine-viewtop/crates/compositor/src/gesture.rs` — header only; the
|
||||
compositor recognises, the shell draws
|
||||
10. `SouveraineOS/docs/tasks/59-her-face-on-the-glass.md` — TASK-59 phase 1
|
||||
passed on hardware; phase 2 is unstarted
|
||||
|
||||
## State of the tree — READ THIS BEFORE TOUCHING ANYTHING
|
||||
|
||||
- **`souveraine` has UNCOMMITTED work.** `ZoneOverview.qml` (new), the rail's
|
||||
gesture rewrite, `Overview.qml` rewired to zone cards, `deploy.sh` manifest,
|
||||
`ViewtopControl.qml`. It is **deployed to the phone** and **not committed**.
|
||||
Commit or discard deliberately; do not deploy over it without looking.
|
||||
- **`souveraine-viewtop` is pushed** at `8e6700b` and **not yet on the phone**.
|
||||
The device runs `r84.g2d0ffdc`. So the compositor there does not yet report
|
||||
`focused`, `app_id` or `title` — which `ZoneOverview` needs to group and
|
||||
label cards. **Until `r85+` installs, the new overview will draw zones but
|
||||
its cards cannot match pictures to windows.** That is expected, not a bug.
|
||||
- CI publishes automatically now: green `main` on viewtop dispatches
|
||||
Pixel3Arch's `packages.yml`. Verified end-to-end (run 1176 → 1177).
|
||||
|
||||
## What was asked, and what happened
|
||||
|
||||
Casey's asks, in order given:
|
||||
|
||||
1. **Remove three-finger tap from everything binding it** — done. It opened the
|
||||
overview; `Action::Overview` is gone.
|
||||
2. **Three-finger tap opens an overlay on the app: resize, split, close** —
|
||||
built (`WindowSheet.qml`), deployed. Now four buttons: Move, Resize, Split,
|
||||
Close. Close is polite on tap, `kill` on a 1 s hold.
|
||||
3. **Zones/states, not workspaces; drop Hyprland verbiage** — done in new code
|
||||
and in the shell. **The wire op is still spelled `workspace`** and the
|
||||
compositor's `workspace.rs` still uses the old noun throughout. A rename
|
||||
sweep is owed and was deliberately not started.
|
||||
4. **Long-press power → context overlay** — built (`PowerMenu.qml`), shows real
|
||||
USB-C data role + charger state. Not lock-gated.
|
||||
5. **Short swipe → multitasking, long swipe → home; dock always on zone one** —
|
||||
built. Home is 18% of panel height.
|
||||
6. **Multitasking must show all backgrounded zones including splits** —
|
||||
**this is the one that was built wrong twice.** First as a live scale of the
|
||||
active zone (a transition mistaken for a destination), then rewritten as
|
||||
`ZoneOverview.qml`. **The rewrite is deployed but UNVERIFIED on glass.**
|
||||
TASK-60 holds the design questions that were answered by fiat, not by Casey:
|
||||
card = zone, `ScreencopyView` per window inside it, focus reported by the
|
||||
compositor, refresh on open, scale-on-drag deleted. **Check those against
|
||||
what he actually wanted.**
|
||||
|
||||
## Bugs found and fixed, worth verifying rather than trusting
|
||||
|
||||
- **`pose` never worked.** It was applied by passing a smaller scale to
|
||||
`render_elements_from_surface_tree`, which positions but does not resize; the
|
||||
size comes from `Element::geometry(scale)`, called later with the *output*
|
||||
scale. Windows moved and never shrank. Fixed with `RescaleRenderElement`.
|
||||
Casey diagnosed it precisely: *"the content slid to meet the upper left
|
||||
corner instead of shrinking."* **Verify on glass — it was confirmed broken by
|
||||
eye, and the fix has not been confirmed the same way.**
|
||||
- **Firefox drew off-centre** because the main render path never subtracted
|
||||
`window.geometry().loc` (CSD shadow inset). The popup and capture paths both
|
||||
did. Fixed. Not folded into `window_origin` on purpose — that feeds the hit
|
||||
test, and correcting it there would move touch targets instead.
|
||||
- **Every window got two zone-membership entries** — `new_toplevel` had a
|
||||
pre-existing `on_workspace.push` plus the new explicit claim. Duplicates keep
|
||||
empty zones alive forever, which is what `prune_workspaces` exists to prevent.
|
||||
- **Three-finger drag deleted outright**, not retuned: it beat the tap to nearly
|
||||
every gesture. The window carry came back as `grab`/`drop`, entered from the
|
||||
sheet. While grabbed, `Route::Client` becomes `Withheld` so the app underneath
|
||||
stops receiving the finger.
|
||||
- **CI never published viewtop.** Its own workflow only ran fmt/clippy/test/
|
||||
cross-build. Fixed by dispatching Pixel3Arch's job on green `main`; the 403
|
||||
was `LOCAL_GITEA_TOKEN` existing only as a Pixel3Arch repo secret.
|
||||
|
||||
## Known-not-done — say so rather than discovering it later
|
||||
|
||||
- **The dock when an app opens.** Casey asked twice; never resolved. The dock is
|
||||
a *global* layer surface whose visibility is now *per-zone* (pinned on home).
|
||||
Its exclusive zone may still reserve space on every zone. Needs eyes on glass.
|
||||
- **A real resize handle.** Resize cycles half → two-thirds → full. No corner
|
||||
drag.
|
||||
- **Widgets on zone one.** It is kept clear and never pruned; nothing fills it.
|
||||
- **The runtime-lock paint gap.** Power → black → lockscreen paints → blank.
|
||||
Decisions are correctly ordered (`lock-before-blank` → `lock-directive` →
|
||||
`panel-off`); the black is `frame_elements` compositing lock surfaces *and
|
||||
nothing else* while the shell's lock surface has not painted. Casey's rule:
|
||||
lock, confirm the lockscreen is up, *then* blank — which is viewtop's to
|
||||
attest, since only it knows a lock surface is presented on every output.
|
||||
- **Casey's OSK bug from the morning handoff** (search bar "jerked", keyboard
|
||||
did not open) — never investigated. Untouched by this session.
|
||||
- **`app_id`+`title` as a join key is approximate.** Two terminals with the same
|
||||
title collide. The sound fix is a shared id in foreign-toplevel.
|
||||
|
||||
## The trap that cost this session the most time
|
||||
|
||||
Reaching for the phone's control socket earlier would have saved an hour:
|
||||
`$XDG_RUNTIME_DIR/souveraine/viewtop.sock`, one JSON object per line.
|
||||
`{"op":"describe"}` is the authoritative verb table, `{"op":"workspaces"}` lists
|
||||
zones and windows, and `{"op":"scene","intent":"close","id":N}` closed two apps
|
||||
Casey could not close — no rebuild, no deploy. The phone has no socat/nc but has
|
||||
python3.
|
||||
|
||||
Also: `qs -c souveraine ipc …` needs `WAYLAND_DISPLAY` set or it reports "no
|
||||
running instances" while the shell is plainly running. sessiond has it; an ssh
|
||||
session does not.
|
||||
|
|
@ -22,6 +22,24 @@ accepted**. The **CS40L20 is absent from this kernel entirely** — zero dmesg
|
|||
hits, no i2c node. So this is no longer "which of two drivers"; it is one live
|
||||
driver and one unbuilt one.
|
||||
|
||||
**Measured again 2026-08-05, at the device, and the chain is hot end-to-end.**
|
||||
A live check from the laptop repeated the 2026-08-02 result and added the two
|
||||
things the O2 did not know:
|
||||
|
||||
- `feedbackd` is **D-Bus activatable** (`/usr/share/dbus-1/services/
|
||||
org.sigxcpu.Feedback.service`), not a systemd unit — the unit will never
|
||||
"exist", it activates on first D-Bus caller. Profile `quiet` is live
|
||||
(`Profile` property reads `"quiet"`), and the udev rule types `spmi_haptics`
|
||||
as `FEEDBACKD_TYPE=vibra` with `TAG+="uaccess"` — `casey` holds `rw` ACL on
|
||||
`/dev/input/event5`.
|
||||
- The shell's exact call (`TriggerFeedback` `button-pressed`/`button-released`/
|
||||
`bell-terminal` as shown in `Haptics.qml`) **all succeeded** (feedback ids
|
||||
59–65 in a row), and feedbackd holds fd 5 open on `/dev/input/event5`.
|
||||
- **Correction to the old acceptance:** `/sys/class/leds/vibrator/brightness`
|
||||
**does not exist on this kernel**. The only output path is the input FF
|
||||
interface (`EVIOCSFF`/play), and that path is confirmed working. The
|
||||
acceptance line forgiving `/sys/class/leds/vibrator` must change.
|
||||
|
||||
**Question 1, now down to one thing a hand can answer: does it buzz?** If yes,
|
||||
the PMIC reaches the motor and the Cirrus part is an upgrade. If no, the LRA is
|
||||
wired to the CS40L20, nothing has ever vibrated, and this task is the *enabler*
|
||||
|
|
@ -73,9 +91,12 @@ CS40L20's, not the PMIC's.
|
|||
## Acceptance
|
||||
|
||||
- Question 1 answered in writing, at the device, before the port starts.
|
||||
- If the port proceeds: probe + firmware load visible in `dmesg`, a vibration
|
||||
from `/sys/class/leds/vibrator/brightness`, and f0 tuned against this unit's
|
||||
factory values rather than the defaults.
|
||||
- If the port proceeds: probe + firmware load visible in `dmesg`, and f0 tuned
|
||||
against this unit's factory values rather than the defaults.
|
||||
- A trigger through feedbackd (the `TriggerFeedback` path the shell already
|
||||
uses) results in the LRA firing — the physical half of Question 1. The old
|
||||
`/sys/class/leds/vibrator/brightness` check is **retired**: that node does not
|
||||
exist on this kernel; the input FF interface is the only output path.
|
||||
- feedbackd drives it through the same profile the dial already uses — the
|
||||
shell must not learn a second haptics API. One owner per input
|
||||
(`SESSION-AUTHORITY-DOCTRINE` §10) applies to outputs too.
|
||||
|
|
|
|||
141
docs/tasks/60-the-multitasking-view.md
Normal file
141
docs/tasks/60-the-multitasking-view.md
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
# TASK 60 — multitasking is a place, not a transition
|
||||
|
||||
**Status: open — design, 2026-08-05.** Written after building the wrong thing
|
||||
and being told so. Casey, on device:
|
||||
|
||||
> *"Still major issues. We might need to take these concepts we've built back to
|
||||
> the workshop. The scaling doesn't follow the selected app. meaning app#2
|
||||
> opened even if focused is not scalling down. Second, all we have is scaling;
|
||||
> it's supposed to be showing me all sorta backgrounded zones; including split
|
||||
> zones. You've seen macOS. That whole multitasking view is a function of it's
|
||||
> own."*
|
||||
|
||||
**Repo:** `souveraine` (shell surfaces), `souveraine-viewtop` (capture + zone
|
||||
facts).
|
||||
|
||||
## What was built, and why it is not the thing
|
||||
|
||||
The rail's short swipe scales the live windows on the active zone from 1.0 to
|
||||
0.6 as the thumb climbs (`ViewtopControl.poseActiveZone`), then opens
|
||||
`GlobalStates.missionControlOpen`. That is a **transition** — the animation of
|
||||
leaving an app — and it was mistaken for the **destination**.
|
||||
|
||||
Two failures follow from that mistake, and both were seen on hardware:
|
||||
|
||||
1. **It scales the wrong windows.** `poseActiveZone` poses everything whose
|
||||
`workspace` matches `ViewtopControl.activeZone`, and that list comes from a
|
||||
**2-second poll** of `{"op":"workspaces"}`. Open a second app and the poll
|
||||
has not caught up, so the newly focused window is not in the set being
|
||||
scaled. There is no "focused window" fact in the shell at all — the
|
||||
compositor knows it (`Surface::focused`) and does not report it in the
|
||||
`workspaces` reply.
|
||||
2. **Scaling one zone is not a view of all of them.** Even done perfectly, it
|
||||
shows the zone you are already on, smaller. It cannot show what is
|
||||
*backgrounded*, which is the entire question multitasking answers.
|
||||
|
||||
## What exists
|
||||
|
||||
| Piece | Where | State |
|
||||
|---|---|---|
|
||||
| `WindowOverview.qml` | `modules/souveraine/navigation/` | One column of equal cards, one per **window**, live `ScreencopyView` texture, flick-up to dismiss |
|
||||
| Zone facts | `{"op":"workspaces"}` | `count`, `active`, `offset` (the float the strip is scrolled to), `settled`, and per window `{id, workspace, at, size}` |
|
||||
| Per-window capture | viewtop, since 2026-08-03 | A card can show a window rather than the screen covering it |
|
||||
| `pose` / `unpose` | viewtop | Now genuinely scales content (fixed 2026-08-05 — it had only ever moved it) |
|
||||
|
||||
**`WindowOverview.qml`'s own header states the gap**: *"viewtop has them
|
||||
[workspaces], and they are not Hyprland's: a continuous strip that grows when a
|
||||
window needs a room and shrinks when the last one leaves… That is a thing this
|
||||
overview could draw and does not yet."* This task is that sentence.
|
||||
|
||||
## What macOS actually does, since it is the named reference
|
||||
|
||||
Mission Control is **two levels in one surface**: a row of *spaces* across the
|
||||
top, and the windows of the current space spread below. Neither alone is the
|
||||
feature — the row answers "where else am I?" and the spread answers "what is
|
||||
here?". Dragging a window onto a space in the row moves it there, which is the
|
||||
same gesture doing placement.
|
||||
|
||||
Ours differs in one load-bearing way and it must not be papered over: **macOS
|
||||
spaces are a fixed set the user creates; viewtop's zones are minted and
|
||||
destroyed by need** (`workspace.rs`: "a workspace exists because something is on
|
||||
it"). A row of zones is therefore a row that grows and shrinks under you, and
|
||||
home is the one that is always there and always empty.
|
||||
|
||||
## The questions
|
||||
|
||||
### Q1. What is a card — a window, or a zone?
|
||||
|
||||
Today it is a window. macOS says both, at two levels. A phone has room for one
|
||||
level at a time. Options: zones-as-cards with their windows composited inside
|
||||
(split zones then read correctly, which is Casey's explicit ask); windows-as-
|
||||
cards grouped by zone; or a two-level surface where the zone row is a strip and
|
||||
tapping one spreads its windows. **Q's real content:** a split zone has two
|
||||
windows and must look like one thing you can switch to, not two.
|
||||
|
||||
### Q2. Live textures, or posed real windows?
|
||||
|
||||
Two mechanisms now exist and they are not the same:
|
||||
|
||||
- `ScreencopyView` — a live *picture*. Cheap to lay out anywhere, but it is not
|
||||
the window; input goes to the card.
|
||||
- `pose` — the *real* window, scaled, still touchable through `unpose`'s inverse
|
||||
mapping.
|
||||
|
||||
The reference compositor composes client windows as external textures directly
|
||||
into the shell's scene, which is the third answer and the one the in-process
|
||||
Flutter shell would make natural (`SHELL-BOUNDARY.md`). Choosing here decides
|
||||
whether the overview is a gallery of pictures or the actual desktop, zoomed
|
||||
out — and whether the transition can be continuous into it.
|
||||
|
||||
### Q3. Where does the focused window come from?
|
||||
|
||||
The shell has no such fact. `Surface::focused` exists in the compositor's
|
||||
registry and is not in the `workspaces` reply. Any answer to Q1 needs it, and it
|
||||
should be reported rather than inferred — inferring "the last window I saw
|
||||
open" is the kind of shadow copy §4 spends its whole argument against.
|
||||
|
||||
### Q4. Is the poll acceptable?
|
||||
|
||||
`ViewtopControl` polls every 2 s. That is already the proximate cause of failure
|
||||
1. A view built on it will be wrong every time something changes faster than
|
||||
two seconds — which is every gesture. Either the compositor pushes zone/window
|
||||
changes, or this surface asks synchronously when it opens and stops guessing in
|
||||
between. **Design the push channel; it is owed anyway.**
|
||||
|
||||
### Q5. What does the transition become?
|
||||
|
||||
If the destination is a real view, the scale-on-drag is either (a) the first
|
||||
frames of it, continuous into the cards, or (b) noise that should be deleted.
|
||||
macOS and Phosh both do (a) — Phosh's `home.c` is a drag surface travelling
|
||||
between two states, not a button. (a) is better and harder: it means the
|
||||
transition and the view share one progress value, which is the reference
|
||||
overview's whole trick (`WindowOverview.qml` already cites it: "one `progress`
|
||||
driving everything").
|
||||
|
||||
### Q6. What can you do to a card?
|
||||
|
||||
Switch to it, certainly. Close it — the `close`/`kill` verbs exist. Move a
|
||||
window between zones — `{"op":"workspace","to":N,"surface":id}` exists too, so
|
||||
drag-a-card-onto-a-zone is reachable today. Decide before building, because it
|
||||
changes whether cards need to be drag targets.
|
||||
|
||||
## Acceptance (measured, on the phone)
|
||||
|
||||
- Every zone with something on it is represented, and a **split zone reads as
|
||||
one destination containing two windows** — not as two unrelated cards.
|
||||
- The view reflects state at the moment it opened, not up to two seconds stale.
|
||||
Open an app, immediately open multitasking: the new app is there.
|
||||
- Tapping a card goes to that zone. Home is reachable from it.
|
||||
- Whatever drives the transition and whatever draws the view agree — no frame
|
||||
where a window is scaled by one and laid out by the other.
|
||||
- The scale-on-drag either continues into the view or is gone. Not both.
|
||||
|
||||
## Connects to
|
||||
|
||||
TASK-14 (the overview pane and its app grid; "a grid of workspace thumbnails is
|
||||
dead weight on a phone" is the constraint that made cards windows in the first
|
||||
place), TASK-55 (the sheet, and `ViewtopControl` as the one path to scene
|
||||
verbs), TASK-43 (the compositor's surface model and per-window capture), TASK-52
|
||||
(atmosphere — `pose` is a gravity well and this is its first real user),
|
||||
`VIEWTOP-AND-DENIAL.md` (external textures into the shell's scene; Q2's third
|
||||
answer), `SHELL-BOUNDARY.md` (the in-process shell that would make it natural).
|
||||
191
docs/tasks/61-color-gamma-panel.md
Normal file
191
docs/tasks/61-color-gamma-panel.md
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
# TASK 61 — the colour path, whole: gamma, gamut, sRGB, precision
|
||||
|
||||
**Status: 2026-08-05 — scoped, not started.** The full design of record for
|
||||
restoring colour fidelity on viewtop. This is one task, not a roadmap with
|
||||
"build this first": every piece below is in scope and nothing is waved off as
|
||||
deferred. It closes three separate, code-confirmed defects:
|
||||
|
||||
1. **Gamma control is dead.** The shell's brightness→gamma slider shells out to
|
||||
`hyprctl hyprsunset …` (`surfaces/quickshell/ii-base/services/Hyprsunset.qml:110,116,125`).
|
||||
That is Hyprland IPC. viewtop exposes no `hyprctl`, no gamma global, no LUT
|
||||
path — so the call silently no-ops since the viewtop move.
|
||||
2. **The panel is wide-gamut and unmapped.** blueline's panel is the LG SW43408 —
|
||||
1080×2160, 4-lane DSI, ~DCI-P3 OLED, 24-bit RGB, DSC, no EDID. viewtop emits
|
||||
raw sRGB bytes onto it with zero gamut mapping (`kms.rs` scans out
|
||||
`[Xrgb8888, Argb8888]`, and the forked GLES has no sRGB path at all), so
|
||||
everything renders against Hyprland saturated.
|
||||
3. **Nothing asks for more than 8-bit sRGB.** `kms.rs:340` hardcaps the scanout
|
||||
formats; the fork renders `GL_RGBA8` and never enables the pieces it already
|
||||
knows about — `egl/display.rs:606` carries `srgb: false` and a `// TODO:
|
||||
EGL_KHR_gl_colorspace to know that`.
|
||||
|
||||
**Repos:** `souveraine-viewtop` (compositor, wire verbs, and the smithay GLES
|
||||
pin that builds the renderer) and `souveraine` (shell surfaces). The kernel/DPU
|
||||
is deliberately not changed by this task; the door is left open and a gate
|
||||
written (Part 6).
|
||||
|
||||
## Part 1 — gamma is a verb, not a hack
|
||||
|
||||
Gamma is an authority claim on the glass, so it goes in the compositor's own
|
||||
verb table — `crates/wire/src/verbs.rs`, the `INTENTS` array whose round-trip
|
||||
test at the bottom of that file makes a lying table fail the build.
|
||||
|
||||
Add one entry:
|
||||
|
||||
```json
|
||||
{"intent":"gamma","value":45}
|
||||
```
|
||||
|
||||
- `mutates: true`; refuses `InvalidArgument` | `RefusedByState` | `Unavailable`
|
||||
(the last for "panel is off").
|
||||
- `value` in 0..=100, mapped to the panel's ramp; the `example` must round-trip
|
||||
through `ToCompositor` like every other line.
|
||||
|
||||
It does **not** route through sessiond. The compositor owns pixels and LUTs;
|
||||
the state machine owns device states. Gamma is a pixel claim, not
|
||||
proprioception — the same boundary TASK-30/31 drew for the other pixel verbs.
|
||||
|
||||
**Apply side.** The fork's `DrmDevice` implements `ControlDevice`
|
||||
(`backend/drm/device/mod.rs:128`), and the pinned `drm` crate exposes
|
||||
`Device::get_gamma` / `set_gamma(crtc, red, green, blue)` plus
|
||||
`crtc.gamma_length()` (`drm-0.14 control/mod.rs:599,627`, `control/crtc.rs:93`).
|
||||
So the handlers:
|
||||
|
||||
- build a `value`→curve ramp of the panel's own length (identity at 100),
|
||||
- `self.kms.drm.set_gamma(crtc, &lut…)` on the verb,
|
||||
- reset to identity on boot and on `session_paused`/`session_resumed`, so a
|
||||
ramp never outlives its session or survives a resume wrong-headed.
|
||||
|
||||
A colour-temperature curve (Part 5) is the same slot with a different curve
|
||||
generator.
|
||||
|
||||
**Shell side.** Replace `Hyprsunset`'s channel, not its API. Wire the QML
|
||||
service to the compositor socket — either directly or through the `shell-host`
|
||||
crate, which already speaks the wire — and keep the public surface byte-identical:
|
||||
the Brightness↔Gamma toggle, `gamma`, `temperature`, automatic timing. Then
|
||||
every caller (`QuickSliders.qml`, `GammaIndicator.qml`, the night-light auto
|
||||
clock) keeps working without touching it.
|
||||
|
||||
## Part 2 — the sRGB/linear pipeline, and the gamut matrix
|
||||
|
||||
This is the heart of the "colour" and lives in the GLES fork that viewtop is
|
||||
already pinned to (the fork is ours; this is one more set of patches on top of
|
||||
that pin).
|
||||
|
||||
1. **Mark the encode.** Import buffers the client claims sRGB as
|
||||
`GL_SRGB8_ALPHA8` so texture sampling *decodes* the gamma the GPU knows, and
|
||||
enable `GL_FRAMEBUFFER_SRGB` around the render so the write re-encodes.
|
||||
Work happens in linear. When colour management is off this must degrade to
|
||||
exactly today's byte-through path — identity is load-bearing.
|
||||
2. **Gamut in linear.** A per‑output 3×3 uniform carries the panel transform.
|
||||
Two presets, decided by config:
|
||||
- **natural** — assume DCI‑P3 primaries (the SW43408's), map linear sRGB into
|
||||
the panel primaries: content renders *correct*, the way stock Android's
|
||||
"Natural" did.
|
||||
- **vivid** — the same matrix composed with a fixed saturation/depth boost,
|
||||
the stock "Vivid"-style look, not a raw profile.
|
||||
No ICC files. The panel has no EDID, so the primaries are a constant, not a
|
||||
probe; a wrong constant is a *fixed* factor, never a lookup that degrades.
|
||||
3. **Clamp.** Out-of-range values are clamped in the shader to [0,1] before the
|
||||
(already clamped) framebuffer write; nothing exotic.
|
||||
|
||||
Default is **off** (identity, byte-through). Every stage is an explicit config
|
||||
diagonal, not a rewrite of the render loop.
|
||||
|
||||
## Part 3 — the framebuffer & blend invariant
|
||||
|
||||
Alpha currently goes through one multiply with no pre-multiplied discipline
|
||||
(the fork's `texture.frag` does `color * alpha`). The renderer's contract with
|
||||
the sources decides: adoption of **premultiplied-alpha** at the surface-import
|
||||
boundary so the linear blending scene doesn't double-count. Invert: the
|
||||
`NO_ALPHA` and the translucent case must both load-test equal to today's
|
||||
behaviour on the identity path. Measured, not assumed (see Acceptance).
|
||||
|
||||
## Part 4 — precision: 10-bit in the chain
|
||||
|
||||
Add the 10-bit variants to `kms.rs`'s scan‑out formats:
|
||||
`[Xrgb8888, Xrgb2101010, Argb2101010]`. The renderer already maps the read/write
|
||||
side — `renderer/gles/format.rs` carries `Abgr2101010 → GL_RGB10_A2` — so this
|
||||
is a formats decision (score the swapchain at 10) and a couple of allocation
|
||||
plugs, not a new sampler.
|
||||
|
||||
Honest expectation: the SW43408 consumes 24-bit, so the visible win is
|
||||
banding/legroom through the DPU+panel dither, not new gamut. It is a *chain*
|
||||
property: it must hold the identity invariant exactly as 8-bit did, including
|
||||
through the DSC compression path, or the 10-bit path is off (config, not
|
||||
action).
|
||||
|
||||
## Part 5 — the sunset that the slider lost
|
||||
|
||||
The gamma verb is the mechanism; the *product* use is the night-light:
|
||||
|
||||
- **Automatic sunset** keeps the same LUT slot: a colour-temperature curve for
|
||||
the chosen mired value, computed once from the temperature and re‑uploaded on
|
||||
change — the `Hyprsunset` automatic mode already schedules that part. Time-of-day belongs
|
||||
to the shell (it owns `Config.options.light.night`); the compositor owns only
|
||||
the applied LUT.
|
||||
- Manual gamma override must disarm the automatic state exactly as before —
|
||||
that rule already lives in the QML service and must survive the rewire.
|
||||
|
||||
## Part 6 — the door that stays open: DPU hardware
|
||||
|
||||
The DPU (Snapdragon Display Engine) has hardware colour processors (CTM,
|
||||
gamma, 3D LUT) that Android's "SDE" stack drove. If the DPU exposes crtc
|
||||
CTM/GAMMA-LUT-properties on this hardware, Part 2's matrices could live in SDE
|
||||
hardware instead of the shader. **This task does not require it** — the
|
||||
software path of record is the shader. The gate is a spike, recorded with the
|
||||
results: attempt to `set_blob`/program the DPU, read back, and decide if the
|
||||
SDE path is usable vs the shader. Not a pre-condition for anything in
|
||||
Parts 1–5.
|
||||
|
||||
## Acceptance (measured, on the phone, using the existing capture path)
|
||||
|
||||
- **The toggle returns.** The side-panel Brightness↔Gamma switch controls a real
|
||||
LUT: `gamma` as a verb in `INTENTS`, every `Hyprsunset` contract callable with
|
||||
no `hyprctl`.
|
||||
- **LUT authority.** `set_gamma` + `get_gamma` round-trip byte-identical on the
|
||||
panel's dynamics; value→ramp matches the table; on blank/resume the identity
|
||||
is restored, never yesterday's ramp.
|
||||
- **Gamut proxies.** Under `natural` vs `vivid`, a known sRGB test swatch
|
||||
(e.g. `#ff0000` on the neutral background) moved in the expected two
|
||||
directions, observed through the compositor's window capture — same sample
|
||||
in the same spot, delta recorded in the stack.
|
||||
- **Identity invariant.** Colour=off is byte-through: capture of the same scene
|
||||
with management off == today's output, no surprise tint.
|
||||
- **Precision.** A 16-step ramp displays without visible step boundaries at the
|
||||
current bit depth; with 10-bit on, the same ramp holds smoother (banding
|
||||
measured by count of unique LSBs across the same capture).
|
||||
- **Night / adaptive.** Sunset automatic mode runs the same calendar
|
||||
temperature; manual override disarms it; the LUT after a resume matches the
|
||||
one before it.
|
||||
|
||||
## Open questions (each recorded; none blocks)
|
||||
|
||||
- LUT size on the panel by `gamma_length()` (assume 256; keep, or scale if 512) —
|
||||
decided on first measurement.
|
||||
- DPU pathway (Part 6) — a spike, its results recorded here, not assumed.
|
||||
- The 3×3 constants for `natural`/`vivid` — fixed from the panel's generation
|
||||
(DCI-P3), with the two columns in the same table that any future measured
|
||||
profile can replace.
|
||||
|
||||
## Repos touched
|
||||
|
||||
- `souveraine-viewtop` — `wire/src/verbs.rs` (+ the round-trip test already
|
||||
enforcing), the verb handler in the compositor, the kms LUT utilities and the
|
||||
scan‑out format list, and the fork-GLES patches (sRGB textures,
|
||||
`FRAMEBUFFER_SRGB`, the linear 3×3 uniform path).
|
||||
- `souveraine` — `Hyprsunset` service rewired to compositor verbs, API intact.
|
||||
|
||||
## Ordering note
|
||||
|
||||
There is an ordering that falls out of the code (the verb first — it is what a
|
||||
caller can already compose). That ordering is *not* the scope; the scope is all
|
||||
six parts.
|
||||
|
||||
## Connects to
|
||||
|
||||
TASK-30/31 (verb tables: this is one more row speaking in the one vocabulary),
|
||||
TASK-51/53 (the daily-driver move is the event where gamma was lost), TASK-52
|
||||
(atmosphere — the same warm glass at night), the smithy fork pin (colour lives
|
||||
in the one GLES surface), and `Pixel3Arch`'s DT for the panel (the DCI-P3
|
||||
constant, and the DPU door).
|
||||
|
|
@ -59,6 +59,7 @@ Four that are cheap relative to what they unblock:
|
|||
|
||||
| # | Task | What's left |
|
||||
|---|------|-------------|
|
||||
| 60 | [Multitasking is a place, not a transition](60-the-multitasking-view.md) | Written after building the wrong thing. The rail scales the active zone's windows on swipe — a *transition* mistaken for the *destination*. It scales the wrong windows (2 s poll, and the shell has no "focused window" fact at all), and scaling one zone can never show what is backgrounded. `WindowOverview` draws one column of window cards and has no concept of zones; its own header admits the gap. Needs deciding before code: card = window or zone, `ScreencopyView` pictures vs posed real windows, where focus comes from, and whether the poll gets replaced by a push. |
|
||||
| 59 | [Her face on the glass: a Live2D presence](59-her-face-on-the-glass.md) | **Phase 1 passed on hardware 2026-08-05: ~58 fps, rig verified drawing (`painted_px` 23.6k/24k), 283 MB WebKit RSS.** Scoped from a Live2D board-girl found in a cloned blog. The reference streams its chat over **SSE terminated by `[DONE]`** — the contract souveraine's server already speaks, so the integration is accidentally done. Base layer is **wry**, not the Chromium already on the phone; the first deliverable is a measured frame rate for the Cubism runtime in WebKit on freedreno, and if that fails the task stops there. Replaces ani-avatar's 2.5 GB of sprite states with one rig. Reference rig ships as the working placeholder and gets edited toward Annie — it just stays out of any public repo. Summoned by app launch and/or the radial dial, not always-on. |
|
||||
| 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. |
|
||||
|
|
|
|||
Loading…
Reference in a new issue