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
586 lines
21 KiB
C
586 lines
21 KiB
C
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
// vgpud -- the virtio-gpu driver, outside the kernel.
|
|
//
|
|
// docs/plan/hybrid.md's first eviction. The same device the kernel drives in
|
|
// kernel/drivers/gpu/virtio_gpu/, driven instead by a process that asks the
|
|
// kernel for four things and nothing else:
|
|
//
|
|
// PCIIOC_CLAIM take the device (the kernel driver is asked to let go)
|
|
// PCIIOC_CFG read config space, to find the vendor capabilities
|
|
// mmap(BAR) reach the registers
|
|
// PCIIOC_DMA memory the device can be pointed at
|
|
//
|
|
// The point is not that this is better than the in-kernel driver -- it is the
|
|
// same code in a worse language for the job. The point is that it is a real
|
|
// driver rather than a test program, so the interface has something to be
|
|
// judged by, and what it asks for is what a foreign driver would ask for too.
|
|
//
|
|
// Note what is *not* here: no syscall knows this is a GPU. The kernel handed
|
|
// over a PCI device and this decided it was a GPU, which is the property that
|
|
// makes the interface general rather than a hole shaped like one thing.
|
|
|
|
#include "types.h"
|
|
#include "user.h"
|
|
#include "fcntl.h"
|
|
#include "posix.h"
|
|
#include "pciuser.h"
|
|
#include "virtio.h"
|
|
#include "schinit.h"
|
|
|
|
// --- small output, since lib/ has no printf ---------------------------------
|
|
|
|
static void
|
|
puts(const char *s)
|
|
{
|
|
int n = 0;
|
|
while (s[n]) n++;
|
|
write(1, (void *)s, n);
|
|
}
|
|
|
|
static void
|
|
putn(long long v)
|
|
{
|
|
char b[24];
|
|
int i = 0;
|
|
if (v < 0) { write(1, "-", 1); v = -v; }
|
|
if (v == 0) { write(1, "0", 1); return; }
|
|
while (v > 0) { b[i++] = '0' + (int)(v % 10); v /= 10; }
|
|
while (i-- > 0) write(1, &b[i], 1);
|
|
}
|
|
|
|
static void
|
|
puthex(unsigned long long v, int digits)
|
|
{
|
|
static const char h[] = "0123456789abcdef";
|
|
char b[17];
|
|
for (int i = digits - 1; i >= 0; i--) { b[i] = h[v & 0xF]; v >>= 4; }
|
|
write(1, b, digits);
|
|
}
|
|
|
|
static void
|
|
die(const char *why)
|
|
{
|
|
puts("vgpud: "); puts(why); puts("\n");
|
|
exit();
|
|
}
|
|
|
|
// Not a failure: a report. There is no virtio-gpu here, or the kernel will not
|
|
// give it up, and neither of those changes by trying again -- so this tells the
|
|
// supervisor to stop, rather than being restarted every three seconds for the
|
|
// rest of the uptime. See SCHINIT_EXIT_NORESTART in include/schinit.h.
|
|
static void
|
|
nothing_to_do(const char *why)
|
|
{
|
|
puts("vgpud: "); puts(why); puts("\n");
|
|
exit_group(SCHINIT_EXIT_NORESTART);
|
|
}
|
|
|
|
// --- register access --------------------------------------------------------
|
|
|
|
static inline unsigned char r8 (volatile unsigned char *p, int o) { return p[o]; }
|
|
static inline void w8 (volatile unsigned char *p, int o, unsigned char v) { p[o] = v; }
|
|
static inline unsigned short r16(volatile unsigned char *p, int o) { return *(volatile unsigned short *)(p + o); }
|
|
static inline void w16(volatile unsigned char *p, int o, unsigned short v) { *(volatile unsigned short *)(p + o) = v; }
|
|
static inline unsigned int r32(volatile unsigned char *p, int o) { return *(volatile unsigned int *)(p + o); }
|
|
static inline void w32(volatile unsigned char *p, int o, unsigned int v) { *(volatile unsigned int *)(p + o) = v; }
|
|
static inline void w64(volatile unsigned char *p, int o, unsigned long long v)
|
|
{
|
|
// Two 32-bit writes, low half first. The virtio spec requires it and the
|
|
// device latches on the high write; a single 64-bit store is a different
|
|
// bus transaction and some implementations simply drop it.
|
|
w32(p, o, (unsigned int)v);
|
|
w32(p, o + 4, (unsigned int)(v >> 32));
|
|
}
|
|
|
|
// --- the device -------------------------------------------------------------
|
|
|
|
static int pcifd;
|
|
static int index_;
|
|
|
|
static volatile unsigned char *common;
|
|
static volatile unsigned char *notify;
|
|
static volatile unsigned char *isr;
|
|
static unsigned int notify_mult;
|
|
|
|
static struct pciuser_info info;
|
|
|
|
static unsigned int
|
|
cfg32(unsigned int off)
|
|
{
|
|
struct pciuser_cfg c;
|
|
c.index = (unsigned int)index_;
|
|
c.offset = off & ~3u;
|
|
c.value = 0;
|
|
if (ioctl(pcifd, PCIIOC_CFG, &c) < 0)
|
|
die("config read failed");
|
|
return c.value;
|
|
}
|
|
|
|
static unsigned char
|
|
cfg8(unsigned int off)
|
|
{
|
|
return (unsigned char)(cfg32(off) >> ((off & 3) * 8));
|
|
}
|
|
|
|
// Find a virtio configuration structure.
|
|
//
|
|
// A virtio 1.0 device does not put its registers at a fixed offset. It walks
|
|
// you there: a vendor capability in config space names which BAR, and how far
|
|
// into it, each structure lives. So a driver that cannot read config space
|
|
// cannot find its own registers -- which is why PCIIOC_CFG exists, and why the
|
|
// test program that came before this never needed it.
|
|
static volatile unsigned char *
|
|
find_cfg(int want, unsigned int *out_mult)
|
|
{
|
|
unsigned char off = cfg8(0x34) & 0xFC;
|
|
|
|
while (off) {
|
|
unsigned char id = cfg8(off);
|
|
unsigned char next = cfg8(off + 1);
|
|
|
|
if (id == PCI_CAP_ID_VNDR) {
|
|
unsigned char type = cfg8(off + 3);
|
|
unsigned char bar = cfg8(off + 4);
|
|
unsigned int o = cfg32(off + 8);
|
|
unsigned int len = cfg32(off + 12);
|
|
|
|
if (type == want && bar < PCIUSER_MAXBAR && info.bar[bar].base) {
|
|
if (out_mult)
|
|
*out_mult = cfg32(off + 16);
|
|
|
|
// Map from the start of the BAR, not from the capability's
|
|
// offset: two capabilities usually live in one BAR, and
|
|
// mapping each separately would hand out two mappings of
|
|
// overlapping pages.
|
|
unsigned long long want_len = (unsigned long long)o + len;
|
|
if (want_len < 4096) want_len = 4096;
|
|
if (want_len > info.bar[bar].size) want_len = info.bar[bar].size;
|
|
|
|
volatile unsigned char *p =
|
|
(volatile unsigned char *)mmap(0, want_len,
|
|
PROT_READ | PROT_WRITE,
|
|
MAP_SHARED, pcifd,
|
|
info.bar[bar].base);
|
|
if (p == 0 || (long)p == -1)
|
|
return 0;
|
|
return p + o;
|
|
}
|
|
}
|
|
off = next & 0xFC;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// --- the queue --------------------------------------------------------------
|
|
|
|
#define CTRL_QUEUE 0
|
|
|
|
static struct vring_desc *desc;
|
|
static struct vring_avail *avail;
|
|
static struct vring_used *used;
|
|
static unsigned long long ring_phys;
|
|
static unsigned short qsize, notify_off, last_used, next_desc;
|
|
|
|
static void *cmdbuf, *respbuf;
|
|
static unsigned long long cmdphys, respphys;
|
|
|
|
static void
|
|
queue_setup(void)
|
|
{
|
|
w16(common, VCOMMON_Q_SELECT, CTRL_QUEUE);
|
|
qsize = r16(common, VCOMMON_Q_SIZE);
|
|
if (qsize == 0)
|
|
die("control queue has no slots");
|
|
if (qsize > 64)
|
|
qsize = 64; // more than one command in flight is not
|
|
// something this driver has any use for
|
|
w16(common, VCOMMON_Q_SIZE, qsize);
|
|
notify_off = r16(common, VCOMMON_Q_NOTIFY_OFF);
|
|
|
|
// One contiguous block for the three rings, laid out the way the spec
|
|
// requires: descriptors, then available, then used, each aligned. The
|
|
// device reads all three by physical address, so they have to come from
|
|
// the kernel's DMA arena rather than from anything this process could
|
|
// allocate for itself.
|
|
unsigned int desc_sz = 16 * qsize;
|
|
unsigned int avail_sz = 6 + 2 * qsize;
|
|
|
|
struct pciuser_dma d;
|
|
d.size = 16384;
|
|
if (ioctl(pcifd, PCIIOC_DMA, &d) < 0)
|
|
die("no DMA memory for the queue");
|
|
|
|
unsigned long long base = d.uaddr;
|
|
unsigned long long basep = d.paddr;
|
|
|
|
unsigned int off_avail = (desc_sz + 1) & ~1u;
|
|
unsigned int off_used = (off_avail + avail_sz + 3) & ~3u;
|
|
|
|
desc = (struct vring_desc *)(base);
|
|
avail = (struct vring_avail *)(base + off_avail);
|
|
used = (struct vring_used *)(base + off_used);
|
|
ring_phys = basep;
|
|
|
|
for (unsigned int i = 0; i < 16384; i++)
|
|
((volatile unsigned char *)base)[i] = 0;
|
|
|
|
// Do not interrupt us. docmd() polls the used ring, so an interrupt is
|
|
// not merely unwanted, it is dangerous: this process holds the device but
|
|
// has no handler registered, and the line is shared with the e1000. A
|
|
// level-triggered line nobody acknowledges re-fires for ever -- 77,000
|
|
// times a second, which is what the first working run of this driver did
|
|
// before this line existed. The storm detector in kernel/dev/irq.c named
|
|
// the line and the neighbour it was drowning.
|
|
avail->flags = VRING_AVAIL_F_NO_INTERRUPT;
|
|
__sync_synchronize();
|
|
|
|
w64(common, VCOMMON_Q_DESC, basep);
|
|
w64(common, VCOMMON_Q_DRIVER, basep + off_avail);
|
|
w64(common, VCOMMON_Q_DEVICE, basep + off_used);
|
|
w16(common, VCOMMON_Q_ENABLE, 1);
|
|
|
|
// Command and response buffers, also from the arena for the same reason.
|
|
struct pciuser_dma b;
|
|
b.size = 8192;
|
|
if (ioctl(pcifd, PCIIOC_DMA, &b) < 0)
|
|
die("no DMA memory for command buffers");
|
|
cmdbuf = (void *)b.uaddr;
|
|
cmdphys = b.paddr;
|
|
respbuf = (void *)(b.uaddr + 4096);
|
|
respphys = b.paddr + 4096;
|
|
}
|
|
|
|
// Submit one command and wait for the answer.
|
|
//
|
|
// Polled, not interrupt-driven. This driver issues a handful of commands at
|
|
// startup and one flush per frame, and a poll on a used-ring index that the
|
|
// device updates by DMA is both simpler and lower-latency than waking a
|
|
// process. The interrupt path exists (PCIIOC_IRQ) and belongs in whatever
|
|
// wants to sleep between frames.
|
|
static int
|
|
docmd(unsigned int cmdlen, unsigned int resplen)
|
|
{
|
|
unsigned short head = next_desc;
|
|
|
|
desc[head].addr = cmdphys;
|
|
desc[head].len = cmdlen;
|
|
desc[head].flags = VRING_DESC_F_NEXT;
|
|
desc[head].next = (unsigned short)(head + 1);
|
|
|
|
desc[head + 1].addr = respphys;
|
|
desc[head + 1].len = resplen;
|
|
desc[head + 1].flags = VRING_DESC_F_WRITE;
|
|
desc[head + 1].next = 0;
|
|
|
|
avail->ring[avail->idx % qsize] = head;
|
|
__sync_synchronize();
|
|
avail->idx++;
|
|
__sync_synchronize();
|
|
|
|
*(volatile unsigned short *)(notify + notify_off * notify_mult) = CTRL_QUEUE;
|
|
|
|
// Spin with a bound. A device that never answers is a bug somewhere, and
|
|
// hanging for ever is a worse way to report it than saying so.
|
|
for (int spin = 0; spin < 100000000; spin++) {
|
|
__sync_synchronize();
|
|
if (used->idx != last_used) {
|
|
last_used = used->idx;
|
|
next_desc = (unsigned short)((next_desc + 2) % qsize);
|
|
return 0;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
static void
|
|
hdr_init(void *p, unsigned int type)
|
|
{
|
|
struct virtio_gpu_ctrl_hdr *h = (struct virtio_gpu_ctrl_hdr *)p;
|
|
for (unsigned int i = 0; i < sizeof(*h); i++) ((unsigned char *)h)[i] = 0;
|
|
h->type = type;
|
|
}
|
|
|
|
static int
|
|
resp_ok(void)
|
|
{
|
|
struct virtio_gpu_ctrl_hdr *h = (struct virtio_gpu_ctrl_hdr *)respbuf;
|
|
return h->type == VIRTIO_GPU_RESP_OK_NODATA ||
|
|
h->type == VIRTIO_GPU_RESP_OK_DISPLAY_INFO;
|
|
}
|
|
|
|
// --- bringing the device up -------------------------------------------------
|
|
|
|
static int width, height;
|
|
static unsigned long long fb_uaddr, fb_paddr;
|
|
static unsigned int fb_bytes;
|
|
|
|
#define RESOURCE_ID 1
|
|
|
|
static void
|
|
bring_up(void)
|
|
{
|
|
// Reset, then the status handshake in the order the spec sets out. A
|
|
// device that finds the sequence wrong fails the feature step rather than
|
|
// telling you which line was at fault, so it is written out plainly.
|
|
w8(common, VCOMMON_STATUS, 0);
|
|
while (r8(common, VCOMMON_STATUS) != 0)
|
|
;
|
|
w8(common, VCOMMON_STATUS, VSTATUS_ACK);
|
|
w8(common, VCOMMON_STATUS, VSTATUS_ACK | VSTATUS_DRIVER);
|
|
|
|
w32(common, VCOMMON_DFEAT_SEL, 1);
|
|
unsigned int hi = r32(common, VCOMMON_DFEAT);
|
|
if (!(hi & (1u << (VIRTIO_F_VERSION_1 - 32)))) {
|
|
w8(common, VCOMMON_STATUS, VSTATUS_FAILED);
|
|
die("device is not virtio 1.0");
|
|
}
|
|
|
|
w32(common, VCOMMON_GFEAT_SEL, 0);
|
|
w32(common, VCOMMON_GFEAT, 0);
|
|
w32(common, VCOMMON_GFEAT_SEL, 1);
|
|
w32(common, VCOMMON_GFEAT, 1u << (VIRTIO_F_VERSION_1 - 32));
|
|
|
|
w8(common, VCOMMON_STATUS, VSTATUS_ACK | VSTATUS_DRIVER | VSTATUS_FEATURES_OK);
|
|
if (!(r8(common, VCOMMON_STATUS) & VSTATUS_FEATURES_OK)) {
|
|
w8(common, VCOMMON_STATUS, VSTATUS_FAILED);
|
|
die("device refused the feature set");
|
|
}
|
|
|
|
queue_setup();
|
|
|
|
w8(common, VCOMMON_STATUS,
|
|
VSTATUS_ACK | VSTATUS_DRIVER | VSTATUS_FEATURES_OK | VSTATUS_DRIVER_OK);
|
|
|
|
// What size is the screen?
|
|
hdr_init(cmdbuf, VIRTIO_GPU_CMD_GET_DISPLAY_INFO);
|
|
if (docmd(sizeof(struct virtio_gpu_ctrl_hdr),
|
|
sizeof(struct virtio_gpu_resp_display_info)) < 0)
|
|
die("the device never answered GET_DISPLAY_INFO");
|
|
if (!resp_ok())
|
|
die("GET_DISPLAY_INFO refused");
|
|
|
|
struct virtio_gpu_resp_display_info *di =
|
|
(struct virtio_gpu_resp_display_info *)respbuf;
|
|
width = (int)di->pmodes[0].r.width;
|
|
height = (int)di->pmodes[0].r.height;
|
|
if (width <= 0 || height <= 0)
|
|
die("the device reports no display");
|
|
|
|
puts("vgpud: display is "); putn(width); puts("x"); putn(height); puts("\n");
|
|
|
|
// A 2D resource the size of the screen.
|
|
struct virtio_gpu_resource_create_2d *c =
|
|
(struct virtio_gpu_resource_create_2d *)cmdbuf;
|
|
hdr_init(c, VIRTIO_GPU_CMD_RESOURCE_CREATE_2D);
|
|
c->resource_id = RESOURCE_ID;
|
|
c->format = VIRTIO_GPU_FORMAT_B8G8R8X8_UNORM;
|
|
c->width = (unsigned int)width;
|
|
c->height = (unsigned int)height;
|
|
if (docmd(sizeof(*c), sizeof(struct virtio_gpu_ctrl_hdr)) < 0 || !resp_ok())
|
|
die("RESOURCE_CREATE_2D refused");
|
|
|
|
// Memory behind it. This is the framebuffer: the process writes here and
|
|
// the device reads it, which is the entire reason PCIIOC_DMA reports a
|
|
// physical address.
|
|
fb_bytes = (unsigned int)(width * height * 4);
|
|
|
|
struct pciuser_dma d;
|
|
d.size = fb_bytes;
|
|
if (ioctl(pcifd, PCIIOC_DMA, &d) < 0) {
|
|
puts("vgpud: wanted "); putn(fb_bytes / 1024);
|
|
puts(" KB of DMA memory for the framebuffer\n");
|
|
die("not enough DMA memory -- see DMA_ARENA_BYTES");
|
|
}
|
|
fb_uaddr = d.uaddr;
|
|
fb_paddr = d.paddr;
|
|
|
|
struct virtio_gpu_resource_attach_backing *a =
|
|
(struct virtio_gpu_resource_attach_backing *)cmdbuf;
|
|
hdr_init(a, VIRTIO_GPU_CMD_RESOURCE_ATTACH_BACKING);
|
|
a->resource_id = RESOURCE_ID;
|
|
a->nr_entries = 1;
|
|
// The entries follow the struct rather than being in it: the count is
|
|
// variable, so the command is a header with an array glued on the end.
|
|
struct virtio_gpu_mem_entry *e = (struct virtio_gpu_mem_entry *)(a + 1);
|
|
e->addr = fb_paddr;
|
|
e->length = fb_bytes;
|
|
e->padding = 0;
|
|
if (docmd(sizeof(*a) + sizeof(*e), sizeof(struct virtio_gpu_ctrl_hdr)) < 0 ||
|
|
!resp_ok())
|
|
die("RESOURCE_ATTACH_BACKING refused");
|
|
|
|
// Put it on the screen.
|
|
struct virtio_gpu_set_scanout *so = (struct virtio_gpu_set_scanout *)cmdbuf;
|
|
hdr_init(so, VIRTIO_GPU_CMD_SET_SCANOUT);
|
|
so->r.x = so->r.y = 0;
|
|
so->r.width = (unsigned int)width;
|
|
so->r.height = (unsigned int)height;
|
|
so->scanout_id = 0;
|
|
so->resource_id = RESOURCE_ID;
|
|
if (docmd(sizeof(*so), sizeof(struct virtio_gpu_ctrl_hdr)) < 0 || !resp_ok())
|
|
die("SET_SCANOUT refused");
|
|
|
|
puts("vgpud: scanout is ours, framebuffer at physical 0x");
|
|
puthex(fb_paddr, 16); puts("\n");
|
|
}
|
|
|
|
// Push what has been drawn to the host. Two commands, and both are needed:
|
|
// the transfer copies from the backing into the device's copy of the resource,
|
|
// the flush tells it to put that on screen.
|
|
static void
|
|
present(void)
|
|
{
|
|
struct virtio_gpu_transfer_to_host_2d *t =
|
|
(struct virtio_gpu_transfer_to_host_2d *)cmdbuf;
|
|
hdr_init(t, VIRTIO_GPU_CMD_TRANSFER_TO_HOST_2D);
|
|
t->r.x = t->r.y = 0;
|
|
t->r.width = (unsigned int)width;
|
|
t->r.height = (unsigned int)height;
|
|
t->offset = 0;
|
|
t->resource_id = RESOURCE_ID;
|
|
t->padding = 0;
|
|
if (docmd(sizeof(*t), sizeof(struct virtio_gpu_ctrl_hdr)) < 0)
|
|
die("TRANSFER_TO_HOST_2D never answered");
|
|
|
|
struct virtio_gpu_resource_flush *f =
|
|
(struct virtio_gpu_resource_flush *)cmdbuf;
|
|
hdr_init(f, VIRTIO_GPU_CMD_RESOURCE_FLUSH);
|
|
f->r.x = f->r.y = 0;
|
|
f->r.width = (unsigned int)width;
|
|
f->r.height = (unsigned int)height;
|
|
f->resource_id = RESOURCE_ID;
|
|
f->padding = 0;
|
|
if (docmd(sizeof(*f), sizeof(struct virtio_gpu_ctrl_hdr)) < 0)
|
|
die("RESOURCE_FLUSH never answered");
|
|
}
|
|
|
|
// --- something to look at ---------------------------------------------------
|
|
|
|
// Not a compositor. A pattern that is obviously deliberate, so a screenshot
|
|
// answers "did a userspace process put this here" without ambiguity -- a black
|
|
// screen and a working screen look identical if all you draw is black.
|
|
static void
|
|
draw_test_pattern(void)
|
|
{
|
|
volatile unsigned int *fb = (volatile unsigned int *)fb_uaddr;
|
|
|
|
// A tally of this instance's pid along the bottom edge, because the thing
|
|
// H2 has to show is that the driver *came back* -- and a second run that
|
|
// redraws an identical picture is indistinguishable from a first run that
|
|
// never stopped. One block per bit, low bit at the left.
|
|
int mypid = getpid();
|
|
|
|
for (int y = 0; y < height; y++) {
|
|
for (int x = 0; x < width; x++) {
|
|
unsigned int c;
|
|
int band = (y * 8) / height;
|
|
switch (band) {
|
|
case 0: c = 0x0C0E13; break; /* SCH_BG */
|
|
case 1: c = 0x7C5CFF; break; /* SCH_ACCENT */
|
|
case 2: c = 0x4B1D9A; break; /* PSOGM purple */
|
|
case 3: c = (unsigned int)((x * 255 / width) * 0x010101); break;
|
|
case 4: c = (unsigned int)(x * 255 / width) << 16; break;
|
|
case 5: c = (unsigned int)(x * 255 / width) << 8; break;
|
|
case 6: c = (unsigned int)(x * 255 / width); break;
|
|
default:
|
|
// A checkerboard, so a stride bug is visible as a shear rather
|
|
// than as a plausible-looking gradient.
|
|
c = (((x / 16) + (y / 16)) & 1) ? 0xE6E8EC : 0x0C0E13;
|
|
break;
|
|
}
|
|
fb[y * width + x] = c;
|
|
}
|
|
}
|
|
|
|
for (int b = 0; b < 12; b++) {
|
|
unsigned int c = (mypid >> b) & 1 ? 0x4ADE80 : 0x12151C; /* SCH_OK */
|
|
for (int y = height - 24; y < height - 8; y++)
|
|
for (int x = 16 + b * 24; x < 16 + b * 24 + 16; x++)
|
|
if (y >= 0 && x < width)
|
|
fb[y * width + x] = c;
|
|
}
|
|
}
|
|
|
|
// --- main -------------------------------------------------------------------
|
|
|
|
int
|
|
main(int argc, char *argv[])
|
|
{
|
|
pcifd = open("/dev/pci", O_RDWR);
|
|
if (pcifd < 0)
|
|
die("cannot open /dev/pci");
|
|
|
|
int count = 0;
|
|
if (ioctl(pcifd, PCIIOC_COUNT, &count) < 0)
|
|
die("PCIIOC_COUNT failed");
|
|
|
|
// Find it by what it is, not by where it is. A driver that takes a bus
|
|
// address on the command line is one nobody can start automatically.
|
|
index_ = -1;
|
|
for (int i = 0; i < count; i++) {
|
|
info.index = (unsigned int)i;
|
|
if (ioctl(pcifd, PCIIOC_INFO, &info) < 0)
|
|
continue;
|
|
if (info.vendor == VIRTIO_VENDOR && info.device == VIRTIO_DEV_GPU) {
|
|
index_ = i;
|
|
break;
|
|
}
|
|
}
|
|
if (index_ < 0)
|
|
nothing_to_do("no virtio-gpu on this machine");
|
|
|
|
puts("vgpud: virtio-gpu at "); puthex(info.bus, 2); puts(":");
|
|
puthex(info.slot, 2); puts("."); puthex(info.func, 1);
|
|
if (info.driver[0]) { puts(", currently held by "); puts(info.driver); }
|
|
puts("\n");
|
|
|
|
// The kernel driver is asked to let go here. It puts the VBE framebuffer
|
|
// back on screen before returning, so there is never a moment with no
|
|
// display -- see vgpu_release() and vga_fb_restore().
|
|
if (ioctl(pcifd, PCIIOC_CLAIM, &index_) < 0)
|
|
nothing_to_do("claim refused -- the kernel driver would not release it");
|
|
puts("vgpud: claimed\n");
|
|
|
|
// The info is re-read because claiming is what enables memory space and
|
|
// bus mastering, and a BAR read before that can legitimately be zero.
|
|
info.index = (unsigned int)index_;
|
|
if (ioctl(pcifd, PCIIOC_INFO, &info) < 0)
|
|
die("PCIIOC_INFO failed after claim");
|
|
|
|
common = find_cfg(VIRTIO_PCI_CAP_COMMON_CFG, 0);
|
|
notify = find_cfg(VIRTIO_PCI_CAP_NOTIFY_CFG, ¬ify_mult);
|
|
isr = find_cfg(VIRTIO_PCI_CAP_ISR_CFG, 0);
|
|
if (!common || !notify)
|
|
die("no modern virtio capabilities -- not a 1.0 device");
|
|
|
|
bring_up();
|
|
draw_test_pattern();
|
|
present();
|
|
|
|
puts("vgpud: pid ");
|
|
putn(getpid());
|
|
puts(" drew a test pattern from userspace\n");
|
|
|
|
if (argc > 1 && argv[1][0] == '-' && argv[1][1] == 'x') {
|
|
// Exit without releasing, to show what a crashed driver leaves behind:
|
|
// the kernel takes the device back and the screen is whatever the
|
|
// device was last told to scan out.
|
|
puts("vgpud: exiting without cleaning up (simulated crash)\n");
|
|
exit();
|
|
}
|
|
|
|
// Hold the device and wait.
|
|
//
|
|
// Mechanism 1 exists now, so what belongs here eventually is an AF_UNIX
|
|
// socket a compositor connects to. It is not built, and building it before
|
|
// there is a compositor to judge it is the mistake this plan spends a
|
|
// section warning about -- so this sleeps instead, and holding the device
|
|
// is the honest state: releasing it would put the console back and make it
|
|
// look like none of this ever worked.
|
|
//
|
|
// Being killed here is not an error path, it is the H2 path: schinit
|
|
// restarts this process, and the device has to come back with it.
|
|
for (;;)
|
|
sleep(100);
|
|
}
|