diff --git a/Makefile b/Makefile index 9045f89..21b2122 100644 --- a/Makefile +++ b/Makefile @@ -108,7 +108,7 @@ KERN_NOENTRY = $(filter-out $(BUILD)/kernel/arch/x86_64/entry.o, $(KERNOBJS)) # Third-party software does NOT go here — see ports/. # ----------------------------------------------------------------------- ULIB = $(addprefix $(BUILD)/lib/, start.o glue.o usys.o) -CMDS = schinit schinitd fbinfo shmtest socktest inputtest nettest reflink schattr ping chvt pcitest vgpud schdbg +CMDS = schinit schinitd fbinfo shmtest socktest inputtest nettest reflink schattr ping chvt pcitest vgpud schdbg schmode CMDBINS = $(addprefix $(BUILD)/cmd/, $(CMDS)) # ----------------------------------------------------------------------- diff --git a/cmd/schmode/main.c b/cmd/schmode/main.c new file mode 100644 index 0000000..ddbeb5c --- /dev/null +++ b/cmd/schmode/main.c @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// schmode -- change the display mode. +// +// docs/plan/hybrid.md milestone H3. The interesting thing about this program +// is how little it is: it opens a socket to `vgpud` and writes a line. No +// syscall it makes has heard of a GPU, no kernel code runs that knows what a +// scanout is, and if the driver on the other end gets the sequence wrong the +// worst that happens is that one process dies and schinit starts another. +// +// That is the whole argument for moving the driver out, stated as a program +// rather than as a paragraph. An in-kernel driver could do the same modeset, +// and the same mistake would take the machine. +// +// schmode what the display is doing now +// schmode modes what it will accept +// schmode 1024 768 set it +// schmode 1024x768 the same + +#include "types.h" +#include "user.h" +#include "socket.h" + +#define VGPUD_SOCKET "/run/vgpud" + +static void +puts_(const char *s) +{ + int n = 0; + while (s[n]) n++; + write(1, (void *)s, n); +} + +static int +dial(void) +{ + struct sockaddr_un sa; + int fd, i; + + fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd < 0) + return -1; + + for (i = 0; i < (int)sizeof(sa.sun_path); i++) + sa.sun_path[i] = 0; + sa.sun_family = AF_UNIX; + for (i = 0; VGPUD_SOCKET[i] && i < (int)sizeof(sa.sun_path) - 1; i++) + sa.sun_path[i] = VGPUD_SOCKET[i]; + + if (connect(fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) { + close(fd); + return -1; + } + return fd; +} + +int +main(int argc, char *argv[]) +{ + char buf[256]; + char line[128]; + int fd, n = 0; + + fd = dial(); + if (fd < 0) { + // Naming the likely cause rather than the errno. On a machine with no + // virtio-gpu, vgpud reports that and exits on purpose -- see + // SCHINIT_EXIT_NORESTART -- so "not running" is the ordinary case and + // not a fault. + puts_("schmode: cannot reach vgpud on " VGPUD_SOCKET "\n"); + puts_(" it is not running -- check: cat /proc/schinit\n"); + return 1; + } + + // Everything after the program name, rejoined. "1024 768" and "1024x768" + // both arrive as one line either way, because the server parses digits and + // skips whatever separates them. + if (argc < 2) { + line[n++] = 'i'; + } else { + if (argv[1][0] >= '0' && argv[1][0] <= '9') { + // A bare size is a set, not a subcommand. + line[n++] = 'm'; line[n++] = 'o'; line[n++] = 'd'; + line[n++] = 'e'; line[n++] = ' '; + } + for (int i = 1; i < argc; i++) { + if (i > 1 && n < (int)sizeof(line) - 1) + line[n++] = ' '; + for (int j = 0; argv[i][j] && n < (int)sizeof(line) - 1; j++) + line[n++] = argv[i][j]; + } + } + line[n++] = '\n'; + + if (write(fd, line, n) != n) { + puts_("schmode: vgpud would not take that\n"); + close(fd); + return 1; + } + + // One answer, then leave. The server writes its reply and waits for the + // next line; it does not close, so reading to EOF would wait for ever. + n = read(fd, buf, sizeof(buf)); + if (n > 0) + write(1, buf, n); + + close(fd); + return 0; +} diff --git a/cmd/vgpud/main.c b/cmd/vgpud/main.c index 6c5263a..4837148 100644 --- a/cmd/vgpud/main.c +++ b/cmd/vgpud/main.c @@ -27,6 +27,7 @@ #include "pciuser.h" #include "virtio.h" #include "schinit.h" +#include "socket.h" // --- small output, since lib/ has no printf --------------------------------- @@ -313,9 +314,16 @@ resp_ok(void) static int width, height; static unsigned long long fb_uaddr, fb_paddr; -static unsigned int fb_bytes; +static unsigned int fb_bytes; // what was allocated, not what is in use +static int max_w, max_h; // the mode the allocation was sized for -#define RESOURCE_ID 1 +// The resource is recreated on every modeset; the framebuffer behind it is +// not. PCIIOC_DMA has no free -- a driver's blocks come back when it exits and +// not before -- so a modeset that allocated would exhaust sixteen slots and +// then the arena, and the fault would look like a GPU bug rather than like +// counting. Allocating once at the largest mode and reusing it for every +// smaller one costs nothing: a 1024x768 frame is a prefix of a 1280x800 one. +static unsigned int resource_id = 1; static void bring_up(void) @@ -373,7 +381,7 @@ bring_up(void) 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->resource_id = resource_id; c->format = VIRTIO_GPU_FORMAT_B8G8R8X8_UNORM; c->width = (unsigned int)width; c->height = (unsigned int)height; @@ -394,11 +402,13 @@ bring_up(void) } fb_uaddr = d.uaddr; fb_paddr = d.paddr; + max_w = width; + max_h = height; 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->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. @@ -417,7 +427,7 @@ bring_up(void) so->r.width = (unsigned int)width; so->r.height = (unsigned int)height; so->scanout_id = 0; - so->resource_id = RESOURCE_ID; + so->resource_id = resource_id; if (docmd(sizeof(*so), sizeof(struct virtio_gpu_ctrl_hdr)) < 0 || !resp_ok()) die("SET_SCANOUT refused"); @@ -438,7 +448,7 @@ present(void) t->r.width = (unsigned int)width; t->r.height = (unsigned int)height; t->offset = 0; - t->resource_id = RESOURCE_ID; + 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"); @@ -449,7 +459,7 @@ present(void) 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->resource_id = resource_id; f->padding = 0; if (docmd(sizeof(*f), sizeof(struct virtio_gpu_ctrl_hdr)) < 0) die("RESOURCE_FLUSH never answered"); @@ -502,6 +512,224 @@ draw_test_pattern(void) } } +// --- the modeset -------------------------------------------------------------- +// +// docs/plan/hybrid.md milestone H3: *"a modeset API, which is the thing an +// out-of-kernel GPU driver makes affordable and an in-kernel one never got."* +// +// The claim is worth testing rather than repeating, so here is the whole of +// it: five commands to the device, no kernel change, and a driver that can be +// killed and restarted if it goes wrong. In the kernel the same sequence would +// be five commands *and* a reason to be nervous, because getting it wrong +// takes the machine rather than one process -- which is precisely why the +// in-kernel driver never grew one. +static int +set_mode(int w, int h) +{ + if (w <= 0 || h <= 0) + return -1; + // Bounded by the allocation, not by the device. See fb_bytes. + if ((unsigned int)(w * h * 4) > fb_bytes) + return -2; + + // Let go of the old resource first. The backing pages are ours and survive + // this; only the device's view of them goes away. + struct virtio_gpu_resource_unref *u = + (struct virtio_gpu_resource_unref *)cmdbuf; + hdr_init(u, VIRTIO_GPU_CMD_RESOURCE_UNREF); + u->resource_id = resource_id; + u->padding = 0; + if (docmd(sizeof(*u), sizeof(struct virtio_gpu_ctrl_hdr)) < 0 || !resp_ok()) + return -3; + + // A fresh id rather than the same one again: the spec does not promise the + // old id is reusable the instant the unref completes, and an id is free. + resource_id++; + + 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)w; + c->height = (unsigned int)h; + if (docmd(sizeof(*c), sizeof(struct virtio_gpu_ctrl_hdr)) < 0 || !resp_ok()) + return -4; + + 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; + struct virtio_gpu_mem_entry *e = (struct virtio_gpu_mem_entry *)(a + 1); + e->addr = fb_paddr; + e->length = (unsigned int)(w * h * 4); + e->padding = 0; + if (docmd(sizeof(*a) + sizeof(*e), sizeof(struct virtio_gpu_ctrl_hdr)) < 0 || + !resp_ok()) + return -5; + + 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)w; + so->r.height = (unsigned int)h; + so->scanout_id = 0; + so->resource_id = resource_id; + if (docmd(sizeof(*so), sizeof(struct virtio_gpu_ctrl_hdr)) < 0 || !resp_ok()) + return -6; + + width = w; + height = h; + return 0; +} + +// --- the control socket ------------------------------------------------------- +// +// Lines of text, the same choice and the same reasons as +// docs/architecture/debugging.md's: it can be driven by hand, it needs no +// client to be useful, and a garbled answer is still mostly readable. +// +// The name is a *name*, not a path. AF_UNIX sockets in this kernel live in a +// table keyed by string and never touch the filesystem -- that is item 3 of +// docs/plan/foundations.md and it is not built. Nothing here depends on which +// it is, and the day it becomes a real path this keeps working. +#define VGPUD_SOCKET "/run/vgpud" + +static void +reply(int fd, const char *s) +{ + int n = 0; + while (s[n]) n++; + write(fd, (void *)s, n); +} + +static void +reply_dim(int fd, int w, int h, const char *tail) +{ + char b[48]; + int n = 0, i, v; + + v = w; i = 0; { char t[12]; if (!v) t[i++]='0'; while (v) { t[i++]='0'+v%10; v/=10; } + while (i-- > 0) b[n++] = t[i]; } + b[n++] = 'x'; + v = h; i = 0; { char t[12]; if (!v) t[i++]='0'; while (v) { t[i++]='0'+v%10; v/=10; } + while (i-- > 0) b[n++] = t[i]; } + b[n] = 0; + reply(fd, b); + reply(fd, tail); +} + +static int +num(const char **sp) +{ + const char *s = *sp; + int v = 0, any = 0; + while (*s == ' ' || *s == 'x' || *s == '\t') s++; + while (*s >= '0' && *s <= '9') { v = v * 10 + (*s++ - '0'); any = 1; } + *sp = s; + return any ? v : -1; +} + +// The modes offered. Not a list the device publishes -- virtio-gpu has no mode +// table, it will scan out whatever size it is given -- so this is the set that +// fits the framebuffer, which is the only real constraint. +static const struct { int w, h; } offered[] = { + { 1280, 800 }, { 1280, 720 }, { 1024, 768 }, { 800, 600 }, { 640, 480 }, +}; +#define NOFFERED ((int)(sizeof(offered) / sizeof(offered[0]))) + +static void +serve(int fd) +{ + char line[128]; + int n; + + while ((n = read(fd, line, sizeof(line) - 1)) > 0) { + const char *p = line; + line[n] = 0; + + while (*p == ' ') p++; + + if (p[0] == 'i') { /* info */ + reply_dim(fd, width, height, " current\n"); + reply_dim(fd, max_w, max_h, " maximum, framebuffer "); + { + char b[16]; int i = 0, v = (int)(fb_bytes / 1024); + char t[12]; int j = 0; + if (!v) t[j++] = '0'; + while (v) { t[j++] = '0' + v % 10; v /= 10; } + while (j-- > 0) b[i++] = t[j]; + b[i] = 0; + reply(fd, b); + } + reply(fd, " KB\n"); + } else if (p[0] == 'm' && p[1] == 'o' && p[2] == 'd' && p[3] == 'e' && + p[4] == 's') { /* modes */ + for (int i = 0; i < NOFFERED; i++) { + if ((unsigned int)(offered[i].w * offered[i].h * 4) > fb_bytes) + continue; + reply_dim(fd, offered[i].w, offered[i].h, + (offered[i].w == width && offered[i].h == height) + ? " *\n" : "\n"); + } + } else if (p[0] == 'm') { /* mode W H or mode WxH */ + int w, h; + while (*p && *p != ' ') p++; + w = num(&p); + h = num(&p); + if (w < 0 || h < 0) { + reply(fd, "usage: mode \n"); + } else { + int r = set_mode(w, h); + if (r == -2) { + reply(fd, "refused: larger than the framebuffer, which is " + "fixed at startup\n"); + } else if (r < 0) { + reply(fd, "the device refused the modeset\n"); + } else { + draw_test_pattern(); + present(); + reply_dim(fd, width, height, " ok\n"); + puts("vgpud: mode is now "); putn(width); puts("x"); + putn(height); puts("\n"); + } + } + } else if (p[0] == 'q') { /* quit */ + return; + } else { + reply(fd, "info | modes | mode | quit\n"); + } + } +} + +static int +control_socket(void) +{ + struct sockaddr_un sa; + int fd, i; + + fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd < 0) + return -1; + + for (i = 0; i < (int)sizeof(sa.sun_path); i++) + sa.sun_path[i] = 0; + sa.sun_family = AF_UNIX; + for (i = 0; VGPUD_SOCKET[i] && i < (int)sizeof(sa.sun_path) - 1; i++) + sa.sun_path[i] = VGPUD_SOCKET[i]; + + if (bind(fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) { + close(fd); + return -1; + } + if (listen(fd, 4) < 0) { + close(fd); + return -1; + } + return fd; +} + // --- main ------------------------------------------------------------------- int @@ -570,17 +798,42 @@ main(int argc, char *argv[]) exit(); } - // Hold the device and wait. + // Hold the device, and answer. // - // 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. + // This used to sleep for ever, with a comment arguing that building a + // control socket before there is a compositor to judge it is the mistake + // the plan warns about. That argument was right about a *compositor* + // protocol and wrong as a reason to have no socket at all: H3 is a modeset + // API, and a modeset has a consumer that exists -- somebody changing the + // mode and looking at the screen. `schmode` is that consumer, and it is + // enough to judge an interface this small. + // + // What is deliberately *not* here is anything a compositor would need: + // no buffer submission, no damage, no vblank. Those wait for a compositor, + // for the original reason. + // + // Blocking accept, one client at a time. A driver whose whole job is to + // hold a device has nothing else to be doing, and a poll loop would be + // machinery in place of a call. // // 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); + int srv = control_socket(); + if (srv < 0) { + puts("vgpud: no control socket; the mode is fixed at " + "whatever the device reported\n"); + for (;;) + sleep(100); + } + + puts("vgpud: listening on " VGPUD_SOCKET "\n"); + for (;;) { + int c = accept(srv, 0, 0); + if (c < 0) { + sleep(10); + continue; + } + serve(c); + close(c); + } } diff --git a/docs/architecture/devices.md b/docs/architecture/devices.md index 7d332c3..645d774 100644 --- a/docs/architecture/devices.md +++ b/docs/architecture/devices.md @@ -113,6 +113,14 @@ console falls back to the VBE framebuffer. **No syscall on that path knows it is talking about a GPU.** The kernel hands over a PCI device; the process decides what it is. +It also **changes the display mode**, on a control socket, with `schmode` as the +client — and that is worth a sentence here because it is the thing the device +model makes possible rather than the thing it does. A modeset is five commands +to the device. In the kernel those five commands are also a reason to be +nervous, because a wrong one takes the machine; in a process the worst case is +that `schinit` starts another one. See +[../plan/hybrid.md](../plan/hybrid.md), "A modeset, from a process". + ### Taking it back A driver that dies is the case this whole arrangement exists for, so release is a diff --git a/docs/plan/1.0.md b/docs/plan/1.0.md index 77ae9b8..96b9c27 100644 --- a/docs/plan/1.0.md +++ b/docs/plan/1.0.md @@ -94,7 +94,7 @@ The galling ones. The work exists, is tested, and nothing uses it. | | Where it stopped | |---|---| | **`foundations.md`** | Item 1 built. Items 2 (a shareable memory object), 3 (`AF_UNIX` in the filesystem namespace), 4 and 5 open. **Wayland stops at item 2.** | -| **`hybrid.md`** | All four mechanisms built. H1 and H2 done — the driver can be killed and comes back, which is the milestone the plan calls the one that proves the whole idea. H3 (a modeset API) and H4 (USB) open. | +| **`hybrid.md`** | All four mechanisms built. H1, H2 and H3 done — the driver can be killed and comes back, and the display mode can be changed from a process. **H4, the USB eviction, is the only one left and is much the largest thing on this register**: `xhci.c` plus a USB core and three class drivers, all of which are on the path to a shell on real hardware. | | **Schcom v0** | 715 lines that composite, over a wire format that is superseded. Frozen: it runs, nothing more is added, and it is kept only because it is the one thing in the tree that has ever put a client's pixels on screen. | | **`debugger.md`** | D0 through D4 built and described in [../architecture/debugging.md](../architecture/debugging.md). Only the network transport is open — a userspace daemon bridging TCP to `/dev/dbg`, which is deliberately last. | diff --git a/docs/plan/hybrid.md b/docs/plan/hybrid.md index e491a6a..7898df6 100644 --- a/docs/plan/hybrid.md +++ b/docs/plan/hybrid.md @@ -158,7 +158,7 @@ is that the storm detector built for history/kernel.md section 13 named the line, the rate and the neighbour without anyone having to look. **All four mechanisms now exist.** What remains on this plan is the second -eviction and then the foreign driver host. +eviction -- USB -- and then the foreign driver host. ### One thing the interface is deliberately not @@ -245,6 +245,73 @@ way rather than counted here. --- +## A modeset, from a process + +H3 is one sentence in the milestone table -- *"a modeset API, which is the +thing an out-of-kernel GPU driver makes affordable and an in-kernel one never +got"* -- and the claim in it is worth testing rather than repeating. + +``` +# schmode +1280x800 current +1280x800 maximum, framebuffer 4000 KB +# schmode modes +1280x800 * +1280x720 +1024x768 +800x600 +640x480 +# schmode 1024 768 +1024x768 ok +``` + +The scanout really resizes: captured from the virtio-gpu head, the frames come +back 1280x800, then 1024x768, then 640x480, then 1280x800 again, with the +checkerboard band showing no shear at any of them. + +**Nothing in the kernel changed for this.** No syscall on the path has heard of +a scanout; `schmode` writes a line to a socket and `vgpud` sends five commands +to the device. That is the whole of the milestone, and the reason it is +interesting is the comparison: the same five commands in the kernel would have +been the same five commands *and a reason to be nervous*, because getting the +sequence wrong there takes the machine instead of one process. **That is why +the in-kernel driver never grew one** -- not difficulty, exposure. + +### What it costs, and what it refuses + +The framebuffer is allocated **once, at the largest mode**, and every smaller +mode reuses it. `PCIIOC_DMA` has no free -- a driver's blocks come back when it +exits and not before -- so a modeset that allocated would spend sixteen slots +and then the arena, and the failure would look like a GPU bug rather than like +counting. A mode larger than the initial allocation is refused, and says why: + +``` +# schmode 1920 1080 +refused: larger than the framebuffer, which is fixed at startup +``` + +That bound is 4 MB, `PCIIOC_DMA`'s cap, which at 32 bits per pixel is about +1280x800. Raising it is a one-line change to a constant and a question about +how much of a 16 MB arena one driver should be able to take, which is a better +question than it looks and is not answered here. + +### The socket, and the argument it settles + +`vgpud` used to end in `for(;;) sleep(100);` under a comment arguing that +building a control socket before there is a compositor to judge it is exactly +the mistake this document spends a section warning about. + +That argument was right about a *compositor protocol* and wrong as a reason to +have no socket at all. A modeset API has a consumer that exists -- somebody +changing the mode and looking at the screen -- and `schmode` is small enough +that it can judge an interface this small. So the socket is there, it speaks +lines of text for the same reasons +[../architecture/debugging.md](../architecture/debugging.md) gives, and what is +deliberately **not** on it is everything a compositor would need: no buffer +submission, no damage, no vblank. Those still wait, for the original reason. + +--- + ## The mechanisms Four, and only the fourth is large. @@ -357,8 +424,8 @@ start because the display driver is a process that needs a compositor. | **H0** | Mechanisms 2, 3 and 4, each with a test that exercises it from a userspace program against a device nothing else is using. | done | | **H1** | A userspace virtio-gpu driver, publishing `display_ops` across the channel. Schcom talks to it and does not know the difference. | done, except that Schcom does not talk to it -- `cmd/vgpud` holds the device and draws its own frame | | **H2** | Kill the driver process on purpose. The screen falls back to VBE, the driver restarts, the compositor reattaches. **This is the milestone that proves the whole idea**, and it is the one that cannot be skipped. | done, less the compositor -- see "The driver came back" | -| **H3** | A modeset API, which is the thing an out-of-kernel GPU driver makes affordable and an in-kernel one never got. | | -| **H4** | USB, same shape. | | +| **H3** | A modeset API, which is the thing an out-of-kernel GPU driver makes affordable and an in-kernel one never got. | done -- see "A modeset, from a process" | +| **H4** | USB, same shape. | the one left, and much the largest | --- diff --git a/include/virtio.h b/include/virtio.h index c2ed601..3d1f61a 100644 --- a/include/virtio.h +++ b/include/virtio.h @@ -158,6 +158,15 @@ struct virtio_gpu_resource_create_2d { uint32 height; } __attribute__((packed)); +// Releasing a resource, which a modeset has to do before it can create one of +// a different size. The backing pages are detached with it; they belong to the +// driver and are not freed by this. +struct virtio_gpu_resource_unref { + struct virtio_gpu_ctrl_hdr hdr; + uint32 resource_id; + uint32 padding; +} __attribute__((packed)); + struct virtio_gpu_mem_entry { uint64 addr; uint32 length;