danos/docs/device-driver-development/display-plan.md

11 KiB
Raw Blame History

Display service — build plan (v1: the dumb-framebuffer compositor)

The ordered, checkpointable build-out for display.md. Each milestone is small, lands on its own, and ends in a verifiable gate — shaped for a /loop run. Read 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 display node (found by class, not name) 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: spell out non-acronym abbreviations in full, kebab-case file names, no Co-Authored-By trailers on commits. New user binaries go through build-support's shared user-binary recipe and get packed into the initial-ramdisk; protocols are modules exported by the library/protocol package. (This section predates the build-packages split; see build-packages-plan.md for the current build shape.)

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

  • 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.
  • 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.
  • 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.
  • 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) 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.

  • library/protocol/display/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.)
  • 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*pitchruntime.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.
  • library/runtime/display.zig (+ barrel export of display and display_protocol): info() and present(), cached .display lookup with retry (model: block.zig).
  • init.zig: "display" added to boot_services.
  • build.zig: display-protocol module on the runtime; display exe via addUserBinary; packed into the initial-ramdisk; installed to /system/services/display.
  • 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.

  • 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.
  • 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.
  • Pure, host-tested 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.
  • 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.

  • Finished runtime/display.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).
  • 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.
  • 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

  • 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 + qemu_test.py. Plus the pure host tests (zig build test).
  • display.md updated to the built state (the "Verifying it" section names the real cases); README index 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

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