//! /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 device = @import("driver"); const channel = @import("channel"); const ipc = @import("ipc"); const process = @import("process"); const service = @import("service"); const time = @import("time"); const device_manager = @import("driver"); const memory = @import("memory"); const logging = @import("logging"); const mmio = @import("mmio"); const pci = @import("pci"); const display_protocol = @import("display-protocol"); const scanout_protocol = @import("scanout-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 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; // DMA memory: the virtqueue rings and the command scratch. var ring: memory.DmaRegion = undefined; var command: memory.DmaRegion = 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: memory.SharedRegion = 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.readRegister(T, common_base + @offsetOf(vp.CommonCfg, field)); } fn cfgWrite(comptime T: type, comptime field: []const u8, value: T) void { mmio.writeRegister(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.writeRegister(u32, at, @truncate(value)); mmio.writeRegister(u32, at + 4, @truncate(value >> 32)); } fn orStatus(bit: u8) void { cfgWrite(u8, "device_status", cfgRead(u8, "device_status") | bit); } // --- 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.writeMemoryBarrier(); avail_shadow +%= 1; mmio.writeRegister(u16, ring.virtual + avail_offset + 2, avail_shadow); // avail.idx mmio.writeMemoryBarrier(); mmio.writeRegister(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.readMemoryBarrier(); const idx = mmio.readRegister(u16, ring.virtual + used_offset + 2); // used.idx if (idx != used_shadow) { used_shadow = idx; return true; } if (tries > 8) time.sleepMillis(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. The registry (/system/configuration/devices.csv) bound this driver by the // exact virtio-gpu identity (vendor 0x1AF4 / device 0x1050), so there is no re-confirm to // do here any more — map config space and 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). var function = pci.Function.map(device_id, descriptor) orelse { std.log.info("config-space map failed", .{}); return false; }; function.enableMemoryAndBusMaster(); // Walk the capability list for the virtio common-config and notify structures (V3 needs // only those two). The generic PCI mechanics — header fields, BAR decode, the capability // chain — are library/device/pci; the virtio cfg_type dispatch stays here. We map only the // common/notify BARs (the other virtio caps, and the cfg_pci back-door's placeholder // bar=0/offset=0, reference BARs we never touch, so mapping them would only log noise). var caps = function.capabilities(); while (caps.next()) |cap| { if (cap.id != vp.pci_cap_vendor) continue; const cfg_type = mmio.readRegister(u8, cap.offset + 3); if (cfg_type != vp.cfg_common and cfg_type != vp.cfg_notify) continue; const bar = mmio.readRegister(u8, cap.offset + 4); const offset = mmio.readRegister(u32, cap.offset + 8); if (function.mapBar(bar)) |bar_base| { if (cfg_type == vp.cfg_common) { common_base = bar_base + offset; } else { notify_base = bar_base + offset; notify_multiplier = mmio.readRegister(u32, cap.offset + 16); // virtio_pci_notify_cap tail } } } if (common_base == 0 or notify_base == 0) { std.log.info("missing common-config or notify capability", .{}); 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 = memory.dmaAlloc(4096, memory.dma_coherent) orelse { std.log.info("virtqueue allocation failed", .{}); return false; }; command = memory.dmaAlloc(4096, memory.dma_coherent) orelse { std.log.info("command-buffer allocation failed", .{}); return false; }; mmio.writeRegister(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 = memory.sharedCreate(scanout_bytes) orelse { std.log.info("scanout surface allocation failed", .{}); return false; }; const surface_physical = memory.sharedPhysical(surface.handle) orelse { std.log.info("could not resolve the scanout surface physical address", .{}); return false; }; // Bind the shared surface into this device's IOMMU domain so the GPU may DMA the // framebuffer (attach_backing points it here). The ring/command buffers are // dma_alloc'd and auto-bound; a shared-memory surface needs an explicit bind. We keep // the handle (it is also passed to the display service), so do not close it. No-op // without an IOMMU. if (!device.dmaBind(device_id, surface.handle)) { std.log.info("could not bind the scanout surface for DMA", .{}); 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). Role: device — we claim one PCI function and serve its scanout; we report // no children. A restarted instance re-hellos here and re-announces below — the compositor // re-attaches to the fresh scanout (V6). Best-effort: standalone bring-up has no manager. _ = device_manager.hello(.device, device_id); // 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.readMemoryBarrier(); 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.writeMemoryBarrier(); // 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; } /// 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 (channel.openEndpoint("display")) |h| break h; time.sleepMillis(20); } else { std.log.info("no display service to announce to (scanout-only)", .{}); return; }; var request = display_protocol.Request{ .operation = @intFromEnum(display_protocol.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: [display_protocol.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 `scanout_protocol.Reply{status}` written into `reply`. fn scanoutStatus(reply: []u8, ok: bool) usize { const response = scanout_protocol.Reply{ .status = if (ok) 0 else -1 }; @memcpy(reply[0..scanout_protocol.reply_size], std.mem.asBytes(&response)); return scanout_protocol.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, arrived: *ipc.Arrival) usize { _ = sender; _ = arrived; // nothing here takes a capability: the harness closes what arrives if (message.len < scanout_protocol.request_size) return 0; const request = std.mem.bytesToValue(scanout_protocol.Request, message[0..scanout_protocol.request_size]); switch (request.operation) { @intFromEnum(scanout_protocol.Operation.present) => return scanoutStatus(reply, presentFull()), @intFromEnum(scanout_protocol.Operation.get_modes) => { var response = scanout_protocol.ModesReply{ .status = 0, .count = offered_modes.len, .modes = undefined }; for (0..scanout_protocol.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..scanout_protocol.modes_reply_size], std.mem.asBytes(&response)); return scanout_protocol.modes_reply_size; }, @intFromEnum(scanout_protocol.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: process.Init) void { const argument = init.arguments.get(1) orelse { _ = logging.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; }; service.run(256, .{ .service = "scanout", .init = initialise, .on_message = onMessage, }); }