//! /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 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_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;

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;

/// 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;
    _ = system.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;
    _ = system.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];
}

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 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) {
        _ = 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, refresh_hz: 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.shared_memory.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 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 = 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
    _ = 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 *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) {
        _ = 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.hasFencedPresent()) _ = system.write("display: fenced 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 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;
        _ = 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");
    updateFrameClock();
    _ = 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) => {
            // 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 ok(reply);
        },
        @intFromEnum(protocol.Operation.attach_scanout) => {
            return attachScanout(request.x, request.width, request.height, request.colour, request.y, 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, and one coalesced badge can carry
/// both, 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.
fn onNotification(badge: u64) void {
    if (badge & ipc.notify_message_bit != 0) renderCursor();
    if (badge & ipc.notify_timer_bit != 0) frameTick();
}

pub fn main() void {
    runtime.service.run(protocol.message_maximum, .{
        .service = .display,
        .init = initialise,
        .on_message = onMessage,
        .on_notification = onNotification,
    });
}
