diff --git a/src/boot/bootctl.c b/src/boot/bootctl.c index ce77020aac..af5cd86e73 100644 --- a/src/boot/bootctl.c +++ b/src/boot/bootctl.c @@ -827,6 +827,7 @@ static int install_loader_config(const char *esp_path) { } fprintf(f, "#timeout 3\n"); + fprintf(f, "#console-mode keep\n"); fprintf(f, "default %s-*\n", sd_id128_to_string(machine_id, machine_string)); r = fflush_sync_and_check(f); diff --git a/src/boot/efi/boot.c b/src/boot/efi/boot.c index 68b3e79155..2e613f58e8 100644 --- a/src/boot/efi/boot.c +++ b/src/boot/efi/boot.c @@ -71,6 +71,8 @@ typedef struct { BOOLEAN editor; BOOLEAN auto_entries; BOOLEAN auto_firmware; + UINTN console_mode; + enum console_mode_change_type console_mode_change; } Config; static VOID cursor_left(UINTN *cursor, UINTN *first) { @@ -497,6 +499,7 @@ static BOOLEAN menu_run(Config *config, ConfigEntry **chosen_entry, CHAR16 *load BOOLEAN exit = FALSE; BOOLEAN run = TRUE; BOOLEAN wait = FALSE; + BOOLEAN cleared_screen = FALSE; graphics_mode(FALSE); uefi_call_wrapper(ST->ConIn->Reset, 2, ST->ConIn, FALSE); @@ -505,7 +508,18 @@ static BOOLEAN menu_run(Config *config, ConfigEntry **chosen_entry, CHAR16 *load /* draw a single character to make ClearScreen work on some firmware */ uefi_call_wrapper(ST->ConOut->OutputString, 2, ST->ConOut, L" "); - uefi_call_wrapper(ST->ConOut->ClearScreen, 1, ST->ConOut); + + if (config->console_mode_change != CONSOLE_MODE_KEEP) { + err = console_set_mode(&config->console_mode, config->console_mode_change); + if (!EFI_ERROR(err)) + cleared_screen = TRUE; + } + + if (!cleared_screen) + uefi_call_wrapper(ST->ConOut->ClearScreen, 1, ST->ConOut); + + if (config->console_mode_change != CONSOLE_MODE_KEEP && EFI_ERROR(err)) + Print(L"Error switching console mode to %ld: %r.\r", (UINT64)config->console_mode, err); err = uefi_call_wrapper(ST->ConOut->QueryMode, 4, ST->ConOut, ST->ConOut->Mode->Mode, &x_max, &y_max); if (EFI_ERROR(err)) { @@ -1030,6 +1044,26 @@ static VOID config_defaults_load_from_file(Config *config, CHAR8 *content) { continue; config->auto_firmware = on; } + + if (strcmpa((CHAR8 *)"console-mode", key) == 0) { + CHAR16 *s; + + if (strcmpa((CHAR8 *)"auto", value) == 0) + config->console_mode_change = CONSOLE_MODE_AUTO; + else if (strcmpa((CHAR8 *)"max", value) == 0) + config->console_mode_change = CONSOLE_MODE_MAX; + else if (strcmpa((CHAR8 *)"keep", value) == 0) + config->console_mode_change = CONSOLE_MODE_KEEP; + else { + s = stra_to_str(value); + config->console_mode = Atoi(s); + config->console_mode_change = CONSOLE_MODE_SET; + FreePool(s); + } + + continue; + } + } } diff --git a/src/boot/efi/console.c b/src/boot/efi/console.c index 4487ed099f..94551a89e1 100644 --- a/src/boot/efi/console.c +++ b/src/boot/efi/console.c @@ -20,6 +20,9 @@ #include "console.h" #include "util.h" +#define SYSTEM_FONT_WIDTH 8 +#define SYSTEM_FONT_HEIGHT 19 + #define EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL_GUID \ { 0xdd9e7534, 0x7762, 0x4698, { 0x8c, 0x14, 0xf5, 0x85, 0x17, 0xa6, 0x25, 0xaa } } @@ -134,3 +137,105 @@ EFI_STATUS console_key_read(UINT64 *key, BOOLEAN wait) { *key = KEYPRESS(0, k.ScanCode, k.UnicodeChar); return 0; } + +static EFI_STATUS change_mode(UINTN mode) { + EFI_STATUS err; + + err = uefi_call_wrapper(ST->ConOut->SetMode, 2, ST->ConOut, mode); + + /* Special case mode 1: when using OVMF and qemu, setting it returns error + * and breaks console output. */ + if (EFI_ERROR(err) && mode == 1) + uefi_call_wrapper(ST->ConOut->SetMode, 2, ST->ConOut, (UINTN)0); + + return err; +} + +static UINT64 text_area_from_font_size(void) { + EFI_STATUS err; + UINT64 text_area; + UINTN rows, columns; + + err = uefi_call_wrapper(ST->ConOut->QueryMode, 4, ST->ConOut, ST->ConOut->Mode->Mode, &columns, &rows); + if (EFI_ERROR(err)) { + columns = 80; + rows = 25; + } + + text_area = SYSTEM_FONT_WIDTH * SYSTEM_FONT_HEIGHT * (UINT64)rows * (UINT64)columns; + + return text_area; +} + +static EFI_STATUS mode_auto(UINTN *mode) { + const UINT32 HORIZONTAL_MAX_OK = 1920; + const UINT32 VERTICAL_MAX_OK = 1080; + const UINT64 VIEWPORT_RATIO = 10; + UINT64 screen_area, text_area; + EFI_GUID GraphicsOutputProtocolGuid = EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID; + EFI_GRAPHICS_OUTPUT_PROTOCOL *GraphicsOutput; + EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *Info; + EFI_STATUS err; + BOOLEAN keep = FALSE; + + err = LibLocateProtocol(&GraphicsOutputProtocolGuid, (VOID **)&GraphicsOutput); + if (!EFI_ERROR(err) && GraphicsOutput->Mode && GraphicsOutput->Mode->Info) { + Info = GraphicsOutput->Mode->Info; + + /* Start verifying if we are in a resolution larger than Full HD + * (1920x1080). If we're not, assume we're in a good mode and do not + * try to change it. */ + if (Info->HorizontalResolution <= HORIZONTAL_MAX_OK && Info->VerticalResolution <= VERTICAL_MAX_OK) + keep = TRUE; + /* For larger resolutions, calculate the ratio of the total screen + * area to the text viewport area. If it's less than 10 times bigger, + * then assume the text is readable and keep the text mode. */ + else { + screen_area = (UINT64)Info->HorizontalResolution * (UINT64)Info->VerticalResolution; + text_area = text_area_from_font_size(); + + if (text_area != 0 && screen_area/text_area < VIEWPORT_RATIO) + keep = TRUE; + } + } + + if (keep) { + /* Just clear the screen instead of changing the mode and return. */ + *mode = ST->ConOut->Mode->Mode; + uefi_call_wrapper(ST->ConOut->ClearScreen, 1, ST->ConOut); + return EFI_SUCCESS; + } + + /* If we reached here, then we have a high resolution screen and the text + * viewport is less than 10% the screen area, so the firmware developer + * screwed up. Try to switch to a better mode. Mode number 2 is first non + * standard mode, which is provided by the device manufacturer, so it should + * be a good mode. + * Note: MaxMode is the number of modes, not the last mode. */ + if (ST->ConOut->Mode->MaxMode > 2) + *mode = 2; + /* Try again with mode different than zero (assume user requests + * auto mode due to some problem with mode zero). */ + else if (ST->ConOut->Mode->MaxMode == 2) + *mode = 1; + /* Else force mode change to zero. */ + else + *mode = 0; + + return change_mode(*mode); +} + +EFI_STATUS console_set_mode(UINTN *mode, enum console_mode_change_type how) { + if (how == CONSOLE_MODE_AUTO) + return mode_auto(mode); + + if (how == CONSOLE_MODE_MAX) { + /* Note: MaxMode is the number of modes, not the last mode. */ + if (ST->ConOut->Mode->MaxMode > 0) + *mode = ST->ConOut->Mode->MaxMode-1; + else + *mode = 0; + } + + return change_mode(*mode); +} diff --git a/src/boot/efi/console.h b/src/boot/efi/console.h index 5664a4e62f..6296d467c4 100644 --- a/src/boot/efi/console.h +++ b/src/boot/efi/console.h @@ -29,5 +29,13 @@ #define KEYCHAR(k) ((k) & 0xffff) #define CHAR_CTRL(c) ((c) - 'a' + 1) +enum console_mode_change_type { + CONSOLE_MODE_KEEP = 0, + CONSOLE_MODE_SET, + CONSOLE_MODE_AUTO, + CONSOLE_MODE_MAX, +}; + EFI_STATUS console_key_read(UINT64 *key, BOOLEAN wait); +EFI_STATUS console_set_mode(UINTN *mode, enum console_mode_change_type how); #endif diff --git a/src/shared/bootspec.c b/src/shared/bootspec.c index 1c9ef40019..7bb66f3d87 100644 --- a/src/shared/bootspec.c +++ b/src/shared/bootspec.c @@ -206,6 +206,8 @@ int boot_loader_read_conf(const char *path, BootConfig *config) { r = free_and_strdup(&config->auto_entries, p); else if (streq(buf, "auto-firmware")) r = free_and_strdup(&config->auto_firmware, p); + else if (streq(buf, "console-mode")) + r = free_and_strdup(&config->console_mode, p); else { log_notice("%s:%u: Unknown line \"%s\"", path, line, field); continue; diff --git a/src/shared/bootspec.h b/src/shared/bootspec.h index fe875454b1..12accc6c44 100644 --- a/src/shared/bootspec.h +++ b/src/shared/bootspec.h @@ -42,6 +42,7 @@ typedef struct BootConfig { char *editor; char *auto_entries; char *auto_firmware; + char *console_mode; char *entry_oneshot; char *entry_default;