boot: a framebuffer address is sixty-four bits, and there is more than one GPU

The first piece of real-hardware work, and it is three bugs that QEMU cannot
show you. The report was an ASUS laptop -- Ryzen 7000-series APU, Radeon
integrated, RTX discrete -- booting to a white underscore on a black screen,
and unable to be offered 1920x1080.

**The framebuffer address was truncated.** The loader handed over
`(UINT32)gop->Mode->FrameBufferBase` and the kernel 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. A machine that puts its
aperture above 4 GB, which is ordinary on a laptop with a discrete GPU, got a
plausible-looking address pointing at nothing. The splash went into it, the
kernel was handed it, and the firmware's own cursor kept blinking on an
otherwise black screen. Every step in that sequence succeeded on a number that
was wrong, which is why there is no error message anywhere in it.

SCHBOOT_FB_BASE is the loader's own display block: 64-bit address, 64-bit
size, 32-bit width, height and stride, and the pixel format. The VBE block is
still written for the BIOS loader, but anything that does not fit is left at
zero rather than wrapped -- a truncated address is this bug, and a zero is at
least honest.

The kernel then has a problem the loader does not: its direct map covers the
low 4 GB, so a framebuffer above it needs ioremap(), which needs kpdpt, which
kvmalloc() has not allocated when the early console starts. A low framebuffer
is attached exactly as before; a high one is attached immediately after
kvmalloc(), still thousands of lines before anything else.

**There was more than one adapter, and the first one was taken.**
LocateProtocol answers with *a* GOP and the specification says nothing about
which. QEMU with a stdvga and a virtio-gpu has **three** handles and two of
them are Blt-only with a framebuffer address of zero -- so this is not a
hardware-only hazard, it was a one-in-three chance of drawing into address 0
that happened to keep coming up heads. Every handle is scored now: no
aperture, no candidacy; a readable current mode beats one that needs changing;
size breaks the tie.

**mode_usable() rejected modes real machines have.** A padded scanline (every
GPU that aligns its rows -- and very likely the reason 1920x1080 could not be
offered), a width not divisible by eight (1366x768, which is half the laptops
in the world), and anything below 800x600 (the only mode some firmware gives
over a KVM). All three passed on QEMU, which is why they survived. The mode
list now offers 1366x768 and 640x480 where it did not.

The loader also prints what it found, to the firmware's text console, which is
cleared before the splash: invisible on a boot that works, and the only thing
on the screen on a boot that dies before the splash.

One process note, recorded because it cost a wrong test: `make UEFI_KEEP=1`
ships the loader that is already built. Its own comment says "a build that
quietly ships a stale bootloader is how you spend an evening debugging code
that is not running", and that is exactly what happened -- a regression run
that looked clean had tested the old loader. Loader work gets a full build.

Both supported configurations verified after a clean build: one boot each, no
traps, no panics, socktest and memfdtest green, and the adapter report shows
the right one being chosen out of three.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Nexoniarz 2026-09-23 19:19:25 +02:00
commit 35ea398334
4 changed files with 358 additions and 24 deletions

View file

@ -205,6 +205,21 @@ print(const CHAR16 *s)
ST->ConOut->OutputString(ST->ConOut, (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 static void
print_hex(UINT64 v) print_hex(UINT64 v)
{ {
@ -618,10 +633,26 @@ static int
mode_usable(EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *mi) mode_usable(EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *mi)
{ {
if (!mi) return 0; 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->PixelFormat > 1) return 0;
if (mi->HorizontalResolution < 800 || mi->VerticalResolution < 600) return 0; if (mi->HorizontalResolution < 640 || mi->VerticalResolution < 480) return 0;
if (mi->HorizontalResolution % 8) return 0;
if (mi->PixelsPerScanLine != mi->HorizontalResolution) 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; 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 /* 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 * 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. * lives in that menu rather than in a second prompt of its own.
@ -1250,6 +1387,24 @@ setup_framebuffer(void)
return 0; 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 // 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 // thing on screen and there is no reason for it to be the one screen that
// does not reach them. // does not reach them.
@ -1263,32 +1418,54 @@ setup_framebuffer(void)
// Re-read after any mode change: Info and FrameBufferBase both describe the mode // 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. // that is current, and the framebuffer can move when the mode changes.
EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *info = gop->Mode->Info; 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 // **Sixty-four bits, and not truncated.** `(UINT32)FrameBufferBase` is
// block as the BIOS path leaves it, and the kernel takes exactly four // what this was, and on a machine whose aperture sits above 4 GB -- any
// fields out of it -- so those four are what get written, at the offsets // laptop with a discrete GPU -- it produced a plausible-looking address
// it uses. // 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; UINT8 *vbe = (UINT8 *)SCHBOOT_VBE_INFO;
zero(vbe, 512); zero(vbe, 512);
UINT32 bpp = 32; // GOP's 8-bit formats are 32bpp if (pitch <= 0xFFFF)
UINT16 stride = (UINT16)(info->PixelsPerScanLine * (bpp / 8)); *(UINT16 *)(vbe + 16) = (UINT16)pitch; // bytes/line
*(UINT16 *)(vbe + 16) = stride; // bytes/line
*(UINT16 *)(vbe + 18) = (UINT16)info->HorizontalResolution; *(UINT16 *)(vbe + 18) = (UINT16)info->HorizontalResolution;
*(UINT16 *)(vbe + 20) = (UINT16)info->VerticalResolution; *(UINT16 *)(vbe + 20) = (UINT16)info->VerticalResolution;
*(UINT8 *)(vbe + 25) = (UINT8)bpp; *(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 // The same UI the BIOS loader draws, on the framebuffer this one just
// obtained. Everything above this point is firmware-specific; nothing below // obtained. Everything above this point is firmware-specific; nothing below
// it is, which is the whole reason it can be shared. // 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; struct ui_fb uifb;
uifb.base = (ui_u8 *)(UINTN)fb; uifb.base = (ui_u8 *)(UINTN)fb;
uifb.width = info->HorizontalResolution; uifb.width = info->HorizontalResolution;
uifb.height = info->VerticalResolution; uifb.height = info->VerticalResolution;
uifb.stride = stride; uifb.stride = pitch;
// Tell the firmware its console is empty before drawing over it. ConOut // 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 // 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, // feels like it -- a screenshot showed a black band across the top third,

View file

@ -68,6 +68,74 @@ a missing table.
**The E820 cap is 40 entries, not 32.** Real UEFI firmware produces far more **The E820 cap is 40 entries, not 32.** Real UEFI firmware produces far more
descriptors than QEMU's, and the space was already there. 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 entry point
The kernel publishes a `sch64_header` — magic `SCH64BOO`, 8-byte aligned within The kernel publishes a `sch64_header` — magic `SCH64BOO`, 8-byte aligned within

View file

@ -112,6 +112,39 @@
#define SCHBOOT_FONT_ADDR (SCHBOOT_BASE + 0x608) /* uint64, physical */ #define SCHBOOT_FONT_ADDR (SCHBOOT_BASE + 0x608) /* uint64, physical */
#define SCHBOOT_FONT_LEN (SCHBOOT_BASE + 0x610) /* uint32, bytes */ #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__ #ifndef __ASSEMBLER__
struct sch64_header { struct sch64_header {

View file

@ -33,6 +33,12 @@ int boot_verbose;
int int
kmain(uint32 fb_addr) 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); boot_verbose = *(uchar*)P2V(SCHBOOT_VERBOSE_FLAG);
// On a verbose boot, put the kernel log on the screen from the first line. // 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 // 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 // early page tables. The machine reset before printing anything and came
// back round to the firmware, for ever. // back round to the firmware, for ever.
uint32 fb_phys = *(uint32*)P2V(SCHBOOT_VBE_LFB); // The UEFI loader's own display block if it wrote one, and the VBE block
bi.ptr = (volatile uchar *)P2V((uint64)fb_phys); // otherwise. See SCHBOOT_FB_BASE: the VBE structure is a BIOS one and its
bi.width = *(uint16*)P2V(SCHBOOT_VBE_WIDTH); // framebuffer address is 32 bits, which is not enough to describe a
bi.height = *(uint16*)P2V(SCHBOOT_VBE_HEIGHT); // machine whose aperture is above 4 GB.
bi.stride = *(uint16*)P2V(SCHBOOT_VBE_STRIDE); uint64 fb_phys = *(uint64*)P2V(SCHBOOT_FB_BASE);
bi.bpp = *(uchar *)P2V(SCHBOOT_VBE_BPP);
// Only if the loader actually described something. A zero here means the if (fb_phys) {
// BIOS path or a firmware that gave no GOP, and drawing into address zero bi.width = *(uint32*)P2V(SCHBOOT_FB_WIDTH);
// is a worse failure than staying quiet. bi.height = *(uint32*)P2V(SCHBOOT_FB_HEIGHT);
if (fb_phys && bi.width && bi.height && bi.stride && bi.bpp >= 16) { 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); gfx_attach_scanout(&bi);
// Before vt_init, which reads the cell size to work out how many rows // 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 // 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 */ gfx_clear(0x0C0E13); /* SCH_BG; theme.h is not included here */
cons_serial_only = 0; cons_serial_only = 0;
} }
fb_info_early = bi;
} }
uartearlyinit(); uartearlyinit();
@ -127,6 +157,32 @@ kmain(uint32 fb_addr)
if (boot_verbose) cprintf("verbose boot: dma_init done\n"); if (boot_verbose) cprintf("verbose boot: dma_init done\n");
kvmalloc(); kvmalloc();
if (boot_verbose) cprintf("verbose boot: kvmalloc done\n"); 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 // 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 // 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. // asking the older interface first means never seeing more than one CPU.