Files
danos/system/services/display/display.zig
T
daniel 7f415e724f display: track the mouse with a listener thread (Shape A) + cursor
The display's first use of threads (docs/threading.md, docs/display.md). The
compositor stays the single owner of the framebuffer — only the main
service.run loop touches the backend and layer stack — and a dedicated
mouse-listener thread runs beside it:

  - Listener: blocks on input.subscribeMouse(), accumulates relative dx/dy
    into an absolute cursor position clamped to the screen, and hands it to
    the compositor. It never touches the compositor, so no lock guards the
    framebuffer; a parked next() lets the core halt.
  - CursorChannel: a single-slot latest-value cell under a Thread.Mutex (the
    renderer wants where the cursor is now, not a replay of deltas), with a
    coalesced self-ipc.send poke that wakes the main loop — parked in
    replyWait — as a message-notification. At most one poke is queued while
    the last is undrained, so a fast mouse can't flood the endpoint.
  - Render: the cursor is a top-z compositor layer; on the poke the main loop
    moves it via configure + present (which damages old + new footprints).

The display binary opts into threads (addThreadedUserBinary), and
input-source gains a "mouse" mode that publishes pure motion to drive it.

Two kernel-level findings this surfaced, both fixed:

  1. IPC handles do not cross threads. The handle table lives on the Task, so
     the listener can't reuse the main loop's endpoint handle — it
     ipc.lookup(.display)s its own handle to the same endpoint to poke through.

  2. Concurrent IPC from two threads raced unlocked kernel state. The display
     is the first process issuing IPC syscalls from two threads at once, which
     exposed a data race (flaky #GP in installEntry): create_ipc_endpoint /
     ipc_register / ipc_lookup allocate from the kernel heap and mutate the
     global registry, endpoint refcounts, and handle tables without the big
     kernel lock. They were safe only while a process couldn't race itself.
     They now sync.enter() like call/reply_wait/send already did (the kernel
     heap has no lock of its own yet — heap.zig: "a lock comes with
     threads/SMP" — so the big lock keeps its callers serialized).

Test: -Dtest-case=display-cursor (smp:4) spawns the input service, the
threaded display, and input-source in mouse mode; asserts the display's
"cursor tracking mouse ok" marker once the cursor has tracked a run of motion
end to end. Verified green 6/6 under stress (the race hit ~1-in-4 before the
lock fix) and in the full 29-case QEMU guardrail suite; zig build test clean.
2026-07-21 02:31:29 +01:00

619 lines
26 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)`. 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 input = runtime.input;
const Thread = runtime.Thread;
const Rect = compositor.Rect;
const Surface = compositor.Surface;
/// 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` 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;
// 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 !dirty.isEmpty()) {
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) {
_ = system.write("display: native present verified\n");
} else {
_ = system.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`.
fn attachScanout(stride: u32, width: u32, height: u32, format: u32, capability: ?ipc.Handle, reply: []u8) usize {
const cap = capability orelse return fail(reply);
if (width == 0 or height == 0 or stride < width) return fail(reply);
const mapped = runtime.shm.map(cap) orelse return fail(reply);
const scanout = ipc.lookup(.scanout) orelse return fail(reply);
// 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 shm_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,
.scanout = scanout,
} };
background = protocol.pack(format, 0x20, 0x30, 0x48); // re-pack the wallpaper for the mode
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
_ = system.timerOnce(service_endpoint, 50); // present once the driver is serving .scanout
_ = system.write(if (reattach)
"display: scanout re-attached\n"
else
"display: scanout upgraded to virtio-gpu\n");
return ok(reply);
}
/// 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 vsync present.
fn modesetSelfCheck() void {
if (!backend.canModeSet()) return;
var mode_list: [4]backend_mod.Mode = undefined;
const count = backend.modes(&mode_list);
if (count == 0) {
_ = system.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 {
_ = system.write("display: mode-set self-check: no alternate mode offered\n");
return;
};
if (!backend.setMode(wanted.width, wanted.height)) {
_ = system.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;
_ = system.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.hasVsync()) _ = system.write("display: vsync present ok\n");
} else {
_ = system.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 = 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");
}
// --- 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 {
_ = system.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 service handle — we look the service up to install a
// handle in this thread's table. A poke posted here wakes the compositor loop parked
// in replyWait (docs/threading.md: handles do not cross threads).
cursor_channel.poke_endpoint = ipc.lookup(.display) orelse {
_ = system.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 repaint the cursor layer at
/// it. Runs on the main loop (the compositor owner) in response to a listener poke.
/// `configureLayer` damages both the old and new footprints, so a plain `present`
/// repaints exactly the two rectangles that changed.
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);
present();
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;
_ = system.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(cursor_origin_x, cursor_origin_y, cursor_size, cursor_size, cursor_z, true) orelse {
_ = system.write("display: could not create cursor layer\n");
return;
};
cursor_layer = id;
_ = fillLayer(id, Rect.init(0, 0, cursor_size, cursor_size), protocol.pack(mode.format, 0xF0, 0xF0, 0xF0));
present(); // show the cursor at its start position
_ = Thread.spawn(.{}, mouseListener, .{ mode.width, mode.height }) catch {
_ = system.write("display: could not spawn mouse listener\n");
};
}
// --- service ----------------------------------------------------------------
fn initialise(endpoint: ipc.Handle) bool {
service_endpoint = 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();
// Bring up the cursor and the mouse-listener thread now that the backend is live.
startCursorTracking();
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;
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);
},
@intFromEnum(protocol.Operation.attach_scanout) => {
return attachScanout(request.x, request.width, request.height, request.colour, capability, reply);
},
@intFromEnum(protocol.Operation.set_mode) => {
if (!backend.setMode(request.width, request.height)) return fail(reply);
addDamage(screenRect()); // repaint the whole screen at the new resolution
present();
return ok(reply);
},
@intFromEnum(protocol.Operation.get_modes) => {
var list: [4]backend_mod.Mode = undefined;
const count = backend.modes(&list);
var response = protocol.ModesReply{ .status = 0, .count = @intCast(count), .modes = undefined };
for (0..protocol.max_modes) |i| {
response.modes[i] = if (i < count)
.{ .width = list[i].width, .height = list[i].height }
else
.{ .width = 0, .height = 0 };
}
const bytes = std.mem.asBytes(&response);
@memcpy(reply[0..bytes.len], bytes);
return bytes.len;
},
else => return fail(reply),
}
}
/// Two notification sources reach the compositor. A **message-notification** is a poke
/// from the mouse-listener thread (a buffered self-`ipc.send`, `notify_message_bit`):
/// repaint the cursor at its latest channel position. Anything else is the post-attach
/// present **timer**: repaint into the freshly attached native surface, verify the frame
/// landed, then run the one-shot mode-set self-check (V5).
fn onNotification(badge: u64) void {
if (badge & ipc.notify_message_bit != 0) {
renderCursor();
return;
}
present(); // native present + verify (first timer fire after the upgrade)
if (pending_modeset_check) {
pending_modeset_check = false;
modesetSelfCheck();
}
}
pub fn main() void {
runtime.service.run(protocol.message_maximum, .{
.service = .display,
.init = initialise,
.on_message = onMessage,
.on_notification = onNotification,
});
}