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:
Nexoniarz 2026-09-14 01:36:05 +02:00
commit ef3e191c6f
7 changed files with 467 additions and 21 deletions

109
cmd/schmode/main.c Normal file
View 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;
}

View file

@ -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 <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 -------------------------------------------------------------------
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);
}
}