docs/ was ten files where a plan, a description, a guide and a bug report all
sat next to each other. It is now architecture/ (what SchOS is), plan/ (what it
will be), guides/ (how to work on it) and history/ (why it is like this), with
an index that states the one rule holding it together: when something is
finished it moves from plan/ to architecture/ and is rewritten as a description
of the thing rather than a record of building it.
A ticked checkbox says the work happened. It does not say what the work
*produced*, and six months later the checkbox is all that is left. The network
stack proved the point: finished, verified end to end, and for a month the only
description of it anywhere was a checkbox four levels of indentation deep.
So roadmap.md went from 534 lines to 181 -- nothing deleted, the done work
graduated. Seven subsystems that were finished and undescribed now have
descriptions: boot, kernel, syscalls, filesystem, networking, graphics, usb,
init. Every one ends with "What it does not do", which is the section that pays;
a document listing only strengths is marketing, and the reader who needs it most
is the one about to trip over a limitation nobody wrote down.
schfs.md still opened with "phases 1 through 3 1/2 are done, phase 4 is next"
long after all five had landed. It is a description now, with the phase record
and the hard-won bugs moved to history/.
Three corrections found on the way, all load-bearing:
* projects/schcom/README.md claimed SCM_RIGHTS was done. There is no
sendmsg/recvmsg in the kernel at all, and an entire GUI plan rested on it.
* The root README said "No TCP/IP" months after the stack landed.
* history/kernel.md sec.7 was marked "in progress" on a problem that process
groups solved.
Also adds plan/1.0.md, which draws a finish line so the open list stops being
infinite, and marks schwm/ and schde/ superseded: in Wayland the compositor is
the window manager, so that split dissolves rather than being abandoned.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
216 lines
7.4 KiB
C
216 lines
7.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,
|
|
};
|