H2 of docs/plan/hybrid.md -- the milestone the plan itself calls the one
that proves the whole idea, and the one that cannot be skipped.
# cat /proc/schinit
1 4 11 os.schos.vgpud
# kill 4
schinitd: restarting os.schos.vgpud
vgpud: claimed
vgpud: pid 8 drew a test pattern from userspace
The test pattern ends in a twelve-block tally of the drawing process's
pid, because a second run that redraws an identical picture is
indistinguishable from a first run that never stopped. Read off the
virtio-gpu scanout across four generations it says 4, 8, 10, 12, and the
DMA arena hands back the same physical address every time.
What it took:
* A halt the kernel can apply to a device it does not understand.
Clearing PCI_CMD_BUS_MASTER is the only one there is, and it has to
happen before the DMA goes back to the arena -- a dead driver's device
keeps reading a framebuffer the kernel is about to give to somebody
else. Claim sets the bit, release clears it; pcitest shows it in a bm
column, with a third state so a failed read is never printed as off.
* A refusal. vgpu_release() asks vga_fb_restore() whether there is a
framebuffer to put the console back on, and declines to hand over the
device if there is not. The rule that the kernel keeps a minimal
driver for everything on the path to a shell is worth writing down
only if something enforces it.
* The DMA arena not being owned by two allocators at once. This is the
bug that stopped H2: freevm() frees every mapped page below PHYSTOP
and the arena is the top 16 MB of exactly that range, so a DMA block
mapped into a driver went onto the page allocator's free list when the
driver died, while pciuser_release_all() handed the same pages back to
the arena. The restarted vgpud asked for its framebuffer, got pages
kalloc() had already given out -- including its own page tables -- and
dma_alloc()'s zeroing wiped them, so it died inside the ioctl on the
return from a syscall whose stack had stopped existing. do_dma() now
records a VMA with no_free set, the way shared memory does.
* One exit status. KeepAlive must restart a driver that crashed and must
not restart one that has looked at the machine and found its device
absent: on hardware with no virtio-gpu that was fifteen spawns in
forty-five seconds, each printing a line to the console, for ever.
SCHINIT_EXIT_NORESTART is 78, sysexits.h's EX_CONFIG. It is not a new
manifest key -- the wait status has always travelled from PID 1 to the
supervisor, and note_exit() simply stopped discarding it.
The milestone's third clause, "the compositor reattaches", is not done
and is not claimed: there is no compositor running. That is recorded in
the plan as a Schcom milestone rather than counted here.
Closes the register row "A released PCI device" in docs/plan/1.0.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
251 lines
8.7 KiB
C
251 lines
8.7 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);
|
|
|
|
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.
|
|
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;
|
|
}
|