Sixty-five headers sat side by side in include/, so `#include "irq.h"` said nothing about where irq.h was or what it belonged to, and errno.h, stat.h and fcntl.h at the top shadowed the system's for any host tool built with -I./include. They are in folders now -- arch/ core/ fs/ dev/ gfx/ net/ uapi/ (and ldls/ for the Linux driver layer) -- and every include names the folder, as Linux's do: "core/proc.h". Done mechanically and checked the hard way: the kernel built from this tree disassembles identically to the one built from the tree before it. The full image boots, and userland is the same set of programs. Paths in docs and comments follow; docs/guides/layout.md has a table of what is where, and tools/fix-includes.py rewrites old-style includes from the folders in the tree, for code written before this. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
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/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 "core/types.h"
|
|
#include "arch/x86.h"
|
|
#include "arch/memlayout.h"
|
|
#include "core/defs.h"
|
|
#include "dev/device.h"
|
|
#include "gfx/display.h"
|
|
#include "gfx/gfx.h"
|
|
#include "core/dma.h"
|
|
#include "arch/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/gfx/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;
|
|
}
|