gfx: the second head shows the boot, not just what comes after it
Two things, both asked for. **The crash report no longer says which monitors it reached.** "The report is on the virtio-gpu display too" was a line about the crash screen printed on the crash screen. The report says what happened and where to read it again; that is not part of it. **And the second head now shows the F8 menu and the boot.** It used to be black until vgpud started several seconds into userspace -- through the splash, through the menu, through every boot message -- which from the outside is indistinguishable from a display that does not work. Three stages now cover the whole boot and hand over to each other without coordinating: *The loader.* LocateProtocol() answers with the first GOP and offers no way to ask for the rest, so the rest are found by handle and fed the primary's framebuffer as a Blt source at the points where a screen is finished: the menu, the resolution chooser, system information, the splash, each segment of the kernel load. Blt rather than a second framebuffer to draw into, because a virtio GOP does not have one -- the same fact that made a virtio-only machine reboot-loop two commits ago. *The kernel.* gfx remembers which rows were drawn, as one span rather than a list of rectangles, because console output is lines and a span is what lines make. display_mirror_console() copies that span across: a line of boot text is one cell-row transfer, not a screen. Primed once after pci_init(), because a display that has just appeared has a black scanout and the console it should be showing was drawn by the loader into a framebuffer it knows nothing about. *Then vgpud*, from the same console framebuffer, unchanged. **Two bugs found building it, both worth the retelling.** The mirror ran per character from inside vt_feed -- which is inside the console lock -- and vga_disp_acquire() announces its back buffer with a cprintf. A CPU taking a lock it already holds, with a call chain that said exactly that. It runs at the end of consolewrite() and cprintf() now, outside the lock, which is also the better batching: one transfer per write instead of one per character. And "is this the console's own screen" was asked of the pointer acquire() returned. vga_fb hands back a back buffer it copies onto the console's framebuffer afterwards, so on a one-screen machine the answer was "a different screen" -- and the console was mirrored onto itself through two megabytes of intermediate, allocated for the purpose. display_ops grew scanout_phys, which is a fact about the hardware rather than a policy a driver can get subtly wrong, and it is asked first: it has no side effects and acquire() has two. Verified with two heads under QEMU, screendumping the virtio head every 400 ms from power-on: splash, splash, splash, then the console, and it stays. The F8 menu appears on it; choosing 1024x768 there brings both heads up at 1024x768 byte-identical. A panic still reaches both. Both supported configurations: one boot each, no traps, no panics, socktest and memfdtest green. One gap remains and is documented rather than hidden: while the kernel's driver resets the device during probe, QEMU shows "Display output is not active" for a fraction of a second. A device being taken over has no scanout until the new driver sets one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
e72205cdc0
commit
182cc76588
14 changed files with 437 additions and 42 deletions
|
|
@ -18,6 +18,7 @@
|
|||
#include "netdev.h"
|
||||
#include "theme.h"
|
||||
#include "text.h"
|
||||
#include "spinlock.h"
|
||||
#include "gfx.h"
|
||||
#include "pci.h"
|
||||
#include "mmu.h"
|
||||
|
|
@ -181,6 +182,18 @@ display_present(int x, int y, int w, int h)
|
|||
((struct display_ops *)d->ops)->present(d, x, y, w, h);
|
||||
}
|
||||
|
||||
// Where this display's pixels actually end up. 0 when the driver does not say,
|
||||
// which callers must read as "assume it is the console's" -- see the comment
|
||||
// on scanout_phys in include/display.h.
|
||||
uint64
|
||||
display_scanout_phys(void)
|
||||
{
|
||||
struct device *d = display_get();
|
||||
if (!d || !d->ops || !((struct display_ops *)d->ops)->scanout_phys)
|
||||
return 0;
|
||||
return ((struct display_ops *)d->ops)->scanout_phys(d);
|
||||
}
|
||||
|
||||
void
|
||||
display_wait_vblank(void)
|
||||
{
|
||||
|
|
@ -219,8 +232,14 @@ display_wait_vblank(void)
|
|||
// mirror uses: the overlap, centred if the surface is larger, top-left if it
|
||||
// is smaller. Both sides are XRGB8888; anything else is refused by whoever
|
||||
// registered it.
|
||||
static void
|
||||
panic_copy_console(volatile uchar *dst, int dw, int dh, uint32 dstride)
|
||||
// Copy rows [r0, r1] of the console framebuffer into a surface, with the same
|
||||
// rules vgpud's mirror uses: the overlap, centred if the surface is larger,
|
||||
// top-left if it is smaller. Both sides are XRGB8888; anything else is refused
|
||||
// by whoever registered it. Returns the destination row span it wrote, so the
|
||||
// caller can present exactly that.
|
||||
static int
|
||||
copy_console_rows(volatile uchar *dst, int dw, int dh, uint32 dstride,
|
||||
int r0, int r1, int *out_y, int *out_h)
|
||||
{
|
||||
const volatile uchar *src = gfx_fb_ptr();
|
||||
int sw = (int)gfx_width(), sh = (int)gfx_height();
|
||||
|
|
@ -229,15 +248,130 @@ panic_copy_console(volatile uchar *dst, int dw, int dh, uint32 dstride)
|
|||
int ch = sh < dh ? sh : dh;
|
||||
int x0 = (dw - cw) / 2, y0 = (dh - ch) / 2;
|
||||
|
||||
if (!src || gfx_fb_bpp() != 32)
|
||||
return;
|
||||
if (!src || gfx_fb_bpp() != 32 || cw <= 0 || ch <= 0)
|
||||
return 0;
|
||||
if (r0 < 0) r0 = 0;
|
||||
if (r1 > ch - 1) r1 = ch - 1;
|
||||
if (r1 < r0)
|
||||
return 0;
|
||||
|
||||
for (int y = 0; y < ch; y++) {
|
||||
for (int y = r0; y <= r1; y++) {
|
||||
const volatile uint32 *s = (const volatile uint32 *)(src + (uint64)y * sstride);
|
||||
volatile uint32 *d = (volatile uint32 *)(dst + (uint64)(y0 + y) * dstride);
|
||||
for (int x = 0; x < cw; x++)
|
||||
d[x0 + x] = s[x];
|
||||
}
|
||||
*out_y = y0 + r0;
|
||||
*out_h = r1 - r0 + 1;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void
|
||||
panic_copy_console(volatile uchar *dst, int dw, int dh, uint32 dstride)
|
||||
{
|
||||
int y, h;
|
||||
copy_console_rows(dst, dw, dh, dstride, 0, dh, &y, &h);
|
||||
}
|
||||
|
||||
// --- the console, on a second screen ---------------------------------------
|
||||
//
|
||||
// A machine with two displays shows the console on one of them and whatever
|
||||
// the other device happened to be left holding on the other -- which during
|
||||
// boot is nothing at all: a black rectangle next to the machine starting up.
|
||||
//
|
||||
// So the rows the console draws are copied across as they are drawn. Cheap
|
||||
// because gfx remembers which rows those were: a line of boot text is one
|
||||
// cell-row transfer, not a screen.
|
||||
//
|
||||
// This is the same job vgpud does from userspace, and the two do not overlap.
|
||||
// While the kernel drives the second GPU this runs; the moment vgpud claims
|
||||
// it, display_acquire() returns nothing here and vgpud's mirror takes over
|
||||
// from the same console framebuffer. The handover needs no coordination
|
||||
// because neither side owns anything the other wants.
|
||||
void
|
||||
display_mirror_console(void)
|
||||
{
|
||||
// cprintf() from inside the display driver would come straight back here
|
||||
// and present again, for ever.
|
||||
static int inside;
|
||||
// And two terminals on two CPUs would interleave into one command queue.
|
||||
// The lock is held across the copy and the present, which is microseconds of
|
||||
// polled virtio and the same thing the panic path does with everything else
|
||||
// already stopped.
|
||||
static struct spinlock lk;
|
||||
static int lk_init;
|
||||
// Which buffer was last brought up to date; see below.
|
||||
static volatile uchar *last_buf;
|
||||
struct display_mode m;
|
||||
uint32 stride = 0;
|
||||
volatile uchar *buf;
|
||||
int r0, r1, y, h;
|
||||
|
||||
if (inside || !gfx_ready())
|
||||
return;
|
||||
|
||||
// Is there a second screen at all? Asked first, and asked of the *scanout*,
|
||||
// because both halves of that matter.
|
||||
//
|
||||
// Of the scanout, because the buffer acquire() returns is not the same
|
||||
// thing: vga_fb hands back a back buffer it copies onto the console's own
|
||||
// framebuffer afterwards, so comparing pointers said "a different screen"
|
||||
// about the only screen there is -- and then mirrored the console onto
|
||||
// itself through two megabytes of intermediate.
|
||||
//
|
||||
// First, because this question has no side effects and acquire() has two:
|
||||
// it allocates a back buffer on first call, and it announces it with a
|
||||
// cprintf -- which comes straight back here. On a one-screen machine both
|
||||
// were pure waste, and the cprintf was a panic.
|
||||
uint64 phys = display_scanout_phys();
|
||||
if (phys == 0 || phys == gfx_fb_phys())
|
||||
return;
|
||||
|
||||
if (!lk_init) {
|
||||
initlock(&lk, "dispmirror");
|
||||
lk_init = 1;
|
||||
}
|
||||
acquire(&lk);
|
||||
// Set before the first call into the driver, not after: a driver may
|
||||
// cprintf, cprintf comes back here, and the second entry would block on a
|
||||
// lock its own call chain is holding.
|
||||
inside = 1;
|
||||
buf = display_acquire(&stride);
|
||||
|
||||
if (!buf || display_mode(&m) != 0 || m.bpp != 32) {
|
||||
inside = 0;
|
||||
release(&lk);
|
||||
return;
|
||||
}
|
||||
|
||||
// A buffer we have not written before is owed the whole screen, not the
|
||||
// damage since last time.
|
||||
//
|
||||
// This is what a handover looks like from here: the loader draws the splash
|
||||
// straight into the framebuffer, the kernel's display driver probes some
|
||||
// milliseconds later with a black scanout of its own, and gfx has no damage
|
||||
// to report because gfx did not draw any of it. Without this the second head
|
||||
// went black at that moment and filled in a line at a time as output
|
||||
// arrived. It happens once per driver, and the same rule covers the device
|
||||
// coming back from a userspace driver that exited.
|
||||
int have = gfx_damage_take(&r0, &r1);
|
||||
if (buf != last_buf) {
|
||||
last_buf = buf;
|
||||
r0 = 0;
|
||||
r1 = (int)m.height - 1;
|
||||
have = 1;
|
||||
}
|
||||
if (!have) {
|
||||
inside = 0;
|
||||
release(&lk);
|
||||
return;
|
||||
}
|
||||
|
||||
if (copy_console_rows(buf, m.width, m.height, stride ? stride : m.stride,
|
||||
r0, r1, &y, &h))
|
||||
display_present(0, y, m.width, h);
|
||||
inside = 0;
|
||||
release(&lk);
|
||||
}
|
||||
|
||||
void
|
||||
|
|
@ -252,15 +386,14 @@ display_panic_present(void)
|
|||
{
|
||||
struct display_mode m;
|
||||
uint32 stride = 0;
|
||||
struct device *d = display_get();
|
||||
volatile uchar *buf = display_acquire(&stride);
|
||||
|
||||
if (buf && display_mode(&m) == 0 && m.bpp == 32 &&
|
||||
(uint64)buf != (uint64)gfx_fb_ptr()) {
|
||||
uint64 phys = display_scanout_phys();
|
||||
|
||||
if (buf && phys && phys != gfx_fb_phys() &&
|
||||
display_mode(&m) == 0 && m.bpp == 32) {
|
||||
panic_copy_console(buf, m.width, m.height, stride ? stride : m.stride);
|
||||
display_present(0, 0, m.width, m.height);
|
||||
cprintf(" The report is on the %s display too.\n",
|
||||
d ? d->name : "second");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -283,8 +416,7 @@ display_panic_present(void)
|
|||
continue;
|
||||
|
||||
panic_copy_console((volatile uchar *)P2V(paddr), w, h, stride);
|
||||
if (drv->panic_present((struct pci_dev *)pci_get(i), paddr, w, h) == 0)
|
||||
cprintf(" The report is on the %s display too.\n", drv->name);
|
||||
drv->panic_present((struct pci_dev *)pci_get(i), paddr, w, h);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue