148 lines
8.7 KiB
Markdown
148 lines
8.7 KiB
Markdown
# The display service v2: a pluggable scanout backend
|
||
|
||
**Status: complete (V1–V6).** The compositor boots on the GOP framebuffer and, when a
|
||
virtio-gpu driver announces itself, hot-attaches a native backend over the shared-memory
|
||
scanout surface — with runtime mode-setting, EDID, and fenced presents, and it
|
||
re-attaches across driver restarts. All serial-gated (see [display-v2-plan.md](display-v2-plan.md)).
|
||
|
||
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 present fence. THE FLOOR.
|
||
│
|
||
└─ VirtioGpuBackend talks to a virtio-gpu driver process over a `scanout`
|
||
service: present via a shared resource + fenced flush,
|
||
a fixed mode list, runtime mode-set, EDID refresh rate.
|
||
```
|
||
|
||
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 fenced virtio flush for the native path),
|
||
- capability queries — `canModeSet`, `hasFencedPresent` — 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 the shared scanout **surface**
|
||
as a capability; the compositor maps it and reaches the driver's present/mode channel by
|
||
looking up the registered `scanout` service). 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. If a native
|
||
driver *permanently* gives up (the device manager's crash-loop cap), nothing tells the
|
||
compositor and there is no path back to GOP — the screen stays frozen on the last
|
||
frame. A GOP revert (sensible only while the LFB is still mappable) is not built.
|
||
|
||
## 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* —
|
||
|
||
```
|
||
shared_memory_create(len) -> {handle, virtual_address} // a shareable, page-aligned RAM region
|
||
… pass `handle` as the send_cap on an ipc_call …
|
||
shared_memory_map(cap) -> virtual_address // 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 **control virtqueue** (queue 0 — the only queue it uses; no cursor queue)
|
||
and the device's config space,
|
||
- creates a **2D scanout resource** backed by a shared-memory region, `attach_backing`s it,
|
||
`set_scanout`s it to a CRTC, and on each present transfers and `resource_flush`es the
|
||
full current-mode rectangle (damage-narrowed flushes are a later refinement),
|
||
- reads **EDID** (the `GET_EDID` control command) to log the monitor's preferred timing
|
||
and derive the refresh rate it announces (the compositor's frame-clock seed); the mode
|
||
list it offers is a fixed pair — 640×480 and 800×600 — and `set_scanout` at a chosen
|
||
mode gives **runtime mode-setting**,
|
||
- registers a `scanout` service and announces to the display service.
|
||
|
||
Its `resource_flush` is the real **present** — and gives a **fenced, tear-free** path a
|
||
dumb GOP framebuffer can't.
|
||
|
||
**Fenced is not vsync.** The fence completes when the device has *consumed* the frame:
|
||
real completion feedback, and tear-freedom by snapshot semantics (the host displays
|
||
discrete transferred frames, never a half-written surface). It is **not** a vblank —
|
||
base virtio-gpu 2D has no display-refresh event at all (Linux's driver for this device
|
||
fakes one with a software timer), so nothing paces presents to the monitor's refresh.
|
||
Refresh-paced presents need either a native driver's vblank interrupt (delivered over
|
||
the existing IRQ-as-IPC path) or the compositor's own frame clock.
|
||
|
||
## What v2 unlocks — and its honest scope
|
||
|
||
Behind the abstraction, a native backend gives runtime **mode-setting** (resolution only —
|
||
the scanout protocol carries neither refresh nor bpp), an **EDID**-derived refresh rate, and
|
||
**fenced presents**. 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, fenced
|
||
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 (fenced), 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 shared-memory 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](../os-development/resilience.md) — the restart machinery the hot-attach leans on.
|