diff --git a/boot/uefi/main.c b/boot/uefi/main.c index d98ffe8..9307d17 100644 --- a/boot/uefi/main.c +++ b/boot/uefi/main.c @@ -323,65 +323,15 @@ load_elf(EFI_FILE_PROTOCOL *file) // every start to offer a menu nobody usually wants makes the common case worse // to serve the rare one. -static void -print_dec(UINT32 v) -{ - CHAR16 buf[12]; - int i = 11; - buf[i--] = 0; - if (v == 0) - buf[i--] = L'0'; - while (v && i >= 0) { - buf[i--] = (CHAR16)(L'0' + (v % 10)); - v /= 10; - } - print(&buf[i + 1]); -} // A key, or 0 if nothing was typed within `ms` milliseconds. Stall is the // simplest clock available before boot services go away, and polling in 10 ms // steps is far below anything a person can notice. -static CHAR16 -wait_key(UINTN ms) -{ - EFI_INPUT_KEY key; - for (UINTN waited = 0; waited < ms; waited += 10) { - if (ST->ConIn && !EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key))) - return key.UnicodeChar; - BS->Stall(10000); - } - return 0; -} -static CHAR16 -wait_key_forever(void) -{ - EFI_INPUT_KEY key; - for (;;) { - if (ST->ConIn && !EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key))) - return key.UnicodeChar; - BS->Stall(10000); - } -} // Read a decimal number, echoing it. Ends on Enter (CR, or LF over a serial // console). Non-digits are ignored rather than aborting, so a stray keystroke // does not silently truncate the number. -static UINT32 -read_dec(void) -{ - UINT32 v = 0; - for (;;) { - CHAR16 c = wait_key_forever(); - if (c == L'\r' || c == L'\n') - return v; - if (c < L'0' || c > L'9') - continue; - CHAR16 echo[2]; echo[0] = c; echo[1] = 0; - print(echo); - v = v * 10 + (UINT32)(c - L'0'); - } -} // Pick a GOP mode. Returns the mode number to set, or the current one. /* --------------------------------------------------------------------------- @@ -425,22 +375,198 @@ res_row(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop, char *out, int cap) /* 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) +/* Is this mode one we can actually draw in? + * + * The width has to be a multiple of 8, and that is not fussiness. OVMF + * advertises a 1366-wide mode; asking for it produces a framebuffer QEMU scans + * out at 1360, so every row we write lands six pixels further along than the + * one above and the whole screen shears. The firmware never says it rounded. + * Eight is the VGA scanout granularity, and every mode that satisfies it has + * been checked to come back the size it claimed. + * + * 32bpp because the whole system assumes it, and 800x600 as the floor because + * that is what the layout is built around. + */ +static int +mode_usable(EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *mi) { - 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) + if (!mi) return 0; + if (mi->PixelFormat > 1) return 0; + if (mi->HorizontalResolution < 800 || mi->VerticalResolution < 600) return 0; + if (mi->HorizontalResolution % 8) return 0; + if (mi->PixelsPerScanLine != mi->HorizontalResolution) return 0; + return 1; +} + + + +/* --- the resolution list --------------------------------------------------- + * + * Every mode the firmware offers that we can actually draw in, plus a row for + * typing one. A list rather than the single cycling row it replaced: cycling + * is fine for two options and useless for twenty-odd, which is what a real + * machine advertises. + */ +#define RES_MAX 24 + +static char RES_TEXT[RES_MAX + 1][24]; +static UINT32 RES_MODE[RES_MAX]; +static int RES_N; + +static void +res_build(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop) +{ + RES_N = 0; + for (UINT32 i = 0; i < gop->Mode->MaxMode && RES_N < RES_MAX; i++) { + EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *mi = 0; + UINTN sz = 0; + if (EFI_ERROR(gop->QueryMode(gop, i, &sz, &mi)) || !mode_usable(mi)) continue; - if (info->PixelFormat > 1) + + /* Skip a size already listed: the firmware offers the same geometry + * several times over with different internals, and a menu with + * 1280x1024 in it three times is a menu nobody trusts. */ + char buf[24]; + char wa[12], ha[12]; + u64_to_dec(mi->HorizontalResolution, wa, sizeof wa); + u64_to_dec(mi->VerticalResolution, ha, sizeof ha); + int k = 0; + for (char *q = wa; *q; q++) buf[k++] = *q; + buf[k++] = 'x'; + for (char *q = ha; *q; q++) buf[k++] = *q; + buf[k] = 0; + + int dup = 0; + for (int j = 0; j < RES_N; j++) { + int e = 1; + for (int c = 0; c < 24; c++) { + if (RES_TEXT[j][c] != buf[c]) { e = 0; break; } + if (!buf[c]) break; + } + if (e) { dup = 1; break; } + } + if (dup) continue; - if (info->HorizontalResolution < 800 || info->VerticalResolution < 600) - continue; - gop->SetMode(gop, i); - return; + + for (int c = 0; c <= k; c++) + RES_TEXT[RES_N][c] = buf[c]; + RES_MODE[RES_N] = i; + RES_N++; + } + + const char *custom = "Type a size..."; + int c = 0; + for (const char *q = custom; *q; q++) RES_TEXT[RES_N][c++] = *q; + RES_TEXT[RES_N][c] = 0; +} + +/* Read a decimal number on the panel, echoing it as it is typed. */ +static UINT32 +res_read_number(const char *prompt) +{ + UINT32 v = 0; + char shown[32]; + + for (;;) { + int i = 0; + for (const char *q = prompt; *q && i < 24; q++) shown[i++] = *q; + char num[12]; + u64_to_dec(v, num, sizeof num); + for (char *q = num; *q && i < 30; q++) shown[i++] = *q; + shown[i] = 0; + + const char *rows[1] = { shown }; + ui_menu("Custom resolution", rows, 1, 0, "digits, Enter to accept"); + + EFI_INPUT_KEY key; + for (;;) { + if (ST->ConIn && + !EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key))) + break; + BS->Stall(10000); + } + if (key.UnicodeChar == L'\r' || key.UnicodeChar == L'\n') + return v; + if (key.UnicodeChar >= L'0' && key.UnicodeChar <= L'9') + v = v * 10 + (UINT32)(key.UnicodeChar - L'0'); + if (key.UnicodeChar == 8 && v) /* backspace */ + v /= 10; + } +} + +static void +res_choose(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop) +{ + int sel = 0; + + for (;;) { + 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_build(gop); + + const char *items[RES_MAX + 1]; + for (int i = 0; i <= RES_N; i++) + items[i] = RES_TEXT[i]; + + if (sel > RES_N) sel = RES_N; + ui_menu("Resolution", items, RES_N + 1, sel, + "W/S to choose, Enter to apply, Esc to go back"); + + EFI_INPUT_KEY key; + for (;;) { + if (ST->ConIn && + !EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key))) + break; + BS->Stall(10000); + } + + CHAR16 c = key.UnicodeChar; + if (key.ScanCode == 0x17 || c == 27) /* Esc */ + return; + if (key.ScanCode == 0x01 || c == L'w' || c == L'W') { + sel = (sel + RES_N) % (RES_N + 1); continue; + } + if (key.ScanCode == 0x02 || c == L's' || c == L'S') { + sel = (sel + 1) % (RES_N + 1); continue; + } + if (c == L'\r' || c == L'\n') { + if (sel < RES_N) { + gop->SetMode(gop, RES_MODE[sel]); + return; + } + /* Typed. Matched against the same filtered list, so a size that + * would shear cannot be reached by typing it either. */ + UINT32 w = res_read_number("Width: "); + UINT32 h = res_read_number("Height: "); + for (UINT32 i = 0; i < gop->Mode->MaxMode; i++) { + EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *mi = 0; + UINTN sz = 0; + if (EFI_ERROR(gop->QueryMode(gop, i, &sz, &mi)) || + !mode_usable(mi)) + continue; + if (mi->HorizontalResolution == w && + mi->VerticalResolution == h) { + gop->SetMode(gop, i); + return; + } + } + /* Nothing matched. Saying so beats silently keeping the old mode + * 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"); + for (;;) { + if (ST->ConIn && + !EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key))) + break; + BS->Stall(10000); + } + return; + } } } @@ -492,7 +618,7 @@ boot_menu(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop) 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 == 2) { res_choose(gop); continue; } if (selected == 3) { ui_clear(); ui_text_centered((int)ui_height() / 2, "Halted -- reset to reach firmware setup",