diff --git a/Makefile b/Makefile index 4f21329..8ca122c 100644 --- a/Makefile +++ b/Makefile @@ -180,13 +180,14 @@ endif # completely different things: # # stdvga the boot splash, then the text console and your shell -# virtio-gpu whatever vgpud drew, which at the moment is a test pattern +# virtio-gpu the mark, and then the same console, mirrored there by vgpud # -# So a window sitting on the virtio-gpu display shows colour bars and no -# prompt, and one that opened before vgpud drew shows black. Nothing is wrong -# when that happens; you are looking at the other screen. `make qemu-console` -# below boots with one display and no GPU to hand over, which is the quickest -# way to tell that apart from an actual failure. +# So the two windows show the same thing once vgpud is up, which is the point: +# a head showing the running system is a head you can tell is working. Before +# vgpud starts, the virtio-gpu display is black -- nothing is wrong when that +# happens, you are looking at a device nobody has drawn on yet. `make +# qemu-console` below boots with one display and no GPU to hand over, which is +# the quickest way to tell that apart from an actual failure. QEMU_DISPLAY = -vga std -device virtio-gpu-pci # One display, carrying the console. No virtio-gpu, so vgpud finds nothing to diff --git a/cmd/vgpud/main.c b/cmd/vgpud/main.c index 565dc09..8d928b7 100644 --- a/cmd/vgpud/main.c +++ b/cmd/vgpud/main.c @@ -30,6 +30,12 @@ #include "socket.h" #include "theme.h" // SCH_BG, SCH_ACCENT -- one palette, see docs/branding #include "logo.h" // the mark, the same bytes the loader and console draw +#include "fb.h" // the console framebuffer, which this mirrors + +// lib/glue.c has these -- they exist because the compiler emits calls to them +// whether or not anything asks -- but include/user.h never declared them. +void *memcpy(void *, const void *, unsigned long); +int memcmp(const void *, const void *, unsigned long); // --- small output, since lib/ has no printf --------------------------------- @@ -528,16 +534,27 @@ bring_up(void) // 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. +// +// A rectangle rather than the screen, because the mirror below repaints a few +// changed rows at a time and a full-screen transfer of a 1280x800 resource to +// move one line of text is most of a megabyte for nothing. `offset` is where +// the rectangle's top-left pixel sits in the *backing*, which the device +// cannot work out for itself -- getting it wrong shifts the update diagonally +// rather than failing, so it is written once here and nowhere else. static void -present(void) +present_rect(int x, int y, int w, int h) { + if (w <= 0 || h <= 0) + return; + 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->r.x = (unsigned int)x; + t->r.y = (unsigned int)y; + t->r.width = (unsigned int)w; + t->r.height = (unsigned int)h; + t->offset = ((unsigned long long)y * (unsigned)width + x) * 4; t->resource_id = resource_id; t->padding = 0; if (docmd(sizeof(*t), sizeof(struct virtio_gpu_ctrl_hdr)) < 0) @@ -546,15 +563,22 @@ present(void) 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->r.x = (unsigned int)x; + f->r.y = (unsigned int)y; + f->r.width = (unsigned int)w; + f->r.height = (unsigned int)h; f->resource_id = resource_id; f->padding = 0; if (docmd(sizeof(*f), sizeof(struct virtio_gpu_ctrl_hdr)) < 0) die("RESOURCE_FLUSH never answered"); } +static void +present(void) +{ + present_rect(0, 0, width, height); +} + // --- something to look at --------------------------------------------------- // The mark, on the theme ground. @@ -603,6 +627,149 @@ draw_mark(void) } } +// --- the mirror -------------------------------------------------------------- +// +// What this display is *for*, which the mark alone never answered. +// +// The mark said "a userspace process is driving this device", which was the +// question H2 and H3 asked and is not a question any more. What was left was +// a second monitor showing a logo for ever, which is indistinguishable from a +// second monitor that does not work -- and that is exactly what it got called. +// +// So: the virtio-gpu head shows the console. Not because mirroring is the +// endpoint -- a compositor (Gate 3) will want this head for itself, and when +// there is one this goes away -- but because until then the honest thing for +// a working display to show is the system that is running on it. +// +// The mechanism is a *copy with a diff*, and the diff is what makes it cheap +// enough to run in a loop. The console framebuffer is mapped read-only-ish +// through /dev/fb0; the virtio backing store is already mapped. The backing +// therefore holds, at all times, exactly what was last sent to the device -- +// which means it doubles as the previous frame, and no third buffer is needed +// to answer "what changed". Rows that compare equal are skipped; the run of +// rows that did not is copied and transferred as one rectangle. +// +// Mapping /dev/fb0 does not take the screen away from the console: the +// mapping is of the same physical pages the console draws into, and the +// kernel's fb_mapped() only arranges for a redraw when the fd is *closed*. +// This one is never closed. +// Ten passes a second. Fast enough that typing does not feel posted from +// elsewhere, slow enough that the comparison -- a few megabytes of memcmp +// against rows that are usually identical -- is not what the machine spends +// its time on. +#define MIRROR_MS 100 +#define MIRROR_TICKS 10 // the same interval, in sleep()'s units + +static volatile unsigned char *con_fb; // the console's framebuffer +static unsigned con_w, con_h, con_stride; +static int mirror_x, mirror_y, mirror_w, mirror_h; +static int mirror_full; // next pass copies every row + +// Where the console lands on this display. The two are not the same size -- +// the console is whatever mode the firmware left, this head is whatever mode +// it was last told -- so the smaller of each dimension is what gets copied. +// +// A head larger than the console centres it, on the theme ground draw_mark() +// leaves behind. A head smaller than the console shows its top-left corner, +// which is not a compromise: a terminal fills from the top-left, so that is +// where everything worth seeing is. Scaling is the alternative and it is the +// wrong one -- text resampled to a non-integer ratio is text you cannot read. +// +// Called again after every modeset, because both halves of that arithmetic +// move. +static void +mirror_geometry(void) +{ + mirror_w = (int)con_w < width ? (int)con_w : width; + mirror_h = (int)con_h < height ? (int)con_h : height; + mirror_x = (width - mirror_w) / 2; + mirror_y = (height - mirror_h) / 2; + mirror_full = 1; +} + +// Returns 0 if there is a console to mirror. Every failure here is a reason to +// keep showing the mark rather than a reason to die: a machine with no +// framebuffer console is a machine this daemon still has to hold the device +// on. +static int +mirror_open(void) +{ + struct fb_var_screeninfo var; + struct fb_fix_screeninfo fix; + unsigned char *p; + + int fd = open("/dev/fb0", O_RDWR); + if (fd < 0) + return -1; + if (ioctl(fd, FBIOGET_VSCREENINFO, &var) < 0 || + ioctl(fd, FBIOGET_FSCREENINFO, &fix) < 0) { + close(fd); + return -1; + } + + // The copy below is a run of 32-bit words, so the two formats have to + // agree exactly rather than approximately. They do today -- both sides are + // XRGB8888 -- and if a console ever comes up 16-bit or BGR, converting per + // pixel is a different function and this one should decline instead of + // painting the screen in the wrong colours. + if (var.bits_per_pixel != 32 || var.red.offset != 16 || + var.green.offset != 8 || var.blue.offset != 0) { + vputs("vgpud: console is not XRGB8888; not mirroring it\n"); + close(fd); + return -1; + } + + p = mmap(0, fix.smem_len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if (p == 0 || (long)p < 0) { + close(fd); + return -1; + } + + con_fb = p; + con_w = var.xres; + con_h = var.yres; + con_stride = fix.line_length; // NOT xres*4; the card may pad each row + mirror_geometry(); + return 0; // fd deliberately left open; see above +} + +// One pass. Cheap when nothing changed, which is almost always. +static void +mirror_pass(void) +{ + volatile unsigned int *dst; + int y, first = -1, last = -1; + unsigned rowbytes; + + if (!con_fb) + return; + + dst = (volatile unsigned int *)fb_uaddr; + rowbytes = (unsigned)mirror_w * 4; + + for (y = 0; y < mirror_h; y++) { + const void *src = (const void *)(con_fb + + (unsigned long)y * con_stride); + void *row = (void *)&dst[(unsigned long)(mirror_y + y) * width + + mirror_x]; + if (!mirror_full && memcmp(row, src, rowbytes) == 0) + continue; + memcpy(row, src, rowbytes); + if (first < 0) + first = y; + last = y; + } + mirror_full = 0; + + // One rectangle covering every changed row, rather than one per run. Text + // scrolling dirties everything anyway, and a cursor blinking at the bottom + // of a full screen is the worst case -- two rows apart, one transfer of + // the span between them. Chasing that with a list of runs is more code + // than the copy it saves. + if (first >= 0) + present_rect(mirror_x, mirror_y + first, mirror_w, last - first + 1); +} + // --- the modeset -------------------------------------------------------------- // // docs/plan/hybrid.md milestone H3: *"a modeset API, which is the thing an @@ -779,7 +946,12 @@ serve(int fd) } else if (r < 0) { reply(fd, "the device refused the modeset\n"); } else { + // The ground first, then the console re-laid onto it: the + // mirror's placement is derived from both sizes, and one + // of them just changed. draw_mark(); + mirror_geometry(); + mirror_pass(); present(); reply_dim(fd, width, height, " ok\n"); puts("vgpud: mode is now "); putn(width); puts("x"); @@ -810,6 +982,22 @@ control_socket(void) for (i = 0; VGPUD_SOCKET[i] && i < (int)sizeof(sa.sun_path) - 1; i++) sa.sun_path[i] = VGPUD_SOCKET[i]; + // The node from the last time this ran, if there is one. + // + // /run is a directory on the root filesystem rather than a tmpfs, so a + // socket node outlives the process that bound it *and* the boot it was + // bound in. bind() onto an existing name fails, and what that produced was + // a vgpud holding the device with no way to talk to it -- on every boot + // after the first, on any image whose root is writable. `schmode` said + // "it is not running" about a process that was running perfectly well. + // + // Unlinking first is what a Unix daemon does, and the usual objection -- + // that it lets a second instance steal a live socket -- does not apply + // here: two vgpuds cannot coexist anyway, because the first holds the PCI + // claim and the second is refused the device long before it reaches this + // function. The claim is the mutual exclusion; this node never was. + unlink(VGPUD_SOCKET); + if (bind(fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) { close(fd); return -1; @@ -883,7 +1071,15 @@ main(int argc, char *argv[]) vputs("vgpud: pid "); vputn(getpid()); - vputs(" drew a test pattern from userspace\n"); + vputs(" drew the mark from userspace\n"); + + // From here the head shows the console rather than the mark. The mark + // stays on screen if there is nothing to mirror, and stays as the ground + // around the console either way. + if (mirror_open() == 0) { + mirror_pass(); + vputs("vgpud: mirroring the console\n"); + } if (argc > 1 && argv[1][0] == '-' && argv[1][1] == 'x') { // Exit without releasing, to show what a crashed driver leaves behind: @@ -917,18 +1113,32 @@ main(int argc, char *argv[]) if (srv < 0) { vputs("vgpud: no control socket; the mode is fixed at " "whatever the device reported\n"); - for (;;) - sleep(100); + for (;;) { + sleep(MIRROR_TICKS); + mirror_pass(); + } } vputs("vgpud: listening on " VGPUD_SOCKET "\n"); + + // Not a blocking accept any more. That was right when holding the device + // was the whole job; now there is a second thing to do between calls, and + // poll() is how a process waits for one of two reasons to wake up. The + // timeout is the frame interval: with nothing happening, this wakes ten + // times a second, compares rows, finds none changed, and goes back to + // sleep. + struct pollfd pf; + pf.fd = srv; + pf.events = POLLIN; for (;;) { - int c = accept(srv, 0, 0); - if (c < 0) { - sleep(10); - continue; + pf.revents = 0; + if (poll(&pf, 1, MIRROR_MS) > 0 && (pf.revents & POLLIN)) { + int c = accept(srv, 0, 0); + if (c >= 0) { + serve(c); + close(c); + } } - serve(c); - close(c); + mirror_pass(); } } diff --git a/docs/architecture/graphics.md b/docs/architecture/graphics.md index 11127c4..20eed21 100644 --- a/docs/architecture/graphics.md +++ b/docs/architecture/graphics.md @@ -106,6 +106,68 @@ and `mmap` of the real framebuffer at its real physical address. > happens with no further syscalls. `fb_mapped()` is called there, and > `fbclose()` puts it back. +## What the second head shows + +A machine with both a stdvga and a virtio-gpu has two displays, and `vgpud` — +the out-of-kernel driver, `cmd/vgpud/main.c` — owns the second one. For a long +time it drew a test card there: colour bands, gradients, a checkerboard, and +its own pid in binary along the bottom. That was scaffolding for +[../plan/hybrid.md](../plan/hybrid.md)'s H2 and H3, where a screenshot had to +answer *did a userspace process put this here* and *did the driver come back*. + +Both of those are closed, so the test card became the Shift mark. Which was +honest about ownership and useless about everything else: **a second monitor +showing a logo for ever is indistinguishable from a second monitor that does +not work**, and that is exactly what it got called. + +It shows the console now. + +### A copy with a diff + +The console framebuffer is mapped through `/dev/fb0`; the virtio backing store +is already mapped, because `vgpud` allocated it. So the backing holds, at all +times, exactly what was last sent to the device — which means it *is* the +previous frame, and answering "what changed" needs no third buffer. Ten times +a second: compare each row, skip the ones that match, copy the ones that do +not, and transfer the single rectangle spanning the rows that changed. + +This is why `present()` became `present_rect()`. Moving one line of text with a +full-screen transfer of a 1280×800 resource is most of a megabyte of copy for a +few hundred bytes of difference, and the loop runs whether or not anything is +happening. + +Mapping `/dev/fb0` does not take the screen away from the console — the +mapping is of the same physical pages the console draws into, and the kernel's +`fb_mapped()` only arranges a redraw when the fd is *closed*. `vgpud` never +closes it. + +A head larger than the console centres it on the theme ground. A head smaller +shows the top-left corner, which is where a terminal puts everything worth +seeing. Scaling is the alternative and it is the wrong one: text resampled to a +non-integer ratio is text you cannot read. + +**This is not the endpoint.** A compositor (Gate 3) will want this head for +itself, and when there is one, the mirror goes away. Until then the honest +thing for a working display to show is the system running on it. + +### /run is not a tmpfs + +Found while testing the above, and older than it: `schmode` reported *"cannot +reach vgpud on /run/vgpud — it is not running"* about a process that was +running perfectly well, on every boot after the first. + +`bind()` creates a node at the path, and `/run` is a directory on the root +filesystem rather than a tmpfs. So the node outlived the process, and the boot. +The next boot's `vgpud` claimed the device, drew, held it — and failed `bind()` +on a name that already existed, leaving a driver nobody could talk to and an +error message that named the wrong cause. + +`vgpud` unlinks the path before binding it, which is what a Unix daemon does. +The usual objection — that it lets a second instance steal a live socket — does +not apply: two `vgpud`s cannot coexist, because the first holds the PCI claim +and the second is refused the device long before it reaches `bind()`. The claim +is the mutual exclusion; the socket node never was. + ## Text [`include/text.h`](../../include/text.h) + `kernel/dev/text.c`.