diff --git a/boot/uefi/main.c b/boot/uefi/main.c index 8071b60..03176ca 100644 --- a/boot/uefi/main.c +++ b/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(). static EFI_FILE_PROTOCOL *boot_root; -static EFI_FILE_PROTOCOL * -open_kernel(EFI_HANDLE image) +// Opened before the display is touched, because what is on the volume decides +// 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 fs_guid = EFI_SIMPLE_FILE_SYSTEM_PROTOCOL_GUID; EFI_LOADED_IMAGE_PROTOCOL *li = 0; EFI_SIMPLE_FILE_SYSTEM_PROTOCOL *fs = 0; - EFI_FILE_PROTOCOL *root = 0, *file = 0; EFI_STATUS st; // 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); 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); +} - 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); if (EFI_ERROR(st)) @@ -524,7 +549,6 @@ 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; @@ -1147,6 +1171,8 @@ static EFI_GRAPHICS_OUTPUT_PROTOCOL *mirror_gop[MAX_MIRRORS]; static EFI_GRAPHICS_OUTPUT_PROTOCOL *primary_gop; static UINTN mirror_n; +static void mirrors_match(EFI_GRAPHICS_OUTPUT_PROTOCOL *primary); + static void mirrors_find(EFI_GRAPHICS_OUTPUT_PROTOCOL *primary) { @@ -1156,6 +1182,18 @@ mirrors_find(EFI_GRAPHICS_OUTPUT_PROTOCOL *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 || EFI_ERROR(BS->LocateHandleBuffer(ByProtocol, &gop_guid, 0, &count, &handles)) || !handles) @@ -1169,6 +1207,9 @@ mirrors_find(EFI_GRAPHICS_OUTPUT_PROTOCOL *primary) continue; mirror_gop[mirror_n++] = g; } + + if (mirror_n) + mirrors_match(primary); } /* 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 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 h = primary->Mode->Info->VerticalResolution; @@ -1223,8 +1276,6 @@ mirrors_show(void) if (!primary->Mode->FrameBufferBase) return; /* nothing to copy *from* */ - mirrors_match(primary); - UINTN sw = primary->Mode->Info->HorizontalResolution; UINTN sh = primary->Mode->Info->VerticalResolution; UINTN delta = (UINTN)primary->Mode->Info->PixelsPerScanLine * 4; @@ -1236,9 +1287,15 @@ mirrors_show(void) 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); + // One refusal and the whole feature stops for this boot. A firmware + // Blt that fails once will fail every time, and retrying it on every + // 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 // and then the splash painted over the result. Setting the mode first also // 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(); EFI_FILE_PROTOCOL *file = open_kernel(image); @@ -1618,10 +1679,16 @@ EfiMain(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab) ui_progress_end(); file->Close(file); - // After open_kernel, because that is what finds the volume, and after the - // kernel rather than before it: the font is optional and the kernel is - // not, so a machine that is going to fail should fail on the thing that - // matters. + // After the kernel rather than before it: the font is optional and the + // kernel is not, so a machine that is going to fail should fail on the + // thing that 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(); 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, // and AllocatePool for the map buffer is itself an allocation -- hence the // static buffer and the retry. + ui_progress_label("Reading the memory map"); + static UINT8 mapbuf[32768]; UINTN mapsize = sizeof mapbuf, mapkey = 0, descsize = 0; UINT32 descver = 0; @@ -1659,6 +1728,7 @@ EfiMain(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab) if (nent >= E820_MAXENTRIES) 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"); st = BS->ExitBootServices(image, mapkey); @@ -1680,6 +1750,12 @@ EfiMain(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab) } // ---- 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"); diff --git a/docs/architecture/boot.md b/docs/architecture/boot.md index df115a6..1692673 100644 --- a/docs/architecture/boot.md +++ b/docs/architecture/boot.md @@ -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 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 `mode_usable()` rejected three things it should not have: