556 lines
26 KiB
Zig
556 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 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,
|
||
});
|
||
}
|