SchOS/kernel/drivers/gpu/virtio_gpu/virtio_gpu.c
Nexoniarz d1f50c4da3 hybrid: the driver is killed, and the driver comes back
H2 of docs/plan/hybrid.md -- the milestone the plan itself calls the one
that proves the whole idea, and the one that cannot be skipped.

    # cat /proc/schinit
    1 4 11 os.schos.vgpud
    # kill 4
    schinitd: restarting os.schos.vgpud
    vgpud: claimed
    vgpud: pid 8 drew a test pattern from userspace

The test pattern ends in a twelve-block tally of the drawing process's
pid, because a second run that redraws an identical picture is
indistinguishable from a first run that never stopped. Read off the
virtio-gpu scanout across four generations it says 4, 8, 10, 12, and the
DMA arena hands back the same physical address every time.

What it took:

* A halt the kernel can apply to a device it does not understand.
  Clearing PCI_CMD_BUS_MASTER is the only one there is, and it has to
  happen before the DMA goes back to the arena -- a dead driver's device
  keeps reading a framebuffer the kernel is about to give to somebody
  else. Claim sets the bit, release clears it; pcitest shows it in a bm
  column, with a third state so a failed read is never printed as off.

* A refusal. vgpu_release() asks vga_fb_restore() whether there is a
  framebuffer to put the console back on, and declines to hand over the
  device if there is not. The rule that the kernel keeps a minimal
  driver for everything on the path to a shell is worth writing down
  only if something enforces it.

* The DMA arena not being owned by two allocators at once. This is the
  bug that stopped H2: freevm() frees every mapped page below PHYSTOP
  and the arena is the top 16 MB of exactly that range, so a DMA block
  mapped into a driver went onto the page allocator's free list when the
  driver died, while pciuser_release_all() handed the same pages back to
  the arena. The restarted vgpud asked for its framebuffer, got pages
  kalloc() had already given out -- including its own page tables -- and
  dma_alloc()'s zeroing wiped them, so it died inside the ioctl on the
  return from a syscall whose stack had stopped existing. do_dma() now
  records a VMA with no_free set, the way shared memory does.

* One exit status. KeepAlive must restart a driver that crashed and must
  not restart one that has looked at the machine and found its device
  absent: on hardware with no virtio-gpu that was fifteen spawns in
  forty-five seconds, each printing a line to the console, for ever.
  SCHINIT_EXIT_NORESTART is 78, sysexits.h's EX_CONFIG. It is not a new
  manifest key -- the wait status has always travelled from PID 1 to the
  supervisor, and note_exit() simply stopped discarding it.

The milestone's third clause, "the compositor reattaches", is not done
and is not claimed: there is no compositor running. That is recorded in
the plan as a Schcom milestone rather than counted here.

Closes the register row "A released PCI device" in docs/plan/1.0.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
2026-09-14 00:05:43 +02:00

610 lines
22 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;
void *ring; // the three vrings, one block, for dma_free
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);
g->ring = mem;
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;
}
// Hand the device to a userspace driver.
//
// docs/plan/hybrid.md's first eviction, and the reason it is this device: the
// boundary already exists (struct display_ops), it has exactly one consumer,
// and there is a working fallback underneath. So letting go is safe in a way
// it would not be for the disk the root filesystem is on.
//
// What "letting go" has to mean, in order:
//
// 1. stop drawing -- put the VBE framebuffer back on screen *first*, so
// there is never a moment where gfx points at hardware nobody drives
// 2. reset the device, so the next driver starts from a known state rather
// than inheriting a configured queue it did not set up
// 3. forget the instance, so a later probe or a second claim cannot find
// half-initialised state
static int
vgpu_release(struct pci_dev *d)
{
struct vgpu *g = 0;
for (int i = 0; i < VGPU_MAX; i++)
if (gpus[i].ready && gpus[i].pdev == d) { g = &gpus[i]; break; }
if (!g)
return -1;
// The rule from docs/plan/hybrid.md, enforced rather than described: **the
// kernel keeps a minimal driver for everything on the path to a shell.** If
// there is no framebuffer to fall back to, this device is the only screen the
// machine has, and handing it to a process that might crash would make a
// failed driver indistinguishable from a dead machine.
//
// Refusing here is what lets the userspace driver be started unconditionally
// at boot: on hardware where it is safe it takes the GPU, and on hardware
// where it is not it is told no and says so, rather than each machine needing
// to be configured for its own display topology.
if (!vga_fb_restore()) {
cprintf("virtio-gpu: refusing to release -- it is the only display, and "
"there is no framebuffer to fall back to\n");
return -1;
}
// Status 0 is the virtio reset. The spec requires a driver to wait for the
// device to acknowledge by reading the register back as zero.
if (g->common) {
w8(g->common, VCOMMON_STATUS, 0);
for (int spin = 0; spin < 1000000 && r8(g->common, VCOMMON_STATUS); spin++)
;
}
if (g->isr)
irq_unregister(d->irq, vgpu_intr, g);
// Give the arena its memory back.
//
// Releasing the *device* without releasing its memory is half a handover:
// the DMA arena is 16 MB and contiguous, a framebuffer at this resolution is
// 4 MB of it, and the process taking over needs one of its own. Keeping ours
// means the new driver asks for a framebuffer and is told there is no room,
// which is what happened the first time this was tried.
if (g->fb) { dma_free(g->fb, g->fb_bytes); g->fb = 0; }
if (g->ring) { dma_free(g->ring, 4096 * 3); g->ring = 0; }
if (g->cmd) { dma_free(g->cmd, 4096); g->cmd = 0; }
if (g->resp) { dma_free(g->resp, 4096); g->resp = 0; }
g->ready = 0;
g->pdev = 0;
cprintf("virtio-gpu: released to userspace; screen is the VBE framebuffer\n");
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,
.release = vgpu_release,
};