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
+111
View File
@@ -58,3 +58,114 @@ pub fn present() bool {
var reply: protocol.Reply = undefined;
return transact(.{ .operation = @intFromEnum(protocol.Operation.present) }, &reply);
}
/// The mode, cached after the first `info()` so `color()` doesn't round-trip per pixel.
var mode: ?Info = null;
fn cachedInfo() ?Info {
if (mode) |m| return m;
const i = info() orelse return null;
mode = i;
return i;
}
/// The native pixel value for an 8-bit-per-channel colour, in the display's format. A
/// client packs colours through this so it never has to know the byte order itself.
pub fn color(r: u8, g: u8, b: u8) u32 {
const format = if (cachedInfo()) |i| i.format else 0;
return protocol.pack(format, r, g, b);
}
/// A handle to a server-owned layer: a positioned, z-ordered surface the client draws
/// into by command. Create with `createLayer`; drawing and moves take effect on the next
/// `present`. Coordinates are signed (a layer may sit partly off-screen).
pub const Layer = struct {
id: u32,
/// Fill a rectangle of this layer (layer-local coordinates) with a native `colour`.
pub fn fill(self: Layer, x: i32, y: i32, w: u32, h: u32, colour: u32) bool {
var reply: protocol.Reply = undefined;
return transact(.{
.operation = @intFromEnum(protocol.Operation.fill_rect),
.layer = self.id,
.x = @bitCast(x),
.y = @bitCast(y),
.width = w,
.height = h,
.colour = colour,
}, &reply);
}
/// Copy a `w`×`h` tile of native pixels (row-major, little-endian bytes) into this
/// layer at (`x`, `y`). The tile rides inline in the request, so `w*h*4` must fit
/// `protocol.maximum_payload`.
pub fn blitTile(self: Layer, x: i32, y: i32, w: u32, h: u32, pixels: []const u8) bool {
var request = protocol.Request{
.operation = @intFromEnum(protocol.Operation.blit_tile),
.layer = self.id,
.x = @bitCast(x),
.y = @bitCast(y),
.width = w,
.height = h,
};
const header = std.mem.asBytes(&request);
if (header.len + pixels.len > protocol.message_maximum) return false;
var buffer: [protocol.message_maximum]u8 = undefined;
@memcpy(buffer[0..header.len], header);
@memcpy(buffer[header.len..][0..pixels.len], pixels);
const h_svc = service() orelse return false;
var reply: [protocol.reply_size]u8 = undefined;
const len = ipc.call(h_svc, buffer[0 .. header.len + pixels.len], &reply) catch return false;
if (len < protocol.reply_size) return false;
return std.mem.bytesToValue(protocol.Reply, reply[0..protocol.reply_size]).status == 0;
}
/// Move / restack / show or hide the layer.
pub fn configure(self: Layer, x: i32, y: i32, z: u32, visible: bool) bool {
var reply: protocol.Reply = undefined;
return transact(.{
.operation = @intFromEnum(protocol.Operation.configure_layer),
.layer = self.id,
.x = @bitCast(x),
.y = @bitCast(y),
.z = z,
.visible = if (visible) 1 else 0,
}, &reply);
}
/// Mark a rectangle of this layer (layer-local) dirty for the next present — for when
/// the layer's pixels changed without a drawing call the compositor already tracked.
pub fn damage(self: Layer, x: i32, y: i32, w: u32, h: u32) bool {
var reply: protocol.Reply = undefined;
return transact(.{
.operation = @intFromEnum(protocol.Operation.damage),
.layer = self.id,
.x = @bitCast(x),
.y = @bitCast(y),
.width = w,
.height = h,
}, &reply);
}
/// Release the layer and its surface.
pub fn destroy(self: Layer) bool {
var reply: protocol.Reply = undefined;
return transact(.{ .operation = @intFromEnum(protocol.Operation.destroy_layer), .layer = self.id }, &reply);
}
};
/// Create a server-owned layer of `w`×`h` pixels at screen (`x`, `y`) with stacking order
/// `z` (higher is nearer the front), initially visible. Returns a handle, or null.
pub fn createLayer(x: i32, y: i32, w: u32, h: u32, z: u32) ?Layer {
var reply: protocol.Reply = undefined;
if (!transact(.{
.operation = @intFromEnum(protocol.Operation.create_layer),
.x = @bitCast(x),
.y = @bitCast(y),
.width = w,
.height = h,
.z = z,
.visible = 1,
}, &reply)) return null;
return .{ .id = reply.layer };
}