176 lines
12 KiB
Markdown
176 lines
12 KiB
Markdown
# 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 + fenced present/flush).
|
|
- **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 shared-memory 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
|
|
|
|
**Every gate is serial-checkable — no screenshots** (this plan is built to run unattended).
|
|
Where "does it actually display" would otherwise need a human eyeball, the code **reads its
|
|
own pixels back**: the scanout resource is CPU-visible RAM (shared-memory-backed) and the back buffer
|
|
is cacheable, so a driver/compositor can write a known value, read it back, and log a
|
|
pass/fail — and a virtio `resource_flush` is confirmed by the device **acking it on the
|
|
used ring**. Those two together (pixel-readback + flush-ack) are the automated stand-in for
|
|
"it's on screen."
|
|
|
|
- `zig build test` — host unit tests (backend selection, virtio struct sizes/encodings,
|
|
pixel-check helpers).
|
|
- `python3 test/qemu_test.py <case>` — boots the kernel in QEMU; asserts on serial markers.
|
|
The virtio cases boot with `-device virtio-gpu` (a per-case `qemu_extra`).
|
|
- `run-x86-64` renders to a window — for the human's own satisfaction, **not** a gate.
|
|
|
|
---
|
|
|
|
## V1 — The scanout backend seam (refactor, no behaviour change) ✅
|
|
|
|
Extract scanout from the compositor so today's path becomes one backend among future ones.
|
|
|
|
- [x] `system/services/display/backend.zig`: a `Backend` tagged union with `info()`,
|
|
`surface()` (the cacheable compose target), `present(damage)`, and capability flags
|
|
(`canModeSet`/`hasFencedPresent`, both false for GOP).
|
|
- [x] The v1 GOP path is now `backend.Gop` (claims the `display` node, WC-maps the LFB,
|
|
keeps the cacheable back buffer, `present` = the damage-rect WC copy). display.zig
|
|
composes into `backend.surface()` and calls `backend.present(damage)` — no LFB or
|
|
framebuffer geometry left in the compositor core.
|
|
- [x] The selection decision is the pure `chooseKind(native_available)` (gop unless a
|
|
native driver announced), split from the syscall-bound `select()`/`Gop.init()`.
|
|
|
|
**Gate (met):** `display-service` + `display-demo` pass **unchanged** (pure refactor; GOP
|
|
is the only backend), and `zig build test` stays green.
|
|
|
|
## V2 — The shared-memory cross-process capability (kernel) ✅
|
|
|
|
- [x] [abi.zig](../system/abi.zig): `shared_memory_create` (34) / `shared_memory_map` (35) syscalls + a
|
|
`shared_memory_test` service id. Handlers in process.zig: `shared_memory_create(len)` allocates contiguous,
|
|
zeroed, **cacheable** frames, wraps them in a refcounted object, installs a capability
|
|
handle, maps them into the caller's shared-memory arena → returns virtual_address + handle; `shared_memory_map(cap)`
|
|
maps the same physical pages into the receiver. Reclaimed on death (see below).
|
|
- [x] The capability core (ipc-synchronous.zig) is now **kind-tagged**: `scheduler.Task`'s
|
|
handle table holds `HandleObject{kind, ptr}`; `closeHandles` and `shareCapability`
|
|
dispatch by kind, so a `SharedMemoryObject` rides an `ipc_call` `send_cap` exactly like an
|
|
endpoint and frees only when its last capability drops. `mapUserSharedInto` (paging)
|
|
maps WB-cacheable + `device_grant`, so a sharer's teardown never frees the shared
|
|
frames — the object owns them.
|
|
- [x] `library/runtime/shared-memory.zig` (+ barrel export): `create(len) -> Region{ptr, handle, len}`,
|
|
`map(handle) -> ptr`.
|
|
|
|
**Gate (met):** `python3 test/qemu_test.py shared-memory` — `shared-memory-client` creates a region, writes a
|
|
pattern, and passes its capability to `shared-memory-server` as an `ipc_call` send_cap; the server
|
|
`shared_memory_map`s it and reads the **same bytes** back → `shared-memory: shared 4096 bytes ok`. Guardrail:
|
|
`ipc`/`ipc-call`/`ipc-cap`, `supervision`, `dma`, `usermem`, `display-service`, and host
|
|
tests all still pass — the handle-table change broke no existing IPC.
|
|
|
|
## V3 — The virtio-gpu driver: bring-up + a frame on screen ✅
|
|
|
|
- [x] `system/drivers/virtio-gpu/`: claim the virtio-gpu PCI function (device-manager
|
|
match on the display/other class triple, driver self-confirms vendor 0x1AF4/device
|
|
0x1050 from config space), enable memory-space + bus-master, walk the vendor
|
|
capabilities in config space to find common-config + notify, map the BAR, negotiate
|
|
VERSION_1, and stand up the control virtqueue in coherent DMA. `virtio-gpu-protocol.zig`
|
|
+ `virtio-pci.zig` for the control/transport structs (host-tested sizes).
|
|
- [x] Create a 2D scanout resource backed by a coherent DMA region (V4 swaps this for the
|
|
shared-memory surface), `attach_backing`, `set_scanout` to scanout 0, `transfer_to_host_2d`
|
|
+ `resource_flush` of a test pattern, and wait on the used ring.
|
|
- [x] Register a `scanout` service (`ServiceId.scanout` = 11).
|
|
|
|
**Gate (met):** the `virtio-gpu` case (QEMU `-device virtio-gpu-pci`) boots the
|
|
device-manager stack, which discovers the function and spawns the driver; the driver writes
|
|
a known test pattern into the scanout backing, `transfer_to_host_2d` + `resource_flush`es
|
|
it, and **waits for the device's used-ring ack**, then reads the backing back and checks the
|
|
pattern — logging `virtio-gpu: scanout 640x480 online` and `virtio-gpu: flush acked, pixel
|
|
check ok`. That proves virtqueue + resource + attach + set_scanout + transfer + flush end to
|
|
end without a screenshot (the used-ring ack is the device confirming it consumed the frame).
|
|
|
|
## V4 — The native backend + hot-attach ✅
|
|
|
|
- [x] `backend.VirtioGpu` in the compositor: `surface()` = the shared-memory scanout surface
|
|
(the compositor composes straight into the device's resource backing; x86 DMA is
|
|
coherent, so the cacheable shared pages need no flush), `present(damage)` = a `present`
|
|
request over the driver's `.scanout` endpoint (→ transfer-to-host + resource flush).
|
|
- [x] The driver **announces** to `.display` after bring-up (looks it up with a bounded retry,
|
|
sends `attach_scanout` with the geometry + the shared surface as an `ipc_call` send_cap).
|
|
The compositor maps it, looks up `.scanout` itself (no need to pass the endpoint — the
|
|
driver registered it), switches backend, and re-composites the current frame full-screen.
|
|
The present is deferred to a one-shot timer so it runs *after* the reply unblocks the
|
|
driver and it serves `.scanout` — presenting inline would deadlock.
|
|
- [x] Boot still starts on `backend.Gop`; the upgrade happens on announce. `shared_memory_physical` (a
|
|
new syscall) gives the driver the guest-physical of the shared surface for `attach_backing`.
|
|
|
|
**Gate (met):** the `display-native` case (QEMU `-device virtio-gpu-pci`, `mem` bumped since it
|
|
boots the whole system) starts the compositor + `display-demo` + device-manager; the driver
|
|
announces, the compositor logs `display: scanout upgraded to virtio-gpu`, drives frames through
|
|
the native backend, and **reads a pixel back** from the shared surface after a present to
|
|
confirm the composited frame landed (`display: native present verified`), while `display-demo:
|
|
ok` still fires — checked order-independently. Without `-device virtio-gpu-pci` nothing is
|
|
announced and it stays on GOP: the v1 `display-service`/`display-demo` gates pass unchanged.
|
|
|
|
## V5 — Mode-setting, EDID, and fenced presents ✅
|
|
|
|
- [x] The driver negotiates `VIRTIO_GPU_F_EDID` (when offered) and reads the monitor's EDID,
|
|
logging its preferred mode; it offers a small mode list over `.scanout` `get_modes`. The
|
|
resource + shared surface are sized to the largest mode, so `set_mode` just re-points the
|
|
scanout rectangle (no resource/surface churn) — a runtime resolution change. `runtime.display`
|
|
gains `modes()` / `setMode()` (display-protocol `get_modes`/`set_mode`, forwarded to the backend).
|
|
- [x] Every `resource_flush` is issued fenced (`VIRTIO_GPU_FLAG_FENCE`); the device signals the
|
|
fence when it has consumed the frame, which the used-ring ack the synchronous present waits
|
|
on already gates — a tear-free present. (Completion feedback, **not vblank**: base
|
|
virtio-gpu 2D has no display-refresh event, so nothing paces presents to the monitor —
|
|
see the "Fenced is not vsync" note in [display-v2.md](display-v2.md).)
|
|
- [x] `backend.VirtioGpu` reports `canModeSet` / `hasFencedPresent` = true.
|
|
|
|
**Gate (met):** the `display-modeset` case (reusing the display-native boot) upgrades to
|
|
virtio-gpu, queries the driver's modes, `setMode`s to a different resolution, and confirms the
|
|
change by reading the backend's geometry back (`display: mode set to {w}x{h}, verified`); the
|
|
fenced present path is exercised and confirmed (`display: fenced present ok`) — both from serial,
|
|
passing 3/3. The driver also logs the EDID preferred mode (`virtio-gpu: EDID preferred mode …`).
|
|
|
|
## V6 — Resilience (restart + re-attach) + tests + docs ✅
|
|
|
|
- [x] The virtio-gpu driver now **hellos** the device manager (role: bus) so it is properly
|
|
supervised — no longer stopped at the hello deadline — and is restarted on death. On
|
|
driver loss the compositor keeps the last frame (its `.scanout` calls now return
|
|
`-EPEER` instead of hanging — a kernel fix: an endpoint is marked dead when its owner
|
|
dies) and **re-attaches** when the restarted driver re-announces. A permanent give-up
|
|
(crash-loop cap) leaves the frozen frame; GOP is not re-taken.
|
|
- [x] `test/qemu_test.py`: the `virtio-gpu`, `display-native` (hot-attach), `display-modeset`,
|
|
and `display-reattach` (driver-kill/re-attach) cases. display-v2.md status updated.
|
|
|
|
**Gate (met):** the `display-reattach` case — device-manager (in `test-scanout-restart` mode)
|
|
kills the virtio-gpu driver once after it hellos; the restart policy respawns it, it
|
|
re-announces, and the compositor logs `display: scanout re-attached` after the initial
|
|
`display: scanout upgraded to virtio-gpu`, with no CPU exception / panic (the compositor
|
|
survives) — passing 3/3. All v1 + v2 cases (host tests, `ipc`/`ipc-call`/`ipc-cap`,
|
|
`supervision`, `shared-memory`, `display-service`, `display-demo`, `virtio-gpu`, `display-native`,
|
|
`display-modeset`) pass; default `zig build` is clean.
|
|
|
|
---
|
|
|
|
## Deferred (explicitly not in this plan)
|
|
|
|
- **Client-rendered surfaces** — now unblocked by the shared-memory 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.
|