gfx: the console and the crash screen stop drawing on a driver by name

Same disease as the network stack, one subsystem over. kernel/dev/vga.c
was three things wearing one name: the owner of the framebuffer, a
bitmap drawing library, and the text console. Only the first is a
driver, and because they shared a file they shared a prefix -- so
panic.c drew the crash screen with vga_fill_rect, console.c printed with
vga_putc, /dev/fb0 handed out vga_fb_phys, and every one of those files
named a display driver it had no business knowing about.

Split three ways:

  - kernel/drivers/gpu/vga/vga_fb.c owns the VBE framebuffer -- the mode
    the loader programmed and handed over, the aperture, and the display
    class vtable. It is a driver and now lives with the driver that
    finds the card on the bus.
  - kernel/dev/gfx.c draws: pixels, shapes, glyphs, the cursor, scroll,
    the ANSI erase operations. It names no driver. The framebuffer
    arrives through gfx_attach_scanout(), so a driver publishes what it
    owns and this file draws on it without knowing what it is.
  - include/gfx.h is the interface, and says why it is deliberately not
    the display class: that one is about frames -- acquire, present,
    vblank -- and the crash screen has to work when nothing else does.

main() no longer calls a display driver. platform_register_devices()
takes the framebuffer address and brings up whatever owns it, which is
where driver names belong; kernel/drivers/registry.c is the file whose
entire job is naming every driver in the kernel.

virtio-gpu got the e1000 treatment as well: instances in an array, the
instance hung on dev->priv by an `attached` hook, and every op reading
it rather than a file-static. It refused the second adapter outright
before, which is not hypothetical -- integrated plus discrete is an
ordinary laptop. The VGA driver deliberately has no such hook, and says
so: there is one framebuffer the loader programmed however many
VGA-class controllers the bus turns up, and a second one publishing the
same scanout is the truth rather than a bug.

Private headers moved next to their drivers: include/virtio.h and
include/xhci.h each had exactly one includer. include/ is for headers
the rest of the kernel needs.

Deleted rather than moved: consoleinit() programmed the sixteen ANSI
colours into the VGA DAC and disabled text-mode blink, by writing ports
0x3C0-0x3C9 from the console. The comment three lines above the colour
table already said this does nothing in a 32bpp linear mode -- there is
no palette to index -- so it was both dead and in the wrong file.

Verified, and this is a refactor so the evidence is that nothing moved:
  - bootcheck -Fb -Net: 59 [ok], no failures. That covers the console
    through every command, /dev/fb0's geometry, writing pixels through
    write() and mapping the framebuffer through mmap.
  - make crashshot: the crash screen renders exactly as before --
    shapes, glyphs, logo and QR code -- which is the whole of gfx.c
    exercised at once, in a panic, with the scanout attached by a driver.
  - make schcom-test: five pixel assertions from the host oracle still
    pass, so the display class and the compositor above it are unmoved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Nexoniarz 2026-08-15 17:04:10 +02:00
commit 817d666068
22 changed files with 942 additions and 752 deletions

View file

@ -0,0 +1,216 @@
// 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. The loader has already drawn the
// splash, and wiping it here is what produced the old two-act boot: a purple
// splash for an instant, then an abrupt cream screen filling with log lines.
// The splash stays up for the whole of boot, and console_takeover() clears it
// exactly once, when userspace starts and the display changes owner.
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);
cprintf("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/roadmap.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;
}
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,
};

View file

@ -1,10 +1,15 @@
// Display controller -- the first driver to find its hardware by asking.
//
// It does not draw anything: kernel/dev/vga.c already owns the framebuffer,
// It does not draw anything: vga_fb.c in this directory owns the framebuffer,
// and it learned where that is from the bootloader's VBE call. What this adds
// is a second, independent answer to the same question, from the bus rather
// than from the BIOS -- and a complaint when the two disagree.
//
// No `attached` hook and no per-instance state, unlike the e1000 and
// virtio-gpu: there is exactly one framebuffer the loader programmed, however
// many VGA-class controllers the bus turns up. A second one binding here
// publishes the same scanout, which is the truth -- it is the same screen.
//
// That is worth more than it sounds. Three places in the kernel used to
// hardcode 0xfd000000 as the framebuffer address, and the only reason nobody
// noticed was that QEMU's stdvga happens to put it there. A disagreement

View file

@ -0,0 +1,180 @@
#pragma once
// virtio 1.0 over PCI — the transport, and virtio-gpu's 2D protocol.
//
// Next to the driver rather than in include/, because exactly one driver reads
// it. When a second virtio device arrives -- virtio-net and virtio-blk are the
// obvious ones -- the *transport* half of this belongs in a shared place and
// the 2D protocol stays here; splitting it before there is a second caller
// would be guessing at where the seam goes.
//
// Written from the OASIS specification (virtio 1.1, chapters 2, 4.1 and 5.7),
// which is a public document with no licence on what you write from it. Same
// reasoning as kernel/drivers/xhci.c.
//
// Why virtio-gpu at all, when there is already a framebuffer: the VBE
// framebuffer is a flat block of memory the loader happened to be told about.
// It has no mode setting, no notion of a frame being complete, and no way to
// ask the host to present anything -- so a compositor drawing into it is
// racing the scanout with no way to synchronise. virtio-gpu has resources,
// explicit transfer and an explicit flush, which is the model every real GPU
// driver has. It is the smallest device that teaches the right shape.
// --- PCI transport ---------------------------------------------------------
#define VIRTIO_VENDOR 0x1AF4
// Modern (1.0) device IDs are 0x1040 + the virtio device type. GPU is type 16.
#define VIRTIO_DEV_GPU 0x1050
// Vendor-specific PCI capability, which is how a virtio device says where in
// its BARs each configuration structure lives.
#define PCI_CAP_ID_VNDR 0x09
#define VIRTIO_PCI_CAP_COMMON_CFG 1
#define VIRTIO_PCI_CAP_NOTIFY_CFG 2
#define VIRTIO_PCI_CAP_ISR_CFG 3
#define VIRTIO_PCI_CAP_DEVICE_CFG 4
// Offsets within the common configuration structure. Written out rather than
// declared as a struct: the fields are little-endian by specification and of
// mixed width, and a struct invites the compiler to pad or a caller to forget
// that these are MMIO and must be accessed at exactly the stated width.
#define VCOMMON_DFEAT_SEL 0x00 // le32
#define VCOMMON_DFEAT 0x04 // le32
#define VCOMMON_GFEAT_SEL 0x08 // le32
#define VCOMMON_GFEAT 0x0C // le32
#define VCOMMON_MSIX_CFG 0x10 // le16
#define VCOMMON_NUM_QUEUES 0x12 // le16
#define VCOMMON_STATUS 0x14 // u8
#define VCOMMON_CFG_GEN 0x15 // u8
#define VCOMMON_Q_SELECT 0x16 // le16
#define VCOMMON_Q_SIZE 0x18 // le16
#define VCOMMON_Q_MSIX 0x1A // le16
#define VCOMMON_Q_ENABLE 0x1C // le16
#define VCOMMON_Q_NOTIFY_OFF 0x1E // le16
#define VCOMMON_Q_DESC 0x20 // le64
#define VCOMMON_Q_DRIVER 0x28 // le64
#define VCOMMON_Q_DEVICE 0x30 // le64
// device_status bits, set in this order during bring-up.
#define VSTATUS_ACK 1
#define VSTATUS_DRIVER 2
#define VSTATUS_DRIVER_OK 4
#define VSTATUS_FEATURES_OK 8
#define VSTATUS_FAILED 0x80
#define VIRTIO_F_VERSION_1 32 // feature bit: this is a 1.0 device
// --- split virtqueue -------------------------------------------------------
#define VRING_DESC_F_NEXT 1
#define VRING_DESC_F_WRITE 2 // device writes it; i.e. this is our buffer
struct vring_desc {
uint64 addr;
uint32 len;
uint16 flags;
uint16 next;
} __attribute__((packed));
struct vring_avail {
uint16 flags;
uint16 idx;
uint16 ring[];
} __attribute__((packed));
struct vring_used_elem {
uint32 id;
uint32 len;
} __attribute__((packed));
struct vring_used {
uint16 flags;
uint16 idx;
struct vring_used_elem ring[];
} __attribute__((packed));
// --- virtio-gpu, 2D --------------------------------------------------------
#define VIRTIO_GPU_CMD_GET_DISPLAY_INFO 0x0100
#define VIRTIO_GPU_CMD_RESOURCE_CREATE_2D 0x0101
#define VIRTIO_GPU_CMD_RESOURCE_UNREF 0x0102
#define VIRTIO_GPU_CMD_SET_SCANOUT 0x0103
#define VIRTIO_GPU_CMD_RESOURCE_FLUSH 0x0104
#define VIRTIO_GPU_CMD_TRANSFER_TO_HOST_2D 0x0105
#define VIRTIO_GPU_CMD_RESOURCE_ATTACH_BACKING 0x0106
#define VIRTIO_GPU_RESP_OK_NODATA 0x1100
#define VIRTIO_GPU_RESP_OK_DISPLAY_INFO 0x1101
#define VIRTIO_GPU_MAX_SCANOUTS 16
// B8G8R8X8, which is what a 32-bit little-endian 0x00RRGGBB word looks like in
// memory -- the same layout vga.c already draws into, so a framebuffer can be
// handed over without a conversion pass.
#define VIRTIO_GPU_FORMAT_B8G8R8X8_UNORM 2
struct virtio_gpu_ctrl_hdr {
uint32 type;
uint32 flags;
uint64 fence_id;
uint32 ctx_id;
uint32 padding;
} __attribute__((packed));
struct virtio_gpu_rect {
uint32 x, y, width, height;
} __attribute__((packed));
struct virtio_gpu_display_one {
struct virtio_gpu_rect r;
uint32 enabled;
uint32 flags;
} __attribute__((packed));
struct virtio_gpu_resp_display_info {
struct virtio_gpu_ctrl_hdr hdr;
struct virtio_gpu_display_one pmodes[VIRTIO_GPU_MAX_SCANOUTS];
} __attribute__((packed));
struct virtio_gpu_resource_create_2d {
struct virtio_gpu_ctrl_hdr hdr;
uint32 resource_id;
uint32 format;
uint32 width;
uint32 height;
} __attribute__((packed));
struct virtio_gpu_mem_entry {
uint64 addr;
uint32 length;
uint32 padding;
} __attribute__((packed));
struct virtio_gpu_resource_attach_backing {
struct virtio_gpu_ctrl_hdr hdr;
uint32 resource_id;
uint32 nr_entries;
} __attribute__((packed));
struct virtio_gpu_set_scanout {
struct virtio_gpu_ctrl_hdr hdr;
struct virtio_gpu_rect r;
uint32 scanout_id;
uint32 resource_id;
} __attribute__((packed));
struct virtio_gpu_transfer_to_host_2d {
struct virtio_gpu_ctrl_hdr hdr;
struct virtio_gpu_rect r;
uint64 offset;
uint32 resource_id;
uint32 padding;
} __attribute__((packed));
struct virtio_gpu_resource_flush {
struct virtio_gpu_ctrl_hdr hdr;
struct virtio_gpu_rect r;
uint32 resource_id;
uint32 padding;
} __attribute__((packed));

