display: layer stack + damage-driven compositor (D3)

Turn the service into a real compositor. A layer is a server-owned surface (its
own mmap'd cacheable buffer) with a screen position, z-order, and visibility.
Clients create layers, draw into them by command, mark damage, and present; the
compositor repaints only the damaged region — clear to the wallpaper, paint the
visible layers bottom-to-top (z-sorted), flush that rectangle back -> front (WC).

- compositor.zig: the pure, host-tested core — Rect (intersect/unite), Surface,
  fillRect, composite (opaque, clipped to a damage rect), blitTile (unaligned-
  safe read of a client tile). No syscall/runtime dependency.
- protocol.zig: pack(format, r, g, b) — native pixel encoding for rgbx/bgrx, the
  shared colour vocabulary of client and server. Host-tested.
- display.zig: the layer table + create/configure/destroy/fill_rect/blit_tile/
  damage/present ops wired onto the compositor, plus a damage-accumulating present.
- Startup self-check: two overlapping layers composited on the real framebuffer,
  read back to confirm the overlap shows the top layer and outside shows the
  bottom — logs `display: compositor self-check ok`.

Gate: `zig build test` green (compositor + pack), and the display-service case's
self-check passes on hardware. Both new pure modules added to the test loop.
This commit is contained in:
Daniel Samson 2026-07-14 01:39:04 +01:00
parent 69b018cc32
commit f9cf0007c5
No known key found for this signature in database
GPG Key ID: A9EB2589C60F7268
6 changed files with 498 additions and 56 deletions

View File

@ -770,6 +770,8 @@ pub fn build(b: *std.Build) void {
"system/services/vfs/protocol.zig", // NodeKind / DirectoryEntry sizes + op values
"system/services/fat/on-disk.zig", // FAT on-disk struct sizes + type detection
"system/services/fat/engine.zig", // FAT read/write over a RAM-backed image
"system/services/display/compositor.zig", // Rect math + fill/composite/blit-tile
"system/services/display/protocol.zig", // pack(): native pixel encoding per format
}) |root| {
const mod_tests = b.addTest(.{
.root_module = b.createModule(.{

View File

@ -94,23 +94,29 @@ fault cases, since a lone blocking service can't reschedule the in-kernel test c
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
## 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.
- [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:** `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.
**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

View File

@ -0,0 +1,192 @@
//! The compositor's pure core: rectangle math and the three blitting primitives the
//! display service composes frames from fill a rectangle of a surface, composite one
//! surface onto another clipped to a damage rectangle, and copy a client-supplied pixel
//! tile in. Deliberately free of any syscall or `runtime` dependency (it takes plain
//! pixel pointers), so it is host-tested under `zig build test`. The service
//! (system/services/display/display.zig) wires real mmap'd surfaces and the framebuffer
//! to it. Pixels are opaque native 32-bit values v1 layers don't alpha-blend, and
//! channel order (rgbx/bgrx) is the caller's concern (see protocol.pack).
const std = @import("std");
/// An axis-aligned rectangle in pixels. Signed, so a surface partly off-screen (a layer
/// dragged past an edge) clips with plain arithmetic. Half-open: covers [x, x+w) × [y, y+h).
pub const Rect = struct {
x: i32,
y: i32,
w: i32,
h: i32,
pub const empty = Rect{ .x = 0, .y = 0, .w = 0, .h = 0 };
pub fn init(x: i32, y: i32, w: i32, h: i32) Rect {
return .{ .x = x, .y = y, .w = w, .h = h };
}
pub fn isEmpty(r: Rect) bool {
return r.w <= 0 or r.h <= 0;
}
pub fn right(r: Rect) i32 {
return r.x + r.w;
}
pub fn bottom(r: Rect) i32 {
return r.y + r.h;
}
/// The overlap of two rectangles, or an empty rectangle if they don't touch.
pub fn intersect(a: Rect, b: Rect) Rect {
const x0 = @max(a.x, b.x);
const y0 = @max(a.y, b.y);
const x1 = @min(a.right(), b.right());
const y1 = @min(a.bottom(), b.bottom());
return .{ .x = x0, .y = y0, .w = x1 - x0, .h = y1 - y0 };
}
/// The bounding box of two rectangles. An empty operand contributes nothing (returns
/// the other), so folding damage rectangles with `unite` from `empty` yields their
/// bounding box.
pub fn unite(a: Rect, b: Rect) Rect {
if (a.isEmpty()) return b;
if (b.isEmpty()) return a;
const x0 = @min(a.x, b.x);
const y0 = @min(a.y, b.y);
const x1 = @max(a.right(), b.right());
const y1 = @max(a.bottom(), b.bottom());
return .{ .x = x0, .y = y0, .w = x1 - x0, .h = y1 - y0 };
}
};
/// A block of 32-bit pixels: `pixels` addressed row-major with `stride` pixels between
/// row starts ( width the framebuffer's stride is pitch/4, a layer's is its width).
pub const Surface = struct {
pixels: [*]u32,
stride: u32, // pixels per row
width: u32,
height: u32,
pub fn bounds(s: Surface) Rect {
return .{ .x = 0, .y = 0, .w = @intCast(s.width), .h = @intCast(s.height) };
}
inline fn row(s: Surface, y: u32) [*]u32 {
return s.pixels + @as(usize, y) * s.stride;
}
};
/// Fill `rect` of `s` with the native pixel `colour`, clipped to `s`'s bounds.
pub fn fillRect(s: Surface, rect: Rect, colour: u32) void {
const c = rect.intersect(s.bounds());
if (c.isEmpty()) return;
var y: i32 = c.y;
while (y < c.bottom()) : (y += 1) {
const r = s.row(@intCast(y));
var x: i32 = c.x;
while (x < c.right()) : (x += 1) r[@intCast(x)] = colour;
}
}
/// Composite the whole of `layer` onto `dst` with the layer's top-left at (`dx`, `dy`),
/// painting only the pixels that fall inside `clip` (a `dst`-space rectangle) and inside
/// `dst`. Opaque copy. This is the primitive `present` repeats over the visible layer
/// stack, bottom to top, for each damaged region.
pub fn composite(dst: Surface, dx: i32, dy: i32, layer: Surface, clip: Rect) void {
const on_screen = Rect{ .x = dx, .y = dy, .w = @intCast(layer.width), .h = @intCast(layer.height) };
const region = on_screen.intersect(clip).intersect(dst.bounds());
if (region.isEmpty()) return;
var y: i32 = region.y;
while (y < region.bottom()) : (y += 1) {
const src = layer.row(@intCast(y - dy));
const d = dst.row(@intCast(y));
var x: i32 = region.x;
while (x < region.right()) : (x += 1) {
d[@intCast(x)] = src[@intCast(x - dx)];
}
}
}
/// Copy a `w`×`h` tile of native pixels from `src` (raw little-endian bytes, row-major,
/// tightly packed) into `dst` at (`dx`, `dy`), clipped to `dst`'s bounds. `src` is read
/// with `readInt` because it comes straight out of an IPC message buffer and carries no
/// alignment guarantee. Returns without touching anything if `src` is short.
pub fn blitTile(dst: Surface, dx: i32, dy: i32, src: []const u8, w: u32, h: u32) void {
if (src.len < @as(usize, w) * h * 4) return;
var ty: u32 = 0;
while (ty < h) : (ty += 1) {
const yy = dy + @as(i32, @intCast(ty));
if (yy < 0 or yy >= dst.height) continue;
const drow = dst.row(@intCast(yy));
var tx: u32 = 0;
while (tx < w) : (tx += 1) {
const xx = dx + @as(i32, @intCast(tx));
if (xx < 0 or xx >= dst.width) continue;
const off = (@as(usize, ty) * w + tx) * 4;
drow[@intCast(xx)] = std.mem.readInt(u32, src[off..][0..4], .little);
}
}
}
// --- tests ------------------------------------------------------------------
test "rect intersect: overlap and disjoint" {
try std.testing.expectEqual(Rect.init(5, 5, 5, 5), Rect.init(0, 0, 10, 10).intersect(Rect.init(5, 5, 10, 10)));
try std.testing.expect(Rect.init(0, 0, 10, 10).intersect(Rect.init(20, 20, 5, 5)).isEmpty());
}
test "rect unite: bounding box, empty is identity" {
const a = Rect.init(2, 2, 4, 4);
try std.testing.expectEqual(Rect.init(2, 1, 10, 5), a.unite(Rect.init(10, 1, 2, 2)));
try std.testing.expectEqual(a, a.unite(Rect.empty));
try std.testing.expectEqual(a, Rect.empty.unite(a));
}
test "fillRect clips to surface and honours stride padding" {
// A 4×3 surface inside a 6-wide allocation (stride 6 > width 4), like pitch padding.
var mem = [_]u32{0} ** (6 * 3);
const s = Surface{ .pixels = &mem, .stride = 6, .width = 4, .height = 3 };
fillRect(s, Rect.init(-1, -1, 3, 3), 0xAB); // straddles the top-left corner
try std.testing.expectEqual(@as(u32, 0xAB), mem[0 * 6 + 0]);
try std.testing.expectEqual(@as(u32, 0xAB), mem[1 * 6 + 1]);
try std.testing.expectEqual(@as(u32, 0), mem[0 * 6 + 2]); // beyond the 2-wide fill
try std.testing.expectEqual(@as(u32, 0), mem[2 * 6 + 0]); // row 2 untouched
try std.testing.expectEqual(@as(u32, 0), mem[0 * 6 + 4]); // stride padding untouched
}
test "composite: overlap shows the top layer, clipped to damage" {
var back = [_]u32{0} ** (8 * 8);
const dst = Surface{ .pixels = &back, .stride = 8, .width = 8, .height = 8 };
var lo = [_]u32{0x11} ** (4 * 4);
var hi = [_]u32{0x22} ** (4 * 4);
const low = Surface{ .pixels = &lo, .stride = 4, .width = 4, .height = 4 };
const high = Surface{ .pixels = &hi, .stride = 4, .width = 4, .height = 4 };
composite(dst, 0, 0, low, dst.bounds()); // bottom at (0,0)
composite(dst, 2, 2, high, dst.bounds()); // top overlaps at (2,2)
try std.testing.expectEqual(@as(u32, 0x11), back[0 * 8 + 0]); // bottom-only
try std.testing.expectEqual(@as(u32, 0x22), back[3 * 8 + 3]); // overlap top wins
try std.testing.expectEqual(@as(u32, 0x22), back[5 * 8 + 5]); // top-only
try std.testing.expectEqual(@as(u32, 0), back[7 * 8 + 7]); // neither
}
test "composite honours the damage rectangle" {
var back = [_]u32{0} ** (8 * 8);
const dst = Surface{ .pixels = &back, .stride = 8, .width = 8, .height = 8 };
var fill = [_]u32{0x33} ** (8 * 8);
const layer = Surface{ .pixels = &fill, .stride = 8, .width = 8, .height = 8 };
composite(dst, 0, 0, layer, Rect.init(2, 2, 2, 2)); // only this damage region
try std.testing.expectEqual(@as(u32, 0x33), back[2 * 8 + 2]);
try std.testing.expectEqual(@as(u32, 0x33), back[3 * 8 + 3]);
try std.testing.expectEqual(@as(u32, 0), back[1 * 8 + 1]); // outside damage
try std.testing.expectEqual(@as(u32, 0), back[4 * 8 + 4]); // outside damage
}
test "blitTile copies a packed tile, clipping and reading unaligned bytes" {
var back = [_]u32{0} ** (4 * 4);
const dst = Surface{ .pixels = &back, .stride = 4, .width = 4, .height = 4 };
// A 2×2 tile in a byte buffer offset by one byte, so reads are unaligned.
var raw = [_]u8{0} ** (1 + 2 * 2 * 4);
const tile = raw[1..];
for (0..4) |i| std.mem.writeInt(u32, tile[i * 4 ..][0..4], @intCast(0xA0 + i), .little);
blitTile(dst, 3, 3, tile, 2, 2); // bottom-right corner; only (3,3) lands on-surface
try std.testing.expectEqual(@as(u32, 0xA0), back[3 * 4 + 3]);
try std.testing.expectEqual(@as(u32, 0), back[0]); // nothing else touched
}

View File

@ -1,21 +1,27 @@
//! /system/services/display the display service (docs/display.md). A ring-3 process
//! that claims the framebuffer the kernel seeded (docs/display-plan.md D1), owns it as a
//! **write-combining front buffer**, composites into a **cacheable back buffer**, and
//! presents finished frames the GUI track's compositor, the sibling of the input
//! service. It is reached by name over `ServiceId.display`.
//! **write-combining front buffer**, composites an ordered stack of **layers** into a
//! **cacheable back buffer**, and presents finished frames the GUI track's compositor,
//! the sibling of the input service. Reached by name over `ServiceId.display`.
//!
//! This is the D2 skeleton: it comes up, claims + maps the framebuffer, allocates the
//! back buffer, and proves the double-buffer path by clearing the back buffer and
//! presenting it. The layer stack, damage tracking, and per-layer drawing arrive in D3;
//! for now `info` and a whole-screen `present` are the live operations.
//! A layer is a server-owned surface (its own cacheable buffer) with a screen position,
//! z-order, and visibility. Clients create layers and draw into them by command
//! (`fill_rect`, `blit_tile`), mark `damage`, and ask for a `present`; the compositor
//! repaints only the damaged region clear it, paint the visible layers bottom-to-top,
//! flush it to the screen. The pixel math lives in the pure, host-tested
//! [compositor.zig](compositor.zig); this file wires real surfaces and the framebuffer to
//! it. Shared-memory client surfaces are a later milestone (docs/display.md).
const std = @import("std");
const runtime = @import("runtime");
const compositor = @import("compositor.zig");
const protocol = runtime.display_protocol;
const ipc = runtime.ipc;
const system = runtime.system;
const device = runtime.device;
const Rect = compositor.Rect;
const Surface = compositor.Surface;
/// The claimed framebuffer and its off-screen twin. The front buffer is the LFB
/// write-combining, so it is **only ever written**, never read; all compositing happens
@ -33,10 +39,219 @@ const Display = struct {
var display: Display = undefined;
/// The wallpaper the compositor clears damaged regions to before painting layers.
var background: u32 = 0;
/// The layer stack. A fixed table (a compositor has few top-level surfaces during
/// bring-up); each used slot owns an mmap'd surface. `damage` accumulates the dirty
/// screen region since the last `present`, so a present touches only what changed.
const maximum_layers = 16;
const Layer = struct {
used: bool = false,
x: i32 = 0,
y: i32 = 0,
z: u32 = 0,
visible: bool = false,
surface: Surface = undefined,
surface_len: usize = 0, // for munmap on destroy
};
var layers: [maximum_layers]Layer = [_]Layer{.{}} ** maximum_layers;
var damage: Rect = Rect.empty;
/// Enumeration buffer kept off the stack a `DeviceDescriptor` is large, and this
/// service only ever needs one scan.
var device_table: [64]device.DeviceDescriptor = undefined;
// --- geometry helpers -------------------------------------------------------
fn screenRect() Rect {
return .{ .x = 0, .y = 0, .w = @intCast(display.width), .h = @intCast(display.height) };
}
fn backSurface() Surface {
return .{
.pixels = @ptrCast(@alignCast(display.back)),
.stride = display.pitch / 4, // pitch is bytes; a 32-bpp row is pitch/4 pixels
.width = display.width,
.height = display.height,
};
}
fn layerScreenRect(l: *const Layer) Rect {
return .{ .x = l.x, .y = l.y, .w = @intCast(l.surface.width), .h = @intCast(l.surface.height) };
}
/// Add `r` (screen coordinates) to the pending damage, clipped to the screen.
fn addDamage(r: Rect) void {
damage = damage.unite(r.intersect(screenRect()));
}
// --- layer operations (called from onMessage and the self-check) ------------
fn freeLayer() ?u32 {
for (&layers, 0..) |*l, i| {
if (!l.used) return @intCast(i);
}
return null;
}
/// A used layer by id, or null if the id is out of range or free.
fn layerAt(id: u32) ?*Layer {
if (id >= maximum_layers or !layers[id].used) return null;
return &layers[id];
}
fn createLayer(x: i32, y: i32, w: u32, h: u32, z: u32, visible: bool) ?u32 {
if (w == 0 or h == 0) return null;
const slot = freeLayer() orelse return null;
const len = @as(usize, w) * h * 4;
const base = system.mmap(len, system.PROT_READ | system.PROT_WRITE);
if (system.mmapFailed(base)) return null;
layers[slot] = .{
.used = true,
.x = x,
.y = y,
.z = z,
.visible = visible,
.surface = .{ .pixels = @ptrFromInt(base), .stride = w, .width = w, .height = h },
.surface_len = len,
};
return slot;
}
fn fillLayer(id: u32, local: Rect, colour: u32) bool {
const l = layerAt(id) orelse return false;
compositor.fillRect(l.surface, local, colour);
// Damage in screen space = the fill, translated by the layer origin, within the layer.
const screen = Rect{ .x = l.x + local.x, .y = l.y + local.y, .w = local.w, .h = local.h };
addDamage(screen.intersect(layerScreenRect(l)));
return true;
}
fn blitLayer(id: u32, x: i32, y: i32, w: u32, h: u32, pixels: []const u8) bool {
const l = layerAt(id) orelse return false;
compositor.blitTile(l.surface, x, y, pixels, w, h);
const screen = Rect{ .x = l.x + x, .y = l.y + y, .w = @intCast(w), .h = @intCast(h) };
addDamage(screen.intersect(layerScreenRect(l)));
return true;
}
fn configureLayer(id: u32, x: i32, y: i32, z: u32, visible: bool) bool {
const l = layerAt(id) orelse return false;
addDamage(layerScreenRect(l)); // the old footprint must repaint
l.x = x;
l.y = y;
l.z = z;
l.visible = visible;
addDamage(layerScreenRect(l)); // and the new one
return true;
}
fn destroyLayer(id: u32) bool {
const l = layerAt(id) orelse return false;
addDamage(layerScreenRect(l));
_ = system.munmap(@intFromPtr(l.surface.pixels), l.surface_len);
l.* = .{};
return true;
}
// --- compositing + present --------------------------------------------------
/// Repaint the damaged region `clip` of the back buffer: clear it to the background, then
/// paint every visible layer that overlaps it, bottom to top (ascending z).
fn compositeInto(clip: Rect) void {
const back = backSurface();
compositor.fillRect(back, clip, background);
// z-order the used, visible layers (n 16; a plain insertion sort of indices).
var order: [maximum_layers]u32 = undefined;
var n: usize = 0;
for (layers, 0..) |l, i| {
if (l.used and l.visible) {
order[n] = @intCast(i);
n += 1;
}
}
var a: usize = 1;
while (a < n) : (a += 1) {
const key = order[a];
var b: usize = a;
while (b > 0 and layers[order[b - 1]].z > layers[key].z) : (b -= 1) order[b] = order[b - 1];
order[b] = key;
}
for (order[0..n]) |i| {
const l = layers[i];
compositor.composite(back, l.x, l.y, l.surface, clip);
}
}
/// Stream the damaged rectangle from the cacheable back buffer to the write-combining
/// front buffer, row by row (sequential writes what WC memory wants; we never read the
/// front buffer). Only the visible width of each row is touched.
fn flushRect(rect: Rect) void {
const c = rect.intersect(screenRect());
if (c.isEmpty()) return;
var y: i32 = c.y;
while (y < c.bottom()) : (y += 1) {
const off = @as(usize, @intCast(y)) * display.pitch;
const src: [*]const u32 = @ptrCast(@alignCast(display.back + off));
const dst: [*]volatile u32 = @ptrCast(@alignCast(display.front + off));
var x: i32 = c.x;
while (x < c.right()) : (x += 1) dst[@intCast(x)] = src[@intCast(x)];
}
}
/// Composite and flush the accumulated damage, then clear it. A no-op when nothing is
/// dirty. The frame counter advances regardless, so callers can name frames.
fn present() void {
const dirty = damage.intersect(screenRect());
if (!dirty.isEmpty()) {
compositeInto(dirty);
flushRect(dirty);
}
damage = Rect.empty;
display.frames += 1;
}
// --- startup self-check -----------------------------------------------------
/// Prove the compositor wiring on the real framebuffer: two overlapping opaque layers,
/// composited, must show the top layer in the overlap and the bottom layer outside it.
/// Exercises the whole path mmap surfaces, the z-sort, damage, composite into the back
/// buffer and reads the composited result back. Cleans up after itself.
fn selfCheck() void {
const red = protocol.pack(display.format, 0xC0, 0x20, 0x20);
const green = protocol.pack(display.format, 0x20, 0xC0, 0x20);
const bottom = createLayer(100, 100, 80, 80, 0, true) orelse return fail_check("create");
const top = createLayer(140, 140, 80, 80, 1, true) orelse return fail_check("create");
_ = fillLayer(bottom, Rect.init(0, 0, 80, 80), red);
_ = fillLayer(top, Rect.init(0, 0, 80, 80), green);
present();
const back = backSurface();
const overlap = back.pixels[@as(usize, 150) * back.stride + 150]; // in both layers top
const bottom_only = back.pixels[@as(usize, 110) * back.stride + 110]; // bottom only
_ = destroyLayer(top);
_ = destroyLayer(bottom);
present(); // repaint the self-check region back to the background
if (overlap == green and bottom_only == red) {
_ = system.write("display: compositor self-check ok\n");
} else {
_ = system.write("display: compositor self-check FAILED\n");
}
}
fn fail_check(_: []const u8) void {
_ = system.write("display: compositor self-check FAILED (setup)\n");
}
// --- service ----------------------------------------------------------------
/// The framebuffer node the kernel seeded (`DeviceClass.display`), or null if none.
fn findDisplay() ?device.DeviceDescriptor {
const total = device.enumerate(&device_table);
@ -88,10 +303,11 @@ fn initialise(endpoint: ipc.Handle) bool {
.pitch = geometry.pitch,
.format = geometry.format,
};
background = protocol.pack(display.format, 0x20, 0x30, 0x48); // a dark slate wallpaper
// Prove the pipeline end to end: compose a cleared frame in the back buffer, then
// present it to the screen. Nothing is drawn directly to the LFB.
clear(0x0020_3048); // a dark slate; exact channel order is a D3 concern
// Clear the whole screen through the back buffer present path (double buffering:
// no direct-to-LFB drawing).
addDamage(screenRect());
present();
var line: [96]u8 = undefined;
@ -99,34 +315,11 @@ fn initialise(endpoint: ipc.Handle) bool {
display.width, display.height, display.pitch, display.format,
}) catch "display: online\n");
_ = system.write("display: presented frame 0\n");
selfCheck();
return true;
}
/// Fill the whole back buffer with `colour`. Cacheable memory, so this is fast; touch
/// only the visible width, stepping rows by `pitch` (which may exceed width*4).
fn clear(colour: u32) void {
var y: u32 = 0;
while (y < display.height) : (y += 1) {
const row: [*]u32 = @ptrCast(@alignCast(display.back + @as(usize, y) * display.pitch));
var x: u32 = 0;
while (x < display.width) : (x += 1) row[x] = colour;
}
}
/// Whole-screen present: stream the back buffer to the write-combining front buffer, row
/// by row. Sequential writes are what WC memory wants; we never read the front buffer.
/// (D3 replaces this with a damage-driven present that copies only changed rectangles.)
fn present() void {
var y: u32 = 0;
while (y < display.height) : (y += 1) {
const src: [*]const u32 = @ptrCast(@alignCast(display.back + @as(usize, y) * display.pitch));
const dst: [*]volatile u32 = @ptrCast(@alignCast(display.front + @as(usize, y) * display.pitch));
var x: u32 = 0;
while (x < display.width) : (x += 1) dst[x] = src[x];
}
display.frames += 1;
}
fn writeReply(reply: []u8, value: protocol.Reply) usize {
const bytes = std.mem.asBytes(&value);
@memcpy(reply[0..bytes.len], bytes);
@ -146,6 +339,7 @@ fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?ipc.Han
_ = capability;
if (message.len < protocol.request_size) return fail(reply);
const request = std.mem.bytesToValue(protocol.Request, message[0..protocol.request_size]);
const payload = message[protocol.request_size..];
// Switch on the raw operation value an out-of-range one must fail cleanly, not
// panic an `@enumFromInt`.
switch (request.operation) {
@ -156,12 +350,33 @@ fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?ipc.Han
.pitch = display.pitch,
.format = display.format,
}),
@intFromEnum(protocol.Operation.create_layer) => {
const slot = createLayer(@intCast(request.x), @intCast(request.y), request.width, request.height, request.z, request.visible != 0) orelse return fail(reply);
return writeReply(reply, .{ .status = 0, .layer = slot });
},
@intFromEnum(protocol.Operation.configure_layer) => {
return if (configureLayer(request.layer, @intCast(request.x), @intCast(request.y), request.z, request.visible != 0)) ok(reply) else fail(reply);
},
@intFromEnum(protocol.Operation.destroy_layer) => {
return if (destroyLayer(request.layer)) ok(reply) else fail(reply);
},
@intFromEnum(protocol.Operation.fill_rect) => {
const local = Rect.init(@intCast(request.x), @intCast(request.y), @intCast(request.width), @intCast(request.height));
return if (fillLayer(request.layer, local, request.colour)) ok(reply) else fail(reply);
},
@intFromEnum(protocol.Operation.blit_tile) => {
return if (blitLayer(request.layer, @intCast(request.x), @intCast(request.y), request.width, request.height, payload)) ok(reply) else fail(reply);
},
@intFromEnum(protocol.Operation.damage) => {
const l = layerAt(request.layer) orelse return fail(reply);
const screen = Rect{ .x = l.x + @as(i32, @intCast(request.x)), .y = l.y + @as(i32, @intCast(request.y)), .w = @intCast(request.width), .h = @intCast(request.height) };
addDamage(screen.intersect(layerScreenRect(l)));
return ok(reply);
},
@intFromEnum(protocol.Operation.present) => {
present();
return ok(reply);
},
// The layer stack and per-layer drawing land in D3; until then those operations
// are unimplemented rather than silently accepted.
else => return fail(reply),
}
}

View File

@ -5,6 +5,8 @@
//! operations, marks damage, and asks for a `present`. v1 surfaces are server-owned (a
//! client draws by command); shared-memory surfaces are a later milestone (docs/display.md).
const std = @import("std");
pub const Operation = enum(u32) {
/// info() -> { width, height, pitch, format }: the display's current mode.
info = 0,
@ -59,3 +61,28 @@ pub const message_maximum: usize = 4096;
pub const request_size: usize = @sizeOf(Request);
pub const reply_size: usize = @sizeOf(Reply);
pub const maximum_payload: usize = message_maximum - request_size;
/// Pack an 8-bit-per-channel colour into the display's native 32-bit pixel for `format`
/// (a device-abi `DisplayFormat`: 0 = rgbx, 1 = bgrx). Shared so a `colour` in a
/// `fill_rect` request means the same thing to the client that sends it and the
/// compositor that paints it. Little-endian memory, reserved byte 0: rgbx puts red in
/// the low byte, bgrx puts blue there.
pub fn pack(format: u32, r: u8, g: u8, b: u8) u32 {
const rr: u32 = r;
const gg: u32 = g;
const bb: u32 = b;
return switch (format) {
1 => bb | (gg << 8) | (rr << 16), // bgrx
else => rr | (gg << 8) | (bb << 16), // rgbx
};
}
test "pack encodes native byte order for rgbx and bgrx" {
// rgbx: red in the low byte, blue in byte 2.
try std.testing.expectEqual(@as(u32, 0x0000_00AA), pack(0, 0xAA, 0, 0));
try std.testing.expectEqual(@as(u32, 0x00AA_0000), pack(0, 0, 0, 0xAA));
// bgrx: blue in the low byte, red in byte 2.
try std.testing.expectEqual(@as(u32, 0x0000_00AA), pack(1, 0, 0, 0xAA));
try std.testing.expectEqual(@as(u32, 0x00AA_0000), pack(1, 0xAA, 0, 0));
try std.testing.expectEqual(@as(u32, 0x0000_3020), pack(0, 0x20, 0x30, 0)); // green in byte 1
}

View File

@ -171,13 +171,13 @@ CASES = [
{"name": "display",
"expect": r"DANOS-TEST-RESULT: PASS",
"fail": r"DANOS-TEST-RESULT: FAIL"},
# Display service (D2): the user-space compositor claims the framebuffer, allocates a
# cacheable back buffer, clears it, and presents that composed frame — proving the
# double-buffer path. Matched on the service's own heartbeats (it prints them only
# after the whole claim -> map(WC) -> back-buffer -> present chain succeeds).
# Display service (D2/D3): the user-space compositor claims the framebuffer, allocates
# a cacheable back buffer, clears it, and presents that composed frame (double-buffer
# path); then a startup self-check composites two overlapping layers and confirms the
# overlap shows the top layer (D3). Matched on the service's own heartbeats.
{"name": "display-service",
"expect": r"display: online \d+x\d+ pitch \d+[\s\S]*display: presented frame 0",
"fail": r"display: could not|CPU EXCEPTION|KERNEL PANIC"},
"expect": r"display: online \d+x\d+ pitch \d+[\s\S]*display: presented frame 0[\s\S]*display: compositor self-check ok",
"fail": r"display: could not|self-check FAILED|CPU EXCEPTION|KERNEL PANIC"},
# Monotonic clock (clock() syscall source): calibrated, advancing, never backwards.
{"name": "clock",
"expect": r"DANOS-TEST-RESULT: PASS",