An x86-64 operating system built from xv6, now largely rewritten: SMP, a copy-on-write root filesystem (SchFS), PCI/USB/virtio drivers, a TTF rasteriser, and a Linux-numbered syscall ABI. No history before this point -- the tree was never under version control. This commit exists so the rename that follows has something to undo to.
435 lines
15 KiB
C
435 lines
15 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 "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
|
|
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;
|
|
};
|
|
|
|
static struct vgpu gpu;
|
|
|
|
// --- 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);
|
|
return (volatile uint8 *)P2V(d->bar[bar].base + 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)
|
|
{
|
|
m->width = gpu.width;
|
|
m->height = gpu.height;
|
|
m->bpp = 32;
|
|
m->stride = (uint32)gpu.width * 4;
|
|
}
|
|
|
|
static void *
|
|
vgpu_acquire(struct device *d, uint32 *stride_out)
|
|
{
|
|
if (stride_out)
|
|
*stride_out = (uint32)gpu.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 gpu.ready ? gpu.fb : 0;
|
|
}
|
|
|
|
static void
|
|
vgpu_present(struct device *d, int x, int y, int w, int h)
|
|
{
|
|
if (!gpu.ready)
|
|
return;
|
|
if (x < 0) { w += x; x = 0; }
|
|
if (y < 0) { h += y; y = 0; }
|
|
if (x + w > gpu.width) w = gpu.width - x;
|
|
if (y + h > gpu.height) h = gpu.height - y;
|
|
if (w <= 0 || h <= 0)
|
|
return;
|
|
|
|
{
|
|
struct virtio_gpu_transfer_to_host_2d *t = gpu.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 * gpu.width * 4 + (uint64)x * 4;
|
|
t->resource_id = 1;
|
|
if (cmd(&gpu, sizeof(*t), sizeof(struct virtio_gpu_ctrl_hdr)) != 0)
|
|
return;
|
|
}
|
|
{
|
|
struct virtio_gpu_resource_flush *f = gpu.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(&gpu, 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 -----------------------------------------------------------------
|
|
|
|
static int
|
|
vgpu_probe(struct pci_dev *d)
|
|
{
|
|
struct vgpu *g = &gpu;
|
|
|
|
if (g->ready)
|
|
return -1; // one display is enough for now
|
|
|
|
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);
|
|
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;
|
|
}
|
|
|
|
g->ready = 1;
|
|
cprintf("virtio-gpu: %dx%d, %d KB framebuffer, queue %d\n",
|
|
g->width, g->height, g->fb_bytes / 1024, g->qsize);
|
|
return 0;
|
|
}
|
|
|
|
static const struct pci_match virtio_gpu_match[] = {
|
|
{ VIRTIO_VENDOR, VIRTIO_DEV_GPU, 0, 0 },
|
|
{ 0, 0, 0, 0 },
|
|
};
|
|
|
|
const struct pci_driver virtio_gpu_driver = {
|
|
.name = "virtio-gpu",
|
|
.match = virtio_gpu_match,
|
|
.ops = &virtio_gpu_display_ops,
|
|
.probe = vgpu_probe,
|
|
};
|