592 lines
26 KiB
Zig
592 lines
26 KiB
Zig
//! /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
|
||
}
|