diff --git a/boot/common/ui.c b/boot/common/ui.c index 7d491d9..b2576e3 100644 --- a/boot/common/ui.c +++ b/boot/common/ui.c @@ -316,9 +316,19 @@ metal_text_centered(int y, const char *s) } } -#define ROW_H 22 +/* Panel metrics. + * + * TITLE_H is the whole title band -- the padding above the heading, the + * heading itself, and the air beneath it before the rule. It was 34 while the + * heading alone needs 18 + 16 = 34, so the rule at TITLE_H - 6 was drawn + * *through* the lettering and the first row started on the heading's baseline. + * Derived from its parts now, so it cannot drift from them again. + */ +#define ROW_H 24 #define PANEL_PAD 18 -#define TITLE_H 34 +#define TITLE_GAP 14 /* heading to the rule */ +#define TITLE_H (PANEL_PAD + GLYPH_H + TITLE_GAP) /* = 48 */ +#define ROW_LIFT ((ROW_H - GLYPH_H) / 2) /* highlight overhang */ static void panel_frame(const char *title, int rows, const char *hint, @@ -342,7 +352,8 @@ panel_frame(const char *title, int rows, const char *hint, panel(x, y, w, h); metal_text_centered(y + PANEL_PAD, title); - ui_fill_rect(x + PANEL_PAD, y + TITLE_H - 6, w - PANEL_PAD * 2, 1, METAL_EDGE); + ui_fill_rect(x + PANEL_PAD, y + PANEL_PAD + GLYPH_H + TITLE_GAP / 2, + w - PANEL_PAD * 2, 1, METAL_EDGE); if (hint) ui_text_centered(y + h + 16, hint, SCH_DIM); @@ -367,7 +378,10 @@ ui_menu(const char *title, const char *const *items, int n, int selected, /* The selected row is the only lit thing on the plate. Filling it * with the accent and putting the label in the ground colour reads * at a glance; an outline does not, at this size. */ - ui_fill_rect(x + PANEL_PAD / 2, ry - 3, w - PANEL_PAD, ROW_H, + /* Centred on the glyph box: (ROW_H - GLYPH_H) / 2 above it, the + * same below. Off-by-a-few here is what makes a highlight look + * like it is sitting on the text rather than around it. */ + ui_fill_rect(x + PANEL_PAD / 2, ry - ROW_LIFT, w - PANEL_PAD, ROW_H, SCH_ACCENT); ui_text(x + PANEL_PAD, ry, items[i], PANEL_BG, SCH_BG_NONE); } else { diff --git a/boot/uefi/main.c b/boot/uefi/main.c index 26f2647..d98ffe8 100644 --- a/boot/uefi/main.c +++ b/boot/uefi/main.c @@ -92,6 +92,10 @@ static EFI_BOOT_SERVICES *BS; */ static int SPLASH_UP; +/* Set by the boot menu; handed to the kernel through the same flag byte the + * BIOS loader writes, so a verbose boot means the same thing either way. */ +static int VERBOSE_BOOT; + static void serial_putc(char c) { @@ -380,37 +384,53 @@ read_dec(void) } // Pick a GOP mode. Returns the mode number to set, or the current one. -static UINT32 -choose_mode(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop) +/* --------------------------------------------------------------------------- + * The boot menu: the same one the BIOS loader shows, on the same panel. + * + * Resolution is a row in here rather than a prompt of its own before it. Two + * questions asked at two different moments by two different mechanisms is what + * made the two firmwares feel like different programs. + * + * This is the half of it the UEFI side can do properly: GOP sets a mode at any + * point before ExitBootServices, so the framebuffer exists first and the + * chooser can be drawn in it. The BIOS side cannot -- see boot/bios/stage2.S. + * ------------------------------------------------------------------------- */ + +static void +u64_to_dec(UINT64 v, char *out, int cap) { - UINT32 current = gop->Mode->Mode; + char tmp[24]; + int n = 0; + if (v == 0) tmp[n++] = '0'; + while (v && n < 23) { tmp[n++] = (char)('0' + (v % 10)); v /= 10; } + int i = 0; + while (n > 0 && i < cap - 1) out[i++] = tmp[--n]; + out[i] = 0; +} - print(L"Press R for resolution... "); - CHAR16 c = wait_key(1500); - if (c != L'r' && c != L'R') { - print(L"keeping "); - print_dec(gop->Mode->Info->HorizontalResolution); print(L"x"); - print_dec(gop->Mode->Info->VerticalResolution); print(L"\r\n"); - return current; - } +static void +res_row(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop, char *out, int cap) +{ + char wa[24], ha[24]; + u64_to_dec(gop->Mode->Info->HorizontalResolution, wa, sizeof wa); + u64_to_dec(gop->Mode->Info->VerticalResolution, ha, sizeof ha); - // List what the firmware actually offers, 32bpp only: the console, the - // fbdev ioctls and Schcom all assume it, and PixelFormat 2 is a packed - // format this loader does not describe to the kernel. - print(L"\r\n"); - for (UINT32 i = 0; i < gop->Mode->MaxMode; i++) { - EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *dbg = 0; - UINTN dsz = 0; - EFI_STATUS dst = gop->QueryMode(gop, i, &dsz, &dbg); - print(L" raw "); print_dec(i); print(L" st="); print_hex(dst); - if (!EFI_ERROR(dst) && dbg) { - print(L" "); print_dec(dbg->HorizontalResolution); - print(L"x"); print_dec(dbg->VerticalResolution); - print(L" fmt="); print_dec(dbg->PixelFormat); - } - print(L"\r\n"); - } - for (UINT32 i = 0; i < gop->Mode->MaxMode; i++) { + int i = 0; + for (const char *p = "Resolution: "; *p && i < cap - 1; p++) out[i++] = *p; + for (char *p = wa; *p && i < cap - 1; p++) out[i++] = *p; + if (i < cap - 1) out[i++] = 'x'; + for (char *p = ha; *p && i < cap - 1; p++) out[i++] = *p; + out[i] = 0; +} + +/* Step to the next 32bpp mode at or above 800x600, wrapping. Cycling one row + * needs no scrolling state to get wrong, and a loader does not need a list. */ +static void +next_mode(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop) +{ + UINT32 start = gop->Mode->Mode; + for (UINT32 n = 1; n <= gop->Mode->MaxMode; n++) { + UINT32 i = (start + n) % gop->Mode->MaxMode; EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *info = 0; UINTN size = 0; if (EFI_ERROR(gop->QueryMode(gop, i, &size, &info)) || !info) @@ -419,52 +439,96 @@ choose_mode(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop) continue; if (info->HorizontalResolution < 800 || info->VerticalResolution < 600) continue; - print(L" "); - print_dec(i); print(L") "); - print_dec(info->HorizontalResolution); print(L"x"); - print_dec(info->VerticalResolution); print(L"\r\n"); + gop->SetMode(gop, i); + return; } - print(L" mode number, or Enter to type a size: "); +} - UINT32 want = read_dec(); - print(L"\r\n"); +static int +boot_menu(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop) +{ + int selected = 0; + char resbuf[48]; - // Entering nothing means "let me type it instead". Zero is a valid mode - // number, so the two are told apart by whether anything was typed at all; - // read_dec returns 0 for an empty line, and mode 0 is reachable from the - // typed path anyway. - if (want != 0 || gop->Mode->MaxMode == 0) { - if (want < gop->Mode->MaxMode) - return want; - print(L"schos: no such mode, keeping the current one\r\n"); - return current; + for (;;) { + /* Re-read the framebuffer every pass: choosing a resolution moves it, + * and drawing where it used to be is a black screen with no + * explanation. */ + struct ui_fb uifb; + uifb.base = (ui_u8 *)(UINTN)gop->Mode->FrameBufferBase; + uifb.width = gop->Mode->Info->HorizontalResolution; + uifb.height = gop->Mode->Info->VerticalResolution; + uifb.stride = gop->Mode->Info->PixelsPerScanLine * 4; + ui_init(&uifb); + + res_row(gop, resbuf, sizeof resbuf); + + const char *items[4]; + items[0] = "Boot SchOS"; + items[1] = "Boot SchOS (verbose)"; + items[2] = resbuf; + items[3] = "Reboot to firmware"; + + ui_menu("Boot Manager", items, 4, selected, + "W/S or 1-4 to choose, Enter to select"); + + EFI_INPUT_KEY key; + CHAR16 c = 0; + UINT16 scan = 0; + for (;;) { + if (ST->ConIn && + !EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key))) { + c = key.UnicodeChar; scan = key.ScanCode; break; + } + BS->Stall(10000); + } + + if (scan == 0x01) { selected = (selected + 3) % 4; continue; } /* up */ + if (scan == 0x02) { selected = (selected + 1) % 4; continue; } /* down */ + if (c == L'w' || c == L'W') { selected = (selected + 3) % 4; continue; } + if (c == L's' || c == L'S') { selected = (selected + 1) % 4; continue; } + if (c >= L'1' && c <= L'4') { selected = (int)(c - L'1'); continue; } + + if (c == L'\r' || c == L'\n') { + if (selected == 0) return 0; + if (selected == 1) return 1; + if (selected == 2) { next_mode(gop); continue; } + if (selected == 3) { + ui_clear(); + ui_text_centered((int)ui_height() / 2, "Halted -- reset to reach firmware setup", + 0xB9BEC7u); + for (;;) __asm__ volatile("hlt"); + } + } } +} - print(L" width: "); - UINT32 w = read_dec(); - print(L"\r\n height: "); - UINT32 h = read_dec(); - print(L"\r\n"); +/* 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. + */ +static int +f8_window(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop) +{ + print(L"Press F8 for boot options... "); - // 800x600 is the floor the system is built around. - if (w < 800 || h < 600) { - print(L"schos: below the 800x600 minimum, keeping the current mode\r\n"); - return current; + EFI_INPUT_KEY key; + for (UINTN waited = 0; waited < 1500; waited += 10) { + if (ST->ConIn && !EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key))) { + /* SCAN_F8 is 0x12. A serial console sends no scan codes at all, so + * 'o' is accepted too -- otherwise this is unreachable for anyone + * working over a serial line, which is how it is usually driven. */ + if (key.ScanCode == 0x12 || key.UnicodeChar == L'o' || + key.UnicodeChar == L'O') { + print(L"\r\n"); + return boot_menu(gop); + } + break; + } + BS->Stall(10000); } - - for (UINT32 i = 0; i < gop->Mode->MaxMode; i++) { - EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *info = 0; - UINTN size = 0; - if (EFI_ERROR(gop->QueryMode(gop, i, &size, &info)) || !info) - continue; - if (info->PixelFormat > 1) - continue; - if (info->HorizontalResolution == w && info->VerticalResolution == h) - return i; - } - - print(L"schos: no such 32bpp mode, keeping the current one\r\n"); - return current; + print(L"booting\r\n"); + return 0; } static UINT32 @@ -478,11 +542,12 @@ setup_framebuffer(void) return 0; } - UINT32 pick = choose_mode(gop); - if (pick != gop->Mode->Mode) - gop->SetMode(gop, pick); + // 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. + VERBOSE_BOOT = f8_window(gop); - // Re-read after SetMode: Info and FrameBufferBase both describe the mode + // Re-read after any mode change: Info and FrameBufferBase both describe the mode // that is current, and the framebuffer can move when the mode changes. EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *info = gop->Mode->Info; UINT32 fb = (UINT32)gop->Mode->FrameBufferBase; @@ -696,6 +761,7 @@ EfiMain(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab) copy((void *)SCHBOOT_E820_MAP, e820, nent * sizeof e820[0]); *(UINT32 *)SCHBOOT_E820_COUNT = nent; *(UINT64 *)SCHBOOT_RSDP_ADDR = rsdp; + *(UINT8 *)SCHBOOT_VERBOSE_FLAG = (UINT8)VERBOSE_BOOT; // The page tables mboot_entry would have built. entry64low's first act is // a movabs to 0xffff8000...., so the high half must be mapped before the