9.2 KiB
Display v2 — build plan (pluggable scanout: GOP floor + virtio-gpu native)
The ordered, checkpointable build-out for display-v2.md. Each milestone
lands on its own and ends in a verifiable gate — shaped for a /loop run, like
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
shmcapability (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: 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 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 (shm-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-caseqemu_extra).run-x86-64renders 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.
system/services/display/backend.zig: aBackendtagged union withinfo(),surface()(the cacheable compose target),present(damage), and capability flags (canModeSet/hasVsync, both false for GOP).- The v1 GOP path is now
backend.Gop(claims thedisplaynode, WC-maps the LFB, keeps the cacheable back buffer,present= the damage-rect WC copy). display.zig composes intobackend.surface()and callsbackend.present(damage)— no LFB or framebuffer geometry left in the compositor core. - The selection decision is the pure
chooseKind(native_available)(gop unless a native driver announced), split from the syscall-boundselect()/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 shm cross-process memory capability (kernel) ✅
- abi.zig:
shm_create(34) /shm_map(35) syscalls + ashm_testservice id. Handlers in process.zig:shm_create(len)allocates contiguous, zeroed, cacheable frames, wraps them in a refcounted object, installs a capability handle, maps them into the caller's shm arena → returns vaddr + handle;shm_map(cap)maps the same physical pages into the receiver. Reclaimed on death (see below). - The capability core (ipc-synchronous.zig) is now kind-tagged:
scheduler.Task's handle table holdsHandleObject{kind, ptr};closeHandlesandshareCapabilitydispatch by kind, so anShmObjectrides anipc_callsend_capexactly 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. library/runtime/shm.zig(+ barrel export):create(len) -> Region{ptr, handle, len},map(handle) -> ptr.
Gate (met): python3 test/qemu_test.py shm — shm-client creates a region, writes a
pattern, and passes its capability to shm-server as an ipc_call send_cap; the server
shm_maps it and reads the same bytes back → shm: 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 ✅
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.zigfor the control/transport structs (host-tested sizes).- Create a 2D scanout resource backed by a coherent DMA region (V4 swaps this for the
shm-shared surface),
attach_backing,set_scanoutto scanout 0,transfer_to_host_2d+resource_flushof a test pattern, and wait on the used ring. - Register a
scanoutservice (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_flushes
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
VirtioGpuBackendin the compositor:surface()= the shared scanout resource,present(damage)=resource_flushof the damaged rect.- The driver announces to
.display(looks it up, sends attach-scanout with itsscanoutendpoint + 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 (automated): boot with virtio-gpu + display-demo; the compositor logs
display: scanout upgraded to virtio-gpu, drives frames through the native backend, and
reads a pixel back from the shared scanout resource after a present to confirm the
composited frame landed (display: native present verified), while display-demo: ok
still fires. Without -device virtio-gpu, no scanout is announced and it stays on GOP —
the v1 display-service/display-demo gates still pass unchanged.
V5 — Mode-setting, EDID, and vsync
- virtio-gpu
GET_EDID→ a mode list;set_scanoutat a chosen mode = runtime resolution change.runtime.displaygainsmodes()/setMode(m). - A vsync/fenced
resource_flushpresent path → genuinely tear-free. - The compositor reports the native backend's
canModeSet/hasVsync= true.
Gate (automated): a display-modeset case reads the EDID mode list, calls setMode
to a different resolution, and confirms the change by reading the driver's scanout geometry
back (display: mode set to {w}x{h}, verified); the vsync/fenced present path is exercised
and confirmed by the flush fence completing (display: vsync present ok) — both from
serial, no eyeballing.
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.pycases:virtio-gpu, hot-attach,display-modeset, and a driver-kill/re-attach case. Update 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
shmcapability (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.