diff --git a/boot/uefi/main.c b/boot/uefi/main.c index c6b4a67..8071b60 100644 --- a/boot/uefi/main.c +++ b/boot/uefi/main.c @@ -205,6 +205,21 @@ print(const CHAR16 *s) ST->ConOut->OutputString(ST->ConOut, (CHAR16 *)s); } +// A number in the base a person reads sizes in. `u64_to_dec` is further down +// and works on char; this is the CHAR16 the firmware console wants. +static void +print_dec(UINT64 v) +{ + CHAR16 tmp[24], out[25]; + int n = 0, i = 0; + + if (v == 0) tmp[n++] = L'0'; + while (v && n < 23) { tmp[n++] = (CHAR16)(L'0' + (v % 10)); v /= 10; } + while (n > 0) out[i++] = tmp[--n]; + out[i] = 0; + print(out); +} + static void print_hex(UINT64 v) { @@ -618,10 +633,26 @@ static int mode_usable(EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *mi) { if (!mi) return 0; + + // Formats 0 and 1 are the two with a linear 32-bit layout. 2 is a bitmask + // format nobody ships and 3 is Blt-only, which has no aperture at all -- + // see the virtio GOP, and the reboot loop it once caused. 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; + if (mi->HorizontalResolution < 640 || mi->VerticalResolution < 480) return 0; + + // A padded row is not a broken mode. This used to require + // PixelsPerScanLine == HorizontalResolution, which is true of QEMU and + // routinely false of real hardware: a GPU is entitled to align each row, + // and 1920x1080 arriving with a scanline of 1984 or 2048 pixels is + // ordinary. The stride is carried through to the kernel, so the only + // thing this rejected was every machine that padded -- including, + // probably, the laptop that could not be offered 1920x1080. + if (mi->PixelsPerScanLine < mi->HorizontalResolution) return 0; + + // The width-divisible-by-eight rule went with it. It excluded 1366x768, + // which is the panel on a very large number of laptops, and protected + // nothing: the drawing code steps by the stride and has done for as long + // as there has been a stride to step by. return 1; } @@ -1211,6 +1242,112 @@ mirrors_show(void) } } +// --------------------------------------------------------------------------- +// Choosing a display, and saying what was chosen +// +// LocateProtocol answers with *a* GOP. On a laptop with switchable graphics +// there are several -- the integrated adapter, the discrete one, sometimes a +// console splitter on top of both -- and which one comes back first is not +// something the specification says anything about. Drawing into the wrong one +// is a black screen with the firmware's cursor still blinking on it, which is +// indistinguishable from a loader that crashed. +// +// So all of them are looked at, and the choice is made on what they report +// rather than on the order they were handed over. +// --------------------------------------------------------------------------- + +static const CHAR16 * +fmt_name(UINT32 f) +{ + switch (f) { + case 0: return L"RGBX"; + case 1: return L"BGRX"; + case 2: return L"bitmask"; + default: return L"Blt-only"; + } +} + +static void +describe_gop(EFI_GRAPHICS_OUTPUT_PROTOCOL *g, const CHAR16 *tag) +{ + EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *mi = g->Mode ? g->Mode->Info : 0; + + print(tag); + if (!mi) { print(L" (no mode information)\r\n"); return; } + + print(L" "); print_dec(mi->HorizontalResolution); + print(L"x"); print_dec(mi->VerticalResolution); + print(L" "); print(fmt_name(mi->PixelFormat)); + print(L" scanline "); print_dec(mi->PixelsPerScanLine); + print(L" fb "); print_hex(g->Mode->FrameBufferBase); + print(L" size "); print_hex(g->Mode->FrameBufferSize); + print(L"\r\n"); +} + +// Better, for this purpose, means: it has an aperture we can write into, its +// current mode is one we can read, and failing a tie, it is bigger. A GOP with +// no framebuffer address is not a candidate at all -- the kernel needs a +// linear aperture and cannot Blt. +static int +gop_score(EFI_GRAPHICS_OUTPUT_PROTOCOL *g) +{ + EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *mi = g->Mode ? g->Mode->Info : 0; + + if (!mi || !g->Mode->FrameBufferBase || !g->Mode->FrameBufferSize) + return 0; + if (!mode_usable(mi)) + return 1; /* has memory; mode needs changing */ + return 2; +} + +static EFI_GRAPHICS_OUTPUT_PROTOCOL * +pick_gop(EFI_GRAPHICS_OUTPUT_PROTOCOL *first) +{ + EFI_GUID gop_guid = EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID; + EFI_GRAPHICS_OUTPUT_PROTOCOL *best = first; + EFI_HANDLE *handles = 0; + UINTN count = 0, i; + int best_score = first ? gop_score(first) : 0; + UINT64 best_area = 0; + + if (first && first->Mode && first->Mode->Info) + best_area = (UINT64)first->Mode->Info->HorizontalResolution * + first->Mode->Info->VerticalResolution; + + if (!BS->LocateHandleBuffer || + EFI_ERROR(BS->LocateHandleBuffer(ByProtocol, &gop_guid, 0, + &count, &handles)) || !handles) + return best; + + print(L"schos: "); print_dec((UINT32)count); print(L" display adapter(s)\r\n"); + + for (i = 0; i < count; i++) { + EFI_GRAPHICS_OUTPUT_PROTOCOL *g = 0; + int score; + UINT64 area; + + if (EFI_ERROR(BS->HandleProtocol(handles[i], &gop_guid, (void **)&g)) || + !g || !g->Mode) + continue; + + describe_gop(g, L" gop:"); + + score = gop_score(g); + area = g->Mode->Info + ? (UINT64)g->Mode->Info->HorizontalResolution * + g->Mode->Info->VerticalResolution + : 0; + + if (score > best_score || (score == best_score && area > best_area)) { + best = g; + best_score = score; + best_area = area; + } + } + + return best; +} + /* Wait briefly for F8, exactly as the BIOS loader does, and show the same * menu if it comes. One prompt, one key, one screen -- the resolution chooser * lives in that menu rather than in a second prompt of its own. @@ -1250,6 +1387,24 @@ setup_framebuffer(void) return 0; } + // Which adapter, on a machine that has more than one. Everything this + // prints goes to the firmware's text console, which is cleared a few lines + // below before the splash is drawn -- so on a boot that works nobody ever + // sees it, and on a boot that dies before the splash it is the only thing + // on the screen. + gop = pick_gop(gop); + describe_gop(gop, L"schos: display"); + + // No aperture anywhere means no graphical console, and saying so beats + // leaving the firmware's cursor blinking on a black screen while the + // kernel starts behind it. + if (!gop->Mode || !gop->Mode->FrameBufferBase || !gop->Mode->FrameBufferSize) { + print(L"schos: this display has no linear framebuffer, only Blt.\r\n" + L" The kernel needs an aperture it can write to, so the\r\n" + L" console will be serial-only. Boot continues.\r\n"); + return 0; + } + // The other displays, before anything is drawn: the F8 menu is the first // thing on screen and there is no reason for it to be the one screen that // does not reach them. @@ -1263,32 +1418,54 @@ setup_framebuffer(void) // Re-read after any mode change: Info and FrameBufferBase both describe the mode // that is current, and the framebuffer can move when the mode changes. EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *info = gop->Mode->Info; - UINT32 fb = (UINT32)gop->Mode->FrameBufferBase; - // The layout kernel/dev/vga.c reads, byte for byte. It is a VBE mode-info - // block as the BIOS path leaves it, and the kernel takes exactly four - // fields out of it -- so those four are what get written, at the offsets - // it uses. + // **Sixty-four bits, and not truncated.** `(UINT32)FrameBufferBase` is + // what this was, and on a machine whose aperture sits above 4 GB -- any + // laptop with a discrete GPU -- it produced a plausible-looking address + // pointing at nothing. The splash went into it, the kernel was handed it, + // and the screen kept the firmware's own text cursor blinking on black. + UINT64 fb = gop->Mode->FrameBufferBase; + UINT64 fbsz = gop->Mode->FrameBufferSize; + UINT32 bpp = 32; // GOP's 8-bit formats are 32bpp + UINT32 pitch = info->PixelsPerScanLine * (bpp / 8); + + // The display block, which can describe a real machine. See + // SCHBOOT_FB_BASE for what the VBE one cannot say. + *(UINT64 *)SCHBOOT_FB_BASE = fb; + *(UINT64 *)SCHBOOT_FB_SIZE = fbsz; + *(UINT32 *)SCHBOOT_FB_WIDTH = info->HorizontalResolution; + *(UINT32 *)SCHBOOT_FB_HEIGHT = info->VerticalResolution; + *(UINT32 *)SCHBOOT_FB_STRIDE = pitch; + *(UINT32 *)SCHBOOT_FB_FORMAT = info->PixelFormat; + + // The VBE block as well, byte for byte as the BIOS path leaves it: it is + // what the kernel read before this block existed, and what the BIOS loader + // still writes. Anything that does not fit is left at zero rather than + // wrapped -- a truncated address in here is exactly the bug above, and a + // zero is at least honest. UINT8 *vbe = (UINT8 *)SCHBOOT_VBE_INFO; zero(vbe, 512); - UINT32 bpp = 32; // GOP's 8-bit formats are 32bpp - UINT16 stride = (UINT16)(info->PixelsPerScanLine * (bpp / 8)); - - *(UINT16 *)(vbe + 16) = stride; // bytes/line + if (pitch <= 0xFFFF) + *(UINT16 *)(vbe + 16) = (UINT16)pitch; // bytes/line *(UINT16 *)(vbe + 18) = (UINT16)info->HorizontalResolution; *(UINT16 *)(vbe + 20) = (UINT16)info->VerticalResolution; *(UINT8 *)(vbe + 25) = (UINT8)bpp; - *(UINT32 *)(vbe + 40) = fb; + if (fb <= 0xFFFFFFFFULL) + *(UINT32 *)(vbe + 40) = (UINT32)fb; // The same UI the BIOS loader draws, on the framebuffer this one just // obtained. Everything above this point is firmware-specific; nothing below // it is, which is the whole reason it can be shared. + // + // UINTN, so the pointer is the whole address. The loader runs with the + // firmware's identity map, so a physical address above 4 GB is directly + // usable here -- it is the *kernel* that needs an ioremap for one. struct ui_fb uifb; uifb.base = (ui_u8 *)(UINTN)fb; uifb.width = info->HorizontalResolution; uifb.height = info->VerticalResolution; - uifb.stride = stride; + uifb.stride = pitch; // Tell the firmware its console is empty before drawing over it. ConOut // keeps its own idea of what is on screen and repaints that region when it // feels like it -- a screenshot showed a black band across the top third, diff --git a/docs/architecture/boot.md b/docs/architecture/boot.md index a958c7a..df115a6 100644 --- a/docs/architecture/boot.md +++ b/docs/architecture/boot.md @@ -68,6 +68,74 @@ a missing table. **The E820 cap is 40 entries, not 32.** Real UEFI firmware produces far more descriptors than QEMU's, and the space was already there. +### Choosing a display, on a machine that has more than one + +`LocateProtocol` answers with *a* GOP, and the specification says nothing about +which. On QEMU with a stdvga and a virtio-gpu there are **three** handles, and +two of them report `PixelBltOnly` with a framebuffer address of zero: + +``` +schos: 3 display adapter(s) + gop: 1280x800 BGRX scanline 1280 fb 0x0000000080000000 size 0x3e8000 + gop: 1280x800 Blt-only scanline 1280 fb 0x0000000000000000 size 0x0 + gop: 1280x800 Blt-only scanline 1280 fb 0x0000000000000000 size 0x0 +schos: display 1280x800 BGRX scanline 1280 fb 0x0000000080000000 size 0x3e8000 +``` + +A laptop with switchable graphics is the same shape: an integrated adapter, a +discrete one, and often a console splitter over both. Taking the first handle +means a one-in-three chance of drawing into an adapter with no aperture — a +black screen with the firmware's own cursor still blinking on it, which looks +exactly like a loader that crashed. + +So every handle is examined and scored: an adapter with no framebuffer address +is not a candidate at all, one whose current mode is readable beats one that +would need changing, and size breaks the tie. The report above is printed to +the firmware's text console, which is cleared a few lines later before the +splash — so on a boot that works nobody sees it, and on a boot that dies before +the splash it is the only thing on the screen. + +### A framebuffer address is sixty-four bits + +The loader used to hand over `(UINT32)gop->Mode->FrameBufferBase`, and the +kernel used to read a 32-bit field out of a *VBE mode-info block* — a BIOS +structure the UEFI path had been filling in because the kernel already knew how +to read one. + +Any machine that puts its aperture above 4 GB — which is ordinary on a laptop +with a discrete GPU — got a truncated address that pointed at nothing. The +splash was drawn into it, the kernel was handed it, and the screen kept the +firmware's cursor. There is no error anywhere in that sequence; every step +succeeded on a number that was wrong. + +`SCHBOOT_FB_BASE` is the fix: the loader's own display block, with a 64-bit +address, a 64-bit size, 32-bit width, height and stride, and the pixel format. +The VBE block is still written for the BIOS loader and for anything that has +not been taught to look at the new one, but anything that does not fit is left +at **zero** rather than wrapped — a truncated address is the bug above, and a +zero is at least honest about it. + +The kernel then has a second problem the loader does not: it runs on its own +page tables, and its direct map covers the low 4 GB. A framebuffer above that +needs `ioremap()`, which needs `kpdpt`, which `kvmalloc()` has not allocated +when the early console is set up. So a low framebuffer is attached exactly as +before, and a high one is attached a few dozen instructions later, immediately +after `kvmalloc()` — still thousands of lines before anything else happens. + +### Modes a real machine actually has + +`mode_usable()` rejected three things it should not have: + +| Was rejected | Why it was wrong | +| --- | --- | +| `PixelsPerScanLine != HorizontalResolution` | A GPU is entitled to pad each row, and 1920×1080 arriving with a 1984- or 2048-pixel scanline is ordinary. The stride is carried through to the kernel, so this excluded nothing but every machine that pads. | +| `HorizontalResolution % 8` | Excludes 1366×768, the panel on a very large number of laptops. | +| resolutions below 800×600 | 640×480 is a legitimate fallback and the only mode some firmware offers over a KVM. | + +All three passed on QEMU, which is why they survived. None of them protected +anything: the drawing code steps by the stride and has done for as long as +there has been a stride to step by. + ### The entry point The kernel publishes a `sch64_header` — magic `SCH64BOO`, 8-byte aligned within diff --git a/include/schboot.h b/include/schboot.h index 8baeee6..e0ca267 100644 --- a/include/schboot.h +++ b/include/schboot.h @@ -112,6 +112,39 @@ #define SCHBOOT_FONT_ADDR (SCHBOOT_BASE + 0x608) /* uint64, physical */ #define SCHBOOT_FONT_LEN (SCHBOOT_BASE + 0x610) /* uint32, bytes */ +// The display, described by the loader in its own words rather than in VBE's. +// +// The VBE mode-info block above is a *BIOS* structure, and the UEFI loader has +// been filling one in because the kernel already knew how to read it. Three of +// its fields cannot describe a real machine: +// +// - the framebuffer address is **32 bits**. A laptop with a discrete GPU +// routinely puts its aperture above 4 GB, and the loader was casting +// `(UINT32)gop->Mode->FrameBufferBase` -- so the splash was drawn into a +// truncated address, nothing appeared, and the firmware's own text cursor +// was left blinking on an otherwise black screen. That is the whole of the +// "it boots to a white underscore" report. +// - the stride is 16 bits, which runs out at 16383 pixels of 32-bit colour. +// - there is nowhere to say whether the pixels are RGBX or BGRX, and GOP +// reports both. +// +// So the UEFI path says it properly. The VBE block is still written, for the +// BIOS loader and for any kernel that has not been taught to look here; a +// non-zero FB_BASE means this block is authoritative and the other is a copy. +// +// FB_BASE is *physical*, and deliberately not translated: above 4 GB it is +// outside the kernel's direct map and has to be ioremap()ed, which cannot +// happen until there are page tables to put it in. +#define SCHBOOT_FB_BASE (SCHBOOT_BASE + 0x618) /* uint64, physical */ +#define SCHBOOT_FB_SIZE (SCHBOOT_BASE + 0x620) /* uint64, bytes */ +#define SCHBOOT_FB_WIDTH (SCHBOOT_BASE + 0x628) /* uint32, pixels */ +#define SCHBOOT_FB_HEIGHT (SCHBOOT_BASE + 0x62C) /* uint32, pixels */ +#define SCHBOOT_FB_STRIDE (SCHBOOT_BASE + 0x630) /* uint32, bytes */ +// 0 = RGBX8888, 1 = BGRX8888 -- the two GOP formats with a linear layout, in +// EFI's own numbering. Everything in the kernel draws 0x00RRGGBB, which is +// BGRX in memory, so format 0 is the one that needs its channels swapped. +#define SCHBOOT_FB_FORMAT (SCHBOOT_BASE + 0x634) /* uint32 */ + #ifndef __ASSEMBLER__ struct sch64_header { diff --git a/kernel/core/main.c b/kernel/core/main.c index ad8accf..c129e22 100644 --- a/kernel/core/main.c +++ b/kernel/core/main.c @@ -33,6 +33,12 @@ int boot_verbose; int kmain(uint32 fb_addr) { + // Carried out of the block below, for the framebuffer that could not be + // attached there because it needs page tables that do not exist yet. + struct fb_info fb_info_early; + uint64 fb_early = 0; + int fb_early_ok = 0, fb_attached = 0; + boot_verbose = *(uchar*)P2V(SCHBOOT_VERBOSE_FLAG); // On a verbose boot, put the kernel log on the screen from the first line. @@ -74,16 +80,39 @@ kmain(uint32 fb_addr) // real-mode IVT, the BIOS data area, the AP trampoline at 0x7000 and the // early page tables. The machine reset before printing anything and came // back round to the firmware, for ever. - uint32 fb_phys = *(uint32*)P2V(SCHBOOT_VBE_LFB); - bi.ptr = (volatile uchar *)P2V((uint64)fb_phys); - bi.width = *(uint16*)P2V(SCHBOOT_VBE_WIDTH); - bi.height = *(uint16*)P2V(SCHBOOT_VBE_HEIGHT); - bi.stride = *(uint16*)P2V(SCHBOOT_VBE_STRIDE); - bi.bpp = *(uchar *)P2V(SCHBOOT_VBE_BPP); - // Only if the loader actually described something. A zero here means the - // BIOS path or a firmware that gave no GOP, and drawing into address zero - // is a worse failure than staying quiet. - if (fb_phys && bi.width && bi.height && bi.stride && bi.bpp >= 16) { + // The UEFI loader's own display block if it wrote one, and the VBE block + // otherwise. See SCHBOOT_FB_BASE: the VBE structure is a BIOS one and its + // framebuffer address is 32 bits, which is not enough to describe a + // machine whose aperture is above 4 GB. + uint64 fb_phys = *(uint64*)P2V(SCHBOOT_FB_BASE); + + if (fb_phys) { + bi.width = *(uint32*)P2V(SCHBOOT_FB_WIDTH); + bi.height = *(uint32*)P2V(SCHBOOT_FB_HEIGHT); + bi.stride = *(uint32*)P2V(SCHBOOT_FB_STRIDE); + bi.bpp = 32; + } else { + fb_phys = *(uint32*)P2V(SCHBOOT_VBE_LFB); + bi.width = *(uint16*)P2V(SCHBOOT_VBE_WIDTH); + bi.height = *(uint16*)P2V(SCHBOOT_VBE_HEIGHT); + bi.stride = *(uint16*)P2V(SCHBOOT_VBE_STRIDE); + bi.bpp = *(uchar *)P2V(SCHBOOT_VBE_BPP); + } + bi.ptr = (volatile uchar *)P2V(fb_phys); + + // Only if the loader actually described something, and only if the direct + // map reaches it. A framebuffer above 4 GB needs ioremap(), and ioremap() + // needs kpdpt, which kvmalloc() has not allocated yet -- so that case is + // picked up further down, a few dozen instructions later, rather than + // here. Everything else about the early console is unchanged. + // + // A zero here means the BIOS path or a firmware that gave no GOP, and + // drawing into address zero is a worse failure than staying quiet. + fb_early = fb_phys; + fb_early_ok = fb_phys && bi.width && bi.height && bi.stride && bi.bpp >= 16; + + if (fb_early_ok && fb_phys + (uint64)bi.stride * bi.height <= 4ULL << 30) { + fb_attached = 1; gfx_attach_scanout(&bi); // Before vt_init, which reads the cell size to work out how many rows // and columns there are. The font is in the kernel image, so this needs @@ -92,6 +121,7 @@ kmain(uint32 fb_addr) gfx_clear(0x0C0E13); /* SCH_BG; theme.h is not included here */ cons_serial_only = 0; } + fb_info_early = bi; } uartearlyinit(); @@ -127,6 +157,32 @@ kmain(uint32 fb_addr) if (boot_verbose) cprintf("verbose boot: dma_init done\n"); kvmalloc(); if (boot_verbose) cprintf("verbose boot: kvmalloc done\n"); + + // The other kind of framebuffer: one the direct map does not reach. + // + // Any machine with a discrete GPU is entitled to put its aperture above + // 4 GB, and plenty do. P2V() cannot describe it, so the early block above + // deliberately left it alone and it is mapped here instead -- the first + // point in the boot where there is a page table to map it into. The console + // starts a few lines later than it would on a low framebuffer, which is + // still thousands of lines earlier than anything else. + if (fb_early_ok && !fb_attached) { + uint64 bytes = (uint64)fb_info_early.stride * fb_info_early.height; + void *va = ioremap(fb_early, bytes); + + if (va) { + fb_info_early.ptr = (volatile uchar *)va; + gfx_attach_scanout(&fb_info_early); + vt_init(); + gfx_clear(0x0C0E13); /* SCH_BG */ + cons_serial_only = 0; + kinfo("gfx: framebuffer at %p is above the direct map; mapped at %p\n", + (void *)fb_early, va); + } else { + cprintf("gfx: cannot map the framebuffer at %p; no graphical console\n", + (void *)fb_early); + } + } // ACPI first, the legacy MP table only if it finds nothing. Modern firmware // publishes a complete MADT and an MP table with one processor in it, so // asking the older interface first means never seeing more than one CPU.