fastfetch reported 3 seconds of uptime after 15 real ones, `time` claimed
3.12s for a 20-second command, and ping's one-second interval was closer to
six. One cause, not three: `ticks` was advancing at ~14.5 Hz instead of 100.
It was not the timer. virtio-gpu-pci shares IRQ 11 with the e1000 and
registered no interrupt handler at all. PCI INTx is level-triggered, so the
device held the line asserted, the kernel signalled EOI, and it fired again
immediately -- 72,000 interrupts a second, 83% of CPU0, starving the tick.
The e1000's handler read ICR as zero half a million times, correctly
answering "not mine", which is what made it look like a network-free problem.
Reading virtio's ISR byte clears it and drops the line. That is the fix.
irq_dispatch now measures each line's rate against the TSC -- not ticks,
which is the first casualty of a storm -- and names the attached handlers
once above 20000/s. It reproduces the original fault as a single line.
Found along the way, and fixed because they were real:
- lapicinit loaded TICR with xv6's uncalibrated 10000000, whose own comment
admitted it was never measured. calibrate_timers() now measures the LAPIC
and the TSC against the 8254 PIT. (It came back at 999.9 MHz, so this was
not the bug -- but the next machine is not obliged to be 1 GHz.)
- microdelay() was an empty function body. Every microdelay(1000) in the
xHCI reset path and the USB hub power-on sequence returned instantly.
- HZ is a constant in param.h now, not the literal 100 written out at a
dozen ticks-to-seconds conversions. USER_HZ is separate and deliberately
does not follow it: musl answers sysconf(_SC_CLK_TCK) with 100 always, so
times() and /proc/stat scale at the syscall boundary the way Linux does.
- TIOCGWINSZ answered a hardcoded 24x80. gfx_cols()/gfx_rows() have known
the real geometry all along; at 1024x768 the console is 128x48, so every
program that lays out a screen was formatting for the wrong one.
- TCFLSH fell through to the catch-all `return 0`, claiming to discard
input without discarding any.
uptime now advances 5.01 seconds per 5 real seconds. schfs-check passes.
Full investigation, including the two wrong hypotheses and the measurement
flaw that protected them, in docs/history/kernel.md section 13.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
537 lines
19 KiB
C
537 lines
19 KiB
C
// virtio-gpu, 2D. Written from the OASIS virtio 1.1 specification.
|
|
//
|
|
// This is the second implementation of the display class (include/display.h),
|
|
// and the first one that is a real GPU driver rather than a flat framebuffer.
|
|
// The difference that matters for a compositor: presenting is an explicit
|
|
// operation. With VBE you write pixels and hope the scanout is not looking;
|
|
// here you transfer a rectangle to the host and then flush it, and the host
|
|
// does not show a half-drawn frame.
|
|
//
|
|
// Bring-up is the standard virtio 1.0 sequence and the order is not negotiable:
|
|
// reset, ACKNOWLEDGE, DRIVER, read features, write features, FEATURES_OK, check
|
|
// FEATURES_OK stuck, set up queues, DRIVER_OK. A device that finds the sequence
|
|
// wrong is entitled to simply refuse, and it will not say why.
|
|
|
|
#include "types.h"
|
|
#include "defs.h"
|
|
#include "irq.h"
|
|
#include "param.h"
|
|
#include "x86.h"
|
|
#include "spinlock.h"
|
|
#include "sleeplock.h"
|
|
#include "fs.h"
|
|
#include "file.h"
|
|
#include "mmu.h"
|
|
#include "memlayout.h"
|
|
#include "pci.h"
|
|
#include "device.h"
|
|
#include "display.h"
|
|
#include "dma.h"
|
|
#include "virtio.h"
|
|
|
|
// One control queue is enough for 2D: there is also a cursor queue, which is
|
|
// only worth having once there is a hardware cursor to move.
|
|
#define CTRL_QUEUE 0
|
|
|
|
struct vgpu {
|
|
volatile uint8 *common; // common configuration structure
|
|
volatile uint8 *notify; // notification area
|
|
// The interrupt status byte. Reading it is what tells the device we have
|
|
// noticed, and therefore what makes it drop the PCI interrupt line.
|
|
volatile uint8 *isr;
|
|
uint32 notify_mult; // queue_notify_off multiplier
|
|
|
|
// The queue. Allocated as one contiguous block from the DMA arena, because
|
|
// the device reads it by physical address and kalloc's pages are adjacent
|
|
// only by chance.
|
|
struct vring_desc *desc;
|
|
struct vring_avail *avail;
|
|
struct vring_used *used;
|
|
uint16 qsize;
|
|
uint16 notify_off;
|
|
uint16 last_used;
|
|
uint16 next_desc;
|
|
|
|
// A command buffer and a response buffer, both in DMA memory, reused for
|
|
// every request. Only one command is ever in flight -- everything here is
|
|
// called from process context during setup or from present(), and a GPU
|
|
// command queue with one entry is not the bottleneck in a 2D compositor.
|
|
void *cmd;
|
|
void *resp;
|
|
|
|
int width, height;
|
|
void *fb; // the framebuffer we scan out from
|
|
uint32 fb_bytes;
|
|
int ready;
|
|
// Which PCI device this instance drives, so attached() can match the device
|
|
// the bus publishes against the instance probe() set up.
|
|
struct pci_dev *pdev;
|
|
};
|
|
|
|
// More than one, because two display adapters in a machine is ordinary --
|
|
// integrated plus discrete on real hardware, two -device virtio-gpu-pci under
|
|
// QEMU -- and this driver used to refuse the second outright.
|
|
#define VGPU_MAX 2
|
|
|
|
static struct vgpu gpus[VGPU_MAX];
|
|
|
|
// The instance behind a device, or 0. Every op goes through this rather than a
|
|
// file-static, which is what makes the second adapter possible at all. See
|
|
// include/netdev.h and the e1000 for the same arrangement on the network side.
|
|
static struct vgpu *
|
|
vgpu_of(struct device *d)
|
|
{
|
|
return d ? (struct vgpu *)d->priv : 0;
|
|
}
|
|
|
|
// --- MMIO accessors --------------------------------------------------------
|
|
//
|
|
// Explicit widths. The common configuration structure has 8-, 16- and 32-bit
|
|
// fields side by side, and a device is entitled to ignore an access made at the
|
|
// wrong width -- silently, with no error anywhere.
|
|
|
|
static inline uint8 r8 (volatile uint8 *p, int o) { return *(volatile uint8 *)(p + o); }
|
|
static inline uint16 r16(volatile uint8 *p, int o) { return *(volatile uint16 *)(p + o); }
|
|
static inline uint32 r32(volatile uint8 *p, int o) { return *(volatile uint32 *)(p + o); }
|
|
static inline void w8 (volatile uint8 *p, int o, uint8 v) { *(volatile uint8 *)(p + o) = v; }
|
|
static inline void w16(volatile uint8 *p, int o, uint16 v) { *(volatile uint16 *)(p + o) = v; }
|
|
static inline void w32(volatile uint8 *p, int o, uint32 v) { *(volatile uint32 *)(p + o) = v; }
|
|
|
|
static void
|
|
w64(volatile uint8 *p, int o, uint64 v)
|
|
{
|
|
// Two 32-bit halves, low first. The specification defines these fields as a
|
|
// pair of 32-bit registers precisely so that a 32-bit driver can write them,
|
|
// and some implementations latch on the high write.
|
|
w32(p, o, (uint32)v);
|
|
w32(p, o + 4, (uint32)(v >> 32));
|
|
}
|
|
|
|
// --- capability walk -------------------------------------------------------
|
|
|
|
// Find a virtio configuration structure and map it.
|
|
//
|
|
// Each lives at an offset inside one of the device's BARs, and which BAR is not
|
|
// fixed -- the device says so in a vendor-specific PCI capability. Assuming BAR
|
|
// 0 works on QEMU today and is exactly the kind of assumption that stops
|
|
// working on the next device.
|
|
static volatile uint8 *
|
|
find_cfg(struct pci_dev *d, int want, uint32 *out_mult)
|
|
{
|
|
uint8 off = pci_read8(d->bus, d->slot, d->func, 0x34) & 0xFC;
|
|
|
|
while (off) {
|
|
uint8 id = pci_read8(d->bus, d->slot, d->func, off);
|
|
uint8 next = pci_read8(d->bus, d->slot, d->func, off + 1);
|
|
|
|
if (id == PCI_CAP_ID_VNDR) {
|
|
uint8 type = pci_read8(d->bus, d->slot, d->func, off + 3);
|
|
uint8 bar = pci_read8(d->bus, d->slot, d->func, off + 4);
|
|
uint32 o = pci_read32(d->bus, d->slot, d->func, off + 8);
|
|
|
|
if (type == want && bar < 6 && d->bar[bar].base) {
|
|
if (out_mult) // only the notify capability has this trailing field
|
|
*out_mult = pci_read32(d->bus, d->slot, d->func, off + 16);
|
|
|
|
// ioremap, not P2V. OVMF puts this device's 64-bit BAR at about 768 GB,
|
|
// far outside the 4 GB the direct map covers, and P2V there produces an
|
|
// address with nothing behind it -- the first register write took a
|
|
// page fault, then a double fault, then a triple fault and a reboot
|
|
// loop. SeaBIOS places the BAR low, which is why the BIOS path never
|
|
// showed it.
|
|
//
|
|
// Map at least as far as the capability's own offset reaches: the BAR
|
|
// size is what the device reports, and a mapping shorter than the
|
|
// offset we are about to add would fault just as mysteriously.
|
|
uint64 need = (uint64)o + PGSIZE;
|
|
uint64 len = d->bar[bar].size > need ? d->bar[bar].size : need;
|
|
volatile uint8 *va = (volatile uint8 *)ioremap(d->bar[bar].base, len);
|
|
if (!va)
|
|
return 0;
|
|
return va + o;
|
|
}
|
|
}
|
|
off = next;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// --- virtqueue -------------------------------------------------------------
|
|
|
|
static int
|
|
queue_setup(struct vgpu *g)
|
|
{
|
|
w16(g->common, VCOMMON_Q_SELECT, CTRL_QUEUE);
|
|
|
|
uint16 qsize = r16(g->common, VCOMMON_Q_SIZE);
|
|
if (qsize == 0)
|
|
return -1;
|
|
if (qsize > 64)
|
|
qsize = 64; // more than we will ever have outstanding
|
|
g->qsize = qsize;
|
|
g->notify_off = r16(g->common, VCOMMON_Q_NOTIFY_OFF);
|
|
|
|
// The three rings, each aligned as the specification requires: 16 for the
|
|
// descriptor table, 2 for available, 4 for used. Allocated as one block and
|
|
// carved up, so there is one physical address to reason about.
|
|
uint32 desc_sz = qsize * sizeof(struct vring_desc);
|
|
uint32 avail_sz = 6 + 2 * qsize;
|
|
uint32 used_sz = 6 + 8 * qsize;
|
|
|
|
uint8 *mem = dma_alloc(4096 * 3, 4096);
|
|
if (!mem)
|
|
return -1;
|
|
memset(mem, 0, 4096 * 3);
|
|
|
|
g->desc = (struct vring_desc *) mem;
|
|
g->avail = (struct vring_avail *)(mem + ((desc_sz + 1) & ~1U));
|
|
g->used = (struct vring_used *)(mem + 4096); // its own page, 4-aligned
|
|
(void)avail_sz; (void)used_sz;
|
|
|
|
w64(g->common, VCOMMON_Q_DESC, dma_phys(g->desc));
|
|
w64(g->common, VCOMMON_Q_DRIVER, dma_phys(g->avail));
|
|
w64(g->common, VCOMMON_Q_DEVICE, dma_phys(g->used));
|
|
w16(g->common, VCOMMON_Q_ENABLE, 1);
|
|
return 0;
|
|
}
|
|
|
|
// Submit one command and wait for the reply.
|
|
//
|
|
// Two descriptors: the request, which the device reads, and the response, which
|
|
// it writes. Polled rather than interrupt-driven -- every caller is either boot
|
|
// setup or present(), both of which want the answer before continuing, and an
|
|
// interrupt would only add a sleep and a wakeup to a wait of a few microseconds.
|
|
static int
|
|
cmd(struct vgpu *g, uint32 cmdlen, uint32 resplen)
|
|
{
|
|
uint16 head = g->next_desc;
|
|
uint16 second = (uint16)((head + 1) % g->qsize);
|
|
|
|
g->desc[head].addr = dma_phys(g->cmd);
|
|
g->desc[head].len = cmdlen;
|
|
g->desc[head].flags = VRING_DESC_F_NEXT;
|
|
g->desc[head].next = second;
|
|
|
|
g->desc[second].addr = dma_phys(g->resp);
|
|
g->desc[second].len = resplen;
|
|
g->desc[second].flags = VRING_DESC_F_WRITE;
|
|
g->desc[second].next = 0;
|
|
|
|
g->next_desc = (uint16)((second + 1) % g->qsize);
|
|
|
|
g->avail->ring[g->avail->idx % g->qsize] = head;
|
|
__sync_synchronize(); // ring entry visible before idx
|
|
g->avail->idx++;
|
|
__sync_synchronize();
|
|
|
|
// Notify. The address is the notify region plus this queue's offset scaled by
|
|
// the multiplier the device advertised; the value written is the queue index.
|
|
*(volatile uint16 *)(g->notify + g->notify_off * g->notify_mult) = CTRL_QUEUE;
|
|
|
|
// Bounded, because a device that never answers must not hang the boot. At
|
|
// QEMU's speed the reply is there almost immediately; on a wedged device this
|
|
// returns an error that a caller can report.
|
|
for (int i = 0; i < 10000000; i++) {
|
|
if (g->used->idx != g->last_used) {
|
|
g->last_used = g->used->idx;
|
|
__sync_synchronize();
|
|
struct virtio_gpu_ctrl_hdr *h = g->resp;
|
|
return (h->type == VIRTIO_GPU_RESP_OK_NODATA ||
|
|
h->type == VIRTIO_GPU_RESP_OK_DISPLAY_INFO) ? 0 : -1;
|
|
}
|
|
}
|
|
cprintf("virtio-gpu: device did not answer\n");
|
|
return -1;
|
|
}
|
|
|
|
static void
|
|
hdr_init(void *p, uint32 type)
|
|
{
|
|
struct virtio_gpu_ctrl_hdr *h = p;
|
|
memset(h, 0, sizeof(*h));
|
|
h->type = type;
|
|
}
|
|
|
|
// --- the 2D sequence -------------------------------------------------------
|
|
|
|
static int
|
|
setup_scanout(struct vgpu *g)
|
|
{
|
|
// What resolution is the display actually at? Asking is the point of
|
|
// GET_DISPLAY_INFO; guessing 1024x768 is how a driver ends up working on
|
|
// exactly one host configuration.
|
|
hdr_init(g->cmd, VIRTIO_GPU_CMD_GET_DISPLAY_INFO);
|
|
if (cmd(g, sizeof(struct virtio_gpu_ctrl_hdr),
|
|
sizeof(struct virtio_gpu_resp_display_info)) != 0)
|
|
return -1;
|
|
|
|
struct virtio_gpu_resp_display_info *info = g->resp;
|
|
if (!info->pmodes[0].enabled || info->pmodes[0].r.width == 0) {
|
|
cprintf("virtio-gpu: no enabled scanout\n");
|
|
return -1;
|
|
}
|
|
g->width = (int)info->pmodes[0].r.width;
|
|
g->height = (int)info->pmodes[0].r.height;
|
|
|
|
// A host resource to scan out from.
|
|
{
|
|
struct virtio_gpu_resource_create_2d *c = g->cmd;
|
|
hdr_init(c, VIRTIO_GPU_CMD_RESOURCE_CREATE_2D);
|
|
c->resource_id = 1;
|
|
c->format = VIRTIO_GPU_FORMAT_B8G8R8X8_UNORM;
|
|
c->width = g->width;
|
|
c->height = g->height;
|
|
if (cmd(g, sizeof(*c), sizeof(struct virtio_gpu_ctrl_hdr)) != 0)
|
|
return -1;
|
|
}
|
|
|
|
// Guest memory to back it. This is the buffer the rest of the kernel draws
|
|
// into; the device never reads it directly -- TRANSFER_TO_HOST_2D copies from
|
|
// here into the host resource, which is what makes presenting explicit.
|
|
g->fb_bytes = (uint32)g->width * g->height * 4;
|
|
g->fb = dma_alloc(g->fb_bytes, 4096);
|
|
if (!g->fb) {
|
|
cprintf("virtio-gpu: no room for a %d KB framebuffer\n", g->fb_bytes / 1024);
|
|
return -1;
|
|
}
|
|
memset(g->fb, 0, g->fb_bytes);
|
|
|
|
{
|
|
struct virtio_gpu_resource_attach_backing *a = g->cmd;
|
|
hdr_init(a, VIRTIO_GPU_CMD_RESOURCE_ATTACH_BACKING);
|
|
a->resource_id = 1;
|
|
a->nr_entries = 1;
|
|
struct virtio_gpu_mem_entry *e = (struct virtio_gpu_mem_entry *)(a + 1);
|
|
e->addr = dma_phys(g->fb);
|
|
e->length = g->fb_bytes;
|
|
e->padding = 0;
|
|
if (cmd(g, sizeof(*a) + sizeof(*e), sizeof(struct virtio_gpu_ctrl_hdr)) != 0)
|
|
return -1;
|
|
}
|
|
|
|
// Point scanout 0 at it.
|
|
{
|
|
struct virtio_gpu_set_scanout *s = g->cmd;
|
|
hdr_init(s, VIRTIO_GPU_CMD_SET_SCANOUT);
|
|
s->r.x = s->r.y = 0;
|
|
s->r.width = g->width;
|
|
s->r.height = g->height;
|
|
s->scanout_id = 0;
|
|
s->resource_id = 1;
|
|
if (cmd(g, sizeof(*s), sizeof(struct virtio_gpu_ctrl_hdr)) != 0)
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// --- display class ---------------------------------------------------------
|
|
|
|
static void
|
|
vgpu_mode(struct device *d, struct display_mode *m)
|
|
{
|
|
struct vgpu *g = vgpu_of(d);
|
|
|
|
if (!g)
|
|
return;
|
|
m->width = g->width;
|
|
m->height = g->height;
|
|
m->bpp = 32;
|
|
m->stride = (uint32)g->width * 4;
|
|
}
|
|
|
|
static void *
|
|
vgpu_acquire(struct device *d, uint32 *stride_out)
|
|
{
|
|
struct vgpu *g = vgpu_of(d);
|
|
|
|
if (!g)
|
|
return 0;
|
|
if (stride_out)
|
|
*stride_out = (uint32)g->width * 4;
|
|
// No separate back buffer: the guest-side framebuffer already *is* one. The
|
|
// host cannot see it until TRANSFER_TO_HOST_2D, so drawing into it is
|
|
// inherently off-screen -- which is the whole reason this device is better to
|
|
// composite against than a raw VBE aperture.
|
|
return g->ready ? g->fb : 0;
|
|
}
|
|
|
|
static void
|
|
vgpu_present(struct device *d, int x, int y, int w, int h)
|
|
{
|
|
struct vgpu *g = vgpu_of(d);
|
|
|
|
if (!g || !g->ready)
|
|
return;
|
|
if (x < 0) { w += x; x = 0; }
|
|
if (y < 0) { h += y; y = 0; }
|
|
if (x + w > g->width) w = g->width - x;
|
|
if (y + h > g->height) h = g->height - y;
|
|
if (w <= 0 || h <= 0)
|
|
return;
|
|
|
|
{
|
|
struct virtio_gpu_transfer_to_host_2d *t = g->cmd;
|
|
hdr_init(t, VIRTIO_GPU_CMD_TRANSFER_TO_HOST_2D);
|
|
t->r.x = x; t->r.y = y; t->r.width = w; t->r.height = h;
|
|
t->offset = (uint64)y * g->width * 4 + (uint64)x * 4;
|
|
t->resource_id = 1;
|
|
if (cmd(g, sizeof(*t), sizeof(struct virtio_gpu_ctrl_hdr)) != 0)
|
|
return;
|
|
}
|
|
{
|
|
struct virtio_gpu_resource_flush *f = g->cmd;
|
|
hdr_init(f, VIRTIO_GPU_CMD_RESOURCE_FLUSH);
|
|
f->r.x = x; f->r.y = y; f->r.width = w; f->r.height = h;
|
|
f->resource_id = 1;
|
|
cmd(g, sizeof(*f), sizeof(struct virtio_gpu_ctrl_hdr));
|
|
}
|
|
}
|
|
|
|
// No wait_vblank. The device presents whole rectangles atomically, so there is
|
|
// nothing to synchronise against -- and a null op is more honest than one that
|
|
// spins and achieves nothing. display_wait_vblank() handles the absence.
|
|
const struct display_ops virtio_gpu_display_ops = {
|
|
.mode = vgpu_mode,
|
|
.acquire = vgpu_acquire,
|
|
.present = vgpu_present,
|
|
.wait_vblank = 0,
|
|
};
|
|
|
|
// --- probe -----------------------------------------------------------------
|
|
|
|
// Acknowledge the device's interrupt.
|
|
//
|
|
// This driver polls for command completion and wants no notifications at all,
|
|
// which is why nothing here reacts to the cause -- but "wants none" and "gets
|
|
// none" are different things, and a PCI interrupt line is level-triggered.
|
|
// The device raises INTx, the CPU takes the interrupt, the kernel signals EOI,
|
|
// and if nobody has cleared the device's interrupt status the line is still
|
|
// asserted, so it fires again immediately. Forever.
|
|
//
|
|
// That is not hypothetical. With no handler registered here at all, this
|
|
// device shared IRQ 11 with the e1000 under QEMU and storme at roughly 72,000
|
|
// interrupts a second, eating 83% of CPU0. The visible symptom was nowhere
|
|
// near the graphics driver: the timer interrupt was starved, so `ticks`
|
|
// advanced at about 14 Hz instead of 100, and every duration in the system --
|
|
// uptime, `time`, sleep, ping's one-second interval -- came out ~7x short.
|
|
// See docs/history/kernel.md.
|
|
//
|
|
// Reading the ISR byte clears it and deasserts the line. That single read is
|
|
// the entire handler, and it is not optional.
|
|
static void
|
|
vgpu_intr(void *arg)
|
|
{
|
|
struct vgpu *g = arg;
|
|
if (g && g->isr)
|
|
(void)*g->isr;
|
|
}
|
|
|
|
static int
|
|
vgpu_probe(struct pci_dev *d)
|
|
{
|
|
struct vgpu *g = 0;
|
|
|
|
for (int i = 0; i < VGPU_MAX; i++)
|
|
if (!gpus[i].ready) {
|
|
g = &gpus[i];
|
|
break;
|
|
}
|
|
if (!g)
|
|
return -1; // every slot already drives an adapter
|
|
g->pdev = d;
|
|
|
|
pci_enable(d, 0x0006); // memory space + bus master
|
|
|
|
g->common = find_cfg(d, VIRTIO_PCI_CAP_COMMON_CFG, 0);
|
|
g->notify = find_cfg(d, VIRTIO_PCI_CAP_NOTIFY_CFG, &g->notify_mult);
|
|
g->isr = find_cfg(d, VIRTIO_PCI_CAP_ISR_CFG, 0);
|
|
if (!g->common || !g->notify) {
|
|
cprintf("virtio-gpu: no modern virtio capabilities; not a 1.0 device\n");
|
|
return -1;
|
|
}
|
|
|
|
// Reset, then the status handshake in the specified order.
|
|
w8(g->common, VCOMMON_STATUS, 0);
|
|
while (r8(g->common, VCOMMON_STATUS) != 0)
|
|
;
|
|
w8(g->common, VCOMMON_STATUS, VSTATUS_ACK);
|
|
w8(g->common, VCOMMON_STATUS, VSTATUS_ACK | VSTATUS_DRIVER);
|
|
|
|
// Accept VIRTIO_F_VERSION_1 and nothing else. Feature bit 32 lives in the
|
|
// second 32-bit window, which is what the select registers are for.
|
|
w32(g->common, VCOMMON_DFEAT_SEL, 1);
|
|
uint32 hi = r32(g->common, VCOMMON_DFEAT);
|
|
if (!(hi & (1u << (VIRTIO_F_VERSION_1 - 32)))) {
|
|
cprintf("virtio-gpu: device is not virtio 1.0\n");
|
|
w8(g->common, VCOMMON_STATUS, VSTATUS_FAILED);
|
|
return -1;
|
|
}
|
|
w32(g->common, VCOMMON_GFEAT_SEL, 0);
|
|
w32(g->common, VCOMMON_GFEAT, 0);
|
|
w32(g->common, VCOMMON_GFEAT_SEL, 1);
|
|
w32(g->common, VCOMMON_GFEAT, 1u << (VIRTIO_F_VERSION_1 - 32));
|
|
|
|
w8(g->common, VCOMMON_STATUS, VSTATUS_ACK | VSTATUS_DRIVER | VSTATUS_FEATURES_OK);
|
|
if (!(r8(g->common, VCOMMON_STATUS) & VSTATUS_FEATURES_OK)) {
|
|
// The device is allowed to clear this bit to reject the feature set, and
|
|
// this readback is the only way it ever tells you.
|
|
cprintf("virtio-gpu: device refused our features\n");
|
|
w8(g->common, VCOMMON_STATUS, VSTATUS_FAILED);
|
|
return -1;
|
|
}
|
|
|
|
g->cmd = dma_alloc(4096, 16);
|
|
g->resp = dma_alloc(4096, 16);
|
|
if (!g->cmd || !g->resp || queue_setup(g) != 0) {
|
|
cprintf("virtio-gpu: queue setup failed\n");
|
|
w8(g->common, VCOMMON_STATUS, VSTATUS_FAILED);
|
|
return -1;
|
|
}
|
|
|
|
w8(g->common, VCOMMON_STATUS,
|
|
VSTATUS_ACK | VSTATUS_DRIVER | VSTATUS_FEATURES_OK | VSTATUS_DRIVER_OK);
|
|
|
|
if (setup_scanout(g) != 0) {
|
|
w8(g->common, VCOMMON_STATUS, VSTATUS_FAILED);
|
|
return -1;
|
|
}
|
|
|
|
// Level-triggered and active low, like every other PCI INTx line -- see
|
|
// vgpu_intr for what happens when this registration is missing.
|
|
if (g->isr)
|
|
irq_register(d->irq, vgpu_intr, g, "virtio-gpu", 1);
|
|
else
|
|
cprintf("virtio-gpu: no ISR capability; leaving irq %d unclaimed\n", d->irq);
|
|
|
|
g->ready = 1;
|
|
cprintf("virtio-gpu: %dx%d, %d KB framebuffer, queue %d, irq %d\n",
|
|
g->width, g->height, g->fb_bytes / 1024, g->qsize, d->irq);
|
|
return 0;
|
|
}
|
|
|
|
static const struct pci_match virtio_gpu_match[] = {
|
|
{ VIRTIO_VENDOR, VIRTIO_DEV_GPU, 0, 0 },
|
|
{ 0, 0, 0, 0 },
|
|
};
|
|
|
|
// Hang the instance on the device the bus has just published. probe() runs
|
|
// before device_add(), so there is nothing to attach it to in there -- see the
|
|
// comment on `attached` in include/pci.h.
|
|
static void
|
|
vgpu_attached(struct pci_dev *d, struct device *dev)
|
|
{
|
|
for (int i = 0; i < VGPU_MAX; i++)
|
|
if (gpus[i].ready && gpus[i].pdev == d) {
|
|
dev->priv = &gpus[i];
|
|
return;
|
|
}
|
|
}
|
|
|
|
const struct pci_driver virtio_gpu_driver = {
|
|
.name = "virtio-gpu",
|
|
.match = virtio_gpu_match,
|
|
.ops = &virtio_gpu_display_ops,
|
|
.probe = vgpu_probe,
|
|
.attached = vgpu_attached,
|
|
};
|