docs/plan/hybrid.md's first eviction. cmd/vgpud is the virtio-gpu driver as a
process: it finds the device by vendor and device id, asks the kernel for it,
and does the whole virtio 1.0 bring-up from outside -- reset, feature
negotiation, a virtqueue in DMA memory, GET_DISPLAY_INFO, RESOURCE_CREATE_2D,
ATTACH_BACKING, SET_SCANOUT, TRANSFER and FLUSH.
vgpud: virtio-gpu at 00:03.0, currently held by virtio-gpu
virtio-gpu: released to userspace; screen is the VBE framebuffer
vgpud: claimed
vgpud: display is 1280x800
vgpud: scanout is ours, framebuffer at physical 0x3eceb000
vgpud: drew a test pattern from userspace
Verified by screendumping the virtio-gpu head: the pattern is there, with a
checkerboard that would shear if the stride were wrong. The console carries on
in VBE on the other head at the same time, which is the fallback rule from the
plan working rather than being described.
No syscall knows this is a GPU. The kernel handed over a PCI device and the
process decided what it was -- which is the property that makes this a general
mechanism, and the reason the evictions come before anything foreign.
Four things the first real driver needed that the test program had not:
- PCI config space (PCIIOC_CFG). A virtio device says where its registers
are in vendor capabilities, which live in config space, so a driver that
cannot read it cannot find its own registers.
- a way for the incumbent to let go: struct pci_driver gained a release
hook. A driver without one keeps its device, which is always safe and for
anything on the path to a shell is also correct.
- the outgoing driver freeing its DMA. Releasing the device but keeping its
4 MB framebuffer is half a handover -- the incoming driver asks for one and
is told there is no room.
- a 16 MB DMA arena. 4 MB was exactly one framebuffer at this resolution,
and either side of a handover there are two.
And one bug it reproduced on its first working run: vgpud polls the used ring,
so it registered no interrupt handler, and a virtio device interrupts on every
completion. The storm detector said so immediately --
irq 11: 77780 interrupts/second -- a device on this line is not
acknowledging it. handler 'e1000' is attached
-- naming the line, the rate and the neighbour it was drowning. One line fixes
it (VRING_AVAIL_F_NO_INTERRUPT before the queue is enabled), and that detector
was worth every minute it cost.
include/virtio.h moved out of the driver directory. Its own comment said it
should when a second reader appeared; one has.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
244 lines
8.4 KiB
C
244 lines
8.4 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. 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/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;
|
|
}
|
|
|
|
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,
|
|
};
|
|
|
|
// 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.
|
|
void
|
|
vga_fb_restore(void)
|
|
{
|
|
struct fb_info info;
|
|
|
|
if (!fb || !fb_width || !fb_height)
|
|
return; // nothing was ever set up; nothing to go back to
|
|
|
|
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);
|
|
}
|