virtio-gpu: a modern virtio 1.0 display driver, bring-up to a flushed frame (v2 V3)

The first native scanout backend's driver half. The device manager matches the
virtio-gpu PCI function by its Display/Other class triple and spawns the driver,
which claims the device, confirms vendor 0x1AF4/device 0x1050 from config space,
enables memory-space + bus mastering, and walks the virtio vendor capabilities to
find the common-config and notify structures in its BAR.

From there it is the standard modern-virtio bring-up: reset, negotiate VERSION_1,
stand up the control virtqueue in coherent DMA, then drive the GPU end to end —
RESOURCE_CREATE_2D, ATTACH_BACKING (a coherent DMA buffer; V4 swaps in the shm-
shared surface), SET_SCANOUT, paint a known pattern, TRANSFER_TO_HOST_2D,
RESOURCE_FLUSH, and wait for the device's used-ring ack. Reading the backing back
proves it is CPU-visible RAM; the ack proves the device consumed the frame —
together the automated stand-in for "it's on screen", no screenshot.

- system/drivers/virtio-gpu/: the driver, plus virtio-gpu-protocol.zig (control
  commands) and virtio-pci.zig (the 1.0 PCI transport + split-virtqueue), both with
  host-tested struct sizes.
- device-manager matches the display/other class triple to "virtio-gpu"; the driver
  self-confirms the vendor/device id, since the class alone cannot distinguish it.
- ServiceId.scanout (11): the driver registers it so the compositor finds it in V4.

Gate: python3 test/qemu_test.py virtio-gpu (QEMU -device virtio-gpu-pci) — the
device-manager stack discovers the function, the driver brings up a 640x480 scanout
and flushes a test pattern: "virtio-gpu: scanout 640x480 online" + "flush acked,
pixel check ok". host tests, display-service, shm, and device-list still pass.

Note: the pre-existing pci-scan case triple-faults on main (verified at 88ad432,
before this change); it is unrelated and tracked separately.
This commit is contained in:
Daniel Samson 2026-07-14 11:29:53 +01:00
parent 88ad432758
commit 6e0e0a62c6
No known key found for this signature in database
GPG Key ID: A9EB2589C60F7268
9 changed files with 772 additions and 13 deletions

View File

@ -468,6 +468,7 @@ pub fn build(b: *std.Build) void {
const fat_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "fat", "system/services/fat/fat.zig");
const display_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "display", "system/services/display/display.zig");
const display_demo_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "display-demo", "system/services/display-demo/display-demo.zig");
const virtio_gpu_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "virtio-gpu", "system/drivers/virtio-gpu/virtio-gpu.zig");
const shm_server_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "shm-server", "system/services/shm-server/shm-server.zig");
const shm_client_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "shm-client", "system/services/shm-client/shm-client.zig");
const fat_test_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "fat-test", "system/services/fat/fat-test.zig");
@ -541,6 +542,8 @@ pub fn build(b: *std.Build) void {
mk_run.addFileArg(display_exe.getEmittedBin());
mk_run.addArg("display-demo");
mk_run.addFileArg(display_demo_exe.getEmittedBin());
mk_run.addArg("virtio-gpu");
mk_run.addFileArg(virtio_gpu_exe.getEmittedBin());
mk_run.addArg("shm-server");
mk_run.addFileArg(shm_server_exe.getEmittedBin());
mk_run.addArg("shm-client");
@ -781,6 +784,8 @@ pub fn build(b: *std.Build) void {
"system/services/fat/engine.zig", // FAT read/write over a RAM-backed image
"system/services/display/compositor.zig", // Rect math + fill/composite/blit-tile
"system/services/display/protocol.zig", // pack(): native pixel encoding per format
"system/drivers/virtio-gpu/virtio-gpu-protocol.zig", // virtio-gpu command struct sizes
"system/drivers/virtio-gpu/virtio-pci.zig", // virtio 1.0 PCI transport struct sizes
}) |root| {
const mod_tests = b.addTest(.{
.root_module = b.createModule(.{

View File

@ -79,21 +79,26 @@ pattern, and passes its capability to `shm-server` as an `ipc_call` send_cap; th
`ipc`/`ipc-call`/`ipc-cap`, `supervision`, `dma`, `usermem`, `display-service`, and host
tests all still pass — the handle-table change broke no existing IPC.
## V3 — The virtio-gpu driver: bring-up + a frame on screen
## V3 — The virtio-gpu driver: bring-up + a frame on screen
- [ ] `system/drivers/virtio-gpu/`: claim the virtio-gpu PCI function (device-manager
match on its PCI/virtio id), map BARs, negotiate features, set up the control
virtqueue. `virtio-gpu-protocol.zig` for the control structs (host-tested sizes).
- [ ] Create a 2D scanout resource backed by an `shm` region, `attach_backing`,
`set_scanout` to CRTC 0, and `resource_flush` a test pattern.
- [ ] Register a `scanout` service (new `ServiceId`).
- [x] `system/drivers/virtio-gpu/`: claim the virtio-gpu PCI function (device-manager
match on the display/other class triple, driver self-confirms vendor 0x1AF4/device
0x1050 from config space), enable memory-space + bus-master, walk the vendor
capabilities in config space to find common-config + notify, map the BAR, negotiate
VERSION_1, and stand up the control virtqueue in coherent DMA. `virtio-gpu-protocol.zig`
+ `virtio-pci.zig` for the control/transport structs (host-tested sizes).
- [x] Create a 2D scanout resource backed by a coherent DMA region (V4 swaps this for the
shm-shared surface), `attach_backing`, `set_scanout` to scanout 0, `transfer_to_host_2d`
+ `resource_flush` of a test pattern, and wait on the used ring.
- [x] Register a `scanout` service (`ServiceId.scanout` = 11).
**Gate (automated):** a `virtio-gpu` case (QEMU `-device virtio-gpu`) where the driver
writes a known test pattern into the shm scanout resource, `resource_flush`es it, and
**waits for the device's used-ring ack**, then reads the resource back and checks the
pattern — logging `virtio-gpu: scanout {w}x{h} online` and `virtio-gpu: flush acked, pixel
check ok`. That proves virtqueue + resource + attach + set_scanout + flush end to end
without a screenshot (the used-ring ack is the device confirming it consumed the frame).
**Gate (met):** the `virtio-gpu` case (QEMU `-device virtio-gpu-pci`) boots the
device-manager stack, which discovers the function and spawns the driver; the driver writes
a known test pattern into the scanout backing, `transfer_to_host_2d` + `resource_flush`es
it, and **waits for the device's used-ring ack**, then reads the backing back and checks the
pattern — logging `virtio-gpu: scanout 640x480 online` and `virtio-gpu: flush acked, pixel
check ok`. That proves virtqueue + resource + attach + set_scanout + transfer + flush end to
end without a screenshot (the used-ring ack is the device confirming it consumed the frame).
## V4 — The native backend + hot-attach

View File

@ -187,6 +187,7 @@ pub const ServiceId = enum(u32) {
fat = 8, // the FAT filesystem server; the VFS mounts it and forwards paths under its mount point (/mnt/usb) to it
display = 9, // the display service: owns the framebuffer, composites a layer stack, presents frames (docs/display.md)
shm_test = 10, // the shm test server (V2): a client passes it a shared-memory capability, it maps + verifies (docs/display-v2.md)
scanout = 11, // a native scanout driver (virtio-gpu): the compositor finds it here to upgrade off the GOP framebuffer (docs/display-v2.md)
_,
};

View File

@ -0,0 +1,135 @@
//! The virtio-gpu control protocol the command/response structs the driver exchanges with
//! the device over its control virtqueue (virtio spec, "GPU Device"). `extern` structs, so
//! the layout matches the little-endian wire format exactly. Host-tested for size. See
//! docs/display-v2.md.
const std = @import("std");
/// Control command / response types (virtio_gpu_ctrl_type). Commands are 0x01xx, responses
/// 0x11xx (ok) / 0x12xx (error).
pub const CmdType = enum(u32) {
get_display_info = 0x0100,
resource_create_2d = 0x0101,
resource_unref = 0x0102,
set_scanout = 0x0103,
resource_flush = 0x0104,
transfer_to_host_2d = 0x0105,
resource_attach_backing = 0x0106,
resource_detach_backing = 0x0107,
get_edid = 0x010a,
resp_ok_nodata = 0x1100,
resp_ok_display_info = 0x1101,
resp_ok_edid = 0x1104,
resp_err_unspec = 0x1200,
_,
};
/// Set in a command's `flags` to request a fence; the device echoes `fence_id` in the
/// response and does not report completion until the command's effects are visible.
pub const flag_fence: u32 = 1 << 0;
/// virtio_gpu_ctrl_hdr the header on every command and response.
pub const CtrlHdr = extern struct {
type: u32,
flags: u32 = 0,
fence_id: u64 = 0,
ctx_id: u32 = 0,
ring_idx: u8 = 0,
padding: [3]u8 = .{ 0, 0, 0 },
};
pub const Rect = extern struct {
x: u32,
y: u32,
width: u32,
height: u32,
};
/// 2D pixel formats. QEMU's virtio-gpu host default is B8G8R8X8 (matches our bgrx).
pub const format_b8g8r8x8_unorm: u32 = 2;
pub const format_r8g8b8x8_unorm: u32 = 134;
pub const ResourceCreate2d = extern struct {
hdr: CtrlHdr,
resource_id: u32,
format: u32,
width: u32,
height: u32,
};
/// One scatter-gather entry of a resource's guest backing (a physical span).
pub const MemEntry = extern struct {
addr: u64,
length: u32,
padding: u32 = 0,
};
/// Header for RESOURCE_ATTACH_BACKING; `nr_entries` `MemEntry` follow it inline.
pub const ResourceAttachBacking = extern struct {
hdr: CtrlHdr,
resource_id: u32,
nr_entries: u32,
};
pub const SetScanout = extern struct {
hdr: CtrlHdr,
rect: Rect,
scanout_id: u32,
resource_id: u32,
};
pub const ResourceFlush = extern struct {
hdr: CtrlHdr,
rect: Rect,
resource_id: u32,
padding: u32 = 0,
};
/// Copy the guest backing into the host resource for `rect` (2D resources must transfer
/// before a flush shows the update).
pub const TransferToHost2d = extern struct {
hdr: CtrlHdr,
rect: Rect,
offset: u64,
resource_id: u32,
padding: u32 = 0,
};
pub const max_scanouts = 16;
pub const DisplayOne = extern struct {
rect: Rect,
enabled: u32,
flags: u32,
};
pub const RespDisplayInfo = extern struct {
hdr: CtrlHdr,
pmodes: [max_scanouts]DisplayOne,
};
pub const GetEdid = extern struct {
hdr: CtrlHdr,
scanout: u32,
padding: u32 = 0,
};
pub const RespEdid = extern struct {
hdr: CtrlHdr,
size: u32,
padding: u32 = 0,
edid: [1024]u8,
};
test "virtio-gpu struct sizes match the wire layout" {
try std.testing.expectEqual(@as(usize, 24), @sizeOf(CtrlHdr));
try std.testing.expectEqual(@as(usize, 16), @sizeOf(Rect));
try std.testing.expectEqual(@as(usize, 40), @sizeOf(ResourceCreate2d));
try std.testing.expectEqual(@as(usize, 16), @sizeOf(MemEntry));
try std.testing.expectEqual(@as(usize, 32), @sizeOf(ResourceAttachBacking));
try std.testing.expectEqual(@as(usize, 48), @sizeOf(SetScanout));
try std.testing.expectEqual(@as(usize, 48), @sizeOf(ResourceFlush));
try std.testing.expectEqual(@as(usize, 56), @sizeOf(TransferToHost2d));
try std.testing.expectEqual(@as(usize, 24 + 4 + 4 + 1024), @sizeOf(RespEdid));
}

View File

@ -0,0 +1,443 @@
//! /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 V4V6. 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 system = runtime.system;
const ipc = runtime.ipc;
const vp = @import("virtio-pci.zig");
const vg = @import("virtio-gpu-protocol.zig");
/// 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 we bring up. A modest, fixed geometry for V3 (mode-set from the EDID is V5)
/// kept small so the backing is an easy contiguous DMA run.
const scanout_width: u32 = 640;
const scanout_height: u32 = 480;
const scanout_bytes: usize = @as(usize, scanout_width) * scanout_height * 4;
const resource_id: u32 = 1;
/// 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, the command scratch, and the scanout backing.
var ring: dma.Region = undefined;
var command: dma.Region = undefined;
var backing: dma.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);
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;
}
cfgWrite(u32, "driver_feature_select", 0);
cfgWrite(u32, "driver_feature", 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, back it with DMA memory, and make it scanout 0.
{
const request = requestAt(vg.ResourceCreate2d);
request.* = .{
.hdr = .{ .type = @intFromEnum(vg.CmdType.resource_create_2d) },
.resource_id = resource_id,
.format = vg.format_b8g8r8x8_unorm,
.width = scanout_width,
.height = scanout_height,
};
if (command_nodata(@sizeOf(vg.ResourceCreate2d)) != ok_nodata) {
log("virtio-gpu: resource_create_2d failed\n", .{});
return false;
}
}
backing = dma.alloc(scanout_bytes, dma.coherent) orelse {
log("virtio-gpu: scanout backing allocation failed\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 = backing.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;
}
}
{
const request = requestAt(vg.SetScanout);
request.* = .{
.hdr = .{ .type = @intFromEnum(vg.CmdType.set_scanout) },
.rect = .{ .x = 0, .y = 0, .width = scanout_width, .height = scanout_height },
.scanout_id = 0,
.resource_id = resource_id,
};
if (command_nodata(@sizeOf(vg.SetScanout)) != ok_nodata) {
log("virtio-gpu: set_scanout failed\n", .{});
return false;
}
}
log("virtio-gpu: scanout {d}x{d} online\n", .{ scanout_width, scanout_height });
// Paint a known pattern, transfer it to the host resource, and flush it to the display.
const pixels: [*]u32 = @ptrFromInt(backing.virtual);
const pixel_count: u32 = scanout_width * scanout_height;
for (0..pixel_count) |i| pixels[i] = testPixel(@intCast(i));
mmio.wmb();
{
const request = requestAt(vg.TransferToHost2d);
request.* = .{
.hdr = .{ .type = @intFromEnum(vg.CmdType.transfer_to_host_2d) },
.rect = .{ .x = 0, .y = 0, .width = scanout_width, .height = scanout_height },
.offset = 0,
.resource_id = resource_id,
};
if (command_nodata(@sizeOf(vg.TransferToHost2d)) != ok_nodata) {
log("virtio-gpu: transfer_to_host_2d failed\n", .{});
return false;
}
}
{
const request = requestAt(vg.ResourceFlush);
request.* = .{
.hdr = .{ .type = @intFromEnum(vg.CmdType.resource_flush) },
.rect = .{ .x = 0, .y = 0, .width = scanout_width, .height = scanout_height },
.resource_id = resource_id,
};
if (command_nodata(@sizeOf(vg.ResourceFlush)) != ok_nodata) {
log("virtio-gpu: resource_flush was not acked\n", .{});
return false;
}
}
// The scanout backing 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(pixel_count / 2)) {
log("virtio-gpu: pixel read-back mismatch\n", .{});
return false;
}
log("virtio-gpu: flush acked, pixel check ok\n", .{});
return true;
}
/// The `scanout` service exists from V3 so the compositor can find it in V4; it carries no
/// requests yet (the ping is answered by the harness), so anything else is a no-op.
fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?ipc.Handle) usize {
_ = message;
_ = reply;
_ = sender;
_ = capability;
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
}

View File

@ -0,0 +1,105 @@
//! virtio 1.0 PCI transport the vendor capabilities in PCI config space that point at the
//! device's structures (common config, notify, ISR) in a BAR, the common-config register
//! block, and the split-virtqueue layout. `extern` structs matching the spec. Host-tested
//! for size. See docs/display-v2.md.
const std = @import("std");
/// PCI vendor-specific capability id (0x09) virtio 1.0 structures are advertised as these.
pub const pci_cap_vendor: u8 = 0x09;
/// virtio_pci_cap `cfg_type`: which structure a vendor capability points at.
pub const cfg_common: u8 = 1;
pub const cfg_notify: u8 = 2;
pub const cfg_isr: u8 = 3;
pub const cfg_device: u8 = 4;
pub const cfg_pci: u8 = 5;
/// virtio_pci_cap a vendor capability naming a structure at (bar, offset, length) within
/// a PCI BAR. Read straight out of config space.
pub const PciCap = extern struct {
cap_vndr: u8, // 0x09
cap_next: u8, // next capability's offset in config space (0 = end)
cap_len: u8,
cfg_type: u8, // cfg_common / cfg_notify / ...
bar: u8, // which BAR the structure lives in
padding: [3]u8,
offset: u32, // offset within the BAR
length: u32, // length of the structure
};
/// virtio_pci_notify_cap: a notify capability carries a multiplier after the base cap; the
/// per-queue notify address is `notify_base + queue_notify_off * notify_off_multiplier`.
pub const NotifyCap = extern struct {
cap: PciCap,
notify_off_multiplier: u32,
};
/// virtio_pci_common_cfg the common configuration register block (little-endian MMIO).
pub const CommonCfg = extern struct {
device_feature_select: u32,
device_feature: u32,
driver_feature_select: u32,
driver_feature: u32,
msix_config: u16,
num_queues: u16,
device_status: u8,
config_generation: u8,
queue_select: u16,
queue_size: u16,
queue_msix_vector: u16,
queue_enable: u16,
queue_notify_off: u16,
queue_desc: u64,
queue_driver: u64,
queue_device: u64,
};
/// device_status bits (written to `CommonCfg.device_status` during bring-up).
pub const status_acknowledge: u8 = 1;
pub const status_driver: u8 = 2;
pub const status_driver_ok: u8 = 4;
pub const status_features_ok: u8 = 8;
/// VIRTIO_F_VERSION_1 feature bit 32 (in the second 32-bit feature word). Required for a
/// modern device; we negotiate exactly this bit and nothing else.
pub const feature_version_1_word: u32 = 1; // device_feature_select value for bits 32..63
pub const feature_version_1_bit: u32 = 1 << 0; // bit 32 within that word
// --- split virtqueue -------------------------------------------------------
pub const Desc = extern struct {
addr: u64, // guest-physical
len: u32,
flags: u16,
next: u16,
};
pub const desc_flag_next: u16 = 1; // buffer continues in `next`
pub const desc_flag_write: u16 = 2; // device-writable (else driver-writable/device-readable)
/// The available ring's fixed header; a `[queue_size]u16` ring and a trailing `used_event`
/// u16 follow it in memory (laid out by the driver).
pub const AvailHdr = extern struct {
flags: u16,
idx: u16,
};
/// One entry of the used ring.
pub const UsedElem = extern struct {
id: u32,
len: u32,
};
/// The used ring's fixed header; a `[queue_size]UsedElem` ring and a trailing `avail_event`
/// u16 follow it.
pub const UsedHdr = extern struct {
flags: u16,
idx: u16,
};
test "virtio-pci struct sizes match the spec" {
try std.testing.expectEqual(@as(usize, 16), @sizeOf(PciCap));
try std.testing.expectEqual(@as(usize, 56), @sizeOf(CommonCfg));
try std.testing.expectEqual(@as(usize, 16), @sizeOf(Desc));
try std.testing.expectEqual(@as(usize, 8), @sizeOf(UsedElem));
}

View File

@ -103,6 +103,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void {
displayDemoTest(boot_information);
} else if (eql(case, "shm")) {
shmTest(boot_information);
} else if (eql(case, "virtio-gpu")) {
virtioGpuTest(boot_information);
} else if (eql(case, "clock")) {
clockTest();
} else if (eql(case, "smp")) {
@ -2409,6 +2411,50 @@ fn shmTest(boot_information: *const BootInformation) void {
while (true) scheduler.yield();
}
/// V3 the virtio-gpu driver, end to end (docs/display-v2.md). Boot the device-manager
/// stack (in its normal mode) so it discovers the virtio-gpu PCI function present because
/// the harness boots this case with QEMU's `-device virtio-gpu-pci` and spawns the driver.
/// The driver claims the device, brings up the control virtqueue, creates a 2D scanout,
/// paints a known pattern, flushes it, and waits for the device's used-ring ack. Its serial
/// heartbeats `virtio-gpu: scanout WxH online` and `virtio-gpu: flush acked, pixel check
/// ok` are the harness's markers (it reads serial directly, like the display cases). The
/// used-ring ack is the device confirming it consumed the frame; the pixel read-back proves
/// the backing is CPU-visible RAM together the automated stand-in for "it's on screen".
fn virtioGpuTest(boot_information: *const BootInformation) void {
log("DANOS-TEST-BEGIN: virtio-gpu\n", .{});
if (boot_information.initial_ramdisk_len == 0) {
check("bootloader handed over an initial_ramdisk", false);
result();
return;
}
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
const rd = initial_ramdisk.Reader.init(image) orelse {
check("initial_ramdisk image is valid", false);
result();
return;
};
// Spawn device-manager in its normal mode: its initialise discovers the PCI host bridge
// from the kernel device tree, spawns pci-bus, and matches the virtio-gpu class triple to
// spawn our driver with the function's device id as argv[1].
process.setInitialRamdisk(image);
var manager: u32 = 0;
var i: u32 = 0;
while (i < rd.count) : (i += 1) {
const item = rd.entry(i) orelse continue;
if (!eql(item.name, "device-manager")) continue;
manager = process.spawnProcessSupervised(item.blob, 4, &.{"device-manager"}, scheduler.currentId(), null) catch 0;
break;
}
if (manager == 0) {
log("virtio-gpu: could not spawn device-manager\n", .{});
result();
return;
}
scheduler.setPriority(1); // below the manager and the driver it spawns, so they run
while (true) scheduler.yield();
}
/// Process arguments, end to end: spawn args-echo bare (its argv[0] is the
/// initial-ramdisk name). Instance 1 sees argc == 1 and respawns itself through
/// `system_spawn` with the extra arguments "alpha beta-42" the syscall argument

View File

@ -41,6 +41,15 @@ const xhci_pci_class: u64 = pci_class.ClassCode.pack(.{
.prog_if = @intFromEnum(pci_class.serial_bus.usb.ProgIf.xhci),
});
/// The PCI class triple of a virtio-gpu Display Controller / Other (0x80) / 0. The class
/// alone cannot tell it from any other display/other function, so the driver re-confirms
/// vendor 0x1AF4 / device 0x1050 from config space once spawned; this only gets it spawned.
const virtio_gpu_pci_class: u64 = pci_class.ClassCode.pack(.{
.base = @intFromEnum(pci_class.BaseClass.display),
.subclass = 0x80, // "Other" no named SubClass member (PCI convention)
.prog_if = 0,
});
/// The driver that serves a *reported* PCI function (M19.3: matching moved
/// from the boot snapshot to the bus reports), or null. A machine can carry
/// several identical controllers one driver instance per reported device,
@ -48,6 +57,7 @@ const xhci_pci_class: u64 = pci_class.ClassCode.pack(.{
fn pciDriverForIdentity(identity: u64) ?[]const u8 {
return switch (identity) {
xhci_pci_class => "usb-xhci-bus",
virtio_gpu_pci_class => "virtio-gpu",
else => null,
};
}

View File

@ -191,6 +191,15 @@ CASES = [
{"name": "shm",
"expect": r"shm: shared 4096 bytes ok",
"fail": r"shm: (shared FAILED|create failed|no server|call failed|map)|CPU EXCEPTION|KERNEL PANIC"},
# virtio-gpu driver (v2 V3): boot with an emulated virtio-gpu. The device-manager stack
# discovers the PCI function and spawns the driver, which brings up the control virtqueue,
# creates a 2D scanout resource backed by DMA memory, set_scanouts it, paints a test
# pattern, transfers + flushes it, and waits for the device's used-ring ack, then reads
# the backing back. `scanout WxH online` + `flush acked, pixel check ok` are the markers.
{"name": "virtio-gpu",
"qemu_extra": ["-device", "virtio-gpu-pci"],
"expect": r"virtio-gpu: scanout \d+x\d+ online[\s\S]*virtio-gpu: flush acked, pixel check ok",
"fail": r"virtio-gpu:.*(failed|not acked|mismatch|unable to claim|not a virtio-gpu|too small|no PCI capability|does not offer|rejected|missing common-config|not a mapped resource|could not spawn)|CPU EXCEPTION|KERNEL PANIC"},
# Monotonic clock (clock() syscall source): calibrated, advancing, never backwards.
{"name": "clock",
"expect": r"DANOS-TEST-RESULT: PASS",