806 lines
35 KiB
Zig
806 lines
35 KiB
Zig
//! /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,
|
|
});
|
|
}
|