docs: display v2 design + plan (pluggable scanout backend)

Keep the GOP framebuffer as the floor, make scanout a pluggable backend, and
upgrade to a native virtio-gpu driver when it announces itself (dynamic
hot-attach; GOP stays the fallback for "no driver ever"). v2 builds the shm
cross-process memory capability, shared with the future client-surface path.
Milestones V1 (backend seam) → V6 (resilience + tests), each with a gate.
This commit is contained in:
Daniel Samson 2026-07-14 09:28:31 +01:00
parent a01a4f3b3d
commit 2723b6f778
No known key found for this signature in database
GPG Key ID: A9EB2589C60F7268
3 changed files with 252 additions and 1 deletions

View File

@ -73,7 +73,9 @@ rather than restate it. Roughly in the order things happen at runtime:
track: a user-space compositor that owns the framebuffer, composes a layer stack into
a double buffer, and presents it. Why GOP and the PCI display device are two views of
one controller, the device-node + write-combining handoff, and what flicker-free buys
that tear-free doesn't. Plan: [display-plan.md](display-plan.md).
that tear-free doesn't. Plan: [display-plan.md](display-plan.md). **v2** makes scanout
a pluggable backend (GOP floor + a native virtio-gpu driver, hot-attached):
[display-v2.md](display-v2.md), plan [display-v2-plan.md](display-v2-plan.md).
20. **[halting.md](halting.md) — halting.** Why a kernel can't just "exit", and
how `while (true) hlt` parks the CPU safely once there's nothing left to do.

123
docs/display-v2-plan.md Normal file
View File

@ -0,0 +1,123 @@
# Display v2 — build plan (pluggable scanout: GOP floor + virtio-gpu native)
The ordered, checkpointable build-out for [display-v2.md](display-v2.md). Each milestone
lands on its own and ends in a **verifiable gate** — shaped for a `/loop` run, like
[display-plan.md](display-plan.md). Read display-v2.md first for the *why*.
## Locked decisions (do not relitigate)
- **First native backend = virtio-gpu** (VM standard: mode-set + present/flush + vsync).
- **Dynamic hot-attach**: boot on GOP, upgrade to native when the driver **announces**
(push, not polling); re-attach across driver restarts; GOP is the floor for "no driver
ever," not a live fall-back after a reprogram.
- **v2 builds the `shm` capability** (endpoints → memory objects), shared with the future
client-surface path.
- The compositor's layers/back-buffer/damage are **unchanged**; only scanout is pluggable.
## Conventions
Follow [coding-standards.md](coding-standards.md): spell out non-acronym abbreviations,
kebab-case file names, no `Co-Authored-By` trailers. New user binaries go through
`addUserBinary` and get packed into the initial-ramdisk; protocols are
`b.addModule("…-protocol", …)` imported into `runtime`; new syscalls extend
[abi.zig](../system/abi.zig) `SystemCall` + a `library/runtime` wrapper.
## How to verify along the way
- `zig build test` — host unit tests (backend selection logic, virtio structs/encodings).
- `python3 test/qemu_test.py <case>` — boots the kernel in QEMU; assert on serial markers.
The virtio path boots with `-device virtio-gpu` (a new per-case `qemu_extra`).
- The `run-x86-64` target renders to a window for visual confirmation.
---
## V1 — The scanout backend seam (refactor, no behaviour change)
Extract scanout from the compositor so today's path becomes one backend among future ones.
- [ ] A `Backend` interface in `system/services/display/`: `surface() -> {ptr, pitch,
format, width, height}`, `present(damage: Rect)`, and capability flags
(`canModeSet`, `hasVsync`, both false for now).
- [ ] Wrap the v1 GOP path as `GopBackend` (claim the `display` node, WC-map the LFB,
`present` = the current damage-rect WC copy). The compositor composes into
`backend.surface()` and calls `backend.present(damage)` — no direct LFB references
left in the compositor core.
- [ ] Pure backend-selection logic factored so it's host-testable.
**Gate:** `display-service` + `display-demo` still pass unchanged (pure refactor; GOP is
the only backend), and `zig build test` stays green.
## V2 — The `shm` cross-process memory capability (kernel)
- [ ] [abi.zig](../system/abi.zig): `shm_create`, `shm_map` syscalls (+ a `ServiceId`/cap
convention if needed). Kernel handlers: `shm_create(len)` allocates page-aligned RAM,
returns a handle + maps it; passing the handle as an `ipc_call` `send_cap` shares it;
`shm_map(cap)` maps the same physical pages into the receiver. Reclaimed on death.
- [ ] `library/runtime/shm.zig` (+ barrel export): `create(len) -> Region{handle, ptr}`,
`map(cap) -> ptr`.
- [ ] Reuse the M13 capability-passing machinery (endpoints → memory objects).
**Gate:** a kernel/qemu `shm` test — process A `shm_create`s a region, writes a pattern,
passes the cap to process B, which `shm_map`s it and reads the same bytes back (proving
shared physical pages, not a copy). Heartbeat `shm: shared N bytes ok`.
## V3 — The virtio-gpu driver: bring-up + a frame on screen
- [ ] `system/drivers/virtio-gpu/`: claim the virtio-gpu PCI function (device-manager
match on its PCI/virtio id), map BARs, negotiate features, set up the control
virtqueue. `virtio-gpu-protocol.zig` for the control structs (host-tested sizes).
- [ ] Create a 2D scanout resource backed by an `shm` region, `attach_backing`,
`set_scanout` to CRTC 0, and `resource_flush` a test pattern.
- [ ] Register a `scanout` service (new `ServiceId`).
**Gate:** QEMU `-device virtio-gpu`: the driver comes up and logs
`virtio-gpu: scanout {w}x{h} online`, and a flushed test pattern is visible (screenshot)
— proving virtqueue + resource + set_scanout + flush end to end.
## V4 — The native backend + hot-attach
- [ ] `VirtioGpuBackend` in the compositor: `surface()` = the shared scanout resource,
`present(damage)` = `resource_flush` of the damaged rect.
- [ ] The driver **announces** to `.display` (looks it up, sends *attach-scanout* with its
`scanout` endpoint + the shared surface as capabilities). The compositor switches
backends and re-presents the current frame full-screen.
- [ ] Boot still starts on `GopBackend`; the upgrade happens on announce.
**Gate:** boot with virtio-gpu + `display-demo`; the compositor logs
`display: scanout upgraded to virtio-gpu`, and the demo animates through the native
backend (serial heartbeat + screenshot). Without `-device virtio-gpu`, it stays on GOP
(the v1 gates still pass).
## V5 — Mode-setting, EDID, and vsync
- [ ] virtio-gpu `GET_EDID` → a mode list; `set_scanout` at a chosen mode = runtime
resolution change. `runtime.display` gains `modes()` / `setMode(m)`.
- [ ] A vsync/fenced `resource_flush` present path → genuinely tear-free.
- [ ] The compositor reports the native backend's `canModeSet`/`hasVsync` = true.
**Gate:** a `display-modeset` case changes resolution at runtime and confirms the new mode
(`display: mode set to {w}x{h}`); a vsync present path is exercised without tearing
artifacts in the frame counter/log.
## V6 — Resilience (restart + re-attach) + tests + docs
- [ ] The virtio-gpu driver is supervised (device-manager / init) and restartable; on
driver loss the compositor freezes the last frame and **re-attaches** when the driver
re-announces. Only a permanent give-up (crash-loop cap) attempts GOP again.
- [ ] `test/qemu_test.py` cases: `virtio-gpu`, hot-attach, `display-modeset`, and a
driver-kill/re-attach case. Update [display-v2.md](display-v2.md) status; README index.
**Gate:** kill the virtio-gpu driver mid-run; the compositor survives and re-attaches on
restart (`display: scanout re-attached`); all v1 + v2 cases pass; default `zig build` clean.
---
## Deferred (explicitly not in this plan)
- **Client-rendered surfaces** — now unblocked by the `shm` capability (V2): an app renders
its own bitmap and hands the compositor a reference. A natural follow-on.
- **Bochs DISPI backend** — a simpler second native backend (mode-set only, dumb scanout);
slots behind the same interface if wanted.
- **Real-GPU (NVIDIA/AMD/Intel) drivers** — out of scope; those devices stay on the GOP
floor by design.
- **Hardware-accelerated compositing / multiple heads** — future.

