# 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.

- [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.
