//! /system/services/display — the display service (docs/display.md, docs/display-v2.md). //! A ring-3 compositor: it composes an ordered stack of **layers** into a cacheable //! surface and presents finished frames. Scanout — how a frame reaches the panel — is a //! pluggable **backend** ([backend.zig](backend.zig)): the GOP framebuffer today, a native //! virtio-gpu driver later; this file never learns which is active. It owns the layer stack //! and damage tracking; the pixel math is the pure, host-tested //! [compositor.zig](compositor.zig). //! //! A layer is a server-owned surface (its own cacheable buffer) with a screen position, //! z-order, and visibility. Clients create layers, 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 into the backend's //! surface, then `backend.present(damage)`. Shared-memory client surfaces are later //! (docs/display-v2.md). const std = @import("std"); const runtime = @import("runtime"); const compositor = @import("compositor.zig"); const backend_mod = @import("backend.zig"); const protocol = runtime.display_protocol; const ipc = runtime.ipc; const system = runtime.system; const Rect = compositor.Rect; const Surface = compositor.Surface; /// The active scanout backend — GOP today, a native driver when one is present. var backend: backend_mod.Backend = undefined; var frames: u64 = 0; /// 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; // --- geometry helpers ------------------------------------------------------- fn screenRect() Rect { const m = backend.info(); return .{ .x = 0, .y = 0, .w = @intCast(m.width), .h = @intCast(m.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 backend's compose surface: clear it to the /// background, then paint every visible layer that overlaps it, bottom to top (ascending z). fn compositeInto(clip: Rect) void { const target = backend.surface(); compositor.fillRect(target, 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(target, l.x, l.y, l.surface, clip); } } /// Composite the accumulated damage into the backend's surface, hand it to the backend to /// put on screen, then clear the damage. 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); backend.present(dirty); } damage = Rect.empty; frames += 1; } // --- startup self-check ----------------------------------------------------- /// Prove the compositor wiring on the real backend: 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 /// backend surface — and reads the composited result back. Cleans up after itself. fn selfCheck() void { const format = backend.info().format; const red = protocol.pack(format, 0xC0, 0x20, 0x20); const green = protocol.pack(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 surface = backend.surface(); const overlap = surface.pixels[@as(usize, 150) * surface.stride + 150]; // in both → top const bottom_only = surface.pixels[@as(usize, 110) * surface.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 ---------------------------------------------------------------- fn initialise(endpoint: ipc.Handle) bool { _ = endpoint; // Pick the scanout backend (GOP today). It logs the reason on failure. backend = backend_mod.select() orelse return false; const mode = backend.info(); background = protocol.pack(mode.format, 0x20, 0x30, 0x48); // a dark slate wallpaper // Clear the whole screen through the compose surface → present path (double buffering: // no direct-to-scanout 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", .{ mode.width, mode.height, mode.pitch, mode.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) => { const m = backend.info(); return writeReply(reply, .{ .status = 0, .width = m.width, .height = m.height, .pitch = m.pitch, .format = m.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; }