126
docs/display-v2.md Normal file
View File

@ -0,0 +1,126 @@
# The display service v2: a pluggable scanout backend
v1 ([display.md](display.md)) is a compositor that owns the **GOP framebuffer** — it
composites a layer stack into a cacheable back buffer and streams damage to the linear
framebuffer the firmware handed over. That path is portable and good: it drives any GPU,
including a real NVIDIA card at an ultrawide's native resolution, with zero GPU-specific
code. v2 keeps it as the **floor** and makes *scanout* — how a finished frame reaches the
panel — a **pluggable backend**, so the compositor can **upgrade to a real GPU driver when
one is present** and fall back to the framebuffer when it isn't.
The compositor itself (layers, back buffer, damage) does not change. Only the last step —
"put this frame on screen" — becomes swappable.
## The shape
```
compositor (display service) ── layer stack + back buffer + damage (unchanged)
│ composites a frame, then: backend.present(damage)
scanout backend (selected at runtime — GOP by default, native when it appears)
├─ GopBackend the v1 path: WC copy back→front to the firmware LFB.
│ Always available. No mode-set, no vsync. THE FLOOR.
└─ VirtioGpuBackend talks to a virtio-gpu driver process over a `scanout`
service: present via a shared resource + flush (real vsync),
EDID mode list, runtime mode-set.
```
A **backend** is a small interface the compositor calls:
- `surface()` → the pixels to compose into and their geometry `{ptr, pitch, format, w, h}`
(the LFB for GOP; a shared scanout resource for virtio-gpu),
- `present(damage: Rect)` → make the damaged region visible (a no-op-ish WC copy for GOP;
a virtio flush, optionally vsync-fenced, for the native path),
- capability queries — `canModeSet`, `hasVsync` — and, when supported, `modes()` /
`setMode(m)`.
The compositor composes into `surface()` and calls `present(damage)` exactly as it does
today; everything device-specific lives behind the interface.
## Selection and hot-attach
The choice is **dynamic**, because a GPU driver is spawned asynchronously (the device
manager brings it up after boot), and because danos is meant to be resilient:
1. **Boot on GOP.** The compositor starts on `GopBackend` immediately, so there is never a
blank screen while drivers load — the exact v1 behaviour.
2. **Upgrade on announce.** When the virtio-gpu driver has claimed its device and set up a
scanout, it **announces itself to the display service** (a `push`: the driver looks up
`.display` and sends an *attach-scanout* message carrying its `scanout` endpoint as a
capability). The compositor switches to `VirtioGpuBackend` and re-presents the current
frame full-screen. Push beats polling — the compositor doesn't know a priori which
driver, if any, exists, and danos has no service-registration pub/sub.
3. **Native is restartable, not fallback-on-crash.** Once a native driver has reprogrammed
the device, the firmware's GOP framebuffer is **stale** — "native → GOP" is not a clean
fall-back. So a native driver that **crashes** is *restarted* by its supervisor (the
resilience work already merged), re-announces, and the compositor **re-attaches**
(native → native). The screen freezes on the last frame during the gap — acceptable.
4. **GOP is the floor for "no driver was ever there."** On a real GPU (NVIDIA/AMD/Intel)
the class-0x03 device matches nothing in the driver table, no `scanout` is ever
announced, and the compositor stays on GOP forever — no special-casing. Only if a
native driver *permanently* gives up (crash-loop cap) does the compositor attempt GOP
again, and even then only if the LFB is still mappable.
## The shared-memory primitive this needs
virtio-gpu's scanout resource is **guest RAM** — the driver allocates it and attaches it
to a virtio resource, and the compositor composes into it. That means the compositor
writing into the driver's buffer is **cross-process memory sharing**, the primitive v1
deferred (docs/display.md, "What v1 does not do"). v2 builds it: the natural generalization
of M13 capability-passing from *endpoints* to *memory objects*
```
shm_create(len) -> {handle, vaddr} // a shareable, page-aligned RAM region
… pass `handle` as the send_cap on an ipc_call …
shm_map(cap) -> vaddr // the receiver maps the same physical pages
```
The payoff is leverage: the **same** primitive unlocks **both** native GPU drivers *and*
client-rendered surfaces (an app composing its own bitmap and handing the compositor a
reference instead of drawing by command). One piece of kernel work, two features.
## The virtio-gpu driver
A new ring-3 driver process (the topology v1 anticipated — "split the driver from the
compositor when a second backend arrives"). It claims the virtio-gpu PCI function, and:
- sets up the **virtqueues** (control + cursor) and the device's config space,
- creates a **2D scanout resource** backed by an `shm` region, `attach_backing`s it,
`set_scanout`s it to a CRTC, and `resource_flush`es damaged rectangles,
- reads **EDID** (the `GET_EDID` control command) for the mode list, and `set_scanout`
at a chosen mode for **runtime mode-setting**,
- registers a `scanout` service and announces to the display service.
Its `resource_flush` is the real **present** — and gives a genuine **vsync/tear-free**
path a dumb GOP framebuffer can't.
## What v2 unlocks — and its honest scope
Behind the abstraction, a native backend gives runtime **mode-setting** (resolution /
refresh / bpp), **EDID** enumeration, and **vsync**. But only on devices we have a driver
for — realistically **VMs** (virtio-gpu, and later maybe Bochs DISPI). Real discrete GPUs
need per-vendor KMS-class drivers that aren't getting written, so they **stay on GOP**
which is genuinely fine (v1 on the NVIDIA box is smooth). So v2's real value is twofold:
the **pluggable architecture** (a driver slots in when one exists) and a **rich, vsync'd
path in VMs**, where danos development happens. The framebuffer floor never goes away.
## Locked decisions
- **First native backend: virtio-gpu** — the VM standard; gives mode-set + a real
present/flush (and vsync), and exercises the whole pluggable design. Tested with QEMU
`-device virtio-gpu`.
- **Dynamic hot-attach** — boot on GOP, upgrade to native on the driver's announce,
re-attach across driver restarts; GOP is the floor for "no driver ever," not a live
fall-back after a reprogram.
- **Detection = push** (the driver announces to `.display`), not compositor polling.
- **v2 builds the `shm` capability** (endpoints → memory objects), shared with the future
client-surface path.
## See also
- [display.md](display.md) — v1: the compositor, the GOP-vs-device split, the WC discipline.
- [display-v2-plan.md](display-v2-plan.md) — the ordered build-out.
- [driver-model.md](driver-model.md) — claim / `mmio_map` / MSI / capability passing (M13).
- [resilience.md](resilience.md) — the restart machinery the hot-attach leans on.