Two things, both asked for. **The crash report no longer says which monitors it reached.** "The report is on the virtio-gpu display too" was a line about the crash screen printed on the crash screen. The report says what happened and where to read it again; that is not part of it. **And the second head now shows the F8 menu and the boot.** It used to be black until vgpud started several seconds into userspace -- through the splash, through the menu, through every boot message -- which from the outside is indistinguishable from a display that does not work. Three stages now cover the whole boot and hand over to each other without coordinating: *The loader.* LocateProtocol() answers with the first GOP and offers no way to ask for the rest, so the rest are found by handle and fed the primary's framebuffer as a Blt source at the points where a screen is finished: the menu, the resolution chooser, system information, the splash, each segment of the kernel load. Blt rather than a second framebuffer to draw into, because a virtio GOP does not have one -- the same fact that made a virtio-only machine reboot-loop two commits ago. *The kernel.* gfx remembers which rows were drawn, as one span rather than a list of rectangles, because console output is lines and a span is what lines make. display_mirror_console() copies that span across: a line of boot text is one cell-row transfer, not a screen. Primed once after pci_init(), because a display that has just appeared has a black scanout and the console it should be showing was drawn by the loader into a framebuffer it knows nothing about. *Then vgpud*, from the same console framebuffer, unchanged. **Two bugs found building it, both worth the retelling.** The mirror ran per character from inside vt_feed -- which is inside the console lock -- and vga_disp_acquire() announces its back buffer with a cprintf. A CPU taking a lock it already holds, with a call chain that said exactly that. It runs at the end of consolewrite() and cprintf() now, outside the lock, which is also the better batching: one transfer per write instead of one per character. And "is this the console's own screen" was asked of the pointer acquire() returned. vga_fb hands back a back buffer it copies onto the console's framebuffer afterwards, so on a one-screen machine the answer was "a different screen" -- and the console was mirrored onto itself through two megabytes of intermediate, allocated for the purpose. display_ops grew scanout_phys, which is a fact about the hardware rather than a policy a driver can get subtly wrong, and it is asked first: it has no side effects and acquire() has two. Verified with two heads under QEMU, screendumping the virtio head every 400 ms from power-on: splash, splash, splash, then the console, and it stays. The F8 menu appears on it; choosing 1024x768 there brings both heads up at 1024x768 byte-identical. A panic still reaches both. Both supported configurations: one boot each, no traps, no panics, socktest and memfdtest green. One gap remains and is documented rather than hidden: while the kernel's driver resets the device during probe, QEMU shows "Display output is not active" for a fraction of a second. A device being taken over has no scanout until the new driver sets one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
279 lines
9.9 KiB
C
279 lines
9.9 KiB
C
// The VBE linear framebuffer: the display the loader left running.
|
|
//
|
|
// This is the driver half of what used to be kernel/dev/vga.c. That file was
|
|
// three things at once -- the owner of the framebuffer, a bitmap drawing
|
|
// library, and the text console -- and only the first is a driver. The other
|
|
// two are now kernel/dev/gfx.c and do not name any driver at all, so the crash
|
|
// screen and the console draw on whatever hardware is bound rather than on
|
|
// this one by name. See include/gfx.h.
|
|
//
|
|
// What stays here is everything that knows *where the pixels are*: the mode the
|
|
// loader programmed and handed over in the boot block, the aperture it lives
|
|
// in, and the display class vtable that presents all of it as one of possibly
|
|
// several displays.
|
|
|
|
#include "types.h"
|
|
#include "x86.h"
|
|
#include "memlayout.h"
|
|
#include "defs.h"
|
|
#include "device.h"
|
|
#include "display.h"
|
|
#include "gfx.h"
|
|
#include "dma.h"
|
|
#include "schboot.h" /* the hand-off block */
|
|
|
|
static volatile uchar *fb;
|
|
// Physical base of the linear framebuffer, as reported by VBE. Everything that
|
|
// needs to map the framebuffer -- fork's VMA copy, mmap of /dev/fb0 -- asks
|
|
// rather than assuming a value. Three places used to hardcode 0xfd000000,
|
|
// which is only where QEMU's stdvga happens to put it.
|
|
static uint64 fb_phys;
|
|
static uint32 fb_stride;
|
|
static uint32 fb_bpp;
|
|
static uint32 fb_width = 800;
|
|
static uint32 fb_height = 600;
|
|
|
|
// Bytes of framebuffer, measured in scanlines rather than as
|
|
// width * height * bpp/8 -- the two differ whenever the card pads its rows,
|
|
// and it is the stride that says how far apart two rows really are.
|
|
uint64
|
|
vga_fb_size(void)
|
|
{
|
|
if (!fb)
|
|
return 0;
|
|
return (uint64)fb_stride * (uint64)fb_height;
|
|
}
|
|
|
|
// The framebuffer address VBE reported, for the PCI half of this driver to
|
|
// cross-check against the aperture it finds on the bus. Zero before init.
|
|
uint64
|
|
vga_fb_phys(void)
|
|
{
|
|
return fb_phys;
|
|
}
|
|
|
|
// Read the mode the loader programmed and publish it as a scanout.
|
|
//
|
|
// Called from platform_register_devices(), because the framebuffer is a fact
|
|
// about the machine that the loader established before any bus was walked --
|
|
// the PCI display driver binds much later, and the console has to be able to
|
|
// draw long before that.
|
|
//
|
|
// Deliberately does NOT clear the screen: whoever wants the framebuffer cleared
|
|
// says so, and this function is only about where it is. kmain clears it once,
|
|
// right after attaching the loader's scanout, and the kernel log runs on screen
|
|
// from there -- see cons_serial_only in kernel/core/console.c for why the
|
|
// splash no longer survives the whole boot.
|
|
void
|
|
vga_fb_init(uint64 phys_addr)
|
|
{
|
|
struct fb_info info;
|
|
|
|
if (phys_addr == 0)
|
|
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_width = *(ushort*)(P2V(SCHBOOT_VBE_WIDTH));
|
|
fb_height = *(ushort*)(P2V(SCHBOOT_VBE_HEIGHT));
|
|
fb_bpp = *(uchar*)(P2V(SCHBOOT_VBE_BPP));
|
|
|
|
if (fb_width == 0) fb_width = 800;
|
|
if (fb_height == 0) fb_height = 600;
|
|
if (fb_stride == 0) fb_stride = fb_width * (fb_bpp / 8);
|
|
|
|
// A framebuffer at physical zero is not a framebuffer.
|
|
//
|
|
// The loader publishes what the firmware's GOP reported, and on a machine
|
|
// whose only display is a virtio-gpu that is a width, a height and a
|
|
// FrameBufferBase of **zero**: OVMF's virtio GOP implements Blt and nothing
|
|
// else, because virtio-gpu has no linear aperture to hand out. Attaching it
|
|
// anyway pointed the scanout at P2V(0) and every subsequent draw wrote over
|
|
// the real-mode IVT, the BIOS data area and the AP trampoline.
|
|
//
|
|
// Refusing leaves the machine with no graphical console, which is the truth
|
|
// about it. The serial line still has everything.
|
|
if (phys_addr == 0) {
|
|
cprintf("vga: firmware reported a %dx%d display with no framebuffer "
|
|
"address; no graphical console\n", fb_width, fb_height);
|
|
return;
|
|
}
|
|
|
|
kinfo("vga: %dx%d, %d bpp, stride %d, LFB: 0x%x\n",
|
|
fb_width, fb_height, fb_bpp, fb_stride, (uint32)phys_addr);
|
|
|
|
info.ptr = fb;
|
|
info.phys = fb_phys;
|
|
info.size = vga_fb_size();
|
|
info.stride = fb_stride;
|
|
info.bpp = fb_bpp;
|
|
info.width = fb_width;
|
|
info.height = fb_height;
|
|
gfx_attach_scanout(&info);
|
|
}
|
|
|
|
// --- display class implementation ------------------------------------------
|
|
//
|
|
// See include/display.h. The VBE framebuffer presenting itself as a generic
|
|
// display, so a compositor is written against the class rather than against
|
|
// this file.
|
|
|
|
static uchar *back; // shadow buffer in RAM, or 0 if never acquired
|
|
static uint64 back_bytes;
|
|
|
|
static void
|
|
vga_disp_mode(struct device *d, struct display_mode *m)
|
|
{
|
|
(void)d;
|
|
m->width = (int)fb_width;
|
|
m->height = (int)fb_height;
|
|
m->bpp = (int)fb_bpp;
|
|
m->stride = fb_stride;
|
|
}
|
|
|
|
static void *
|
|
vga_disp_acquire(struct device *d, uint32 *stride_out)
|
|
{
|
|
(void)d;
|
|
if (stride_out)
|
|
*stride_out = fb_stride;
|
|
if (back)
|
|
return back;
|
|
if (!fb || fb_height == 0 || fb_stride == 0)
|
|
return 0;
|
|
|
|
back_bytes = (uint64)fb_stride * fb_height;
|
|
|
|
// From the DMA arena, which is the wrong pool and is used here because it is
|
|
// the only *physically contiguous* allocator in the kernel -- kalloc hands
|
|
// out single pages whose neighbours are adjacent only by chance, and a
|
|
// framebuffer wants one flat span.
|
|
//
|
|
// At 800x600x32 this is 1.9 MB of a 4 MB arena meant for device rings, which
|
|
// is tolerable only because nothing else large lives there yet. The real fix
|
|
// is a contiguous page allocator, or reserving this at boot from the memory
|
|
// map -- which is work that belongs with the E820 change in docs/architecture/boot.md
|
|
// §3b, since that is what stops the kernel pretending 224 MB exists.
|
|
//
|
|
// Allocated on first use rather than at boot on purpose: a machine that never
|
|
// starts a compositor should not pay two megabytes for one.
|
|
back = dma_alloc(back_bytes, 4096);
|
|
if (!back) {
|
|
cprintf("display: no room for a %d KB back buffer; drawing will tear\n",
|
|
(int)(back_bytes / 1024));
|
|
return 0;
|
|
}
|
|
memset(back, 0, back_bytes);
|
|
cprintf("display: %d KB back buffer, double buffered\n",
|
|
(int)(back_bytes / 1024));
|
|
return back;
|
|
}
|
|
|
|
// Blit a rectangle from the shadow buffer to the framebuffer.
|
|
//
|
|
// Row at a time rather than one memmove for the whole rectangle: only a
|
|
// full-width rectangle is contiguous in either buffer, and a compositor
|
|
// updating one window is the case worth being right about.
|
|
static void
|
|
vga_disp_present(struct device *d, int x, int y, int w, int h)
|
|
{
|
|
(void)d;
|
|
if (!back || !fb)
|
|
return;
|
|
|
|
if (x < 0) { w += x; x = 0; }
|
|
if (y < 0) { h += y; y = 0; }
|
|
if (x + w > (int)fb_width) w = (int)fb_width - x;
|
|
if (y + h > (int)fb_height) h = (int)fb_height - y;
|
|
if (w <= 0 || h <= 0)
|
|
return;
|
|
|
|
int bytes = w * (int)(fb_bpp / 8);
|
|
uint32 off = (uint32)x * (fb_bpp / 8);
|
|
for (int row = y; row < y + h; row++)
|
|
memmove((void *)(fb + row * fb_stride + off),
|
|
back + row * fb_stride + off, bytes);
|
|
}
|
|
|
|
// Wait for vertical retrace, by polling the VGA input status register.
|
|
//
|
|
// Bit 3 of 0x3DA is set during vertical retrace. This is the original 1987 VGA
|
|
// interface and it still works on everything that emulates a VGA, QEMU's
|
|
// stdvga included -- VBE's own protected-mode interface for this is optional
|
|
// and frequently absent, so the old port is the portable answer.
|
|
//
|
|
// Two loops, not one: waiting only for the flag to *be* set would return
|
|
// immediately if a retrace is already in progress, having just missed the
|
|
// start of it. Waiting for the current one to end and the next to begin is
|
|
// what actually lands the blit between frames.
|
|
//
|
|
// The counters are a deadlock guard. On hardware that never reports retrace
|
|
// this returns after a bounded spin instead of hanging the caller forever,
|
|
// which matters because this can be called with the console locked.
|
|
static void
|
|
vga_disp_wait_vblank(struct device *d)
|
|
{
|
|
int guard;
|
|
|
|
(void)d;
|
|
for (guard = 0; guard < 1000000; guard++)
|
|
if (!(inb(0x3DA) & 0x08))
|
|
break;
|
|
for (guard = 0; guard < 1000000; guard++)
|
|
if (inb(0x3DA) & 0x08)
|
|
break;
|
|
}
|
|
|
|
// Whatever acquire() handed out, the pixels end up here: the linear
|
|
// framebuffer this driver published as the console's scanout. Saying so is
|
|
// what keeps the console mirror from copying the console onto itself.
|
|
static uint64
|
|
vga_disp_scanout_phys(struct device *d)
|
|
{
|
|
(void)d;
|
|
return fb_phys;
|
|
}
|
|
|
|
const struct display_ops vga_display_ops = {
|
|
.mode = vga_disp_mode,
|
|
.acquire = vga_disp_acquire,
|
|
.present = vga_disp_present,
|
|
.wait_vblank = vga_disp_wait_vblank,
|
|
.scanout_phys = vga_disp_scanout_phys,
|
|
};
|
|
|
|
// Put the VBE framebuffer back on screen.
|
|
//
|
|
// Called when a better display driver lets go of the hardware -- see
|
|
// virtio-gpu's release hook. gfx holds one scanout, last writer wins, so the
|
|
// driver that leaves has to name its replacement rather than simply detaching;
|
|
// detaching alone leaves gfx pointing at a device nobody is driving.
|
|
//
|
|
// This is the fallback rule from docs/plan/hybrid.md made real: the kernel
|
|
// keeps a minimal driver for everything on the path to a shell, so a fancy
|
|
// driver leaving degrades to a working screen instead of to no screen.
|
|
int
|
|
vga_fb_restore(void)
|
|
{
|
|
struct fb_info info;
|
|
|
|
// Zero means there is no framebuffer to go back to, and the caller has to
|
|
// care. It used to return void and say nothing here, which reads as "done"
|
|
// -- so on a machine whose only display is the one being handed over, the
|
|
// eviction went ahead and left the console drawing into a driver that had
|
|
// just freed its memory. The answer is not to try harder; it is to say no,
|
|
// which is what the caller now does.
|
|
if (!fb || !fb_width || !fb_height)
|
|
return 0;
|
|
|
|
info.ptr = fb;
|
|
info.phys = fb_phys;
|
|
info.size = vga_fb_size();
|
|
info.stride = fb_stride;
|
|
info.bpp = fb_bpp;
|
|
info.width = fb_width;
|
|
info.height = fb_height;
|
|
gfx_attach_scanout(&info);
|
|
return 1;
|
|
}
|