display: a 64-bit framebuffer address, no mirroring onto itself
Three fixes for real machines that broke at any resolution but 800x600: - kmain() took the loader's framebuffer address as uint32, and vga_fb_init() re-published the scanout from that and the 16-bit VBE block. An aperture above 4 GB moved the console onto the wrong memory right after the vga: line. Both now use the loader's 64-bit display block, mapped with ioremap(). - The loader mirrored onto the console splitter's GOP, which forwards to the primary itself, and onto GOPs that share the primary's aperture. After a mode change that rewrote the screen over itself with another mode's stride. Handles without a device path and overlapping apertures are skipped, and a mode change re-matches the remaining mirrors. - A mode whose reported size, stride or aperture does not add up after SetMode is refused, and the loader returns to the previous one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
This commit is contained in:
parent
c9790df04c
commit
fe32833ade
5 changed files with 193 additions and 16 deletions
|
|
@ -218,6 +218,15 @@ typedef struct {
|
||||||
void *VendorTable;
|
void *VendorTable;
|
||||||
} EFI_CONFIGURATION_TABLE;
|
} EFI_CONFIGURATION_TABLE;
|
||||||
|
|
||||||
|
// --- device path ----------------------------------------------------------
|
||||||
|
//
|
||||||
|
// Only ever asked whether a handle *has* one: a protocol installed by a real
|
||||||
|
// device's driver sits on a handle with a path to that device, and a virtual
|
||||||
|
// one -- the console splitter's GOP -- sits on a handle with none.
|
||||||
|
|
||||||
|
#define EFI_DEVICE_PATH_PROTOCOL_GUID \
|
||||||
|
{ 0x09576E91, 0x6D3F, 0x11D2, { 0x8E, 0x39, 0x00, 0xA0, 0xC9, 0x69, 0x72, 0x3B } }
|
||||||
|
|
||||||
// --- loaded image ---------------------------------------------------------
|
// --- loaded image ---------------------------------------------------------
|
||||||
|
|
||||||
#define EFI_LOADED_IMAGE_PROTOCOL_GUID \
|
#define EFI_LOADED_IMAGE_PROTOCOL_GUID \
|
||||||
|
|
|
||||||
106
boot/uefi/main.c
106
boot/uefi/main.c
|
|
@ -88,6 +88,8 @@ static EFI_BOOT_SERVICES *BS;
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
static void mirrors_show(void); /* see "the other displays", further down */
|
static void mirrors_show(void); /* see "the other displays", further down */
|
||||||
|
static void mirrors_match(EFI_GRAPHICS_OUTPUT_PROTOCOL *primary);
|
||||||
|
static void describe_gop(EFI_GRAPHICS_OUTPUT_PROTOCOL *g, const CHAR16 *tag);
|
||||||
|
|
||||||
/* Once the splash is up, nothing may write to the console.
|
/* Once the splash is up, nothing may write to the console.
|
||||||
*
|
*
|
||||||
|
|
@ -793,6 +795,64 @@ res_read_number(const char *prompt)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* Set a mode, and keep it only if the firmware's own account of it adds up.
|
||||||
|
*
|
||||||
|
* A GOP can accept a mode and then describe it in a way the display does not
|
||||||
|
* scan out: a row length that is not the one being shown, or an aperture too
|
||||||
|
* small for the rows it claims. Drawn into anyway, that is the picture sheared
|
||||||
|
* into dotted lines, or a logo repeated across the screen, both of which a
|
||||||
|
* desktop with an NVIDIA card showed at sizes other than 800x600. The numbers
|
||||||
|
* cannot prove a mode is right, but they can prove one is wrong, and a wrong
|
||||||
|
* one is refused here, in the menu, where there is still a screen to say so on.
|
||||||
|
*/
|
||||||
|
static int
|
||||||
|
mode_set_checked(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop, UINT32 mode)
|
||||||
|
{
|
||||||
|
UINT32 was = gop->Mode->Mode;
|
||||||
|
EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *want = 0;
|
||||||
|
UINTN sz = 0;
|
||||||
|
|
||||||
|
if (EFI_ERROR(gop->QueryMode(gop, mode, &sz, &want)) || !want)
|
||||||
|
return 0;
|
||||||
|
if (EFI_ERROR(gop->SetMode(gop, mode)))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *got = gop->Mode->Info;
|
||||||
|
UINT64 need = (UINT64)got->PixelsPerScanLine * 4 * got->VerticalResolution;
|
||||||
|
if (got->HorizontalResolution == want->HorizontalResolution &&
|
||||||
|
got->VerticalResolution == want->VerticalResolution &&
|
||||||
|
got->PixelsPerScanLine >= got->HorizontalResolution &&
|
||||||
|
gop->Mode->FrameBufferBase &&
|
||||||
|
gop->Mode->FrameBufferSize >= need)
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
describe_gop(gop, L"schos: mode does not add up, going back:");
|
||||||
|
gop->SetMode(gop, was);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Said on the screen that is still working, then back to the menu. */
|
||||||
|
static void
|
||||||
|
res_refused(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
|
||||||
|
{
|
||||||
|
struct ui_fb uifb;
|
||||||
|
EFI_INPUT_KEY key;
|
||||||
|
const char *msg[1] = { "The display refused that size -- keeping this one" };
|
||||||
|
|
||||||
|
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);
|
||||||
|
ui_menu("Resolution", msg, 1, 0, "any key to go back");
|
||||||
|
mirrors_show();
|
||||||
|
for (;;) {
|
||||||
|
if (ST->ConIn && !EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key)))
|
||||||
|
break;
|
||||||
|
BS->Stall(10000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
res_choose(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
|
res_choose(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
|
||||||
{
|
{
|
||||||
|
|
@ -850,7 +910,13 @@ res_choose(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
|
||||||
}
|
}
|
||||||
if (c == L'\r' || c == L'\n') {
|
if (c == L'\r' || c == L'\n') {
|
||||||
if (sel < RES_N) {
|
if (sel < RES_N) {
|
||||||
gop->SetMode(gop, RES_MODE[sel]);
|
if (!mode_set_checked(gop, RES_MODE[sel])) {
|
||||||
|
res_refused(gop);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// The one other time the mirrors' modes are worth touching:
|
||||||
|
// the person just said what size the display is.
|
||||||
|
mirrors_match(gop);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
/* Typed. Matched against the same filtered list, so a size that
|
/* Typed. Matched against the same filtered list, so a size that
|
||||||
|
|
@ -865,7 +931,11 @@ res_choose(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
|
||||||
continue;
|
continue;
|
||||||
if (mi->HorizontalResolution == w &&
|
if (mi->HorizontalResolution == w &&
|
||||||
mi->VerticalResolution == h) {
|
mi->VerticalResolution == h) {
|
||||||
gop->SetMode(gop, i);
|
if (!mode_set_checked(gop, i)) {
|
||||||
|
res_refused(gop);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mirrors_match(gop);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1236,6 +1306,7 @@ static EFI_GRAPHICS_OUTPUT_PROTOCOL *primary_gop;
|
||||||
static UINTN mirror_n;
|
static UINTN mirror_n;
|
||||||
|
|
||||||
static void mirrors_match(EFI_GRAPHICS_OUTPUT_PROTOCOL *primary);
|
static void mirrors_match(EFI_GRAPHICS_OUTPUT_PROTOCOL *primary);
|
||||||
|
static void describe_gop(EFI_GRAPHICS_OUTPUT_PROTOCOL *g, const CHAR16 *tag);
|
||||||
|
|
||||||
static void
|
static void
|
||||||
mirrors_find(EFI_GRAPHICS_OUTPUT_PROTOCOL *primary)
|
mirrors_find(EFI_GRAPHICS_OUTPUT_PROTOCOL *primary)
|
||||||
|
|
@ -1269,6 +1340,32 @@ mirrors_find(EFI_GRAPHICS_OUTPUT_PROTOCOL *primary)
|
||||||
continue;
|
continue;
|
||||||
if (!g || g == primary || !g->Blt || !g->Mode || !g->Mode->Info)
|
if (!g || g == primary || !g->Blt || !g->Mode || !g->Mode->Info)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
// Not a second display: the console splitter. Firmware installs one
|
||||||
|
// GOP of its own on a virtual handle and forwards every Blt to all of
|
||||||
|
// the real ones -- the primary included. Mirroring onto it copied the
|
||||||
|
// primary onto itself through the splitter's idea of the mode, which
|
||||||
|
// stops being the primary's the moment the resolution menu changes
|
||||||
|
// it: the picture landed again, offset and with the wrong row length,
|
||||||
|
// over the one already there. That was the doubled, sheared screen with
|
||||||
|
// the logo's edges repeated down it, at every size but the one the
|
||||||
|
// firmware booted in. A real adapter's GOP is on a handle with a path
|
||||||
|
// to the device; the splitter's has none.
|
||||||
|
void *dp = 0;
|
||||||
|
EFI_GUID dp_guid = EFI_DEVICE_PATH_PROTOCOL_GUID;
|
||||||
|
if (EFI_ERROR(BS->HandleProtocol(handles[i], &dp_guid, &dp)) || !dp)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Nor is a second GOP whose aperture is the primary's: one panel
|
||||||
|
// reached twice, which laptops do. Blitting onto it rewrites the
|
||||||
|
// primary's own memory in another mode's layout.
|
||||||
|
UINT64 pb = primary->Mode->FrameBufferBase;
|
||||||
|
UINT64 pe = pb + primary->Mode->FrameBufferSize;
|
||||||
|
UINT64 gb = g->Mode->FrameBufferBase;
|
||||||
|
UINT64 ge = gb + g->Mode->FrameBufferSize;
|
||||||
|
if (gb && pb && gb < pe && pb < ge)
|
||||||
|
continue;
|
||||||
|
|
||||||
mirror_gop[mirror_n++] = g;
|
mirror_gop[mirror_n++] = g;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1292,8 +1389,9 @@ mirrors_find(EFI_GRAPHICS_OUTPUT_PROTOCOL *primary)
|
||||||
static void
|
static void
|
||||||
mirrors_match(EFI_GRAPHICS_OUTPUT_PROTOCOL *primary)
|
mirrors_match(EFI_GRAPHICS_OUTPUT_PROTOCOL *primary)
|
||||||
{
|
{
|
||||||
// Called exactly once, from mirrors_find(), before anything has been
|
// Called from mirrors_find(), before anything has been drawn, and again
|
||||||
// drawn. It used to be called from mirrors_show() -- which is to say on
|
// when the resolution menu sets a mode -- never per frame. It used to be
|
||||||
|
// called from mirrors_show() -- which is to say on
|
||||||
// every menu redraw and on every segment of the kernel load -- and that is
|
// every menu redraw and on every segment of the kernel load -- and that is
|
||||||
// not a safe thing to do to another display adapter.
|
// not a safe thing to do to another display adapter.
|
||||||
//
|
//
|
||||||
|
|
|
||||||
|
|
@ -1212,3 +1212,46 @@ fallback while claiming not to be.
|
||||||
"A font is installed" and "a typeface was chosen" had been the same test
|
"A font is installed" and "a typeface was chosen" had been the same test
|
||||||
(`cfont == 0`) and quietly stopped being. They are two flags now, and
|
(`cfont == 0`) and quietly stopped being. They are two flags now, and
|
||||||
`gfx_font_ready()` answers the second one.
|
`gfx_font_ready()` answers the second one.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 23. Every resolution but 800x600, on two real machines
|
||||||
|
|
||||||
|
Two reports in one afternoon. A desktop with an NVIDIA card booted fine at
|
||||||
|
800x600 and at anything else showed the kernel log as dotted lines across the
|
||||||
|
screen, and the loader's splash as four squashed copies of the logo side by
|
||||||
|
side. A laptop stopped dead after the kernel's `vga:` line. Three separate
|
||||||
|
defects were found under them; the third is a guard rather than a cure, and
|
||||||
|
the desktop has not yet been retested.
|
||||||
|
|
||||||
|
**The framebuffer address was cut in half.** The loader hands the kernel all
|
||||||
|
sixty-four bits of the aperture address in RBX, and `kmain()` took it as a
|
||||||
|
`uint32`. Below 4 GB that is harmless. Above it -- where a laptop's GPU is
|
||||||
|
entitled to put its aperture -- the low half is a plausible address pointing
|
||||||
|
at nothing. `kmain()` itself reads the loader's display block and puts the
|
||||||
|
early console on the right memory, but `vga_fb_init()` then re-published the
|
||||||
|
scanout from the truncated argument and the BIOS-era VBE block (32-bit
|
||||||
|
address, 16-bit sizes). The second publication won, and every line after
|
||||||
|
`vga:` went somewhere else. Both are fixed: `kmain(uint64)`, and
|
||||||
|
`vga_fb_init()` reads the same 64-bit block `kmain()` does and maps it with
|
||||||
|
`ioremap()` instead of `P2V()`.
|
||||||
|
|
||||||
|
**The loader mirrored the screen onto itself.** It copies its picture to
|
||||||
|
every other GOP it finds, for machines with two displays. But the firmware's
|
||||||
|
console splitter is itself a GOP, on a virtual handle, and its `Blt` forwards
|
||||||
|
to every real adapter -- the primary included. A laptop that reaches one panel
|
||||||
|
through two GOPs is the same thing with a shared aperture. Once the resolution
|
||||||
|
menu changed the primary's mode, the splitter still described the old one, so
|
||||||
|
each mirror pass wrote the primary's own pixels back over themselves at an
|
||||||
|
offset and with another mode's row length. Handles without a device path (the
|
||||||
|
splitter) and GOPs whose aperture overlaps the primary's are no longer mirror
|
||||||
|
targets, and a mode chosen in the menu re-matches the mirrors that remain.
|
||||||
|
QEMU's OVMF has the splitter too: `schos: 2 display adapter(s)`, both at the
|
||||||
|
same address.
|
||||||
|
|
||||||
|
**A mode the firmware cannot describe is refused.** After `SetMode`, the
|
||||||
|
loader now checks that the mode it got is the size it asked for, that the row
|
||||||
|
is at least as long as the width, and that the aperture holds that many rows.
|
||||||
|
If not, it goes back to the previous mode and says so in the menu. This proves
|
||||||
|
nothing about a mode that passes, but it stops a lying one from reaching the
|
||||||
|
kernel.
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,13 @@ int boot_debug;
|
||||||
// Bootstrap processor starts running C code here.
|
// Bootstrap processor starts running C code here.
|
||||||
// Allocate a real stack and switch to it, first
|
// Allocate a real stack and switch to it, first
|
||||||
// doing some setup required for memory allocator to work.
|
// doing some setup required for memory allocator to work.
|
||||||
|
// fb_addr is RBX from the loader, all sixty-four bits of it. It was declared
|
||||||
|
// uint32, which cut an aperture above 4 GB down to whatever its low half
|
||||||
|
// happened to be -- a plausible address below the hole, pointing at nothing
|
||||||
|
// in particular -- and vga_fb_init() moved the console onto it. On a laptop
|
||||||
|
// that is the screen stopping dead at the "vga:" line.
|
||||||
int
|
int
|
||||||
kmain(uint32 fb_addr)
|
kmain(uint64 fb_addr)
|
||||||
{
|
{
|
||||||
// Carried out of the block below, for the framebuffer that could not be
|
// 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.
|
// attached there because it needs page tables that do not exist yet.
|
||||||
|
|
|
||||||
|
|
@ -69,15 +69,29 @@ vga_fb_init(uint64 phys_addr)
|
||||||
{
|
{
|
||||||
struct fb_info info;
|
struct fb_info info;
|
||||||
|
|
||||||
|
// The UEFI loader's display block first, and the VBE block only without it.
|
||||||
|
//
|
||||||
|
// This read the VBE block alone, which is a BIOS structure: a 32-bit
|
||||||
|
// address and 16-bit sizes. kmain() reads the loader's block and puts the
|
||||||
|
// console on the right framebuffer; this then re-published the scanout from
|
||||||
|
// the other one, so the two disagreed on any machine the VBE block cannot
|
||||||
|
// describe, and the second one won.
|
||||||
|
uint64 blk = *(uint64*)P2V(SCHBOOT_FB_BASE);
|
||||||
|
if (blk) {
|
||||||
|
phys_addr = blk;
|
||||||
|
fb_stride = *(uint32*)P2V(SCHBOOT_FB_STRIDE);
|
||||||
|
fb_width = *(uint32*)P2V(SCHBOOT_FB_WIDTH);
|
||||||
|
fb_height = *(uint32*)P2V(SCHBOOT_FB_HEIGHT);
|
||||||
|
fb_bpp = 32;
|
||||||
|
} else {
|
||||||
if (phys_addr == 0)
|
if (phys_addr == 0)
|
||||||
phys_addr = *(uint32*)(P2V(SCHBOOT_VBE_LFB));
|
phys_addr = *(uint32*)(P2V(SCHBOOT_VBE_LFB));
|
||||||
|
|
||||||
fb = (volatile uchar*)P2V(phys_addr);
|
|
||||||
fb_phys = phys_addr;
|
|
||||||
fb_stride = *(ushort*)(P2V(SCHBOOT_VBE_STRIDE));
|
fb_stride = *(ushort*)(P2V(SCHBOOT_VBE_STRIDE));
|
||||||
fb_width = *(ushort*)(P2V(SCHBOOT_VBE_WIDTH));
|
fb_width = *(ushort*)(P2V(SCHBOOT_VBE_WIDTH));
|
||||||
fb_height = *(ushort*)(P2V(SCHBOOT_VBE_HEIGHT));
|
fb_height = *(ushort*)(P2V(SCHBOOT_VBE_HEIGHT));
|
||||||
fb_bpp = *(uchar*)(P2V(SCHBOOT_VBE_BPP));
|
fb_bpp = *(uchar*)(P2V(SCHBOOT_VBE_BPP));
|
||||||
|
}
|
||||||
|
fb_phys = phys_addr;
|
||||||
|
|
||||||
if (fb_width == 0) fb_width = 800;
|
if (fb_width == 0) fb_width = 800;
|
||||||
if (fb_height == 0) fb_height = 600;
|
if (fb_height == 0) fb_height = 600;
|
||||||
|
|
@ -100,8 +114,16 @@ vga_fb_init(uint64 phys_addr)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
kinfo("vga: %dx%d, %d bpp, stride %d, LFB: 0x%x\n",
|
// ioremap() rather than P2V(): the same address below 4 GB, and the window
|
||||||
fb_width, fb_height, fb_bpp, fb_stride, (uint32)phys_addr);
|
// kmain() already opened for an aperture above it.
|
||||||
|
fb = (volatile uchar*)ioremap(phys_addr, (uint64)fb_stride * fb_height);
|
||||||
|
if (fb == 0) {
|
||||||
|
cprintf("vga: cannot map the framebuffer at %p\n", (void *)phys_addr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
kinfo("vga: %dx%d, %d bpp, stride %d, LFB: %p\n",
|
||||||
|
fb_width, fb_height, fb_bpp, fb_stride, (void *)phys_addr);
|
||||||
|
|
||||||
info.ptr = fb;
|
info.ptr = fb;
|
||||||
info.phys = fb_phys;
|
info.phys = fb_phys;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue