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
|
|
@ -171,7 +171,12 @@ typedef struct {
|
|||
void *OpenProtocolInformation;
|
||||
|
||||
void *ProtocolsPerHandle;
|
||||
void *LocateHandleBuffer;
|
||||
/* SearchType 2 is ByProtocol: every handle that speaks a given protocol,
|
||||
* which is how the loader finds the displays LocateProtocol does not
|
||||
* return -- it answers with the first one and there is no second call. */
|
||||
EFI_STATUS (*LocateHandleBuffer)(UINTN SearchType, EFI_GUID *Protocol,
|
||||
void *SearchKey, UINTN *NoHandles,
|
||||
EFI_HANDLE **Buffer);
|
||||
EFI_STATUS (*LocateProtocol)(EFI_GUID *Protocol, void *Registration,
|
||||
void **Interface);
|
||||
} EFI_BOOT_SERVICES;
|
||||
|
|
@ -307,10 +312,26 @@ typedef EFI_STATUS (*EFI_GOP_QUERY_MODE)(
|
|||
typedef EFI_STATUS (*EFI_GOP_SET_MODE)(
|
||||
struct EFI_GRAPHICS_OUTPUT_PROTOCOL *This, UINT32 ModeNumber);
|
||||
|
||||
/* Blt is how you draw on a GOP that has no linear framebuffer to write into --
|
||||
* and there is one of those on every machine with a virtio-gpu, because the
|
||||
* device has no aperture. It is also the only way to put pixels on a *second*
|
||||
* display: the loader draws into the primary's framebuffer directly and then
|
||||
* hands that same memory to the other GOP's Blt as a source buffer. */
|
||||
struct EFI_GRAPHICS_OUTPUT_PROTOCOL;
|
||||
#define ByProtocol 2
|
||||
#define EfiBltVideoFill 0
|
||||
#define EfiBltBufferToVideo 2
|
||||
|
||||
typedef EFI_STATUS (*EFI_GOP_BLT)(struct EFI_GRAPHICS_OUTPUT_PROTOCOL *This,
|
||||
void *BltBuffer, UINTN BltOperation,
|
||||
UINTN SourceX, UINTN SourceY,
|
||||
UINTN DestinationX, UINTN DestinationY,
|
||||
UINTN Width, UINTN Height, UINTN Delta);
|
||||
|
||||
typedef struct EFI_GRAPHICS_OUTPUT_PROTOCOL {
|
||||
EFI_GOP_QUERY_MODE QueryMode;
|
||||
EFI_GOP_SET_MODE SetMode;
|
||||
void *Blt;
|
||||
EFI_GOP_BLT Blt;
|
||||
EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE *Mode;
|
||||
} EFI_GRAPHICS_OUTPUT_PROTOCOL;
|
||||
|
||||
|
|
|
|||
|
|
@ -79,6 +79,8 @@ static EFI_BOOT_SERVICES *BS;
|
|||
// Console. Enough to say what went wrong, and no more.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static void mirrors_show(void); /* see "the other displays", further down */
|
||||
|
||||
/* Once the splash is up, nothing may write to the console.
|
||||
*
|
||||
* ConOut on a graphical firmware renders text into the same framebuffer the
|
||||
|
|
@ -412,6 +414,7 @@ load_elf(EFI_FILE_PROTOCOL *file)
|
|||
|
||||
done += ph.p_memsz;
|
||||
ui_progress_tick((ui_u32)done);
|
||||
mirrors_show(); /* once per segment: a handful of times, not a loop */
|
||||
|
||||
if (ph.p_paddr < lowest) lowest = ph.p_paddr;
|
||||
if (ph.p_paddr + ph.p_memsz > highest) highest = ph.p_paddr + ph.p_memsz;
|
||||
|
|
@ -606,6 +609,7 @@ res_read_number(const char *prompt)
|
|||
|
||||
const char *rows[1] = { shown };
|
||||
ui_menu("Custom resolution", rows, 1, 0, "digits, Enter to accept");
|
||||
mirrors_show();
|
||||
|
||||
EFI_INPUT_KEY key;
|
||||
for (;;) {
|
||||
|
|
@ -623,6 +627,8 @@ res_read_number(const char *prompt)
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
static void
|
||||
res_choose(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
|
||||
{
|
||||
|
|
@ -654,6 +660,7 @@ res_choose(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
|
|||
serial_item(items, sel, sel, 0);
|
||||
}
|
||||
echoed = sel;
|
||||
mirrors_show();
|
||||
|
||||
EFI_INPUT_KEY key;
|
||||
for (;;) {
|
||||
|
|
@ -702,6 +709,7 @@ res_choose(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
|
|||
* and letting the user wonder whether the key registered. */
|
||||
const char *msg[1] = { "No such mode -- keeping this one" };
|
||||
ui_menu("Resolution", msg, 1, 0, "any key to go back");
|
||||
mirrors_show();
|
||||
for (;;) {
|
||||
if (ST->ConIn &&
|
||||
!EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key)))
|
||||
|
|
@ -905,6 +913,7 @@ sys_info(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
|
|||
serial_puts("\n");
|
||||
}
|
||||
serial_puts("Any key to go back\n");
|
||||
mirrors_show();
|
||||
|
||||
EFI_INPUT_KEY key;
|
||||
for (;;) {
|
||||
|
|
@ -955,6 +964,7 @@ boot_menu(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
|
|||
serial_item(items, selected, selected, 1);
|
||||
}
|
||||
echoed = selected;
|
||||
mirrors_show();
|
||||
|
||||
EFI_INPUT_KEY key;
|
||||
CHAR16 c = 0;
|
||||
|
|
@ -988,6 +998,83 @@ boot_menu(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
|
|||
}
|
||||
}
|
||||
|
||||
/* --- the other displays -----------------------------------------------------
|
||||
*
|
||||
* A machine with two adapters has two screens, and the loader drew on one of
|
||||
* them. The other was black through the splash, black through the F8 menu, and
|
||||
* black until vgpud started several seconds into userspace -- which from the
|
||||
* outside is indistinguishable from a display that does not work.
|
||||
*
|
||||
* LocateProtocol() answers with the first GOP and offers no way to ask for the
|
||||
* rest, so the rest are found by handle. Each one is then fed the *primary's
|
||||
* framebuffer* as a Blt source: the loader keeps drawing into one piece of
|
||||
* memory exactly as before, and this copies it outward at the points where a
|
||||
* screen is finished.
|
||||
*
|
||||
* Blt rather than a second framebuffer to draw into, because a virtio GOP does
|
||||
* not have one -- it reports a FrameBufferBase of zero and implements Blt and
|
||||
* nothing else. Which is the same fact that made a virtio-only machine
|
||||
* reboot-loop; see kernel/core/main.c.
|
||||
*/
|
||||
#define MAX_MIRRORS 4
|
||||
static EFI_GRAPHICS_OUTPUT_PROTOCOL *mirror_gop[MAX_MIRRORS];
|
||||
static EFI_GRAPHICS_OUTPUT_PROTOCOL *primary_gop;
|
||||
static UINTN mirror_n;
|
||||
|
||||
static void
|
||||
mirrors_find(EFI_GRAPHICS_OUTPUT_PROTOCOL *primary)
|
||||
{
|
||||
EFI_GUID gop_guid = EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID;
|
||||
EFI_HANDLE *handles = 0;
|
||||
UINTN count = 0;
|
||||
|
||||
primary_gop = primary;
|
||||
|
||||
if (!BS->LocateHandleBuffer ||
|
||||
EFI_ERROR(BS->LocateHandleBuffer(ByProtocol, &gop_guid, 0,
|
||||
&count, &handles)) || !handles)
|
||||
return;
|
||||
|
||||
for (UINTN i = 0; i < count && mirror_n < MAX_MIRRORS; i++) {
|
||||
EFI_GRAPHICS_OUTPUT_PROTOCOL *g = 0;
|
||||
if (EFI_ERROR(BS->HandleProtocol(handles[i], &gop_guid, (void **)&g)))
|
||||
continue;
|
||||
if (!g || g == primary || !g->Blt || !g->Mode || !g->Mode->Info)
|
||||
continue;
|
||||
mirror_gop[mirror_n++] = g;
|
||||
}
|
||||
}
|
||||
|
||||
/* Put what is on the primary onto all of them. Centred when the sizes differ,
|
||||
* cropped when the other display is smaller -- the same rules the kernel's
|
||||
* console mirror and vgpud's both use, because it is the same picture. */
|
||||
static void
|
||||
mirrors_show(void)
|
||||
{
|
||||
EFI_GRAPHICS_OUTPUT_PROTOCOL *primary = primary_gop;
|
||||
|
||||
if (!mirror_n || !primary || !primary->Mode || !primary->Mode->Info)
|
||||
return;
|
||||
if (!primary->Mode->FrameBufferBase)
|
||||
return; /* nothing to copy *from* */
|
||||
|
||||
UINTN sw = primary->Mode->Info->HorizontalResolution;
|
||||
UINTN sh = primary->Mode->Info->VerticalResolution;
|
||||
UINTN delta = (UINTN)primary->Mode->Info->PixelsPerScanLine * 4;
|
||||
|
||||
for (UINTN i = 0; i < mirror_n; i++) {
|
||||
EFI_GRAPHICS_OUTPUT_PROTOCOL *g = mirror_gop[i];
|
||||
UINTN w = g->Mode->Info->HorizontalResolution;
|
||||
UINTN h = g->Mode->Info->VerticalResolution;
|
||||
UINTN cw = w < sw ? w : sw;
|
||||
UINTN ch = h < sh ? h : sh;
|
||||
|
||||
g->Blt(g, (void *)(UINTN)primary->Mode->FrameBufferBase,
|
||||
EfiBltBufferToVideo, 0, 0, (w - cw) / 2, (h - ch) / 2,
|
||||
cw, ch, delta);
|
||||
}
|
||||
}
|
||||
|
||||
/* Wait briefly for F8, exactly as the BIOS loader does, and show the same
|
||||
* menu if it comes. One prompt, one key, one screen -- the resolution chooser
|
||||
* lives in that menu rather than in a second prompt of its own.
|
||||
|
|
@ -1027,6 +1114,11 @@ setup_framebuffer(void)
|
|||
return 0;
|
||||
}
|
||||
|
||||
// The other displays, before anything is drawn: the F8 menu is the first
|
||||
// thing on screen and there is no reason for it to be the one screen that
|
||||
// does not reach them.
|
||||
mirrors_find(gop);
|
||||
|
||||
// F8 first, on whatever mode the firmware left us in, because the menu has
|
||||
// to be visible in order to offer a different one. Anything it changes is
|
||||
// reflected below, which is why nothing is read from gop->Mode until after.
|
||||
|
|
@ -1070,6 +1162,7 @@ setup_framebuffer(void)
|
|||
|
||||
ui_init(&uifb);
|
||||
ui_splash(0);
|
||||
mirrors_show();
|
||||
|
||||
// From here on, diagnostics go to COM1 rather than the console, or each one
|
||||
// is drawn across the splash.
|
||||
|
|
|
|||
Loading…
Reference in a new issue