diff --git a/build.zig b/build.zig index f23a755..6bb020c 100644 --- a/build.zig +++ b/build.zig @@ -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"); diff --git a/docs/display-plan.md b/docs/display-plan.md index 96e2427..2cfde96 100644 --- a/docs/display-plan.md +++ b/docs/display-plan.md @@ -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 diff --git a/library/runtime/display.zig b/library/runtime/display.zig index 7779cb8..a43ee39 100644 --- a/library/runtime/display.zig +++ b/library/runtime/display.zig @@ -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 }; +} diff --git a/system/kernel/tests.zig b/system/kernel/tests.zig index 7fc513f..b8fc8f2 100644 --- a/system/kernel/tests.zig +++ b/system/kernel/tests.zig @@ -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 diff --git a/system/services/display-demo/display-demo.zig b/system/services/display-demo/display-demo.zig new file mode 100644 index 0000000..4e06526 --- /dev/null +++ b/system/services/display-demo/display-demo.zig @@ -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; +} diff --git a/system/services/display/display.zig b/system/services/display/display.zig index f5430c6..b5bc451 100644 --- a/system/services/display/display.zig +++ b/system/services/display/display.zig @@ -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); }, diff --git a/system/services/display/protocol.zig b/system/services/display/protocol.zig index e69b777..f6ebdda 100644 --- a/system/services/display/protocol.zig +++ b/system/services/display/protocol.zig @@ -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; diff --git a/test/qemu_test.py b/test/qemu_test.py index 3d352ee..e44e853 100644 --- a/test/qemu_test.py +++ b/test/qemu_test.py @@ -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",