//! /system/drivers/virtio-gpu — the virtio-gpu (virtio 1.0, modern PCI) display driver.
//! The device manager spawns it for the display/other PCI function (class 0x0380) whose
//! config space says vendor 0x1AF4 / device 0x1050; this instance claims that device and
//! brings up a single 2D scanout.
//!
//! V3 (this increment): the whole path end to end, proven from serial without a screenshot.
//! Claim the function, map its config space (resource 0) and the BAR that carries the
//! virtio structures, walk the vendor capabilities to find common-config / notify, reset
//! and negotiate VERSION_1, stand up the control virtqueue in DMA memory, then drive the
//! GPU: RESOURCE_CREATE_2D → ATTACH_BACKING (a coherent DMA buffer) → SET_SCANOUT, paint a
//! known test pattern, TRANSFER_TO_HOST_2D → RESOURCE_FLUSH, and **wait for the device's
//! used-ring ack**. Reading the backing back confirms it is CPU-visible; the ack confirms
//! the device consumed the frame. The compositor backend, hot-attach, mode-set/EDID, and
//! restart/re-attach are V4–V6. See docs/display-v2.md.

const std = @import("std");
const runtime = @import("runtime");
const mmio = @import("mmio");
const device = runtime.device;
const dma = runtime.dma;
const shared_memory = runtime.shared_memory;
const system = runtime.system;
const ipc = runtime.ipc;
const dp = runtime.display_protocol;
const sp = runtime.scanout_protocol;
const dm = runtime.device_manager_protocol;
const vp = @import("virtio-pci.zig");
const vg = @import("virtio-gpu-protocol.zig");

/// The DisplayFormat (device-abi) our B8G8R8X8 scanout resource presents: bgrx = 1. Handed to
/// the compositor in the announce so it packs colours in the surface's byte order.
const display_format_bgrx: u32 = 1;

/// The PCI vendor/device ids of a modern virtio-gpu (Red Hat / virtio; GPU is a
/// virtio-1.0-only device, so the id is always the modern 0x1050 — no legacy variant).
const virtio_vendor: u16 = 0x1AF4;
const virtio_gpu_device: u16 = 0x1050;

/// The scanout resource + shared surface are sized to the *largest* mode we offer; a mode
/// change (V5) re-points the scanout rectangle within it, so the resource, its backing, and
/// the shared surface never churn — and the surface's row stride is always `max_width`, which
/// the compositor is told in the announce. Kept modest so the backing is an easy contiguous run.
const max_width: u32 = 800;
const max_height: u32 = 600;
const scanout_bytes: usize = @as(usize, max_width) * max_height * 4;
const resource_id: u32 = 1;

/// The modes this scanout offers (all ≤ max). The first is the mode it comes up in.
const Mode = struct { width: u32, height: u32 };
const offered_modes = [_]Mode{ .{ .width = 640, .height = 480 }, .{ .width = 800, .height = 600 } };

/// The active mode — the scanout rectangle within the max-sized surface. Changed by `set_mode`.
var current_width: u32 = offered_modes[0].width;
var current_height: u32 = offered_modes[0].height;

/// Monotonic fence id for fenced flushes; the device signals the fence when the flush is
/// complete, which its used-ring ack already gates our synchronous present on. Completion
/// feedback, not vblank — nothing here is paced to the display's refresh.
var fence_next: u64 = 1;

/// Whether the device offered VIRTIO_GPU_F_EDID, so `get_edid` is worth issuing.
var edid_available = false;

/// The panel refresh rate parsed from the EDID preferred timing (0 = unknown). Carried to
/// the compositor in the announce so its frame clock paces to the panel, not a guess.
var edid_refresh_hz: u32 = 0;

/// The control virtqueue. We drive it synchronously — one command, notify, poll the used
/// ring — so a depth of 16 is ample; we ask the device to shrink to it (virtio 1.0 lets the
/// driver reduce queue_size), keeping the whole ring inside one page.
const queue_size: u16 = 16;
const desc_offset: usize = 0; // 16 * 16 = 256 bytes
const avail_offset: usize = 256; // flags + idx + ring[16] + used_event = 38 bytes
const used_offset: usize = 1024; // flags + idx + ring[16] + avail_event = 134 bytes

