//! /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 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`. //! //! 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 /// in the cacheable back buffer, which is then streamed to the front (docs/display.md). const Display = struct { device_id: u64, front: [*]volatile u8, // the LFB (write-combining) back: [*]u8, // cacheable, same geometry width: u32, height: u32, pitch: u32, // bytes per row (shared by both buffers) format: u32, // a device-abi DisplayFormat value frames: u64 = 0, }; 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); const n = @min(total, device_table.len); for (device_table[0..n]) |d| { if (d.class == @intFromEnum(device.DeviceClass.display)) return d; } return null; } fn initialise(endpoint: ipc.Handle) bool { _ = endpoint; // Find the framebuffer, retrying while device discovery catches up with our spawn. var tries: u32 = 0; const found = while (tries < 100) : (tries += 1) { if (findDisplay()) |d| break d; system.sleep(50); } else { _ = system.write("display: no framebuffer device (headless?)\n"); return false; // clean exit: nothing to drive }; if (!device.claim(found.id)) { _ = system.write("display: could not claim the framebuffer\n"); return false; } // Resource 0 is the framebuffer memory window; the kernel maps it write-combining // because the resource carries that flag (docs/display-plan.md D1). const front_base = device.mmioMap(found.id, 0) orelse { _ = system.write("display: could not map the framebuffer\n"); return false; }; const geometry = found.display; const size = @as(usize, geometry.height) * geometry.pitch; const back_base = system.mmap(size, system.PROT_READ | system.PROT_WRITE); if (system.mmapFailed(back_base)) { _ = system.write("display: could not allocate the back buffer\n"); return false; } display = .{ .device_id = found.id, .front = @ptrFromInt(front_base), .back = @ptrFromInt(back_base), .width = geometry.width, .height = geometry.height, .pitch = geometry.pitch, .format = geometry.format, }; background = protocol.pack(display.format, 0x20, 0x30, 0x48); // a dark slate wallpaper // 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; _ = system.write(std.fmt.bufPrint(&line, "display: online {d}x{d} pitch {d} format {d}\n", .{ display.width, display.height, display.pitch, display.format, }) catch "display: online\n"); _ = system.write("display: presented frame 0\n"); selfCheck(); return true; } fn writeReply(reply: []u8, value: protocol.Reply) usize { const bytes = std.mem.asBytes(&value); @memcpy(reply[0..bytes.len], bytes); return bytes.len; } fn ok(reply: []u8) usize { return writeReply(reply, .{ .status = 0 }); } fn fail(reply: []u8) usize { return writeReply(reply, .{ .status = -1 }); } fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?ipc.Handle) usize { _ = sender; _ = 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) { @intFromEnum(protocol.Operation.info) => return writeReply(reply, .{ .status = 0, .width = display.width, .height = display.height, .pitch = display.pitch, .format = display.format, }), @intFromEnum(protocol.Operation.create_layer) => { // 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, @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(@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, @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, @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); }, @intFromEnum(protocol.Operation.present) => { present(); return ok(reply); }, else => return fail(reply), } } pub fn main() void { runtime.service.run(protocol.message_maximum, .{ .service = .display, .init = initialise, .on_message = onMessage, }); } pub const panic = runtime.panic; comptime { _ = &runtime.start._start; }