boot: mirroring cannot stall the machine, and the loader says where it is
Two reports from an Asus TUF FA607NU, which now gets as far as the splash: the progress bar does not move, and after a resolution change the screen keeps the background colour with no mark and no bar. Both point at the same code, and it is code this loader did not have a week ago. **Modes were being matched on every frame.** mirrors_match() ran from inside mirrors_show(), so every menu redraw and every segment of the kernel load asked another display adapter to change mode. On QEMU that is instant and idempotent. On real hardware a mode set can take the best part of a second, can blank a panel while it happens, and -- on a laptop -- is quite often being asked of the *same* panel reached through a second GOP. A loader that does that in the middle of drawing a progress bar is a loader that shows a background colour and then waits on a firmware call that is not coming back. It now happens exactly once, at discovery, before anything is drawn. The second head is centred and cropped afterwards rather than re-moded, and the booted system matches resolutions anyway because the kernel driver and vgpud both follow the console. **And mirroring no longer runs per segment of the kernel load.** Copying a megabyte-and-a-half framebuffer to every other adapter, three times, to animate a bar on a screen nobody is looking at. **One refusal now turns it off for the rest of the boot**, and `\schos\nomirror` -- an empty file whose only job is to exist -- turns it off before it starts. There is no console to ask a question on at that point and no menu to reach if the menu is what is broken, so the answer has to already be on the volume. That also makes it a one-file experiment on hardware we cannot instrument. **The loader names its stages on screen.** After the splash, print() goes to the serial port and nowhere else, so on a machine with no serial port a stall leaves a progress bar and no information at all. Every stage after the kernel load now labels itself under the bar -- Reading the console font, Reading the memory map, Leaving the firmware -- so the last thing on the screen names the call that did not return. Past ExitBootServices there is nothing left to report with, which is itself worth knowing: a machine stopped on the last label stopped in the jump. Opening the boot volume moved ahead of the display, because what is on the volume now decides some of what the display does. Both supported configurations: one boot each, no traps, no panics, socktest and memfdtest green. Mirroring still verified end to end -- F8, a resolution change, boot, and both heads byte-identical at the new mode. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
26e5d89c73
commit
3a0b570c23
2 changed files with 130 additions and 15 deletions
106
boot/uefi/main.c
106
boot/uefi/main.c
|
|
@ -320,14 +320,15 @@ copy(void *dst, const void *src, UINTN n)
|
||||||
// read from the same place the kernel was. Set by open_kernel().
|
// read from the same place the kernel was. Set by open_kernel().
|
||||||
static EFI_FILE_PROTOCOL *boot_root;
|
static EFI_FILE_PROTOCOL *boot_root;
|
||||||
|
|
||||||
static EFI_FILE_PROTOCOL *
|
// Opened before the display is touched, because what is on the volume decides
|
||||||
open_kernel(EFI_HANDLE image)
|
// some of what the display does -- see nomirror in mirrors_find().
|
||||||
|
static void
|
||||||
|
open_boot_volume(EFI_HANDLE image)
|
||||||
{
|
{
|
||||||
EFI_GUID li_guid = EFI_LOADED_IMAGE_PROTOCOL_GUID;
|
EFI_GUID li_guid = EFI_LOADED_IMAGE_PROTOCOL_GUID;
|
||||||
EFI_GUID fs_guid = EFI_SIMPLE_FILE_SYSTEM_PROTOCOL_GUID;
|
EFI_GUID fs_guid = EFI_SIMPLE_FILE_SYSTEM_PROTOCOL_GUID;
|
||||||
EFI_LOADED_IMAGE_PROTOCOL *li = 0;
|
EFI_LOADED_IMAGE_PROTOCOL *li = 0;
|
||||||
EFI_SIMPLE_FILE_SYSTEM_PROTOCOL *fs = 0;
|
EFI_SIMPLE_FILE_SYSTEM_PROTOCOL *fs = 0;
|
||||||
EFI_FILE_PROTOCOL *root = 0, *file = 0;
|
|
||||||
EFI_STATUS st;
|
EFI_STATUS st;
|
||||||
|
|
||||||
// The volume this loader was itself loaded from, rather than a search of
|
// The volume this loader was itself loaded from, rather than a search of
|
||||||
|
|
@ -339,10 +340,34 @@ open_kernel(EFI_HANDLE image)
|
||||||
st = BS->HandleProtocol(li->DeviceHandle, &fs_guid, (void **)&fs);
|
st = BS->HandleProtocol(li->DeviceHandle, &fs_guid, (void **)&fs);
|
||||||
if (EFI_ERROR(st)) die(L"boot volume has no filesystem", st);
|
if (EFI_ERROR(st)) die(L"boot volume has no filesystem", st);
|
||||||
|
|
||||||
st = fs->OpenVolume(fs, &root);
|
st = fs->OpenVolume(fs, &boot_root);
|
||||||
if (EFI_ERROR(st)) die(L"cannot open the boot volume", st);
|
if (EFI_ERROR(st)) die(L"cannot open the boot volume", st);
|
||||||
|
}
|
||||||
|
|
||||||
boot_root = root;
|
// Is there a file of this name on the boot volume? Used for the switches that
|
||||||
|
// have to be answerable before there is any way to ask a person.
|
||||||
|
static int
|
||||||
|
boot_file_exists(const CHAR16 *path)
|
||||||
|
{
|
||||||
|
EFI_FILE_PROTOCOL *f = 0;
|
||||||
|
|
||||||
|
if (!boot_root)
|
||||||
|
return 0;
|
||||||
|
if (EFI_ERROR(boot_root->Open(boot_root, &f, (CHAR16 *)path,
|
||||||
|
EFI_FILE_MODE_READ, 0)) || !f)
|
||||||
|
return 0;
|
||||||
|
f->Close(f);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static EFI_FILE_PROTOCOL *
|
||||||
|
open_kernel(EFI_HANDLE image)
|
||||||
|
{
|
||||||
|
EFI_FILE_PROTOCOL *root = boot_root, *file = 0;
|
||||||
|
EFI_STATUS st;
|
||||||
|
|
||||||
|
(void)image;
|
||||||
|
if (!root) die(L"boot volume was not opened", 0);
|
||||||
|
|
||||||
st = root->Open(root, &file, (CHAR16 *)KERNEL_PATH, EFI_FILE_MODE_READ, 0);
|
st = root->Open(root, &file, (CHAR16 *)KERNEL_PATH, EFI_FILE_MODE_READ, 0);
|
||||||
if (EFI_ERROR(st))
|
if (EFI_ERROR(st))
|
||||||
|
|
@ -524,7 +549,6 @@ load_elf(EFI_FILE_PROTOCOL *file)
|
||||||
|
|
||||||
done += ph.p_memsz;
|
done += ph.p_memsz;
|
||||||
ui_progress_tick((ui_u32)done);
|
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 < lowest) lowest = ph.p_paddr;
|
||||||
if (ph.p_paddr + ph.p_memsz > highest) highest = ph.p_paddr + ph.p_memsz;
|
if (ph.p_paddr + ph.p_memsz > highest) highest = ph.p_paddr + ph.p_memsz;
|
||||||
|
|
@ -1147,6 +1171,8 @@ static EFI_GRAPHICS_OUTPUT_PROTOCOL *mirror_gop[MAX_MIRRORS];
|
||||||
static EFI_GRAPHICS_OUTPUT_PROTOCOL *primary_gop;
|
static EFI_GRAPHICS_OUTPUT_PROTOCOL *primary_gop;
|
||||||
static UINTN mirror_n;
|
static UINTN mirror_n;
|
||||||
|
|
||||||
|
static void mirrors_match(EFI_GRAPHICS_OUTPUT_PROTOCOL *primary);
|
||||||
|
|
||||||
static void
|
static void
|
||||||
mirrors_find(EFI_GRAPHICS_OUTPUT_PROTOCOL *primary)
|
mirrors_find(EFI_GRAPHICS_OUTPUT_PROTOCOL *primary)
|
||||||
{
|
{
|
||||||
|
|
@ -1156,6 +1182,18 @@ mirrors_find(EFI_GRAPHICS_OUTPUT_PROTOCOL *primary)
|
||||||
|
|
||||||
primary_gop = primary;
|
primary_gop = primary;
|
||||||
|
|
||||||
|
// An off switch, because this is a nicety and booting is not.
|
||||||
|
//
|
||||||
|
// Everything below talks to display adapters the machine may be using for
|
||||||
|
// something else, through firmware drivers of wildly varying quality. If
|
||||||
|
// that goes wrong there is no console left to say so on and no way to ask
|
||||||
|
// a person anything -- so the answer has to already be on the volume.
|
||||||
|
// `\schos\nomirror` is an empty file whose only job is to exist.
|
||||||
|
if (boot_file_exists(L"\\schos\\nomirror")) {
|
||||||
|
print(L"schos: \\schos\\nomirror present; not mirroring\r\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!BS->LocateHandleBuffer ||
|
if (!BS->LocateHandleBuffer ||
|
||||||
EFI_ERROR(BS->LocateHandleBuffer(ByProtocol, &gop_guid, 0,
|
EFI_ERROR(BS->LocateHandleBuffer(ByProtocol, &gop_guid, 0,
|
||||||
&count, &handles)) || !handles)
|
&count, &handles)) || !handles)
|
||||||
|
|
@ -1169,6 +1207,9 @@ mirrors_find(EFI_GRAPHICS_OUTPUT_PROTOCOL *primary)
|
||||||
continue;
|
continue;
|
||||||
mirror_gop[mirror_n++] = g;
|
mirror_gop[mirror_n++] = g;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mirror_n)
|
||||||
|
mirrors_match(primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Put every mirror into the primary's mode, if it has one.
|
/* Put every mirror into the primary's mode, if it has one.
|
||||||
|
|
@ -1187,6 +1228,18 @@ mirrors_find(EFI_GRAPHICS_OUTPUT_PROTOCOL *primary)
|
||||||
static void
|
static void
|
||||||
mirrors_match(EFI_GRAPHICS_OUTPUT_PROTOCOL *primary)
|
mirrors_match(EFI_GRAPHICS_OUTPUT_PROTOCOL *primary)
|
||||||
{
|
{
|
||||||
|
// Called exactly once, from mirrors_find(), before anything has been
|
||||||
|
// drawn. It used to be called from mirrors_show() -- which is to say on
|
||||||
|
// every menu redraw and on every segment of the kernel load -- and that is
|
||||||
|
// not a safe thing to do to another display adapter.
|
||||||
|
//
|
||||||
|
// Setting a mode is not a cheap call on real hardware. It can take the
|
||||||
|
// best part of a second, it can blank a panel while it happens, and on a
|
||||||
|
// laptop the "other" adapter is quite often the *same* panel reached
|
||||||
|
// through a second GOP -- so doing it repeatedly, in the middle of drawing
|
||||||
|
// a progress bar, is a good way to produce a machine that shows a
|
||||||
|
// background colour and nothing else while the loader waits on a firmware
|
||||||
|
// call that is not coming back.
|
||||||
UINT32 w = primary->Mode->Info->HorizontalResolution;
|
UINT32 w = primary->Mode->Info->HorizontalResolution;
|
||||||
UINT32 h = primary->Mode->Info->VerticalResolution;
|
UINT32 h = primary->Mode->Info->VerticalResolution;
|
||||||
|
|
||||||
|
|
@ -1223,8 +1276,6 @@ mirrors_show(void)
|
||||||
if (!primary->Mode->FrameBufferBase)
|
if (!primary->Mode->FrameBufferBase)
|
||||||
return; /* nothing to copy *from* */
|
return; /* nothing to copy *from* */
|
||||||
|
|
||||||
mirrors_match(primary);
|
|
||||||
|
|
||||||
UINTN sw = primary->Mode->Info->HorizontalResolution;
|
UINTN sw = primary->Mode->Info->HorizontalResolution;
|
||||||
UINTN sh = primary->Mode->Info->VerticalResolution;
|
UINTN sh = primary->Mode->Info->VerticalResolution;
|
||||||
UINTN delta = (UINTN)primary->Mode->Info->PixelsPerScanLine * 4;
|
UINTN delta = (UINTN)primary->Mode->Info->PixelsPerScanLine * 4;
|
||||||
|
|
@ -1236,9 +1287,15 @@ mirrors_show(void)
|
||||||
UINTN cw = w < sw ? w : sw;
|
UINTN cw = w < sw ? w : sw;
|
||||||
UINTN ch = h < sh ? h : sh;
|
UINTN ch = h < sh ? h : sh;
|
||||||
|
|
||||||
g->Blt(g, (void *)(UINTN)primary->Mode->FrameBufferBase,
|
// One refusal and the whole feature stops for this boot. A firmware
|
||||||
EfiBltBufferToVideo, 0, 0, (w - cw) / 2, (h - ch) / 2,
|
// Blt that fails once will fail every time, and retrying it on every
|
||||||
cw, ch, delta);
|
// screen is how a cosmetic feature turns into a hang.
|
||||||
|
if (EFI_ERROR(g->Blt(g, (void *)(UINTN)primary->Mode->FrameBufferBase,
|
||||||
|
EfiBltBufferToVideo, 0, 0,
|
||||||
|
(w - cw) / 2, (h - ch) / 2, cw, ch, delta))) {
|
||||||
|
mirror_n = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1611,6 +1668,10 @@ EfiMain(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab)
|
||||||
// framebuffer that did not exist yet -- every tick was silently discarded
|
// framebuffer that did not exist yet -- every tick was silently discarded
|
||||||
// and then the splash painted over the result. Setting the mode first also
|
// and then the splash painted over the result. Setting the mode first also
|
||||||
// puts the menu where a person expects it: at the start.
|
// puts the menu where a person expects it: at the start.
|
||||||
|
// The volume first: what is on it decides some of what the display does,
|
||||||
|
// and the display is the only thing that can report a failure afterwards.
|
||||||
|
open_boot_volume(image);
|
||||||
|
|
||||||
UINT32 fb = setup_framebuffer();
|
UINT32 fb = setup_framebuffer();
|
||||||
|
|
||||||
EFI_FILE_PROTOCOL *file = open_kernel(image);
|
EFI_FILE_PROTOCOL *file = open_kernel(image);
|
||||||
|
|
@ -1618,10 +1679,16 @@ EfiMain(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab)
|
||||||
ui_progress_end();
|
ui_progress_end();
|
||||||
file->Close(file);
|
file->Close(file);
|
||||||
|
|
||||||
// After open_kernel, because that is what finds the volume, and after the
|
// After the kernel rather than before it: the font is optional and the
|
||||||
// kernel rather than before it: the font is optional and the kernel is
|
// kernel is not, so a machine that is going to fail should fail on the
|
||||||
// not, so a machine that is going to fail should fail on the thing that
|
// thing that matters.
|
||||||
// matters.
|
//
|
||||||
|
// Every stage from here to the jump names itself under the progress bar.
|
||||||
|
// On a desktop this is invisible -- the stages take microseconds -- and on
|
||||||
|
// a machine that stops, the label is the last thing on the screen and says
|
||||||
|
// which call did not come back. That is the only diagnostic a laptop with
|
||||||
|
// no serial port can give, and it costs a string.
|
||||||
|
ui_progress_label("Reading the console font");
|
||||||
load_console_font();
|
load_console_font();
|
||||||
|
|
||||||
vprint(L"schos: kernel entry "); vprint_hex(entry); vprint(L"\r\n");
|
vprint(L"schos: kernel entry "); vprint_hex(entry); vprint(L"\r\n");
|
||||||
|
|
@ -1636,6 +1703,8 @@ EfiMain(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab)
|
||||||
// ExitBootServices: the key it returns is invalidated by any allocation,
|
// ExitBootServices: the key it returns is invalidated by any allocation,
|
||||||
// and AllocatePool for the map buffer is itself an allocation -- hence the
|
// and AllocatePool for the map buffer is itself an allocation -- hence the
|
||||||
// static buffer and the retry.
|
// static buffer and the retry.
|
||||||
|
ui_progress_label("Reading the memory map");
|
||||||
|
|
||||||
static UINT8 mapbuf[32768];
|
static UINT8 mapbuf[32768];
|
||||||
UINTN mapsize = sizeof mapbuf, mapkey = 0, descsize = 0;
|
UINTN mapsize = sizeof mapbuf, mapkey = 0, descsize = 0;
|
||||||
UINT32 descver = 0;
|
UINT32 descver = 0;
|
||||||
|
|
@ -1659,6 +1728,7 @@ EfiMain(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab)
|
||||||
if (nent >= E820_MAXENTRIES)
|
if (nent >= E820_MAXENTRIES)
|
||||||
print(L"schos: WARNING: memory map truncated, some RAM will be unused\r\n");
|
print(L"schos: WARNING: memory map truncated, some RAM will be unused\r\n");
|
||||||
|
|
||||||
|
ui_progress_label("Leaving the firmware");
|
||||||
vprint(L"schos: exiting boot services\r\n");
|
vprint(L"schos: exiting boot services\r\n");
|
||||||
|
|
||||||
st = BS->ExitBootServices(image, mapkey);
|
st = BS->ExitBootServices(image, mapkey);
|
||||||
|
|
@ -1680,6 +1750,12 @@ EfiMain(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- no firmware services exist past this line ----
|
// ---- no firmware services exist past this line ----
|
||||||
|
//
|
||||||
|
// Nothing below can report anything: ConOut is gone, and ui_progress_label
|
||||||
|
// would be drawing into a framebuffer the firmware may no longer own. The
|
||||||
|
// last label on screen is therefore "Leaving the firmware", and a machine
|
||||||
|
// stopped on that one stopped somewhere in the handful of instructions
|
||||||
|
// between here and the kernel's first.
|
||||||
|
|
||||||
__asm__ volatile("cli");
|
__asm__ volatile("cli");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,45 @@ when the early console is set up. So a low framebuffer is attached exactly as
|
||||||
before, and a high one is attached a few dozen instructions later, immediately
|
before, and a high one is attached a few dozen instructions later, immediately
|
||||||
after `kvmalloc()` — still thousands of lines before anything else happens.
|
after `kvmalloc()` — still thousands of lines before anything else happens.
|
||||||
|
|
||||||
|
### The loader says which stage it is in
|
||||||
|
|
||||||
|
After the splash, `print()` goes to the serial port and nowhere else — writing
|
||||||
|
text into the framebuffer would scribble it across the splash. On a laptop with
|
||||||
|
no serial port that means the last thing on screen is a progress bar, and a
|
||||||
|
loader that stops has no way to say where.
|
||||||
|
|
||||||
|
So every stage after the kernel load names itself under the bar:
|
||||||
|
*Reading the console font*, *Reading the memory map*, *Leaving the firmware*.
|
||||||
|
On a machine that works these are invisible — they take microseconds — and on
|
||||||
|
one that stops, the label is the last thing on the screen and names the call
|
||||||
|
that did not come back. It costs three strings.
|
||||||
|
|
||||||
|
Past `ExitBootServices` there is nothing left to report with, so the final
|
||||||
|
label stands for the whole of the jump into the kernel.
|
||||||
|
|
||||||
|
### Mirroring is a nicety, and booting is not
|
||||||
|
|
||||||
|
`mirrors_show()` copies the primary's framebuffer onto every other display
|
||||||
|
adapter with `Blt`. Three rules keep a cosmetic feature from being able to stop
|
||||||
|
a machine:
|
||||||
|
|
||||||
|
**Modes are matched once, at discovery, before anything is drawn.** It used to
|
||||||
|
be done inside `mirrors_show()` — which is to say on every menu redraw and
|
||||||
|
every segment of the kernel load. Setting a mode is not a cheap call on real
|
||||||
|
hardware: it can take the best part of a second, it can blank a panel while it
|
||||||
|
happens, and on a laptop the "other" adapter is quite often the *same* panel
|
||||||
|
reached through a second GOP. Doing that repeatedly, in the middle of drawing a
|
||||||
|
progress bar, produces a machine showing a background colour and nothing else
|
||||||
|
while the loader waits on a firmware call.
|
||||||
|
|
||||||
|
**One refusal turns it off for the boot.** A `Blt` that fails once will fail
|
||||||
|
every time, and retrying it on every screen is how a nicety becomes a hang.
|
||||||
|
|
||||||
|
**`\schos
|
||||||
|
omirror` disables it entirely.** An empty file whose only job is to
|
||||||
|
exist. There is no console to ask a question on and no way to reach a menu if
|
||||||
|
the menu is what is broken, so the answer has to already be on the volume.
|
||||||
|
|
||||||
### Modes a real machine actually has
|
### Modes a real machine actually has
|
||||||
|
|
||||||
`mode_usable()` rejected three things it should not have:
|
`mode_usable()` rejected three things it should not have:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue