SchOS/kernel/drivers/gpu/vga/vga_fb.c
Nexoniarz c65bfa3fa3 boot: driver chatter goes to the log; the console clears its margins
ACPI, DMI, SMP, xHCI, USB, I2C, NVMe, r8169 and ramdisk progress lines are
kinfo now: in dmesg and on a verbose boot, off the screen of a working
machine. Failures stay cprintf.

vt_repaint also paints the strip right of the last column and below the
last row, which kept the bottom of the X desktop after Ctrl+Alt+F-key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-25 01:53:46 +02:00

301 lines
11 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;
// 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)
phys_addr = *(uint32*)(P2V(SCHBOOT_VBE_LFB));
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));
}
fb_phys = phys_addr;
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;
}
// ioremap() rather than P2V(): the same address below 4 GB, and the window
// 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.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);
kinfo("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;
}