]> git.ipfire.org Git - thirdparty/systemd.git/blob - src/boot/efi/console.c
Merge pull request #20303 from andir/sysconfig-example
[thirdparty/systemd.git] / src / boot / efi / console.c
1 /* SPDX-License-Identifier: LGPL-2.1-or-later */
2
3 #include <efi.h>
4 #include <efilib.h>
5
6 #include "console.h"
7 #include "util.h"
8
9 #define SYSTEM_FONT_WIDTH 8
10 #define SYSTEM_FONT_HEIGHT 19
11
12 #define EFI_SIMPLE_TEXT_INPUT_EX_GUID &(EFI_GUID) EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL_GUID
13
14 static inline void EventClosep(EFI_EVENT *event) {
15 if (!*event)
16 return;
17
18 uefi_call_wrapper(BS->CloseEvent, 1, *event);
19 }
20
21 /*
22 * Reading input from the console sounds like an easy task to do, but thanks to broken
23 * firmware it is actually a nightmare.
24 *
25 * There is a ConIn and TextInputEx API for this. Ideally we want to use TextInputEx,
26 * because that gives us Ctrl/Alt/Shift key state information. Unfortunately, it is not
27 * always available and sometimes just non-functional.
28 *
29 * On the other hand we have ConIn, where some firmware likes to just freeze on us
30 * if we call ReadKeyStroke on it.
31 *
32 * Therefore, we use WaitForEvent on both ConIn and TextInputEx (if available) along
33 * with a timer event. The timer ensures there is no need to call into functions
34 * that might freeze on us, while still allowing us to show a timeout counter.
35 */
36 EFI_STATUS console_key_read(UINT64 *key, UINT64 timeout_usec) {
37 static EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL *TextInputEx;
38 static BOOLEAN checked;
39 UINTN index;
40 EFI_INPUT_KEY k;
41 EFI_STATUS err;
42 _cleanup_(EventClosep) EFI_EVENT timer = NULL;
43 EFI_EVENT events[3] = { ST->ConIn->WaitForKey };
44 UINTN n_events = 1;
45
46 assert(key);
47
48 if (!checked) {
49 err = LibLocateProtocol(EFI_SIMPLE_TEXT_INPUT_EX_GUID, (VOID **)&TextInputEx);
50 if (EFI_ERROR(err) ||
51 uefi_call_wrapper(BS->CheckEvent, 1, TextInputEx->WaitForKeyEx) == EFI_INVALID_PARAMETER)
52 /* If WaitForKeyEx fails here, the firmware pretends it talks this
53 * protocol, but it really doesn't. */
54 TextInputEx = NULL;
55 else
56 events[n_events++] = TextInputEx->WaitForKeyEx;
57
58 checked = TRUE;
59 }
60
61 if (timeout_usec > 0) {
62 err = uefi_call_wrapper(BS->CreateEvent, 5, EVT_TIMER, 0, NULL, NULL, &timer);
63 if (EFI_ERROR(err))
64 return log_error_status_stall(err, L"Error creating timer event: %r", err);
65
66 /* SetTimer expects 100ns units for some reason. */
67 err = uefi_call_wrapper(BS->SetTimer, 3, timer, TimerRelative, timeout_usec * 10);
68 if (EFI_ERROR(err))
69 return log_error_status_stall(err, L"Error arming timer event: %r", err);
70
71 events[n_events++] = timer;
72 }
73
74 err = uefi_call_wrapper(BS->WaitForEvent, 3, n_events, events, &index);
75 if (EFI_ERROR(err))
76 return log_error_status_stall(err, L"Error waiting for events: %r", err);
77
78 if (timeout_usec > 0 && timer == events[index])
79 return EFI_TIMEOUT;
80
81 /* TextInputEx might be ready too even if ConIn got to signal first. */
82 if (TextInputEx && !EFI_ERROR(uefi_call_wrapper(BS->CheckEvent, 1, TextInputEx->WaitForKeyEx))) {
83 EFI_KEY_DATA keydata;
84 UINT64 keypress;
85 UINT32 shift = 0;
86
87 err = uefi_call_wrapper(TextInputEx->ReadKeyStrokeEx, 2, TextInputEx, &keydata);
88 if (EFI_ERROR(err))
89 return err;
90
91 /* do not distinguish between left and right keys */
92 if (keydata.KeyState.KeyShiftState & EFI_SHIFT_STATE_VALID) {
93 if (keydata.KeyState.KeyShiftState & (EFI_RIGHT_CONTROL_PRESSED|EFI_LEFT_CONTROL_PRESSED))
94 shift |= EFI_CONTROL_PRESSED;
95 if (keydata.KeyState.KeyShiftState & (EFI_RIGHT_ALT_PRESSED|EFI_LEFT_ALT_PRESSED))
96 shift |= EFI_ALT_PRESSED;
97 };
98
99 /* 32 bit modifier keys + 16 bit scan code + 16 bit unicode */
100 keypress = KEYPRESS(shift, keydata.Key.ScanCode, keydata.Key.UnicodeChar);
101 if (keypress > 0) {
102 *key = keypress;
103 return EFI_SUCCESS;
104 }
105
106 return EFI_NOT_READY;
107 }
108
109 err = uefi_call_wrapper(ST->ConIn->ReadKeyStroke, 2, ST->ConIn, &k);
110 if (EFI_ERROR(err))
111 return err;
112
113 *key = KEYPRESS(0, k.ScanCode, k.UnicodeChar);
114 return EFI_SUCCESS;
115 }
116
117 static EFI_STATUS change_mode(UINTN mode) {
118 EFI_STATUS err;
119
120 err = uefi_call_wrapper(ST->ConOut->SetMode, 2, ST->ConOut, mode);
121
122 /* Special case mode 1: when using OVMF and qemu, setting it returns error
123 * and breaks console output. */
124 if (EFI_ERROR(err) && mode == 1)
125 uefi_call_wrapper(ST->ConOut->SetMode, 2, ST->ConOut, (UINTN)0);
126
127 return err;
128 }
129
130 static UINT64 text_area_from_font_size(void) {
131 EFI_STATUS err;
132 UINT64 text_area;
133 UINTN rows, columns;
134
135 err = uefi_call_wrapper(ST->ConOut->QueryMode, 4, ST->ConOut, ST->ConOut->Mode->Mode, &columns, &rows);
136 if (EFI_ERROR(err)) {
137 columns = 80;
138 rows = 25;
139 }
140
141 text_area = SYSTEM_FONT_WIDTH * SYSTEM_FONT_HEIGHT * (UINT64)rows * (UINT64)columns;
142
143 return text_area;
144 }
145
146 static EFI_STATUS mode_auto(UINTN *mode) {
147 const UINT32 HORIZONTAL_MAX_OK = 1920;
148 const UINT32 VERTICAL_MAX_OK = 1080;
149 const UINT64 VIEWPORT_RATIO = 10;
150 UINT64 screen_area, text_area;
151 EFI_GUID GraphicsOutputProtocolGuid = EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID;
152 EFI_GRAPHICS_OUTPUT_PROTOCOL *GraphicsOutput;
153 EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *Info;
154 EFI_STATUS err;
155 BOOLEAN keep = FALSE;
156
157 assert(mode);
158
159 err = LibLocateProtocol(&GraphicsOutputProtocolGuid, (VOID **)&GraphicsOutput);
160 if (!EFI_ERROR(err) && GraphicsOutput->Mode && GraphicsOutput->Mode->Info) {
161 Info = GraphicsOutput->Mode->Info;
162
163 /* Start verifying if we are in a resolution larger than Full HD
164 * (1920x1080). If we're not, assume we're in a good mode and do not
165 * try to change it. */
166 if (Info->HorizontalResolution <= HORIZONTAL_MAX_OK && Info->VerticalResolution <= VERTICAL_MAX_OK)
167 keep = TRUE;
168 /* For larger resolutions, calculate the ratio of the total screen
169 * area to the text viewport area. If it's less than 10 times bigger,
170 * then assume the text is readable and keep the text mode. */
171 else {
172 screen_area = (UINT64)Info->HorizontalResolution * (UINT64)Info->VerticalResolution;
173 text_area = text_area_from_font_size();
174
175 if (text_area != 0 && screen_area/text_area < VIEWPORT_RATIO)
176 keep = TRUE;
177 }
178 }
179
180 if (keep) {
181 /* Just clear the screen instead of changing the mode and return. */
182 *mode = ST->ConOut->Mode->Mode;
183 uefi_call_wrapper(ST->ConOut->ClearScreen, 1, ST->ConOut);
184 return EFI_SUCCESS;
185 }
186
187 /* If we reached here, then we have a high resolution screen and the text
188 * viewport is less than 10% the screen area, so the firmware developer
189 * screwed up. Try to switch to a better mode. Mode number 2 is first non
190 * standard mode, which is provided by the device manufacturer, so it should
191 * be a good mode.
192 * Note: MaxMode is the number of modes, not the last mode. */
193 if (ST->ConOut->Mode->MaxMode > 2)
194 *mode = 2;
195 /* Try again with mode different than zero (assume user requests
196 * auto mode due to some problem with mode zero). */
197 else if (ST->ConOut->Mode->MaxMode == 2)
198 *mode = 1;
199 /* Else force mode change to zero. */
200 else
201 *mode = 0;
202
203 return change_mode(*mode);
204 }
205
206 EFI_STATUS console_set_mode(UINTN *mode, enum console_mode_change_type how) {
207 assert(mode);
208
209 if (how == CONSOLE_MODE_AUTO)
210 return mode_auto(mode);
211
212 if (how == CONSOLE_MODE_MAX) {
213 /* Note: MaxMode is the number of modes, not the last mode. */
214 if (ST->ConOut->Mode->MaxMode > 0)
215 *mode = ST->ConOut->Mode->MaxMode-1;
216 else
217 *mode = 0;
218 }
219
220 return change_mode(*mode);
221 }