/// The command scratch: the request the device reads, then its response, in one DMA page.
const request_offset: usize = 0;
const response_offset: usize = 2048;

var device_id: u64 = 0;

// Mapped virtio structures (virtual addresses into the device's BAR).
var common_base: usize = 0;
var notify_base: usize = 0;
var notify_multiplier: u32 = 0;
var notify_addr: usize = 0;

// Per-BAR mapping cache: several capabilities usually share one BAR, and mmio_map must not
// be asked to map the same resource twice.
var bar_virtual: [6]usize = .{ 0, 0, 0, 0, 0, 0 };

// DMA memory: the virtqueue rings and the command scratch.
var ring: dma.Region = undefined;
var command: dma.Region = undefined;

// The scanout backing is a **shared** (shared-memory) region, not DMA: cacheable so the compositor
// composites into it cheaply (x86 DMA is coherent, so the device still sees the writes), and
// shareable so the same physical pages the device scans out of are the ones the compositor
// paints. The driver keeps the capability to hand to the compositor in the announce.
var surface: shared_memory.Region = undefined;

// Split-virtqueue producer/consumer shadows.
var avail_shadow: u16 = 0;
var used_shadow: u16 = 0;

// --- common-config register access (little-endian MMIO at `common_base`) ---------------

fn cfgRead(comptime T: type, comptime field: []const u8) T {
    return mmio.read(T, common_base + @offsetOf(vp.CommonCfg, field));
}
fn cfgWrite(comptime T: type, comptime field: []const u8, value: T) void {
    mmio.write(T, common_base + @offsetOf(vp.CommonCfg, field), value);
}
/// Write a 64-bit common-config register as two 32-bit halves (low then high) — the widest
/// access every virtio-pci host is required to accept for the queue-address registers.
fn cfgWrite64(comptime field: []const u8, value: u64) void {
    const at = common_base + @offsetOf(vp.CommonCfg, field);
    mmio.write(u32, at, @truncate(value));
    mmio.write(u32, at + 4, @truncate(value >> 32));
}
fn orStatus(bit: u8) void {
    cfgWrite(u8, "device_status", cfgRead(u8, "device_status") | bit);
}

// --- PCI config-space capability walk (config space is resource 0) ---------------------

/// Map the BAR numbered `bar` (0..5) and return its virtual base, correlating the BAR's
/// physical address (read from config space) with one of our device resources — because a
/// virtio capability names a BAR *number*, while `mmio_map` takes a *resource index* (and
/// resource 0 is config space, so BAR resources are re-numbered and gaps skipped).
fn mapBar(config: usize, descriptor: *const device.DeviceDescriptor, bar: u8) ?usize {
    if (bar >= 6) return null;
    if (bar_virtual[bar] != 0) return bar_virtual[bar];

    const low = mmio.read(u32, config + 0x10 + @as(usize, bar) * 4);
    if (low & 0x1 != 0) return null; // an I/O-space BAR — virtio structures are in memory BARs
    var base: u64 = low & 0xFFFF_FFF0;
    if ((low & 0x6) == 0x4) { // 64-bit memory BAR: the high half is the next dword
        const high = mmio.read(u32, config + 0x10 + (@as(usize, bar) + 1) * 4);
        base |= @as(u64, high) << 32;
    }

    for (descriptor.resources[0..@intCast(descriptor.resource_count)], 0..) |resource, index| {
        if (resource.kind == @intFromEnum(device.ResourceKind.memory) and resource.start == base) {
            const v = device.mmioMap(device_id, index) orelse return null;
            bar_virtual[bar] = v;
            return v;
        }
    }
    std.log.info("BAR {d} (physical 0x{x}) is not a mapped resource", .{ bar, base });
    return null;
}

/// Walk the PCI capability list from mapped config space, recording the common-config and
/// notify structures (the only two V3 needs). Returns false if either is missing.
fn walkCapabilities(config: usize, descriptor: *const device.DeviceDescriptor) bool {
    if (mmio.read(u16, config + 0x06) & 0x10 == 0) { // Status bit 4: capabilities list present
        std.log.info("device has no PCI capability list", .{});
        return false;
    }
    var cap: u8 = @as(u8, @truncate(mmio.read(u8, config + 0x34))) & 0xFC;
    var guard: u32 = 0;
    while (cap != 0 and guard < 48) : (guard += 1) {
        const at = config + cap;
        const id = mmio.read(u8, at + 0);
        const next = mmio.read(u8, at + 1) & 0xFC;
        // Only map BARs for the structures V3 uses (common + notify). The other virtio
        // capabilities (isr, device, and especially the cfg_pci back-door, which carries a
        // placeholder bar=0/offset=0) reference BARs we never touch, so mapping them would
        // just log spurious "not a mapped resource" noise.
        if (id == vp.pci_cap_vendor) {
            const cfg_type = mmio.read(u8, at + 3);
            if (cfg_type == vp.cfg_common or cfg_type == vp.cfg_notify) {
                const bar = mmio.read(u8, at + 4);
                const offset = mmio.read(u32, at + 8);
                if (mapBar(config, descriptor, bar)) |bar_base| {
                    if (cfg_type == vp.cfg_common) {
                        common_base = bar_base + offset;
                    } else {
                        notify_base = bar_base + offset;
                        notify_multiplier = mmio.read(u32, at + 16); // virtio_pci_notify_cap tail
                    }
                }
            }
        }
        cap = next;
    }
    if (common_base == 0 or notify_base == 0) {
        std.log.info("missing common-config or notify capability", .{});
        return false;
    }
    return true;
}

// --- the control virtqueue -------------------------------------------------------------

/// Publish the two-descriptor chain (request read by the device, response written by it),
/// notify the control queue, and wait for the device to return the buffer on the used ring.
fn submit(request_len: usize, response_len: usize) bool {
    const desc: [*]vp.Desc = @ptrFromInt(ring.virtual + desc_offset);
    desc[0] = .{
        .addr = command.physical + request_offset,
        .len = @intCast(request_len),
        .flags = vp.desc_flag_next,
        .next = 1,
    };
    desc[1] = .{
        .addr = command.physical + response_offset,
        .len = @intCast(response_len),
        .flags = vp.desc_flag_write,
        .next = 0,
    };

    const avail_ring: [*]u16 = @ptrFromInt(ring.virtual + avail_offset + 4);
    avail_ring[avail_shadow % queue_size] = 0; // head of the chain is descriptor 0
    mmio.wmb();
    avail_shadow +%= 1;
    mmio.write(u16, ring.virtual + avail_offset + 2, avail_shadow); // avail.idx
    mmio.wmb();

    mmio.write(u16, notify_addr, 0); // ring the control queue's doorbell
    return waitUsed();
}

/// Spin, then sleep-poll, on the used-ring index until the device advances it. QEMU
/// processes the notify on its own thread, so the ack usually lands immediately; the sleep
/// fallback covers a device that defers it without burning the CPU.
fn waitUsed() bool {
    var tries: u32 = 0;
    while (tries < 2000) : (tries += 1) {
        mmio.rmb();
        const idx = mmio.read(u16, ring.virtual + used_offset + 2); // used.idx
        if (idx != used_shadow) {
            used_shadow = idx;
            return true;
        }
        if (tries > 8) system.sleep(1);
    }
    return false;
}

/// The type field of the response the device wrote — `resp_ok_nodata` on success.
fn responseType() u32 {
    const response: *vg.CtrlHdr = @ptrFromInt(command.virtual + response_offset);
    return response.type;
}

/// Submit a command whose response is a bare header, returning its response type (0 if the
/// device never acked).
fn command_nodata(request_len: usize) u32 {
    if (!submit(request_len, @sizeOf(vg.CtrlHdr))) return 0;
    return responseType();
}

const ok_nodata: u32 = @intFromEnum(vg.CmdType.resp_ok_nodata);

fn requestAt(comptime T: type) *T {
    return @ptrFromInt(command.virtual + request_offset);
}

/// A deterministic, recognisable pixel so a read-back is a real check, not a tautology.
fn testPixel(index: u32) u32 {
    return 0xFF00_0000 | (index *% 0x9E37_79B1);
}

// --- bring-up --------------------------------------------------------------------------

fn initialise(endpoint: ipc.Handle) bool {
    _ = endpoint;
    if (!device.claim(device_id)) {
        std.log.info("unable to claim device {d}", .{device_id});
        return false;
    }

    var descriptors: [64]device.DeviceDescriptor = undefined;
    const total = device.enumerate(&descriptors);
    const descriptor = for (descriptors[0..@min(total, descriptors.len)]) |*d| {
        if (d.id == device_id) break d;
    } else {
        std.log.info("device {d} not in the device tree", .{device_id});
        return false;
    };

    // Config space is resource 0. Confirm it really is a virtio-gpu, then enable memory-space
    // decode + bus mastering (the device DMAs the ring and backing out of RAM); pci-bus only
    // preserves whatever the firmware left, and a secondary display is often left disabled.
    const config = device.mmioMap(device_id, 0) orelse {
        std.log.info("config-space map failed", .{});
        return false;
    };
    const vendor = mmio.read(u16, config + 0x00);
    const dev = mmio.read(u16, config + 0x02);
    if (vendor != virtio_vendor or dev != virtio_gpu_device) {
        std.log.info("not a virtio-gpu (vendor 0x{x} device 0x{x})", .{ vendor, dev });
        return false;
    }
    mmio.write(u16, config + 0x04, mmio.read(u16, config + 0x04) | 0x06); // MEM + bus master

    if (!walkCapabilities(config, descriptor)) return false;

    // Reset, then the modern feature handshake: acknowledge, take driver ownership, require
    // VERSION_1 and offer nothing else, and confirm the device accepts that.
    cfgWrite(u8, "device_status", 0);
    orStatus(vp.status_acknowledge);
    orStatus(vp.status_driver);

    // Low feature word (device-specific): note whether the device offers EDID (bit 1).
    cfgWrite(u32, "device_feature_select", 0);
    edid_available = cfgRead(u32, "device_feature") & vg.feature_edid != 0;
    // High feature word: VERSION_1 (bit 32) is required for a modern device.
    cfgWrite(u32, "device_feature_select", vp.feature_version_1_word);
    if (cfgRead(u32, "device_feature") & vp.feature_version_1_bit == 0) {
        std.log.info("device does not offer VERSION_1 (not a modern device)", .{});
        return false;
    }
    // Accept exactly VERSION_1, plus EDID when the device offered it (never a feature it didn't).
    cfgWrite(u32, "driver_feature_select", 0);
    cfgWrite(u32, "driver_feature", if (edid_available) vg.feature_edid else 0);
    cfgWrite(u32, "driver_feature_select", vp.feature_version_1_word);
    cfgWrite(u32, "driver_feature", vp.feature_version_1_bit);
    orStatus(vp.status_features_ok);
    if (cfgRead(u8, "device_status") & vp.status_features_ok == 0) {
        std.log.info("device rejected the negotiated features", .{});
        return false;
    }

    // Stand up the control virtqueue (queue 0) in coherent DMA memory.
    cfgWrite(u16, "queue_select", 0);
    const device_qsize = cfgRead(u16, "queue_size");
    if (device_qsize < queue_size) {
        std.log.info("control queue too small ({d})", .{device_qsize});
        return false;
    }
    ring = dma.alloc(4096, dma.coherent) orelse {
        std.log.info("virtqueue allocation failed", .{});
        return false;
    };
    command = dma.alloc(4096, dma.coherent) orelse {
        std.log.info("command-buffer allocation failed", .{});
        return false;
    };
    mmio.write(u16, ring.virtual + avail_offset, 1); // VIRTQ_AVAIL_F_NO_INTERRUPT: we poll
    cfgWrite(u16, "queue_size", queue_size);
    cfgWrite64("queue_desc", ring.physical + desc_offset);
    cfgWrite64("queue_driver", ring.physical + avail_offset);
    cfgWrite64("queue_device", ring.physical + used_offset);
    cfgWrite(u16, "queue_msix_vector", 0xFFFF); // VIRTIO_MSI_NO_VECTOR
    cfgWrite(u16, "queue_enable", 1);

    cfgWrite(u16, "queue_select", 0);
    notify_addr = notify_base + @as(usize, cfgRead(u16, "queue_notify_off")) * notify_multiplier;

    orStatus(vp.status_driver_ok);

    // Drive the GPU: create a 2D resource at the *max* mode, back it with a shared surface, and
    // scan out the current-mode rectangle within it.
    {
        const request = requestAt(vg.ResourceCreate2d);
        request.* = .{
            .hdr = .{ .type = @intFromEnum(vg.CmdType.resource_create_2d) },
            .resource_id = resource_id,
            .format = vg.format_b8g8r8x8_unorm,
            .width = max_width,
            .height = max_height,
        };
        if (command_nodata(@sizeOf(vg.ResourceCreate2d)) != ok_nodata) {
            std.log.info("resource_create_2d failed", .{});
            return false;
        }
    }

    // Back the resource with a shared (shared-memory) surface, so the compositor and the device work
    // the same physical pages. The device needs the guest-physical base for attach_backing.
    surface = shared_memory.create(scanout_bytes) orelse {
        std.log.info("scanout surface allocation failed", .{});
        return false;
    };
    const surface_physical = shared_memory.physical(surface.handle) orelse {
        std.log.info("could not resolve the scanout surface physical address", .{});
        return false;
    };
    {
        const request = requestAt(vg.ResourceAttachBacking);
        request.* = .{
            .hdr = .{ .type = @intFromEnum(vg.CmdType.resource_attach_backing) },
            .resource_id = resource_id,
            .nr_entries = 1,
        };
        const entry: *vg.MemEntry = @ptrFromInt(command.virtual + request_offset + @sizeOf(vg.ResourceAttachBacking));
        entry.* = .{ .addr = surface_physical, .length = @intCast(scanout_bytes) };
        if (command_nodata(@sizeOf(vg.ResourceAttachBacking) + @sizeOf(vg.MemEntry)) != ok_nodata) {
            std.log.info("resource_attach_backing failed", .{});
            return false;
        }
    }
    if (!setScanoutRect()) {
        std.log.info("set_scanout failed", .{});
        return false;
    }
    std.log.info("scanout {d}x{d} online", .{ current_width, current_height });

    // Hello the device manager so it counts us as up (and does not stop us at the hello
    // deadline). A restarted instance re-hellos here and re-announces below — the compositor
    // re-attaches to the fresh scanout (V6).
    helloManager();

    // Read the monitor's EDID (best-effort, when the device offers it) — the mode list a real
    // driver derives from it; we log the preferred mode and keep our fixed offered list.
    readEdid();

    // Paint a known pattern, present it, and read it back — the V3 self-test that proves the
    // whole path (virtqueue, resource, shared backing, transfer, flush) before a client attaches.
    const pixels: [*]u32 = @ptrCast(@alignCast(surface.ptr));
    const pixel_count: usize = @as(usize, max_width) * max_height;
    for (0..pixel_count) |i| pixels[i] = testPixel(@intCast(i));

    if (!presentFull()) {
        std.log.info("initial present failed", .{});
        return false;
    }
    // The scanout surface is CPU-visible RAM: read the pattern back to prove the mapping,
    // which together with the flush ack above is the automated stand-in for "it's on screen".
    mmio.rmb();
    if (pixels[0] != testPixel(0) or pixels[pixel_count / 2] != testPixel(@intCast(pixel_count / 2))) {
        std.log.info("pixel read-back mismatch", .{});
        return false;
    }
    std.log.info("flush acked, pixel check ok", .{});

    // Offer the shared surface to the compositor so it upgrades off the GOP floor (V4).
    announce();
    return true;
}

/// Point scanout 0 at the current-mode rectangle of the resource. Reused by initial bring-up
/// and by `set_mode`.
fn setScanoutRect() bool {
    const request = requestAt(vg.SetScanout);
    request.* = .{
        .hdr = .{ .type = @intFromEnum(vg.CmdType.set_scanout) },
        .rect = .{ .x = 0, .y = 0, .width = current_width, .height = current_height },
        .scanout_id = 0,
        .resource_id = resource_id,
    };
    return command_nodata(@sizeOf(vg.SetScanout)) == ok_nodata;
}

/// Read and log the monitor's preferred mode from its EDID (VIRTIO_GPU_F_EDID). Best-effort:
/// a device that doesn't offer EDID, or a missing/short block, is logged and ignored.
fn readEdid() void {
    if (!edid_available) {
        std.log.info("EDID not offered by device", .{});
        return;
    }
    const request = requestAt(vg.GetEdid);
    request.* = .{ .hdr = .{ .type = @intFromEnum(vg.CmdType.get_edid) }, .scanout = 0 };
    if (!submit(@sizeOf(vg.GetEdid), @sizeOf(vg.RespEdid))) {
        std.log.info("EDID request not acked", .{});
        return;
    }
    const response: *vg.RespEdid = @ptrFromInt(command.virtual + response_offset);
    if (response.hdr.type != @intFromEnum(vg.CmdType.resp_ok_edid) or response.size < 64) {
        std.log.info("EDID unavailable", .{});
        return;
    }
    // The first detailed timing descriptor (EDID base-block offset 54) is the preferred mode:
    // active pixels are 12-bit, low byte + high nibble (bytes 2/4 horizontal, 5/7 vertical).
    // The refresh rate is derived from the same descriptor: pixel clock (bytes 0-1, 10 kHz
    // units) over total (active + blanking) pixels per frame — the loader does the identical
    // computation for the boot framebuffer (boot/efi.zig edidNative).
    const e = &response.edid;
    const h_active = @as(u32, e[56]) | (@as(u32, e[58] & 0xF0) << 4);
    const v_active = @as(u32, e[59]) | (@as(u32, e[61] & 0xF0) << 4);
    const clock_hz = (@as(u64, e[54]) | (@as(u64, e[55]) << 8)) * 10_000;
    const h_blank = @as(u64, e[57]) | (@as(u64, e[58] & 0x0F) << 8);
    const v_blank = @as(u64, e[60]) | (@as(u64, e[61] & 0x0F) << 8);
    const total = (@as(u64, h_active) + h_blank) * (@as(u64, v_active) + v_blank);
    if (total != 0) edid_refresh_hz = @intCast((clock_hz + total / 2) / total);
    std.log.info("EDID preferred mode {d}x{d} @ {d} Hz", .{ h_active, v_active, edid_refresh_hz });
}

/// Present the whole surface: copy the guest backing into the host resource, then flush it to
/// the panel. Reused by the V3 self-test and by every compositor present over `.scanout`. V4
/// presents the full surface; the damage-rect fast path is a later refinement.
fn presentFull() bool {
    mmio.wmb(); // the surface writes must be visible before the device transfers them
    {
        // Transfer the current-mode rectangle from the guest backing to the host resource. The
        // device uses the resource's (max) width as the row stride, so the top-left rect at
        // offset 0 is exactly the visible area — the compositor composes at that same stride.
        const request = requestAt(vg.TransferToHost2d);
        request.* = .{
            .hdr = .{ .type = @intFromEnum(vg.CmdType.transfer_to_host_2d) },
            .rect = .{ .x = 0, .y = 0, .width = current_width, .height = current_height },
            .offset = 0,
            .resource_id = resource_id,
        };
        if (command_nodata(@sizeOf(vg.TransferToHost2d)) != ok_nodata) return false;
    }
    {
        // A fenced flush: the device signals the fence once it has consumed the frame — which
        // its used-ring ack, what our synchronous submit waits on, already gates. Completion
        // feedback and a tear-free snapshot, not vblank pacing.
        const request = requestAt(vg.ResourceFlush);
        request.* = .{
            .hdr = .{ .type = @intFromEnum(vg.CmdType.resource_flush), .flags = vg.flag_fence, .fence_id = fence_next },
            .rect = .{ .x = 0, .y = 0, .width = current_width, .height = current_height },
            .resource_id = resource_id,
        };
        fence_next += 1;
        if (command_nodata(@sizeOf(vg.ResourceFlush)) != ok_nodata) return false;
    }
    return true;
}

