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:
parent
f9cf0007c5
commit
105203b447
|
|
@ -467,6 +467,7 @@ pub fn build(b: *std.Build) void {
|
|||
// at /mnt/usb. Its engine (engine.zig / on-disk.zig) is imported relatively.
|
||||
const fat_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "fat", "system/services/fat/fat.zig");
|
||||
const display_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "display", "system/services/display/display.zig");
|
||||
const display_demo_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "display-demo", "system/services/display-demo/display-demo.zig");
|
||||
const fat_test_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "fat-test", "system/services/fat/fat-test.zig");
|
||||
const pci_bus_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "pci-bus", "system/drivers/pci-bus/pci-bus.zig");
|
||||
// The PCI bus driver decodes each function's class triple to human names in its
|
||||
|
|
@ -536,6 +537,8 @@ pub fn build(b: *std.Build) void {
|
|||
mk_run.addFileArg(fat_test_exe.getEmittedBin());
|
||||
mk_run.addArg("display");
|
||||
mk_run.addFileArg(display_exe.getEmittedBin());
|
||||
mk_run.addArg("display-demo");
|
||||
mk_run.addFileArg(display_demo_exe.getEmittedBin());
|
||||
mk_run.addArg("pci-bus");
|
||||
mk_run.addFileArg(pci_bus_exe.getEmittedBin());
|
||||
mk_run.addArg("crash-test");
|
||||
|
|
|
|||
|
|
@ -118,17 +118,29 @@ The heart: composite an ordered layer stack, present only what changed.
|
|||
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
|
||||
## D4 — Client API + the demo client ✅
|
||||
|
||||
Prove the pipeline end-to-end from a separate process.
|
||||
|
||||
- [ ] Finish [runtime/display.zig](../library/runtime/runtime.zig): a `Layer` handle with
|
||||
`fill` / `blitTile` / `damage`, plus `present()`.
|
||||
- [ ] `system/services/display-demo/`: a hardware-free client (the
|
||||
[`input-source`](../system/services/input-source/) analog) — a wallpaper layer, a
|
||||
layer with a rectangle it moves each frame, and a small cursor layer; `present`s in
|
||||
a loop paced by [`runtime.time`](../library/runtime/time.zig). Wire into build +
|
||||
initial-ramdisk.
|
||||
- [x] Finished [runtime/display.zig](../library/runtime/runtime.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).
|
||||
- [x] `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.
|
||||
- [x] **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 shm 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.
|
||||
|
||||
**Gate:** run `run-efi`; a screenshot (or two, apart in time) shows the wallpaper, the
|
||||
cursor, and the rectangle in different positions — motion, from a client, through the
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -178,6 +178,13 @@ CASES = [
|
|||
{"name": "display-service",
|
||||
"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"},
|
||||
# Display demo (D4): a separate process (display-demo) drives the compositor over the
|
||||
# layer client API — wallpaper + a moving rectangle + a cursor, presented in a loop.
|
||||
# `display-demo: ok` is printed only after it drove a run of frames of motion through
|
||||
# the service (the visible motion is a screenshot via `zig build run-x86-64`).
|
||||
{"name": "display-demo",
|
||||
"expect": r"display-demo: scene up[\s\S]*display-demo: ok",
|
||||
"fail": r"display-demo: (no display|create failed)|display: could not|CPU EXCEPTION|KERNEL PANIC"},
|
||||
# Monotonic clock (clock() syscall source): calibrated, advancing, never backwards.
|
||||
{"name": "clock",
|
||||
"expect": r"DANOS-TEST-RESULT: PASS",
|
||||
|
|
|
|||
Loading…
Reference in New Issue