11 KiB
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) fromBootInformation.framebuffer; the service claims + WC-maps it. (Not a bespokeframebuffer_mapsyscall — the device route inherits ownership, release-on-death, and re-claim-on-restart.) - v1 = the full compositor pipeline on the dumb framebuffer. One
displayservice 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 addUserBinary in 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 is the registry).- The
run-efitarget 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; aDisplayInfo{ width, height, pitch, format }carried on the descriptor; aflagsfield onResourceDescriptor+resource_flag_write_combining. - devices-broker.zig:
seedDisplay(base, w, h, pitch, format)publishes a root-leveldisplaynode with one WC-flaggedmemoryresource[base, height*pitch]+ theDisplayInfo;displayDevice()/displayClaimed(). Seeded fromkmainafterdevices_broker.init. - process.zig
systemMmioMap+ paging (mapUserDeviceIntogains awrite_combiningbool): a resource's WC flag maps it through the WC PAT slot (setupPat) instead of strong-uncacheable. - console.zig:
setSuppressedquiesceswritewhile the display device is claimed (driven fromsystemDeviceClaim/ 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.
system/services/display/protocol.zig:Operation{ info, create_layer, configure_layer, destroy_layer, fill_rect, blit_tile, damage, present };externRequest/Reply; size +maximum_payloadconsts. (Model: block/protocol.zig.)- abi.zig:
ServiceId.display = 9. system/services/display/display.zig:main→ enumerate + claim + WC-map the LFB (front) →mmapa cacheable back buffer ofheight*pitch→runtime.service.run.infoand a whole-screenpresent(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
displayanddisplay_protocol):info()andpresent(), cached.displaylookup with retry (model: block.zig). - init.zig:
"display"added toboot_services. - build.zig:
display-protocolmodule on the runtime;displayexe viaaddUserBinary; packed into the initial-ramdisk; installed to/system/services/display. - Kernel fix the back buffer surfaced:
mmapwas capped at 256 pages (1 MiB) by a fixed kernel-stackframesarray. RewrotesystemMmapto 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-ownedmmap'd surface (freed ondestroy_layer).damageaccumulates 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.presentclears 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) isprotocol.pack, also host-tested. - Host tests (
zig build test, green): rect intersect/unite,fillRectclipping +stride > widthpadding,compositeoverlap-shows-top + damage clipping,blitTileunaligned read + clipping, andpackfor 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
Layerhandle withfill/blitTile(inline tile) /configure(move/restack/show) /damage/destroy,createLayer, and acolor(r,g,b)helper (caches the mode, packs viaprotocol.pack). Coordinates are signed over the wire (@bitCastboth ways). system/services/display-demo/: a hardware-free client (theinput-sourceanalog) — a full-screen wallpaper layer, a rectangle that slides back and forth (moved byconfigureeach frame, so the compositor repaints old + new), and a cursor layer; presents in a loop paced byruntime.time. Wired into build + initial-ramdisk.- Bug this surfaced:
protocol.message_maximumwas 4096, but the kernel caps every IPC message atMESSAGE_MAXIMUM= 256 — soreplyWaitrejected the oversized receive buffer with-E2BIGand the serve loop had been spinning since D2 (unseen, as D2/D3 matched init-time heartbeats). Set it to 256;blit_tileis 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), anddisplay-demo(D4 full pipeline: spawndisplay+display-demo, matchdisplay-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-trackmemory 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.