display: layer client API + the display-demo client (D4)

Drive the compositor from a separate process, proving the pipeline end to end.

- runtime.display: a Layer handle (fill / blitTile / configure / damage /
  destroy), createLayer, and a color(r,g,b) helper that caches the mode and
  packs via protocol.pack. Coordinates are signed over the u32 wire fields
  (@bitCast both ways), so a layer may sit or move partly off-screen.
- system/services/display-demo: the input-source analog for the compositor — a
  full-screen wallpaper, a rectangle it slides back and forth (moved by
  configure each frame, so the damage-driven present repaints old + new), and a
  cursor. Presents in a loop paced by runtime.time. Wired into build + initrd.

Fix 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 (invisibly, as
those gates matched init-time heartbeats). Set it to 256; blit_tile is now
explicitly a small-tile path (larger bitmaps are the deferred shm surface).

Gate: `python3 test/qemu_test.py display-demo` — the demo drives frames of
motion through the layer client API and logs `display-demo: ok`. Regression:
zig build test, display (D1), display-service (D2/D3), and default zig build.
This commit is contained in:
Daniel Samson
2026-07-14 01:56:37 +01:00
parent f9cf0007c5
commit 105203b447
8 changed files with 253 additions and 17 deletions
+33
View File
@@ -99,6 +99,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void {
displayTest(boot_information);
} else if (eql(case, "display-service")) {
displayServiceTest(boot_information);
} else if (eql(case, "display-demo")) {
displayDemoTest(boot_information);
} else if (eql(case, "clock")) {
clockTest();
} else if (eql(case, "smp")) {
@@ -2344,6 +2346,37 @@ fn displayServiceTest(boot_information: *const BootInformation) void {
while (true) scheduler.yield();
}
/// D4 — a separate process drives the compositor. Spawn the display service and the
/// hardware-free `display-demo` client, which creates a wallpaper, a moving rectangle,
/// and a cursor and presents a run of frames. Its `display-demo: ok` heartbeat — printed
/// only after it drove frames of motion through the layer client API and the compositor —
/// is the harness's marker (the visible motion itself is a screenshot away via run-x86-64).
/// The demo keeps presenting, so unlike a lone blocking service the scheduler stays busy;
/// we still match on serial rather than poll, for consistency.
fn displayDemoTest(boot_information: *const BootInformation) void {
log("DANOS-TEST-BEGIN: display-demo\n", .{});
if (boot_information.initial_ramdisk_len == 0) {
check("bootloader handed over an initial_ramdisk", false);
result();
return;
}
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
const rd = initial_ramdisk.Reader.init(image) orelse {
check("initial_ramdisk image is valid", false);
result();
return;
};
if (!spawnNamed(rd, "display")) {
log("display-demo: could not spawn the display service\n", .{});
result();
return;
}
_ = spawnNamed(rd, "display-demo");
scheduler.setPriority(1); // below the service + demo, so they run
while (true) scheduler.yield();
}
/// Process arguments, end to end: spawn args-echo bare (its argv[0] is the
/// initial-ramdisk name). Instance 1 sees argc == 1 and respawns itself through
/// `system_spawn` with the extra arguments "alpha beta-42" — the syscall argument
@@ -0,0 +1,66 @@
//! system/services/display-demo — a hardware-free client of the display service, the
//! `input-source` analog for the compositor. It creates a wallpaper, a rectangle it moves
//! each frame, and a small cursor, then drives the compositor in a present loop — proof
//! that a *separate process* can compose a moving scene through the display service over
//! IPC, exercising the layer client API and damage-driven present end to end
//! (docs/display.md). It logs `display-demo: ok` once it has driven a run of frames.
const runtime = @import("runtime");
const display = runtime.display;
const system = runtime.system;
const time = runtime.time;
pub fn main() void {
const mode = display.info() orelse {
_ = system.write("display-demo: no display service\n");
return;
};
// A full-screen wallpaper under everything.
const wallpaper = display.createLayer(0, 0, mode.width, mode.height, 0) orelse return createFailed();
_ = wallpaper.fill(0, 0, mode.width, mode.height, display.color(0x10, 0x18, 0x28));
// A rectangle that slides back and forth.
const box_w: u32 = 140;
const box_h: u32 = 100;
const box_y: i32 = 200;
const box = display.createLayer(0, box_y, box_w, box_h, 1) orelse return createFailed();
_ = box.fill(0, 0, box_w, box_h, display.color(0xE0, 0x60, 0x40));
// A little cursor on top.
const cursor = display.createLayer(40, 40, 12, 12, 2) orelse return createFailed();
_ = cursor.fill(0, 0, 12, 12, display.color(0xF0, 0xF0, 0xF0));
_ = display.present();
_ = system.write("display-demo: scene up; animating\n");
const span: i32 = @as(i32, @intCast(mode.width)) - @as(i32, @intCast(box_w));
var x: i32 = 0;
var dx: i32 = 8;
var frame: u32 = 0;
while (true) : (frame += 1) {
x += dx;
if (x <= 0) {
x = 0;
dx = -dx;
} else if (x >= span) {
x = span;
dx = -dx;
}
_ = box.configure(x, box_y, 1, true); // move it; the compositor repaints old + new
_ = display.present();
// A run of frames drawn through the compositor is the automated proof (the visible
// motion is a screenshot away via `zig build run-x86-64`).
if (frame == 20) _ = system.write("display-demo: ok\n");
time.sleep(time.Duration.fromMillis(30));
}
}
fn createFailed() void {
_ = system.write("display-demo: create failed\n");
}
pub const panic = runtime.panic;
comptime {
_ = &runtime.start._start;
}
+7 -5
View File
@@ -351,25 +351,27 @@ fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?ipc.Han
.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);
// x/y are signed coordinates carried in the u32 wire fields — reinterpret the
// bits (@bitCast), don't range-check (@intCast) which a negative would fail.
const slot = createLayer(@bitCast(request.x), @bitCast(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);
return if (configureLayer(request.layer, @bitCast(request.x), @bitCast(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));
const local = Rect.init(@bitCast(request.x), @bitCast(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);
return if (blitLayer(request.layer, @bitCast(request.x), @bitCast(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) };
const screen = Rect{ .x = l.x + @as(i32, @bitCast(request.x)), .y = l.y + @as(i32, @bitCast(request.y)), .w = @intCast(request.width), .h = @intCast(request.height) };
addDamage(screen.intersect(layerScreenRect(l)));
return ok(reply);
},
+6 -4
View File
@@ -54,10 +54,12 @@ pub const Reply = extern struct {
reserved2: u32 = 0,
};
/// Sized to hold a modest `blit_tile` payload (a cursor / glyph-cell tile) inline on top
/// of the header, not just the fixed messages — the compositor's receive/reply buffers
/// (`runtime.service.run`) are this big.
pub const message_maximum: usize = 4096;
/// The IPC message size — the kernel caps every message at `MESSAGE_MAXIMUM` (256 bytes,
/// system/kernel/ipc-synchronous.zig), so this matches it (a larger receive/reply buffer
/// is rejected with -E2BIG). A `blit_tile` therefore carries only a *small* tile inline —
/// `maximum_payload` bytes = up to 54 pixels, enough for a cursor or small sprite; larger
/// bitmaps are the deferred shared-memory surface path (docs/display.md).
pub const message_maximum: usize = 256;
pub const request_size: usize = @sizeOf(Request);
pub const reply_size: usize = @sizeOf(Reply);
pub const maximum_payload: usize = message_maximum - request_size;