View file

@ -59,9 +59,26 @@ struct vgpu {
void *fb; // the framebuffer we scan out from
uint32 fb_bytes;
int ready;
// Which PCI device this instance drives, so attached() can match the device
// the bus publishes against the instance probe() set up.
struct pci_dev *pdev;
};
static struct vgpu gpu;
// More than one, because two display adapters in a machine is ordinary --
// integrated plus discrete on real hardware, two -device virtio-gpu-pci under
// QEMU -- and this driver used to refuse the second outright.
#define VGPU_MAX 2
static struct vgpu gpus[VGPU_MAX];
// The instance behind a device, or 0. Every op goes through this rather than a
// file-static, which is what makes the second adapter possible at all. See
// include/netdev.h and the e1000 for the same arrangement on the network side.
static struct vgpu *
vgpu_of(struct device *d)
{
return d ? (struct vgpu *)d->priv : 0;
}
// --- MMIO accessors --------------------------------------------------------
//
@ -309,51 +326,61 @@ setup_scanout(struct vgpu *g)
static void
vgpu_mode(struct device *d, struct display_mode *m)
{
m->width = gpu.width;
m->height = gpu.height;
struct vgpu *g = vgpu_of(d);
if (!g)
return;
m->width = g->width;
m->height = g->height;
m->bpp = 32;
m->stride = (uint32)gpu.width * 4;
m->stride = (uint32)g->width * 4;
}
static void *
vgpu_acquire(struct device *d, uint32 *stride_out)
{
struct vgpu *g = vgpu_of(d);
if (!g)
return 0;
if (stride_out)
*stride_out = (uint32)gpu.width * 4;
*stride_out = (uint32)g->width * 4;
// No separate back buffer: the guest-side framebuffer already *is* one. The
// host cannot see it until TRANSFER_TO_HOST_2D, so drawing into it is
// inherently off-screen -- which is the whole reason this device is better to
// composite against than a raw VBE aperture.
return gpu.ready ? gpu.fb : 0;
return g->ready ? g->fb : 0;
}
static void
vgpu_present(struct device *d, int x, int y, int w, int h)
{
if (!gpu.ready)
struct vgpu *g = vgpu_of(d);
if (!g || !g->ready)
return;
if (x < 0) { w += x; x = 0; }
if (y < 0) { h += y; y = 0; }
if (x + w > gpu.width) w = gpu.width - x;
if (y + h > gpu.height) h = gpu.height - y;
if (x + w > g->width) w = g->width - x;
if (y + h > g->height) h = g->height - y;
if (w <= 0 || h <= 0)
return;
{
struct virtio_gpu_transfer_to_host_2d *t = gpu.cmd;
struct virtio_gpu_transfer_to_host_2d *t = g->cmd;
hdr_init(t, VIRTIO_GPU_CMD_TRANSFER_TO_HOST_2D);
t->r.x = x; t->r.y = y; t->r.width = w; t->r.height = h;
t->offset = (uint64)y * gpu.width * 4 + (uint64)x * 4;
t->offset = (uint64)y * g->width * 4 + (uint64)x * 4;
t->resource_id = 1;
if (cmd(&gpu, sizeof(*t), sizeof(struct virtio_gpu_ctrl_hdr)) != 0)
if (cmd(g, sizeof(*t), sizeof(struct virtio_gpu_ctrl_hdr)) != 0)
return;
}
{
struct virtio_gpu_resource_flush *f = gpu.cmd;
struct virtio_gpu_resource_flush *f = g->cmd;
hdr_init(f, VIRTIO_GPU_CMD_RESOURCE_FLUSH);
f->r.x = x; f->r.y = y; f->r.width = w; f->r.height = h;
f->resource_id = 1;
cmd(&gpu, sizeof(*f), sizeof(struct virtio_gpu_ctrl_hdr));
cmd(g, sizeof(*f), sizeof(struct virtio_gpu_ctrl_hdr));
}
}
@ -372,10 +399,16 @@ const struct display_ops virtio_gpu_display_ops = {
static int
vgpu_probe(struct pci_dev *d)
{
struct vgpu *g = &gpu;
struct vgpu *g = 0;
if (g->ready)
return -1; // one display is enough for now
for (int i = 0; i < VGPU_MAX; i++)
if (!gpus[i].ready) {
g = &gpus[i];
break;
}
if (!g)
return -1; // every slot already drives an adapter
g->pdev = d;
pci_enable(d, 0x0006); // memory space + bus master
@ -443,9 +476,23 @@ static const struct pci_match virtio_gpu_match[] = {
{ 0, 0, 0, 0 },
};
// Hang the instance on the device the bus has just published. probe() runs
// before device_add(), so there is nothing to attach it to in there -- see the
// comment on `attached` in include/pci.h.
static void
vgpu_attached(struct pci_dev *d, struct device *dev)
{
for (int i = 0; i < VGPU_MAX; i++)
if (gpus[i].ready && gpus[i].pdev == d) {
dev->priv = &gpus[i];
return;
}
}
const struct pci_driver virtio_gpu_driver = {
.name = "virtio-gpu",
.match = virtio_gpu_match,
.ops = &virtio_gpu_display_ops,
.probe = vgpu_probe,
.attached = vgpu_attached,
};