From 38ea83bf0529156656a4a783bf589277de74d1de Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Tue, 22 Sep 2026 16:21:08 +0200 Subject: [PATCH] vgpud: the head follows the F8 resolution, not the window "virtio gpu doesn't follow f8 menu default resolution" -- it did not, and now that the head mirrors the console that is visible rather than academic. vgpud sized itself from GET_DISPLAY_INFO, which answers with the size of the *host's window*: a number set by dragging a corner, that says nothing about the machine and everything about QEMU. Meanwhile the resolution the F8 menu offered at boot is the one that was actually chosen, and it is the console's. So picking 1024x768 at the menu gave a second display still at 1280x800, with the console letterboxed inside it on the theme ground -- the right pixels in the wrong mode, on a display that could simply have been the right size. console_open() now runs *before* bring_up(), and bring_up() takes the console's mode when there is a console. That ordering is the whole fix and it is not cosmetic: the DMA allocation is sized from those numbers and fixed for the life of the process, so a mode chosen after the fact could only ever be smaller than the window already paid for. A machine with no console to mirror brings up whatever the device reports, exactly as before. Verified by driving the F8 menu over the serial line -- 'o', Resolution, 1024x768, boot -- and screendumping both heads over QMP: both come up 1024x768 and are byte-identical, where the virtio head used to stay 1280x800. `schmode 1024 768` from a 1280x800 boot still works. Both supported configurations: one boot each, no traps, no panics, socktest and memfdtest green. Co-Authored-By: Claude Opus 5 --- cmd/vgpud/main.c | 53 ++++++++++++++++++++++++++++++----- docs/architecture/graphics.md | 18 ++++++++++++ 2 files changed, 64 insertions(+), 7 deletions(-) diff --git a/cmd/vgpud/main.c b/cmd/vgpud/main.c index 8d928b7..d2c1f16 100644 --- a/cmd/vgpud/main.c +++ b/cmd/vgpud/main.c @@ -366,6 +366,14 @@ static unsigned long long fb_uaddr, fb_paddr; 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 +// The console, learned by console_open() and read by bring_up() one line +// before it picks a mode. Declared up here rather than beside the mirror +// because that ordering -- the console decides the mode, not the other way +// round -- is the whole of why the head comes up the size the F8 menu asked +// for. +static volatile unsigned char *con_fb; // the console's framebuffer +static unsigned con_w, con_h, con_stride; + // 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 @@ -426,6 +434,30 @@ bring_up(void) vputs("vgpud: display is "); vputn(width); vputs("x"); vputn(height); vputs("\n"); + // But the console's mode wins, when there is a console. + // + // GET_DISPLAY_INFO answers with the size of the *host's window*, which the + // person running QEMU drags to whatever they like and which has nothing to + // do with the machine. The resolution the F8 menu offered at boot is the + // one that was actually chosen, and it is the console's -- so a head that + // mirrors the console and ignores that choice shows the right pixels in + // the wrong mode, letterboxed or cropped, on a display that could simply + // have been the right size. + // + // This is also the only chance to get it right without a second modeset: + // the DMA allocation below is sized from these numbers and fixed for the + // life of the process, so asking for the console's size here is what makes + // `schmode` able to reach it later. + if (con_w && con_h) { + if ((int)con_w != width || (int)con_h != height) { + vputs("vgpud: following the console at "); + vputn((int)con_w); vputs("x"); vputn((int)con_h); + vputs(" rather than the window\n"); + } + width = (int)con_w; + height = (int)con_h; + } + // The framebuffer is claimed **before** the resource is created, because // the resource has to be created at a size we can actually back. // @@ -660,8 +692,6 @@ draw_mark(void) #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 @@ -687,12 +717,15 @@ mirror_geometry(void) mirror_full = 1; } -// Returns 0 if there is a console to mirror. Every failure here is a reason to +// Learn the console: its size, its format, and where its pixels are. +// +// Called *before* the device is brought up, because the answer decides what +// mode to bring it up in -- see bring_up(). 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) +console_open(void) { struct fb_var_screeninfo var; struct fb_fix_screeninfo fix; @@ -729,7 +762,6 @@ mirror_open(void) 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 } @@ -1065,8 +1097,15 @@ main(int argc, char *argv[]) if (!common || !notify) die("no modern virtio capabilities -- not a 1.0 device"); + // Before bring_up(), which needs to know the console's size to pick a + // mode. A machine with no console to mirror brings up whatever the device + // reports, as it always did. + int mirroring = console_open() == 0; + bring_up(); draw_mark(); + if (mirroring) + mirror_geometry(); present(); vputs("vgpud: pid "); @@ -1075,8 +1114,8 @@ main(int argc, char *argv[]) // 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) { + // around the console when the two sizes do not match. + if (mirroring) { mirror_pass(); vputs("vgpud: mirroring the console\n"); } diff --git a/docs/architecture/graphics.md b/docs/architecture/graphics.md index 20eed21..ee69952 100644 --- a/docs/architecture/graphics.md +++ b/docs/architecture/graphics.md @@ -150,6 +150,24 @@ non-integer ratio is text you cannot read. 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. +### The F8 resolution is the head's resolution + +`vgpud` used to size itself from `GET_DISPLAY_INFO`, which answers with the +size of the *host's window* — a number the person running QEMU sets by dragging +a corner, and which says nothing about the machine. The resolution the F8 menu +offered at boot is the one that was actually chosen, and it is the console's. + +A head that mirrors the console and ignores that choice shows the right pixels +in the wrong mode: pick 1024×768 at the menu and the second display stayed at +whatever the window was, with the console letterboxed inside it. So +`console_open()` runs *before* `bring_up()`, and the mode comes from the +console when there is one. + +That ordering is the whole of it, and it is not cosmetic: the DMA allocation is +sized from those numbers and fixed for the life of the process, so a mode +chosen after the fact could only ever be smaller than the window that was +already paid for. + ### /run is not a tmpfs Found while testing the above, and older than it: `schmode` reported *"cannot