diff --git a/boot/uefi/efi.h b/boot/uefi/efi.h index c8c5850..908c453 100644 --- a/boot/uefi/efi.h +++ b/boot/uefi/efi.h @@ -97,6 +97,27 @@ typedef struct EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL { void *Mode; } EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL; +// --- console input --------------------------------------------------------- + +typedef struct { + UINT16 ScanCode; + UINT16 UnicodeChar; +} EFI_INPUT_KEY; + +struct EFI_SIMPLE_TEXT_INPUT_PROTOCOL; +typedef EFI_STATUS (*EFI_INPUT_RESET)( + struct EFI_SIMPLE_TEXT_INPUT_PROTOCOL *This, unsigned char ExtendedVerification); +// Returns EFI_NOT_READY when nothing has been typed, which is what makes a +// timed prompt possible without blocking the boot. +typedef EFI_STATUS (*EFI_INPUT_READ_KEY)( + struct EFI_SIMPLE_TEXT_INPUT_PROTOCOL *This, EFI_INPUT_KEY *Key); + +typedef struct EFI_SIMPLE_TEXT_INPUT_PROTOCOL { + EFI_INPUT_RESET Reset; + EFI_INPUT_READ_KEY ReadKeyStroke; + void *WaitForKey; +} EFI_SIMPLE_TEXT_INPUT_PROTOCOL; + // --- boot services -------------------------------------------------------- typedef struct { @@ -160,7 +181,7 @@ typedef struct { CHAR16 *FirmwareVendor; UINT32 FirmwareRevision; EFI_HANDLE ConsoleInHandle; - void *ConIn; + EFI_SIMPLE_TEXT_INPUT_PROTOCOL *ConIn; EFI_HANDLE ConsoleOutHandle; EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *ConOut; EFI_HANDLE StandardErrorHandle; @@ -259,17 +280,36 @@ typedef struct { } EFI_GRAPHICS_OUTPUT_MODE_INFORMATION; typedef struct { - UINT32 Mode; + // MaxMode first, then Mode. That is the specification's order and getting + // it backwards is invisible until something reads them: Info, + // SizeOfInfo and FrameBufferBase all sit at the same offsets either way, + // because both fields are UINT32. It was backwards here, so the first code + // to ask how many modes existed was told the current mode number instead -- + // zero, on a machine with plenty of them. UINT32 MaxMode; + UINT32 Mode; EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *Info; UINTN SizeOfInfo; EFI_PHYSICAL_ADDRESS FrameBufferBase; UINTN FrameBufferSize; } EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE; +struct EFI_GRAPHICS_OUTPUT_PROTOCOL; + +// Ask what a mode is, and switch to one. Declared properly rather than as +// void*, because choosing a resolution means walking every mode the firmware +// offers and asking each one its size -- the same shape as the BIOS path's +// VBE enumeration, and for the same reason: a table of numbers can only +// describe hardware somebody already tested. +typedef EFI_STATUS (*EFI_GOP_QUERY_MODE)( + struct EFI_GRAPHICS_OUTPUT_PROTOCOL *This, UINT32 ModeNumber, + UINTN *SizeOfInfo, EFI_GRAPHICS_OUTPUT_MODE_INFORMATION **Info); +typedef EFI_STATUS (*EFI_GOP_SET_MODE)( + struct EFI_GRAPHICS_OUTPUT_PROTOCOL *This, UINT32 ModeNumber); + typedef struct EFI_GRAPHICS_OUTPUT_PROTOCOL { - void *QueryMode; - void *SetMode; + EFI_GOP_QUERY_MODE QueryMode; + EFI_GOP_SET_MODE SetMode; void *Blt; EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE *Mode; } EFI_GRAPHICS_OUTPUT_PROTOCOL; diff --git a/boot/uefi/main.c b/boot/uefi/main.c index c46726d..8616e71 100644 --- a/boot/uefi/main.c +++ b/boot/uefi/main.c @@ -252,6 +252,166 @@ load_elf(EFI_FILE_PROTOCOL *file) // Framebuffer // --------------------------------------------------------------------------- + +// Offer a resolution choice, briefly. The UEFI half of the same feature the +// BIOS loader has -- see boot/bios/stage2.S. +// +// Different mechanism, same reasoning. GOP has no mode numbers worth +// hardcoding: the firmware publishes however many modes the hardware can do, +// and which index means what varies by machine. So this walks them, and the +// menu is built from what is actually there rather than from a table. +// +// Four seconds, then it boots on its own. A loader that waits for a keypress +// is a machine that does not come back up after a power cut. + +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. +static UINT32 +choose_mode(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop) +{ + UINT32 current = gop->Mode->Mode; + + print(L"Press R within 4s to choose a resolution... "); + CHAR16 c = wait_key(4000); + 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; + } + + // 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++) { + 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 < 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"); + } + print(L" mode number, or Enter to type a size: "); + + UINT32 want = read_dec(); + print(L"\r\n"); + + // 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; + } + + print(L" width: "); + UINT32 w = read_dec(); + print(L"\r\n height: "); + UINT32 h = read_dec(); + print(L"\r\n"); + + // 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; + } + + 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; +} + static UINT32 setup_framebuffer(void) { @@ -263,6 +423,12 @@ setup_framebuffer(void) return 0; } + UINT32 pick = choose_mode(gop); + if (pick != gop->Mode->Mode) + gop->SetMode(gop, pick); + + // Re-read after SetMode: 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;