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:
parent
55a585792e
commit
751f5718cd
1 changed files with 189 additions and 63 deletions
248
boot/uefi/main.c
248
boot/uefi/main.c
|
|
@ -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,24 +375,200 @@ 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
|
||||
boot_menu(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue