//! /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 shm = runtime.shm; const system = runtime.system; const ipc = runtime.ipc; const dp = runtime.display_protocol; const sp = runtime.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 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 (vsync) flushes; the device signals the fence when the flush /// is complete, which its used-ring ack already gates our synchronous present on. var fence_next: u64 = 1; /// Whether the device offered VIRTIO_GPU_F_EDID, so `get_edid` is worth issuing. var edid_available = false; /// 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** (shm) 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: shm.Region = undefined; // Split-virtqueue producer/consumer shadows. var avail_shadow: u16 = 0; var used_shadow: u16 = 0; /// Format one whole log line and emit it in a single `write`, so this driver's output can /// never interleave mid-line with the other drivers the manager runs concurrently. fn log(comptime fmt: []const u8, arguments: anytype) void { var line: [160]u8 = undefined; _ = system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return); } // --- 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; } } log("virtio-gpu: BAR {d} (physical 0x{x}) is not a mapped resource\n", .{ 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 log("virtio-gpu: device has no PCI capability list\n", .{}); 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) { log("virtio-gpu: missing common-config or notify capability\n", .{}); 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)) { log("virtio-gpu: unable to claim device {d}\n", .{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 { log("virtio-gpu: device {d} not in the device tree\n", .{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 { log("virtio-gpu: config-space map failed\n", .{}); 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) { log("virtio-gpu: not a virtio-gpu (vendor 0x{x} device 0x{x})\n", .{ 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) { log("virtio-gpu: device does not offer VERSION_1 (not a modern device)\n", .{}); 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) { log("virtio-gpu: device rejected the negotiated features\n", .{}); 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) { log("virtio-gpu: control queue too small ({d})\n", .{device_qsize}); return false; } ring = dma.alloc(4096, dma.coherent) orelse { log("virtio-gpu: virtqueue allocation failed\n", .{}); return false; }; command = dma.alloc(4096, dma.coherent) orelse { log("virtio-gpu: command-buffer allocation failed\n", .{}); 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) { log("virtio-gpu: resource_create_2d failed\n", .{}); return false; } } // Back the resource with a shared (shm) surface, so the compositor and the device work // the same physical pages. The device needs the guest-physical base for attach_backing. surface = shm.create(scanout_bytes) orelse { log("virtio-gpu: scanout surface allocation failed\n", .{}); return false; }; const surface_physical = shm.physical(surface.handle) orelse { log("virtio-gpu: could not resolve the scanout surface physical address\n", .{}); 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) { log("virtio-gpu: resource_attach_backing failed\n", .{}); return false; } } if (!setScanoutRect()) { log("virtio-gpu: set_scanout failed\n", .{}); return false; } log("virtio-gpu: scanout {d}x{d} online\n", .{ current_width, current_height }); // 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()) { log("virtio-gpu: initial present failed\n", .{}); 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))) { log("virtio-gpu: pixel read-back mismatch\n", .{}); return false; } log("virtio-gpu: flush acked, pixel check ok\n", .{}); // 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) { log("virtio-gpu: EDID not offered by device\n", .{}); return; } const request = requestAt(vg.GetEdid); request.* = .{ .hdr = .{ .type = @intFromEnum(vg.CmdType.get_edid) }, .scanout = 0 }; if (!submit(@sizeOf(vg.GetEdid), @sizeOf(vg.RespEdid))) { log("virtio-gpu: EDID request not acked\n", .{}); return; } const response: *vg.RespEdid = @ptrFromInt(command.virtual + response_offset); if (response.hdr.type != @intFromEnum(vg.CmdType.resp_ok_edid) or response.size < 64) { log("virtio-gpu: EDID unavailable\n", .{}); 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). 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); log("virtio-gpu: EDID preferred mode {d}x{d}\n", .{ h_active, v_active }); } /// 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 (vsync): the device signals the fence when the frame is actually on // screen — which its used-ring ack, what our synchronous submit waits on, already gates. 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 (ipc.lookup(.display)) |h| break h; system.sleep(20); } else { log("virtio-gpu: no display service to announce to (scanout-only)\n", .{}); 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) .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 { log("virtio-gpu: announce to display failed\n", .{}); return; }; log("virtio-gpu: announced scanout to display\n", .{}); } /// 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 { log("virtio-gpu: malformed device id '{s}'\n", .{argument}); return; }; runtime.service.run(256, .{ .service = .scanout, .init = initialise, .on_message = onMessage, }); } pub const panic = runtime.panic; comptime { _ = &runtime.start._start; // pull the runtime entry shim into the image }