hybrid: a modeset, from a process
H3 of docs/plan/hybrid.md -- "a modeset API, which is the thing an
out-of-kernel GPU driver makes affordable and an in-kernel one never got".
# schmode modes
1280x800 *
1280x720
1024x768
800x600
640x480
# schmode 1024 768
1024x768 ok
Captured off the virtio-gpu head, the frames really come back 1280x800,
1024x768, 640x480 and 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. The comparison is the point -- the same five commands in the
kernel would have been the same five commands *and a reason to be nervous*,
because a wrong one there takes the machine instead of one process. That is
why the in-kernel driver never grew one: not difficulty, exposure.
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 larger mode is refused and says why.
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 the
mistake this plan warns about. That was right about a *compositor protocol*
and wrong as a reason to have no socket: a modeset has a consumer that
exists, somebody changing the mode and looking at the screen, and schmode
is small enough to judge an interface this small. What is deliberately not
on that socket is everything a compositor would need -- no buffer
submission, no damage, no vblank -- which still waits, for the original
reason.
H4, the USB eviction, is now the only milestone left on the plan and is by
some distance the largest thing on the register.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
This commit is contained in:
parent
415df0fd4d
commit
ef3e191c6f
7 changed files with 467 additions and 21 deletions
2
Makefile
2
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/.
|
# Third-party software does NOT go here — see ports/.
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
ULIB = $(addprefix $(BUILD)/lib/, start.o glue.o usys.o)
|
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))
|
CMDBINS = $(addprefix $(BUILD)/cmd/, $(CMDS))
|
||||||
|
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
|
|
|
||||||
109
cmd/schmode/main.c
Normal file
109
cmd/schmode/main.c
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
285
cmd/vgpud/main.c
285
cmd/vgpud/main.c
|
|
@ -27,6 +27,7 @@
|
||||||
#include "pciuser.h"
|
#include "pciuser.h"
|
||||||
#include "virtio.h"
|
#include "virtio.h"
|
||||||
#include "schinit.h"
|
#include "schinit.h"
|
||||||
|
#include "socket.h"
|
||||||
|
|
||||||
// --- small output, since lib/ has no printf ---------------------------------
|
// --- small output, since lib/ has no printf ---------------------------------
|
||||||
|
|
||||||
|
|
@ -313,9 +314,16 @@ resp_ok(void)
|
||||||
|
|
||||||
static int width, height;
|
static int width, height;
|
||||||
static unsigned long long fb_uaddr, fb_paddr;
|
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
|
static void
|
||||||
bring_up(void)
|
bring_up(void)
|
||||||
|
|
@ -373,7 +381,7 @@ bring_up(void)
|
||||||
struct virtio_gpu_resource_create_2d *c =
|
struct virtio_gpu_resource_create_2d *c =
|
||||||
(struct virtio_gpu_resource_create_2d *)cmdbuf;
|
(struct virtio_gpu_resource_create_2d *)cmdbuf;
|
||||||
hdr_init(c, VIRTIO_GPU_CMD_RESOURCE_CREATE_2D);
|
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->format = VIRTIO_GPU_FORMAT_B8G8R8X8_UNORM;
|
||||||
c->width = (unsigned int)width;
|
c->width = (unsigned int)width;
|
||||||
c->height = (unsigned int)height;
|
c->height = (unsigned int)height;
|
||||||
|
|
@ -394,11 +402,13 @@ bring_up(void)
|
||||||
}
|
}
|
||||||
fb_uaddr = d.uaddr;
|
fb_uaddr = d.uaddr;
|
||||||
fb_paddr = d.paddr;
|
fb_paddr = d.paddr;
|
||||||
|
max_w = width;
|
||||||
|
max_h = height;
|
||||||
|
|
||||||
struct virtio_gpu_resource_attach_backing *a =
|
struct virtio_gpu_resource_attach_backing *a =
|
||||||
(struct virtio_gpu_resource_attach_backing *)cmdbuf;
|
(struct virtio_gpu_resource_attach_backing *)cmdbuf;
|
||||||
hdr_init(a, VIRTIO_GPU_CMD_RESOURCE_ATTACH_BACKING);
|
hdr_init(a, VIRTIO_GPU_CMD_RESOURCE_ATTACH_BACKING);
|
||||||
a->resource_id = RESOURCE_ID;
|
a->resource_id = resource_id;
|
||||||
a->nr_entries = 1;
|
a->nr_entries = 1;
|
||||||
// The entries follow the struct rather than being in it: the count is
|
// 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.
|
// 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.width = (unsigned int)width;
|
||||||
so->r.height = (unsigned int)height;
|
so->r.height = (unsigned int)height;
|
||||||
so->scanout_id = 0;
|
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())
|
if (docmd(sizeof(*so), sizeof(struct virtio_gpu_ctrl_hdr)) < 0 || !resp_ok())
|
||||||
die("SET_SCANOUT refused");
|
die("SET_SCANOUT refused");
|
||||||
|
|
||||||
|
|
@ -438,7 +448,7 @@ present(void)
|
||||||
t->r.width = (unsigned int)width;
|
t->r.width = (unsigned int)width;
|
||||||
t->r.height = (unsigned int)height;
|
t->r.height = (unsigned int)height;
|
||||||
t->offset = 0;
|
t->offset = 0;
|
||||||
t->resource_id = RESOURCE_ID;
|
t->resource_id = resource_id;
|
||||||
t->padding = 0;
|
t->padding = 0;
|
||||||
if (docmd(sizeof(*t), sizeof(struct virtio_gpu_ctrl_hdr)) < 0)
|
if (docmd(sizeof(*t), sizeof(struct virtio_gpu_ctrl_hdr)) < 0)
|
||||||
die("TRANSFER_TO_HOST_2D never answered");
|
die("TRANSFER_TO_HOST_2D never answered");
|
||||||
|
|
@ -449,7 +459,7 @@ present(void)
|
||||||
f->r.x = f->r.y = 0;
|
f->r.x = f->r.y = 0;
|
||||||
f->r.width = (unsigned int)width;
|
f->r.width = (unsigned int)width;
|
||||||
f->r.height = (unsigned int)height;
|
f->r.height = (unsigned int)height;
|
||||||
f->resource_id = RESOURCE_ID;
|
f->resource_id = resource_id;
|
||||||
f->padding = 0;
|
f->padding = 0;
|
||||||
if (docmd(sizeof(*f), sizeof(struct virtio_gpu_ctrl_hdr)) < 0)
|
if (docmd(sizeof(*f), sizeof(struct virtio_gpu_ctrl_hdr)) < 0)
|
||||||
die("RESOURCE_FLUSH never answered");
|
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 <width> <height>\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 <w> <h> | 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 -------------------------------------------------------------------
|
// --- main -------------------------------------------------------------------
|
||||||
|
|
||||||
int
|
int
|
||||||
|
|
@ -570,17 +798,42 @@ main(int argc, char *argv[])
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hold the device and wait.
|
// Hold the device, and answer.
|
||||||
//
|
//
|
||||||
// Mechanism 1 exists now, so what belongs here eventually is an AF_UNIX
|
// This used to sleep for ever, with a comment arguing that building a
|
||||||
// socket a compositor connects to. It is not built, and building it before
|
// control socket before there is a compositor to judge it is the mistake
|
||||||
// there is a compositor to judge it is the mistake this plan spends a
|
// the plan warns about. That argument was right about a *compositor*
|
||||||
// section warning about -- so this sleeps instead, and holding the device
|
// protocol and wrong as a reason to have no socket at all: H3 is a modeset
|
||||||
// is the honest state: releasing it would put the console back and make it
|
// API, and a modeset has a consumer that exists -- somebody changing the
|
||||||
// look like none of this ever worked.
|
// 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
|
// 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.
|
// restarts this process, and the device has to come back with it.
|
||||||
for (;;)
|
int srv = control_socket();
|
||||||
sleep(100);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
**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.
|
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
|
### Taking it back
|
||||||
|
|
||||||
A driver that dies is the case this whole arrangement exists for, so release is a
|
A driver that dies is the case this whole arrangement exists for, so release is a
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@ The galling ones. The work exists, is tested, and nothing uses it.
|
||||||
| | Where it stopped |
|
| | 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.** |
|
| **`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. |
|
| **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. |
|
| **`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. |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
line, the rate and the neighbour without anyone having to look.
|
||||||
|
|
||||||
**All four mechanisms now exist.** What remains on this plan is the second
|
**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
|
### 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
|
## The mechanisms
|
||||||
|
|
||||||
Four, and only the fourth is large.
|
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 |
|
| **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 |
|
| **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" |
|
| **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. | |
|
| **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. | |
|
| **H4** | USB, same shape. | the one left, and much the largest |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,15 @@ struct virtio_gpu_resource_create_2d {
|
||||||
uint32 height;
|
uint32 height;
|
||||||
} __attribute__((packed));
|
} __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 {
|
struct virtio_gpu_mem_entry {
|
||||||
uint64 addr;
|
uint64 addr;
|
||||||
uint32 length;
|
uint32 length;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue