boot/uefi: a real resolution list, and the mode that sheared

The screenshot was mode 16. OVMF advertises it as 1366 wide; asking for
it produces a framebuffer QEMU scans out at 1360, so every row written
lands six pixels further along than the one above and the whole screen
shears. The firmware never says it rounded, and nothing in the mode
information hints at it -- PixelsPerScanLine agrees with the width it
claims.

So a mode is only usable if its width is a multiple of 8, which is the
VGA scanout granularity. Checked rather than assumed: 1400x1050 and
1280x720 render perfectly centred, 1366x768 does not, and it is the only
advertised width that is not a multiple of 8.

The single cycling row is now a list. Every usable mode the firmware
offers, deduplicated -- it advertises the same geometry several times with
different internals, and a menu with 1280x1024 in it three times is one
nobody trusts -- plus a row for typing a size. A typed size is matched
against the same filtered list, so a mode that would shear cannot be
reached by typing it either, and a size that does not exist says so
rather than silently keeping the old one.

Also fixes the panel layout you spotted: the title band was exactly as
tall as the heading, so the rule was drawn through the lettering and the
first row sat on its baseline; and the selected row's highlight had a
fixed 3px offset against a 24px row, so it sat on the text rather than
around it.

Verified by applying four resolutions from the list -- 1280x800,
1024x768, 1280x960, 1600x1200 -- and checking the panel is centred in
each. bootcheck 108 [ok].

BIOS still asks in a text prompt before the framebuffer exists, and that
is the remaining half of "the same for both". See the next commit message
or the note in stage2.S: VBE is INT 10h, so it needs real mode, and a
graphical chooser needs either unreal mode before the switch or a return
to real mode after it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Nexoniarz 2026-08-11 09:29:11 +02:00
commit 751f5718cd

View file

@ -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,23 +375,199 @@ 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)
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;
}
}
}
static int
@ -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",