//! /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)`. Presents are paced by a ~60 Hz **frame clock** //! (see `schedulePresent`), so any number of client presents and cursor moves inside one //! interval coalesce into a single frame. Shared-memory client surfaces are later //! (docs/display-v2.md). const std = @import("std"); const channel = @import("channel"); const ipc = @import("ipc"); const input = @import("input-client"); const process = @import("process"); const Thread = @import("thread").Thread; const service = @import("service"); const time = @import("time"); const display = @import("display-client"); const memory = @import("memory"); const logging = @import("logging"); const compositor = @import("compositor.zig"); const backend_mod = @import("backend.zig"); const envelope = @import("envelope"); const display_protocol = @import("display-protocol"); const Rect = compositor.Rect; const Surface = compositor.Surface; /// The generated display dispatch. One compositor per process, so the handler /// context is empty and the layer stack stays in this file's globals. const Serve = display_protocol.Protocol.Provider(void); const Invocation = envelope.Invocation; const Answer = envelope.Answer; /// What a handler returns when the layer named in `Header.target` is not one of /// ours, or a mode was refused. const refused: isize = -envelope.ENOENT; /// The active scanout backend — the GOP framebuffer at boot, upgraded to a native driver /// (virtio-gpu) when one announces itself (V4). var backend: backend_mod.Backend = undefined; var frames: u64 = 0; /// This service's endpoint, kept so `attach_scanout` can arm a one-shot timer: the very first /// native present must happen in a *later* loop iteration, after the reply to the driver's /// announce has unblocked it and it is serving its `.scanout` channel — presenting inline /// would deadlock (we'd call the driver while it waits on our reply). var service_endpoint: ipc.Handle = 0; /// Set when the backend has just been upgraded to virtio-gpu: the next present repaints the /// whole screen into the shared surface and reads a pixel back to confirm the frame landed. var pending_native_verify: bool = false; /// Set alongside it: after the native present is verified, run the mode-set self-check once /// (query the driver's modes, switch to a different one, confirm the geometry changed) — the /// serial proof the runtime-resolution-change + fenced-present paths work (V5). var pending_modeset_check: bool = false; /// 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_list` accumulates the dirty /// screen rectangles since the last `present`, so a present touches only what changed — /// and keeps far-apart changes (the cursor here, an animating layer there) as *separate* /// small copies rather than one huge bounding box (see compositor.DamageList). const maximum_layers = 16; /// A layer belongs to the client that created it. `owner` is the kernel-stamped /// badge of that task, and `service_owned` (0, an id no task wears) marks the /// compositor's own layers — the cursor sprite and the self-check pair — which /// this file creates by direct call rather than over the protocol. /// /// Layer ids are slots in a sixteen-entry table: small, dense, and guessable, so /// before this field any client could configure, draw into, or destroy any /// other's layer — including the cursor. The check lives in the protocol handlers /// (docs/os-development/protocol-namespace.md: handles validated against the /// badge); the internal helpers stay unscoped precisely so the compositor can /// still drive its own. const service_owned: u32 = 0; const Layer = struct { used: bool = false, owner: u32 = service_owned, 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; /// Which damage tracker drives `present` — a compile-time A/B switch (both are in /// compositor.zig with the trade-off discussion): /// .list — free-form dirty rectangles (tight bounds, heuristic merging) /// .grid — a fixed 64-px tile grid (exact O(1) merging, tile-quantized repaints) const DamageMode = enum { list, grid }; const damage_mode: DamageMode = .grid; var damage_list: compositor.DamageList = .{}; var damage_grid: compositor.TileGrid = .{}; /// The **frame clock**: client `present` requests and cursor motion don't repaint /// immediately — they accumulate damage and arm a one-shot timer, and the tick composites /// everything pending as one frame. That paces presents to ~60 Hz no matter how fast /// clients draw or the mouse moves (previously every mouse event became a full present). /// No backend has a real vblank to pace by (docs/display-v2.md, "Fenced is not vsync"); /// this is the software stand-in, the same strategy Linux uses atop virtio-gpu. Bring-up /// paths that need pixels on screen *now* (initialise, the self-checks) still call /// `present()` directly. /// /// The interval comes from the *active backend's* panel refresh rate (EDID: the loader /// captures it for the GOP floor while firmware still runs; the native driver reads its /// own and carries it in the announce). `updateFrameClock` re-derives it whenever the /// backend changes — the boot framebuffer's clock dies with the GOP floor at upgrade. /// Without a rate the clock defaults to 60 Hz, and it is clamped to [30, 120] Hz so a /// mis-parsed EDID can neither starve nor flood the compositor. var frame_interval_milliseconds: u64 = 16; var frame_timer_armed = false; /// Derive the frame-clock interval from the active backend's refresh rate and log what /// the clock is now pacing to. Called at bring-up and again on every backend change. fn updateFrameClock() void { const reported = backend.info().refresh_hz; const rate: u64 = if (reported == 0) 60 else @min(@max(reported, 30), 120); frame_interval_milliseconds = @max(1000 / rate, 1); var line: [96]u8 = undefined; _ = logging.write(std.fmt.bufPrint(&line, "display: frame clock {d} Hz ({s})\n", .{ 1000 / frame_interval_milliseconds, if (reported == 0) "default" else "panel EDID", }) catch return); } /// Arm the frame clock unless a tick is already pending: any number of requests inside /// one interval coalesce into that single tick's present. fn schedulePresent() void { if (frame_timer_armed) return; frame_timer_armed = true; _ = time.timerOnce(service_endpoint, frame_interval_milliseconds); } /// A timer landing — the frame clock, or the deferred first native present armed by /// `attach_scanout`: present the accumulated damage, then run the one-shot mode-set /// self-check if the native upgrade queued it. fn frameTick() void { frame_timer_armed = false; present(); if (pending_modeset_check) { pending_modeset_check = false; modesetSelfCheck(); } } // --- 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. In grid /// mode the grid re-sizes itself lazily when the screen geometry changes — every /// geometry-changing path (`attach_scanout`, `set_mode`) damages the whole new screen /// right after, so damage pending from the old geometry is safely superseded. fn addDamage(r: Rect) void { const clipped = r.intersect(screenRect()); switch (damage_mode) { .list => damage_list.add(clipped), .grid => { const mode = backend.info(); if (!damage_grid.matches(mode.width, mode.height)) damage_grid.reset(mode.width, mode.height); damage_grid.add(clipped); }, } } // --- 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]; } /// Create a layer for `owner` — `service_owned` for the compositor's own. fn createLayer(owner: u32, 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 = memory.mmap(len, memory.PROT_READ | memory.PROT_WRITE); if (memory.mmapFailed(base)) return null; layers[slot] = .{ .used = true, .owner = owner, .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)); _ = memory.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 each accumulated damage rectangle into the backend's surface, hand the list /// 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 { var scratch: [compositor.TileGrid.maximum_rects]Rect = undefined; const dirty: []const Rect = switch (damage_mode) { .list => damage_list.slice(), .grid => damage_grid.collect(&scratch), }; const had_damage = dirty.len != 0; if (had_damage) { for (dirty) |region| compositeInto(region); backend.present(dirty); } switch (damage_mode) { .list => damage_list.clear(), .grid => damage_grid.clear(), } frames += 1; // The first present after a native upgrade confirms the composited frame actually reached // the shared scanout surface (the automated stand-in for "it's on screen"). if (pending_native_verify and had_damage) { pending_native_verify = false; verifyNativePresent(); } } /// Read a pixel straight back from the shared scanout surface after a native present. The /// surface starts zeroed, so a non-zero centre pixel means the compositor wrote the frame into /// the pages the driver transfers-and-flushes from — that, plus the driver acking the present /// over `.scanout`, is the serial proof the native path works. fn verifyNativePresent() void { const s = backend.surface(); const sample = s.pixels[@as(usize, s.height / 2) * s.stride + s.width / 2]; if (sample != 0) { _ = logging.write("display: native present verified\n"); } else { _ = logging.write("display: native present FAILED (blank surface)\n"); } } /// A native scanout driver announced itself: map the shared surface it handed over, find its /// present channel, switch the backend to virtio-gpu, and queue a full-screen repaint. The /// present is deferred to a timer (see `service_endpoint`) so it happens after this reply /// unblocks the driver and it starts serving `.scanout`. /// The surface arrives as the call's capability, and the harness's ownership rule /// applies: nothing here claims it, so the turn closes it on every path. Safe /// because a **mapping holds its own kernel reference** (system/kernel/process.zig /// `systemSharedMemoryMap`) — the pixels stay ours after the handle naming them /// goes, and a driver that dies and re-announces no longer costs a handle slot /// per restart. fn onAttachScanout(_: void, invocation: Invocation(display_protocol.AttachScanout), _: Answer(void)) isize { const announce = invocation.request; const stride = announce.stride; const width = announce.width; const height = announce.height; const format = announce.format; const refresh_hz = announce.refresh_hz; const cap = invocation.capability orelse return refused; if (width == 0 or height == 0 or stride < width) return refused; const mapped = memory.sharedMap(cap) orelse return refused; const scanout = channel.openEndpoint("scanout") orelse return refused; // A second announce means the driver died and was restarted (V6): re-attach to its fresh // scanout. (The previous shared mapping leaks — there is no shared_memory_unmap syscall yet — but the // frames are the dead driver's, reclaimed on its exit; a handful across a crash is benign.) const reattach = switch (backend) { .virtio => true, else => false, }; backend = .{ .virtio = .{ .pixels = @ptrCast(@alignCast(mapped)), .stride = stride, .width = width, .height = height, .format = format, .refresh_hz = refresh_hz, .scanout = scanout, } }; background = display_protocol.pack(format, 0x20, 0x30, 0x48); // re-pack the wallpaper for the mode updateFrameClock(); // the GOP floor's clock dies here — pace by the GPU's EDID now addDamage(screenRect()); // the whole new surface must be painted pending_native_verify = true; if (!reattach) pending_modeset_check = true; // the mode-set self-check runs once, on first upgrade _ = time.timerOnce(service_endpoint, 50); // present once the driver is serving .scanout _ = logging.write(if (reattach) "display: scanout re-attached\n" else "display: scanout upgraded to virtio-gpu\n"); return 0; } /// After the native upgrade is verified, prove the runtime-resolution-change and fenced-present /// paths: query the driver's modes, switch to one that differs from the current, re-composite /// the whole screen at the new size, and confirm the backend now reports that geometry. The /// present goes through the driver's fenced flush, so a clean present is a *fenced* present — /// completion-acknowledged and tear-free, not vblank-paced (docs/display-v2.md). fn modesetSelfCheck() void { if (!backend.canModeSet()) return; var mode_list: [4]backend_mod.Mode = undefined; const count = backend.modes(&mode_list); if (count == 0) { _ = logging.write("display: mode-set self-check: no modes reported\n"); return; } const current = backend.info(); var target: ?backend_mod.Mode = null; for (mode_list[0..count]) |m| { if (m.width != current.width or m.height != current.height) { target = m; break; } } const wanted = target orelse { _ = logging.write("display: mode-set self-check: no alternate mode offered\n"); return; }; if (!backend.setMode(wanted.width, wanted.height)) { _ = logging.write("display: mode set FAILED\n"); return; } addDamage(screenRect()); // repaint the whole screen at the new resolution, then present it present(); const now = backend.info(); if (now.width == wanted.width and now.height == wanted.height) { var line: [80]u8 = undefined; _ = logging.write(std.fmt.bufPrint(&line, "display: mode set to {d}x{d}, verified\n", .{ now.width, now.height }) catch "display: mode set, verified\n"); if (backend.hasFencedPresent()) _ = logging.write("display: fenced present ok\n"); } else { _ = logging.write("display: mode set FAILED (geometry unchanged)\n"); } } // --- 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 = display_protocol.pack(format, 0xC0, 0x20, 0x20); const green = display_protocol.pack(format, 0x20, 0xC0, 0x20); const bottom = createLayer(service_owned, 100, 100, 80, 80, 0, true) orelse return fail_check("create"); const top = createLayer(service_owned, 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) { _ = logging.write("display: compositor self-check ok\n"); } else { _ = logging.write("display: compositor self-check FAILED\n"); } } fn fail_check(_: []const u8) void { _ = logging.write("display: compositor self-check FAILED (setup)\n"); } // --- cursor + mouse-input thread -------------------------------------------- // // The compositor is the single owner of the framebuffer: only the main service // loop touches `backend` and the layer stack. A dedicated listener thread (spawned // in `initialise`) blocks on the input service's mouse stream, accumulates relative // motion into an absolute cursor position, and hands that position to the main loop // through `cursor_channel` — a single-slot latest-value cell (the renderer wants // where the cursor *is*, not a replay of every delta). The listener never touches // the compositor; it only writes the channel and pokes the main loop awake with a // self-directed `ipc.send`, which arrives as a message-notification in the service // loop (docs/threading.md, docs/display.md). Shared fate: a fault in the listener // takes the whole display down and the supervisor restarts it (docs/resilience.md). const cursor_size = 10; // a small square sprite — enough to prove tracking const cursor_z = 0xFFFF_FFFF; // always above client layers const cursor_report_threshold = 5; // px of travel before the tracking marker latches var cursor_layer: ?u32 = null; var cursor_origin_x: i32 = 0; var cursor_origin_y: i32 = 0; /// Latched once the cursor has demonstrably tracked a run of motion end to end /// (source -> input service -> listener -> channel -> render): the `display-cursor` /// test's success marker. var cursor_tracking_reported: bool = false; const poke_byte = [_]u8{0}; // the poke carries no payload; the value lives in the channel /// Shared between the listener thread (producer) and the main loop (consumer). /// Latest-value semantics with a coalesced wake: at most one poke is queued while /// the main loop has not drained the last one, so a fast mouse cannot flood the /// service endpoint. const CursorChannel = struct { lock: Thread.Mutex = .{}, poke_endpoint: ipc.Handle = 0, x: i32 = 0, y: i32 = 0, buttons: u32 = 0, dirty: bool = false, poke_pending: bool = false, const Snapshot = struct { x: i32, y: i32, buttons: u32 }; /// Producer (listener thread): record the newest position and, unless a wake is /// already queued, poke the main loop awake. fn publish(self: *CursorChannel, x: i32, y: i32, buttons: u32) void { self.lock.lock(); self.x = x; self.y = y; self.buttons = buttons; self.dirty = true; const need_poke = !self.poke_pending; if (need_poke) self.poke_pending = true; self.lock.unlock(); if (need_poke) _ = ipc.send(self.poke_endpoint, &poke_byte); } /// Consumer (main loop): take the latest position, or null if nothing changed /// since the last take. Clears the wake latch so the next publish pokes again. fn take(self: *CursorChannel) ?Snapshot { self.lock.lock(); defer self.lock.unlock(); self.poke_pending = false; if (!self.dirty) return null; self.dirty = false; return .{ .x = self.x, .y = self.y, .buttons = self.buttons }; } }; var cursor_channel: CursorChannel = .{}; fn clampAxis(value: i32, max: i32) i32 { if (value < 0) return 0; if (value > max) return max; return value; } /// The mouse-listener thread. Blocks on the input service's mouse stream, accumulates /// relative motion into an absolute position clamped to the screen, and publishes each /// update. Runs for the life of the process; a parked `next()` leaves the core free to /// halt (docs/halting.md). It reads only its own state and the channel — never the /// compositor — so no lock guards the framebuffer. fn mouseListener(width: u32, height: u32) void { var mouse = input.subscribeMouse() orelse { _ = logging.write("display: mouse subscribe failed\n"); return; }; // Our own handle to the compositor's endpoint. IPC handles are per-thread, so we // cannot reuse the main thread's — this thread resolves and opens // `/protocol/display` exactly like any other client would, once at startup, and // gets its own handle. There is no special mechanism for reaching yourself: the // registry does not know or care that the provider is this process. A poke posted // here wakes the compositor loop parked in replyWait (docs/threading.md: handles // do not cross threads). cursor_channel.poke_endpoint = channel.openEndpoint("display") orelse { _ = logging.write("display: mouse listener could not reach the compositor endpoint\n"); return; }; const max_x: i32 = @as(i32, @intCast(width)) - 1; const max_y: i32 = @as(i32, @intCast(height)) - 1; var x: i32 = @divTrunc(max_x, 2); var y: i32 = @divTrunc(max_y, 2); var buttons: u32 = 0; while (true) { const event = mouse.next() orelse continue; // Switch on the raw kind (not @enumFromInt, which would panic on a scroll or // future kind): motion moves the cursor, anything else just updates buttons. if (event.kind == @intFromEnum(input.MouseEventKind.motion)) { x = clampAxis(x + event.dx, max_x); y = clampAxis(y + event.dy, max_y); } else { buttons = event.buttons; } cursor_channel.publish(x, y, buttons); } } /// Consume the latest cursor position from the channel and move the cursor layer to it. /// Runs on the main loop (the compositor owner) in response to a listener poke. /// `configureLayer` damages both the old and new footprints; the frame clock presents /// them at the next tick, so a fast mouse coalesces to at most ~60 repaints a second. fn renderCursor() void { const snapshot = cursor_channel.take() orelse return; const id = cursor_layer orelse return; _ = configureLayer(id, snapshot.x, snapshot.y, cursor_z, true); schedulePresent(); if (!cursor_tracking_reported and @abs(snapshot.x - cursor_origin_x) >= cursor_report_threshold and @abs(snapshot.y - cursor_origin_y) >= cursor_report_threshold) { cursor_tracking_reported = true; _ = logging.write("display: cursor tracking mouse ok\n"); } } /// Create the cursor sprite (a top-z square) at screen centre and spawn the listener /// thread. Called from `initialise` once the backend is up. If either step fails the /// display still serves drawing clients — it just has no cursor. fn startCursorTracking() void { const mode = backend.info(); cursor_origin_x = @divTrunc(@as(i32, @intCast(mode.width)), 2); cursor_origin_y = @divTrunc(@as(i32, @intCast(mode.height)), 2); const id = createLayer(service_owned, cursor_origin_x, cursor_origin_y, cursor_size, cursor_size, cursor_z, true) orelse { _ = logging.write("display: could not create cursor layer\n"); return; }; cursor_layer = id; _ = fillLayer(id, Rect.init(0, 0, cursor_size, cursor_size), display_protocol.pack(mode.format, 0xF0, 0xF0, 0xF0)); present(); // show the cursor at its start position _ = Thread.spawn(.{}, mouseListener, .{ mode.width, mode.height }) catch { _ = logging.write("display: could not spawn mouse listener\n"); }; } // --- service ---------------------------------------------------------------- fn initialise(endpoint: ipc.Handle) bool { service_endpoint = endpoint; // Layers are per-client state, so the compositor needs deaths: a client that // crashes leaves its surfaces on screen and its slots spent otherwise. _ = process.subscribeExits(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 = display_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; _ = logging.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"); updateFrameClock(); _ = logging.write("display: presented frame 0\n"); selfCheck(); // Bring up the cursor and the mouse-listener thread now that the backend is live. startCursorTracking(); return true; } // --- the protocol handlers -------------------------------------------------- // // A layer id is `Header.target` on every verb that names one, so no handler // reads a layer out of its own request any more. `target` is a u64 and a layer // id a u32: a value that does not fit is not a layer of ours, and `layerAt` // refuses it the same way an out-of-range one is refused. /// The layer a packet addresses, **for the task that sent it**: null unless the /// target names a used slot this sender created. A layer that is somebody else's /// is refused exactly as one that never existed, so a client cannot use the /// refusal to learn which ids are live (P3's refusal-equals-absence, applied to /// ids rather than names). fn targetLayer(target: u64, sender: u32) ?u32 { if (target > std.math.maxInt(u32)) return null; // a layer id is a u32 const id: u32 = @intCast(target); const layer = layerAt(id) orelse return null; if (layer.owner != sender) return null; return id; } /// Destroy every layer a dead client left behind — its surface is pages nobody /// will ever draw into again, and its slot is one of sixteen. The published /// exit events are the notice, the same sweep idiom the FAT server uses for open /// files and the harness uses for subscribers. fn releaseLayersOf(dead: u32) void { var released: u32 = 0; for (&layers, 0..) |*layer, id| { if (layer.used and layer.owner == dead) { _ = destroyLayer(@intCast(id)); released += 1; } } if (released != 0) { std.log.info("released {d} layer(s) for dead client {d}", .{ released, dead }); schedulePresent(); // the screen still shows what they painted } } fn onInfo(_: void, _: Invocation(void), answer: Answer(display_protocol.Info)) isize { const mode = backend.info(); answer.set(.{ .width = mode.width, .height = mode.height, .pitch = mode.pitch, .format = mode.format }); return 0; } fn onCreateLayer(_: void, invocation: Invocation(display_protocol.CreateLayer), answer: Answer(display_protocol.Created)) isize { const request = invocation.request; const slot = createLayer(invocation.sender, request.x, request.y, request.width, request.height, request.z, request.visible != 0) orelse return refused; answer.set(.{ .layer = slot }); return 0; } fn onConfigureLayer(_: void, invocation: Invocation(display_protocol.ConfigureLayer), _: Answer(void)) isize { const id = targetLayer(invocation.target, invocation.sender) orelse return refused; const request = invocation.request; return if (configureLayer(id, request.x, request.y, request.z, request.visible != 0)) 0 else refused; } fn onDestroyLayer(_: void, invocation: Invocation(void), _: Answer(void)) isize { const id = targetLayer(invocation.target, invocation.sender) orelse return refused; return if (destroyLayer(id)) 0 else refused; } fn onFillRect(_: void, invocation: Invocation(display_protocol.FillRect), _: Answer(void)) isize { const id = targetLayer(invocation.target, invocation.sender) orelse return refused; const request = invocation.request; const local = Rect.init(request.x, request.y, @intCast(request.width), @intCast(request.height)); return if (fillLayer(id, local, request.colour)) 0 else refused; } fn onBlitTile(_: void, invocation: Invocation(display_protocol.BlitTile), _: Answer(void)) isize { const id = targetLayer(invocation.target, invocation.sender) orelse return refused; const request = invocation.request; return if (blitLayer(id, request.x, request.y, request.width, request.height, invocation.tail)) 0 else refused; } fn onDamage(_: void, invocation: Invocation(display_protocol.Damage), _: Answer(void)) isize { const id = targetLayer(invocation.target, invocation.sender) orelse return refused; const l = layerAt(id) orelse return refused; const request = invocation.request; const screen = Rect{ .x = l.x + request.x, .y = l.y + request.y, .w = @intCast(request.width), .h = @intCast(request.height) }; addDamage(screen.intersect(layerScreenRect(l))); return 0; } fn onPresent(_: void, _: Invocation(void), _: Answer(void)) isize { // Scheduled, not immediate: the frame clock composites the accumulated damage // at the next tick, so back-to-back client presents coalesce into one frame. schedulePresent(); return 0; } fn onSetMode(_: void, invocation: Invocation(display_protocol.SetMode), _: Answer(void)) isize { if (!backend.setMode(invocation.request.width, invocation.request.height)) return refused; addDamage(screenRect()); // repaint the whole screen at the new resolution present(); return 0; } fn onGetModes(_: void, _: Invocation(void), answer: Answer(display_protocol.Modes)) isize { var list: [4]backend_mod.Mode = undefined; const count = backend.modes(&list); var modes = display_protocol.Modes{ .count = @intCast(count) }; for (0..@min(count, display_protocol.max_modes)) |i| { modes.modes[i] = .{ .width = list[i].width, .height = list[i].height }; } answer.set(modes); return 0; } const handlers = Serve.Handlers{ .info = onInfo, .create_layer = onCreateLayer, .configure_layer = onConfigureLayer, .destroy_layer = onDestroyLayer, .fill_rect = onFillRect, .blit_tile = onBlitTile, .damage = onDamage, .present = onPresent, .attach_scanout = onAttachScanout, .set_mode = onSetMode, .get_modes = onGetModes, }; fn onMessage(message: []const u8, reply: []u8, sender: u32, arrived: *ipc.Arrival) usize { // The one capability this service is ever handed is the scanout driver's // shared surface, and `attachScanout` deliberately does not claim it (the // mapping holds its own reference) — so the capability is peeked, never // taken, and the turn closes it. return Serve.dispatch({}, handlers, message, sender, arrived.peek(), reply); } /// Three notification sources reach the compositor, and one coalesced badge can carry /// more than one, so each bit is handled independently. A **message-notification** is a /// poke from the mouse-listener thread (a buffered self-`ipc.send`, `notify_message_bit`): /// fold the newest cursor position into the scene. A **timer** (`notify_timer_bit`) is the /// frame clock — or the deferred first native present after `attach_scanout` — either way, /// present the accumulated damage. A **published exit** (`notify_exit_bit`) is a client /// gone: release the layers it left. fn onNotification(badge: u64) void { if (badge & ipc.notify_exit_bit != 0) { releaseLayersOf(@intCast(badge & ~(ipc.notify_badge_bit | ipc.notify_exit_bit))); return; } if (badge & ipc.notify_message_bit != 0) renderCursor(); if (badge & ipc.notify_timer_bit != 0) frameTick(); } pub fn main() void { service.run(display_protocol.message_maximum, .{ .service = "display", .init = initialise, .on_message = onMessage, .on_notification = onNotification, }); }