1 /* SPDX-License-Identifier: LGPL-2.1-or-later */
9 #define SYSTEM_FONT_WIDTH 8
10 #define SYSTEM_FONT_HEIGHT 19
11 #define HORIZONTAL_MAX_OK 1920
12 #define VERTICAL_MAX_OK 1080
13 #define VIEWPORT_RATIO 10
15 static inline void event_closep(EFI_EVENT
*event
) {
19 BS
->CloseEvent(*event
);
23 * Reading input from the console sounds like an easy task to do, but thanks to broken
24 * firmware it is actually a nightmare.
26 * There is a SimpleTextInput and SimpleTextInputEx API for this. Ideally we want to use
27 * TextInputEx, because that gives us Ctrl/Alt/Shift key state information. Unfortunately,
28 * it is not always available and sometimes just non-functional.
30 * On some firmware, calling ReadKeyStroke or ReadKeyStrokeEx on the default console input
31 * device will just freeze no matter what (even though it *reported* being ready).
32 * Also, multiple input protocols can be backed by the same device, but they can be out of
33 * sync. Falling back on a different protocol can end up with double input.
35 * Therefore, we will preferably use TextInputEx for ConIn if that is available. Additionally,
36 * we look for the first TextInputEx device the firmware gives us as a fallback option. It
37 * will replace ConInEx permanently if it ever reports a key press.
38 * Lastly, a timer event allows us to provide a input timeout without having to call into
39 * any input functions that can freeze on us or using a busy/stall loop. */
40 EFI_STATUS
console_key_read(uint64_t *key
, uint64_t timeout_usec
) {
41 static EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL
*conInEx
= NULL
, *extraInEx
= NULL
;
42 static bool checked
= false;
45 _cleanup_(event_closep
) EFI_EVENT timer
= NULL
;
50 /* Get the *first* TextInputEx device.*/
51 err
= BS
->LocateProtocol(&SimpleTextInputExProtocol
, NULL
, (void **) &extraInEx
);
52 if (err
!= EFI_SUCCESS
|| BS
->CheckEvent(extraInEx
->WaitForKeyEx
) == EFI_INVALID_PARAMETER
)
53 /* If WaitForKeyEx fails here, the firmware pretends it talks this
54 * protocol, but it really doesn't. */
57 /* Get the TextInputEx version of ST->ConIn. */
58 err
= BS
->HandleProtocol(ST
->ConsoleInHandle
, &SimpleTextInputExProtocol
, (void **) &conInEx
);
59 if (err
!= EFI_SUCCESS
|| BS
->CheckEvent(conInEx
->WaitForKeyEx
) == EFI_INVALID_PARAMETER
)
62 if (conInEx
== extraInEx
)
68 err
= BS
->CreateEvent(EVT_TIMER
, 0, NULL
, NULL
, &timer
);
69 if (err
!= EFI_SUCCESS
)
70 return log_error_status_stall(err
, L
"Error creating timer event: %r", err
);
72 EFI_EVENT events
[] = {
74 conInEx
? conInEx
->WaitForKeyEx
: ST
->ConIn
->WaitForKey
,
75 extraInEx
? extraInEx
->WaitForKeyEx
: NULL
,
77 UINTN n_events
= extraInEx
? 3 : 2;
79 /* Watchdog rearming loop in case the user never provides us with input or some
80 * broken firmware never returns from WaitForEvent. */
82 uint64_t watchdog_timeout_sec
= 5 * 60,
83 watchdog_ping_usec
= watchdog_timeout_sec
/ 2 * 1000 * 1000;
85 /* SetTimer expects 100ns units for some reason. */
89 MIN(timeout_usec
, watchdog_ping_usec
) * 10);
90 if (err
!= EFI_SUCCESS
)
91 return log_error_status_stall(err
, L
"Error arming timer event: %r", err
);
93 (void) BS
->SetWatchdogTimer(watchdog_timeout_sec
, 0x10000, 0, NULL
);
94 err
= BS
->WaitForEvent(n_events
, events
, &index
);
95 (void) BS
->SetWatchdogTimer(watchdog_timeout_sec
, 0x10000, 0, NULL
);
97 if (err
!= EFI_SUCCESS
)
98 return log_error_status_stall(err
, L
"Error waiting for events: %r", err
);
100 /* We have keyboard input, process it after this loop. */
101 if (timer
!= events
[index
])
104 /* The EFI timer fired instead. If this was a watchdog timeout, loop again. */
105 if (timeout_usec
== UINT64_MAX
)
107 else if (timeout_usec
> watchdog_ping_usec
) {
108 timeout_usec
-= watchdog_ping_usec
;
112 /* The caller requested a timeout? They shall have one! */
116 /* If the extra input device we found returns something, always use that instead
117 * to work around broken firmware freezing on ConIn/ConInEx. */
118 if (extraInEx
&& BS
->CheckEvent(extraInEx
->WaitForKeyEx
) == EFI_SUCCESS
) {
123 /* Do not fall back to ConIn if we have a ConIn that supports TextInputEx.
124 * The two may be out of sync on some firmware, giving us double input. */
126 EFI_KEY_DATA keydata
;
129 err
= conInEx
->ReadKeyStrokeEx(conInEx
, &keydata
);
130 if (err
!= EFI_SUCCESS
)
133 if (FLAGS_SET(keydata
.KeyState
.KeyShiftState
, EFI_SHIFT_STATE_VALID
)) {
134 /* Do not distinguish between left and right keys (set both flags). */
135 if (keydata
.KeyState
.KeyShiftState
& EFI_CONTROL_PRESSED
)
136 shift
|= EFI_CONTROL_PRESSED
;
137 if (keydata
.KeyState
.KeyShiftState
& EFI_ALT_PRESSED
)
138 shift
|= EFI_ALT_PRESSED
;
139 if (keydata
.KeyState
.KeyShiftState
& EFI_LOGO_PRESSED
)
140 shift
|= EFI_LOGO_PRESSED
;
142 /* Shift is not supposed to be reported for keys that can be represented as uppercase
143 * unicode chars (Shift+f is reported as F instead). Some firmware does it anyway, so
144 * filter those out. */
145 if ((keydata
.KeyState
.KeyShiftState
& EFI_SHIFT_PRESSED
) &&
146 keydata
.Key
.UnicodeChar
== 0)
147 shift
|= EFI_SHIFT_PRESSED
;
150 /* 32 bit modifier keys + 16 bit scan code + 16 bit unicode */
151 *key
= KEYPRESS(shift
, keydata
.Key
.ScanCode
, keydata
.Key
.UnicodeChar
);
153 } else if (BS
->CheckEvent(ST
->ConIn
->WaitForKey
) == EFI_SUCCESS
) {
156 err
= ST
->ConIn
->ReadKeyStroke(ST
->ConIn
, &k
);
157 if (err
!= EFI_SUCCESS
)
160 *key
= KEYPRESS(0, k
.ScanCode
, k
.UnicodeChar
);
164 return EFI_NOT_READY
;
167 static EFI_STATUS
change_mode(int64_t mode
) {
171 /* SetMode expects a UINTN, so make sure these values are sane. */
172 mode
= CLAMP(mode
, CONSOLE_MODE_RANGE_MIN
, CONSOLE_MODE_RANGE_MAX
);
173 old_mode
= MAX(CONSOLE_MODE_RANGE_MIN
, ST
->ConOut
->Mode
->Mode
);
175 err
= ST
->ConOut
->SetMode(ST
->ConOut
, mode
);
176 if (err
== EFI_SUCCESS
)
179 /* Something went wrong. Output is probably borked, so try to revert to previous mode. */
180 if (ST
->ConOut
->SetMode(ST
->ConOut
, old_mode
) == EFI_SUCCESS
)
183 /* Maybe the device is on fire? */
184 ST
->ConOut
->Reset(ST
->ConOut
, true);
185 ST
->ConOut
->SetMode(ST
->ConOut
, CONSOLE_MODE_RANGE_MIN
);
189 EFI_STATUS
query_screen_resolution(uint32_t *ret_w
, uint32_t *ret_h
) {
191 EFI_GRAPHICS_OUTPUT_PROTOCOL
*go
;
193 err
= BS
->LocateProtocol(&GraphicsOutputProtocol
, NULL
, (void **) &go
);
194 if (err
!= EFI_SUCCESS
)
197 if (!go
->Mode
|| !go
->Mode
->Info
)
198 return EFI_DEVICE_ERROR
;
200 *ret_w
= go
->Mode
->Info
->HorizontalResolution
;
201 *ret_h
= go
->Mode
->Info
->VerticalResolution
;
205 static int64_t get_auto_mode(void) {
206 uint32_t screen_width
, screen_height
;
208 if (query_screen_resolution(&screen_width
, &screen_height
) == EFI_SUCCESS
) {
211 /* Start verifying if we are in a resolution larger than Full HD
212 * (1920x1080). If we're not, assume we're in a good mode and do not
213 * try to change it. */
214 if (screen_width
<= HORIZONTAL_MAX_OK
&& screen_height
<= VERTICAL_MAX_OK
)
216 /* For larger resolutions, calculate the ratio of the total screen
217 * area to the text viewport area. If it's less than 10 times bigger,
218 * then assume the text is readable and keep the text mode. */
222 uint64_t screen_area
= (uint64_t)screen_width
* (uint64_t)screen_height
;
224 console_query_mode(&x_max
, &y_max
);
225 text_area
= SYSTEM_FONT_WIDTH
* SYSTEM_FONT_HEIGHT
* (uint64_t)x_max
* (uint64_t)y_max
;
227 if (text_area
!= 0 && screen_area
/text_area
< VIEWPORT_RATIO
)
232 return ST
->ConOut
->Mode
->Mode
;
235 /* If we reached here, then we have a high resolution screen and the text
236 * viewport is less than 10% the screen area, so the firmware developer
237 * screwed up. Try to switch to a better mode. Mode number 2 is first non
238 * standard mode, which is provided by the device manufacturer, so it should
240 * Note: MaxMode is the number of modes, not the last mode. */
241 if (ST
->ConOut
->Mode
->MaxMode
> CONSOLE_MODE_FIRMWARE_FIRST
)
242 return CONSOLE_MODE_FIRMWARE_FIRST
;
244 /* Try again with mode different than zero (assume user requests
245 * auto mode due to some problem with mode zero). */
246 if (ST
->ConOut
->Mode
->MaxMode
> CONSOLE_MODE_80_50
)
247 return CONSOLE_MODE_80_50
;
249 return CONSOLE_MODE_80_25
;
252 EFI_STATUS
console_set_mode(int64_t mode
) {
254 case CONSOLE_MODE_KEEP
:
255 /* If the firmware indicates the current mode is invalid, change it anyway. */
256 if (ST
->ConOut
->Mode
->Mode
< CONSOLE_MODE_RANGE_MIN
)
257 return change_mode(CONSOLE_MODE_RANGE_MIN
);
260 case CONSOLE_MODE_NEXT
:
261 if (ST
->ConOut
->Mode
->MaxMode
<= CONSOLE_MODE_RANGE_MIN
)
262 return EFI_UNSUPPORTED
;
264 mode
= MAX(CONSOLE_MODE_RANGE_MIN
, ST
->ConOut
->Mode
->Mode
);
266 mode
= (mode
+ 1) % ST
->ConOut
->Mode
->MaxMode
;
267 if (change_mode(mode
) == EFI_SUCCESS
)
269 /* If this mode is broken/unsupported, try the next.
270 * If mode is 0, we wrapped around and should stop. */
271 } while (mode
> CONSOLE_MODE_RANGE_MIN
);
275 case CONSOLE_MODE_AUTO
:
276 return change_mode(get_auto_mode());
278 case CONSOLE_MODE_FIRMWARE_MAX
:
279 /* Note: MaxMode is the number of modes, not the last mode. */
280 return change_mode(ST
->ConOut
->Mode
->MaxMode
- 1LL);
283 return change_mode(mode
);
287 EFI_STATUS
console_query_mode(UINTN
*x_max
, UINTN
*y_max
) {
293 err
= ST
->ConOut
->QueryMode(ST
->ConOut
, ST
->ConOut
->Mode
->Mode
, x_max
, y_max
);
294 if (err
!= EFI_SUCCESS
) {
295 /* Fallback values mandated by UEFI spec. */
296 switch (ST
->ConOut
->Mode
->Mode
) {
297 case CONSOLE_MODE_80_50
:
301 case CONSOLE_MODE_80_25
: