display: framebuffer handoff primitive + service design (D1)
Kick off the display service track (docs/display.md, docs/display-plan.md): a
user-space compositor that owns the framebuffer. GOP and the PCI display device
are two views of one controller; GOP dies at ExitBootServices, so the portable
base is the boot-handoff linear framebuffer.
D1 makes that framebuffer reachable from user space over the existing device
claim/mmio_map path rather than a bespoke syscall:
- device-abi: a `display` DeviceClass, a DisplayInfo{w,h,pitch,format} on the
descriptor, and a flags field on resources with a write-combining bit.
- devices-broker: seedDisplay() publishes the loader's framebuffer as a
root-level `display` node (one WC-flagged memory resource); kmain seeds it
after discovery. displayDevice()/displayClaimed() track the claim.
- paging/mmio_map: mapUserDeviceInto gains a write_combining bool — a WC-flagged
resource maps through PAT entry 4 instead of strong-uncacheable (an
uncacheable framebuffer blit is glacial).
- console: falls silent while a display service holds the framebuffer, and is
forced back on by the panic/exception paths.
Gate: the `display` kernel test asserts the seeded node's shape and that the
claim + mmio_map leaf is genuinely write-combining (PAT bit set, PCD/PWT clear).
Regression-checked discovery/ioport/claim-release/supervision/device-list/
device-manager with the +1 device in the table.
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
# Display service — build plan (v1: the dumb-framebuffer compositor)
|
||||
|
||||
The ordered, checkpointable build-out for [display.md](display.md). Each milestone is
|
||||
small, lands on its own, and ends in a **verifiable gate** — shaped for a `/loop` run.
|
||||
Read [display.md](display.md) first for the *why*; this is the *what* and the *order*.
|
||||
|
||||
## Locked decisions (do not relitigate)
|
||||
|
||||
- **Handoff = device node + write-combining `mmio_map`.** The kernel seeds a synthetic
|
||||
`display0` node from `BootInformation.framebuffer`; the service claims + WC-maps it.
|
||||
(Not a bespoke `framebuffer_map` syscall — the device route inherits ownership,
|
||||
release-on-death, and re-claim-on-restart.)
|
||||
- **v1 = the full compositor pipeline on the dumb framebuffer.** One `display` service
|
||||
owns the LFB + a cacheable back buffer + a layer stack; double-buffer + damage-driven
|
||||
present; clients draw via server-side commands. **No** runtime mode-setting, **no**
|
||||
shared-memory surfaces — both deferred (see display.md, "What v1 does not do").
|
||||
|
||||
## Conventions
|
||||
|
||||
Follow [coding-standards.md](coding-standards.md): spell out non-acronym abbreviations in
|
||||
full, kebab-case file names, no `Co-Authored-By` trailers on commits. New user binaries
|
||||
go through `addUserBinary` in [build.zig](../build.zig) and get packed into the
|
||||
initial-ramdisk; protocols are `b.addModule("…-protocol", …)` and imported into the
|
||||
`runtime` module.
|
||||
|
||||
## How to verify along the way
|
||||
|
||||
- `zig build test` — host unit tests (compositor math: layer clipping, damage merge,
|
||||
pitch/format blits are all host-testable with a fake framebuffer).
|
||||
- `python3 test/qemu_test.py <case>` — boots the real kernel in QEMU; assert on the
|
||||
serial log ([tests.zig](../system/kernel/tests.zig) is the registry).
|
||||
- The `run-efi` target renders to QEMU's display (`-device VGA,edid=on,xres=1280,yres=720`)
|
||||
— a screenshot confirms pixels for the milestones whose gate is visual.
|
||||
|
||||
---
|
||||
|
||||
## D1 — The handoff primitive (kernel) ✅
|
||||
|
||||
Make the boot framebuffer reachable and mappable **write-combining** from user space.
|
||||
|
||||
- [x] [device-abi.zig](../system/devices/device-abi.zig): added `DeviceClass.display`; a
|
||||
`DisplayInfo{ width, height, pitch, format }` carried on the descriptor; a
|
||||
`flags` field on `ResourceDescriptor` + `resource_flag_write_combining`.
|
||||
- [x] [devices-broker.zig](../system/kernel/devices-broker.zig): `seedDisplay(base, w, h,
|
||||
pitch, format)` publishes a root-level `display` node with one WC-flagged `memory`
|
||||
resource `[base, height*pitch]` + the `DisplayInfo`; `displayDevice()` /
|
||||
`displayClaimed()`. Seeded from `kmain` after `devices_broker.init`.
|
||||
- [x] [process.zig](../system/kernel/process.zig) `systemMmioMap` + paging
|
||||
(`mapUserDeviceInto` gains a `write_combining` bool): a resource's WC flag maps it
|
||||
through the WC PAT slot (`setupPat`) instead of strong-uncacheable.
|
||||
- [x] [console.zig](../system/kernel/console.zig): `setSuppressed` quiesces `write` while
|
||||
the display device is claimed (driven from `systemDeviceClaim` / release); the
|
||||
terminal panic + exception paths clear it first so a dying machine still draws.
|
||||
|
||||
**Gate (met, automated):** the `display` kernel test (`python3 test/qemu_test.py display`,
|
||||
`displayTest` in [tests.zig](../system/kernel/tests.zig)) asserts the seeded node's shape
|
||||
and geometry, then walks the real claim + `mmio_map` path into a throwaway address space
|
||||
and verifies the leaf is **write-combining** (PAT entry 4: PAT bit set, PCD/PWT clear) —
|
||||
with an uncacheable-still-uncacheable regression guard. Chosen over the original
|
||||
screenshot-of-a-fill gate because it proves the *actual* WC property headlessly; the
|
||||
visible fill folds into D2's gate (the service clears the screen through the back buffer).
|
||||
Regression-checked: `discovery`, `ioport`, `claim-release`, `supervision`, `device-list`,
|
||||
`device-manager` all still pass with the +1 device in the table.
|
||||
|
||||
## D2 — Service skeleton, protocol, runtime module
|
||||
|
||||
Stand up the named service and the double-buffer, no layers yet.
|
||||
|
||||
- [ ] `system/services/display/protocol.zig`: `Operation{ info, create_layer,
|
||||
configure_layer, destroy_layer, fill_rect, blit_tile, damage, present }`; `extern`
|
||||
`Request`/`Reply`; `message_maximum`/`request_size`/`reply_size` consts. (Model:
|
||||
[block/protocol.zig](../system/services/block/protocol.zig).)
|
||||
- [ ] [abi.zig](../system/abi.zig): `ServiceId.display = 9`.
|
||||
- [ ] `system/services/display/display.zig`: `main` → enumerate + claim + WC-map the LFB
|
||||
(front) → `mmap` a cacheable back buffer of `height*pitch` → `runtime.service.run(
|
||||
.{ .service = .display, .init, .on_message })`. Implement `info` and a whole-screen
|
||||
`present` (back → front) first.
|
||||
- [ ] [library/runtime/display.zig](../library/runtime/runtime.zig) (+ export in
|
||||
`runtime.zig`): `info()`, and a stub `present()`. Cached `.display` lookup with
|
||||
retry (model: [block.zig](../library/runtime/block.zig)).
|
||||
- [ ] [init.zig](../system/services/init/init.zig): add `"display"` to `boot_services`.
|
||||
- [ ] [build.zig](../build.zig): `display-protocol` module; `display` exe via
|
||||
`addUserBinary`; import the protocol into `runtime` and into the exe; pack into the
|
||||
initial-ramdisk; install to `/system/services/display`.
|
||||
|
||||
**Gate:** boot; `init` spawns `display`; it logs `display: online {w}x{h}` and clears the
|
||||
screen to a colour **through the back buffer → present path** (double buffering proven —
|
||||
no direct-to-LFB drawing). Screenshot confirms.
|
||||
|
||||
## D3 — Layer stack + compositor + damage present
|
||||
|
||||
The heart: composite an ordered layer stack, present only what changed.
|
||||
|
||||
- [ ] A layer table (fixed capacity, like the input service's subscriber table): each
|
||||
layer = rect, z-order, visible flag, a server-owned surface (`mmap` cacheable).
|
||||
- [ ] Implement `create_layer` / `configure_layer` / `destroy_layer`, `fill_rect`,
|
||||
`blit_tile` (inline tile in the IPC message), `damage`.
|
||||
- [ ] `composite()`: walk layers bottom-to-top, paint dirty regions into the back buffer
|
||||
(clip to layer rect ∩ damage; handle `rgbx`/`bgrx`; step by `pitch`).
|
||||
- [ ] `present()`: flush merged damage rects back → front (sequential WC writes).
|
||||
- [ ] Host tests (`zig build test`): layer clipping, damage-rect merge, and a
|
||||
`blit`/`fill` against a fake in-memory framebuffer for both pixel formats and a
|
||||
`pitch > width*4` case.
|
||||
|
||||
**Gate:** `zig build test` green for the compositor unit tests; an in-service self-check
|
||||
composites two overlapping layers and the overlap shows the top layer's colour.
|
||||
|
||||
## D4 — Client API + the demo client
|
||||
|
||||
Prove the pipeline end-to-end from a separate process.
|
||||
|
||||
- [ ] Finish [runtime/display.zig](../library/runtime/runtime.zig): a `Layer` handle with
|
||||
`fill` / `blitTile` / `damage`, plus `present()`.
|
||||
- [ ] `system/services/display-demo/`: a hardware-free client (the
|
||||
[`input-source`](../system/services/input-source/) analog) — a wallpaper layer, a
|
||||
layer with a rectangle it moves each frame, and a small cursor layer; `present`s in
|
||||
a loop paced by [`runtime.time`](../library/runtime/time.zig). Wire into build +
|
||||
initial-ramdisk.
|
||||
|
||||
**Gate:** run `run-efi`; a screenshot (or two, apart in time) shows the wallpaper, the
|
||||
cursor, and the rectangle in different positions — motion, from a client, through the
|
||||
compositor, on screen.
|
||||
|
||||
## D5 — Test case + docs
|
||||
|
||||
- [ ] `test/qemu_test.py display` + `displayTest` in
|
||||
[tests.zig](../system/kernel/tests.zig): boot, spawn `display` + `display-demo`,
|
||||
pass on `display: presented frame {N}` and `display-demo: ok` heartbeats.
|
||||
- [ ] Flip [display.md](display.md)'s status notes from "planned/built" as appropriate;
|
||||
confirm the [README index](README.md) entry; update the `display-track` memory to
|
||||
DONE with the commit.
|
||||
|
||||
**Gate:** `python3 test/qemu_test.py display` passes in CI-equivalent local run.
|
||||
|
||||
---
|
||||
|
||||
## Deferred (explicitly not in this plan)
|
||||
|
||||
- **Shared-memory surfaces** — generalize M13 capability passing to memory objects
|
||||
(`shm_create`/`shm_map`), so bitmap clients hand the compositor a rendered surface
|
||||
instead of drawing commands. The compositor's layer model already anticipates it.
|
||||
- **Native backend (Bochs DISPI, then virtio-gpu)** — behind the same internal backend
|
||||
interface as the dumb framebuffer: EDID mode list + runtime resolution/bpp change +
|
||||
(eventually) a vblank/flip path for true vsync.
|
||||
- **Driver/compositor process split** — only when a second backend or a second head makes
|
||||
the abstraction pay for itself.
|
||||
Reference in New Issue
Block a user