/// Hello the device manager (role: bus — we own a PCI function, though we report no children):
/// the handshake that marks us up so the manager doesn't stop us at the hello deadline, and
/// (as a supervised driver) restarts us if we die. Best-effort: without a manager we still run.
fn helloManager() void {
    var tries: u32 = 0;
    const manager = while (tries < 100) : (tries += 1) {
        if (ipc.lookup(.device_manager)) |h| break h;
        system.sleep(20);
    } else {
        std.log.info("no device manager to hello", .{});
        return;
    };
    const hello = dm.Hello{ .role = @intFromEnum(dm.Role.bus), .device_id = device_id };
    var reply: [dm.reply_size]u8 = undefined;
    const n = ipc.call(manager, std.mem.asBytes(&hello), &reply) catch {
        std.log.info("hello call failed", .{});
        return;
    };
    if (n < dm.reply_size or std.mem.bytesToValue(dm.HelloReply, reply[0..dm.reply_size]).status != 0) {
        std.log.info("hello refused", .{});
        return;
    }
    std.log.info("hello acknowledged", .{});
}

/// Announce the scanout to the display service so it upgrades off the GOP framebuffer: hand it
/// the shared surface as a capability plus the geometry. Best-effort and non-fatal — without a
/// display service (the standalone virtio-gpu bring-up test) the driver is still a valid
/// scanout service; it just serves no one. The display replies immediately (it defers its
/// first present to a timer), so this returns before we start serving `.scanout` — no deadlock.
fn announce() void {
    var tries: u32 = 0;
    const display = while (tries < 50) : (tries += 1) {
        if (ipc.lookup(.display)) |h| break h;
        system.sleep(20);
    } else {
        std.log.info("no display service to announce to (scanout-only)", .{});
        return;
    };
    var request = dp.Request{
        .operation = @intFromEnum(dp.Operation.attach_scanout),
        .x = max_width, // the shared surface's row stride in pixels (it is sized to the max mode)
        .y = edid_refresh_hz, // the panel refresh from EDID (0 = unknown) — the frame-clock seed
        .width = current_width,
        .height = current_height,
        .colour = display_format_bgrx,
    };
    var reply: [dp.reply_size]u8 = undefined;
    _ = ipc.callCap(display, std.mem.asBytes(&request), &reply, surface.handle) catch {
        std.log.info("announce to display failed", .{});
        return;
    };
    std.log.info("announced scanout to display", .{});
}

/// A `sp.Reply{status}` written into `reply`.
fn scanoutStatus(reply: []u8, ok: bool) usize {
    const response = sp.Reply{ .status = if (ok) 0 else -1 };
    @memcpy(reply[0..sp.reply_size], std.mem.asBytes(&response));
    return sp.reply_size;
}

/// The `.scanout` service: the compositor drives present / mode queries here. The pixels are
/// already in the shared surface, so a present is a transfer-to-host + fenced flush; a mode
/// change just re-points the scanout rectangle (the surface is sized to the largest mode).
fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?ipc.Handle) usize {
    _ = sender;
    _ = capability;
    if (message.len < sp.request_size) return 0;
    const request = std.mem.bytesToValue(sp.Request, message[0..sp.request_size]);
    switch (request.operation) {
        @intFromEnum(sp.Operation.present) => return scanoutStatus(reply, presentFull()),
        @intFromEnum(sp.Operation.get_modes) => {
            var response = sp.ModesReply{ .status = 0, .count = offered_modes.len, .modes = undefined };
            for (0..sp.max_modes) |i| {
                response.modes[i] = if (i < offered_modes.len)
                    .{ .width = offered_modes[i].width, .height = offered_modes[i].height }
                else
                    .{ .width = 0, .height = 0 };
            }
            @memcpy(reply[0..sp.modes_reply_size], std.mem.asBytes(&response));
            return sp.modes_reply_size;
        },
        @intFromEnum(sp.Operation.set_mode) => {
            const w = request.width;
            const h = request.height;
            if (w == 0 or h == 0 or w > max_width or h > max_height) return scanoutStatus(reply, false);
            current_width = w;
            current_height = h;
            return scanoutStatus(reply, setScanoutRect());
        },
        else => return 0,
    }
}

pub fn main(init: runtime.process.Init) void {
    const argument = init.arguments.get(1) orelse {
        _ = system.write("virtio-gpu: missing device id (argv[1])\n");
        return;
    };
    device_id = std.fmt.parseInt(u64, argument, 10) catch {
        std.log.info("malformed device id '{s}'", .{argument});
        return;
    };
    runtime.service.run(256, .{
        .service = .scanout,
        .init = initialise,
        .on_message = onMessage,
    });
}
