# 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 ` — 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. - [x] `system/services/display/protocol.zig`: `Operation{ info, create_layer, configure_layer, destroy_layer, fill_rect, blit_tile, damage, present }`; `extern` `Request`/`Reply`; size + `maximum_payload` consts. (Model: block/protocol.zig.) - [x] [abi.zig](../system/abi.zig): `ServiceId.display = 9`. - [x] `system/services/display/display.zig`: `main` → enumerate + claim + WC-map the LFB (front) → `mmap` a cacheable back buffer of `height*pitch` → `runtime.service.run`. `info` and a whole-screen `present` (back → front) are live; layer ops fail-stub until D3. Init clears the back buffer and presents it — the double-buffer path. - [x] [library/runtime/display.zig](../library/runtime/runtime.zig) (+ barrel export of `display` and `display_protocol`): `info()` and `present()`, cached `.display` lookup with retry (model: block.zig). - [x] [init.zig](../system/services/init/init.zig): `"display"` added to `boot_services`. - [x] [build.zig](../build.zig): `display-protocol` module on the runtime; `display` exe via `addUserBinary`; packed into the initial-ramdisk; installed to `/system/services/display`. - [x] **Kernel fix the back buffer surfaced:** `mmap` was capped at 256 pages (1 MiB) by a fixed kernel-stack `frames` array. Rewrote `systemMmap` to map page-by-page with rollback (no scratch array) and raised the cap to 8192 pages (32 MiB) — enough for a 4K back buffer. A real limitation met, exactly the kind this project chases. **Gate (met, automated):** `python3 test/qemu_test.py display-service` spawns the compositor and matches its own serial heartbeats — `display: online {w}x{h} pitch …` followed by `display: presented frame 0` — which it prints only after the whole claim → WC-map → back-buffer → clear → present chain succeeds (matched on serial like the fault cases, since a lone blocking service can't reschedule the in-kernel test context to poll). Regression-checked: `usermem`, `heap` (the `mmap` rewrite), `init` (the boot-list addition), and D1's `display` all still pass. ## D3 — Layer stack + compositor + damage present ✅ The heart: composite an ordered layer stack, present only what changed. - [x] A layer table (16 slots): each `Layer` = position, z, visible, a server-owned `mmap`'d surface (freed on `destroy_layer`). `damage` accumulates the dirty screen region since the last present. - [x] `create_layer` / `configure_layer` (damages old + new footprints) / `destroy_layer`, `fill_rect`, `blit_tile` (reads the inline tile from the IPC payload, unaligned-safe), `damage`, `present`. - [x] Pure, host-tested [compositor.zig](../system/services/display/compositor.zig): `Rect` (intersect/unite), `Surface`, `fillRect`, `composite` (opaque, clipped to a damage rect), `blitTile`. `present` clears the damaged region to the wallpaper, paints the visible layers bottom-to-top (z-sorted), and flushes just that rect back → front (WC). Colour packing (rgbx/bgrx) is `protocol.pack`, also host-tested. - [x] Host tests (`zig build test`, green): rect intersect/unite, `fillRect` clipping + `stride > width` padding, `composite` overlap-shows-top + damage clipping, `blitTile` unaligned read + clipping, and `pack` for both pixel formats. **Gate (met):** `zig build test` green for the compositor + pack unit tests, **and** the `display-service` case's startup self-check composites two overlapping layers on the real framebuffer and reads back the composited pixels — overlap = top layer, outside = bottom layer — logging `display: compositor self-check ok` (matched by the harness). ## D4 — Client API + the demo client ✅ Prove the pipeline end-to-end from a separate process. - [x] Finished [runtime/display.zig](../library/runtime/runtime.zig): a `Layer` handle with `fill` / `blitTile` (inline tile) / `configure` (move/restack/show) / `damage` / `destroy`, `createLayer`, and a `color(r,g,b)` helper (caches the mode, packs via `protocol.pack`). Coordinates are signed over the wire (`@bitCast` both ways). - [x] `system/services/display-demo/`: a hardware-free client (the `input-source` analog) — a full-screen wallpaper layer, a rectangle that slides back and forth (moved by `configure` each frame, so the compositor repaints old + new), and a cursor layer; presents in a loop paced by `runtime.time`. Wired into build + initial-ramdisk. - [x] **Bug this surfaced:** `protocol.message_maximum` was 4096, but the kernel caps every IPC message at `MESSAGE_MAXIMUM` = 256 — so `replyWait` rejected the oversized receive buffer with `-E2BIG` and the serve loop had been *spinning* since D2 (unseen, as D2/D3 matched init-time heartbeats). Set it to 256; `blit_tile` is now explicitly a small-tile path (≤ 54 px inline), larger bitmaps being the deferred shared-memory surface. **Gate (met):** `python3 test/qemu_test.py display-demo` spawns the service + `display-demo`; the demo drives a run of frames of motion through the layer client API and logs `display-demo: ok` (the visible motion is a screenshot via `zig build run-x86-64`). Regression-checked: `zig build test`, `display` (D1), and `display-service` (D2/D3) all still pass, and the default `zig build` is clean. ## D5 — Test cases + docs ✅ - [x] The three integration cases exist and pass: `display` (D1 handoff, kernel), `display-service` (D2/D3 compositor + self-check), and `display-demo` (D4 full pipeline: spawn `display` + `display-demo`, match `display-demo: ok`) — [tests.zig](../system/kernel/tests.zig) + [qemu_test.py](../test/qemu_test.py). Plus the pure host tests (`zig build test`). - [x] [display.md](display.md) updated to the built state (the "Verifying it" section names the real cases); [README index](README.md) entry present (#19); the `display-track` memory marked DONE with the commits. **Gate (met):** `python3 test/qemu_test.py display display-service display-demo` all pass, `zig build test` is green, and the default `zig build` is clean. --- ## v1 status: complete D1–D5 done. The display service is a working framebuffer compositor: it owns the framebuffer (write-combining), composites a z-ordered layer stack into a cacheable back buffer, presents only the damaged region, and is driven over IPC by the `runtime.display` client — proven end-to-end by a separate demo process. Two limitations are deliberate and documented (docs/display.md): no runtime mode-setting (native backend) and no true vsync (no vblank on a dumb framebuffer). Next steps are the Deferred items below. --- ## Deferred (explicitly not in this plan) - **Shared-memory surfaces** — generalize M13 capability passing to memory objects (`shared_memory_create`/`shared_memory_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.