SchOS/cmd/fbinfo/main.c
Nexoniarz 24f79194ab hybrid: /dev/pci -- a driver can live outside the kernel now
docs/plan/hybrid.md mechanisms 2 and 4: MMIO and DMA to a process.

A process can enumerate the bus, claim a device exclusively, map its BARs and
get memory the device can be pointed at. Proven end to end against an e1000e
-- the exact device class we cannot drive and would want a foreign driver for:

    device 7: 8086:10d3 (network)
      claimed
      BAR0 at 0x81060000, 128 KB
      regs[0..3] = 0x00140261 0x00000000 0x00080283 0x00000000
      MMIO reaches the device
      DMA 8 KB at virtual 0x40001000, physical 0x3f8eb000

Those are the e1000e's CTRL and STATUS with full-duplex and link-up set, which
is how you tell a real mapping from one reading 0xFFFFFFFF off an unmapped
address. Claiming a device the kernel drives is refused -- two writers of one
register set looks like hardware that has started lying -- and everything a
process held is released when it exits, because a driver that crashed must not
keep its hardware away from its replacement.

mmap's offset is the BAR's physical address, checked against the BARs of
devices *this process has claimed*. Without that check the device is /dev/mem
with extra steps and a process could map the kernel's own memory. Mappings are
write-through: a cached write to a doorbell arrives whenever the cache feels
like it, which from a driver's side is indistinguishable from hardware that
ignores you.

The interface is about PCI devices, not about any driver model, and its first
users will be SchOS's own drivers when virtio-gpu is evicted. That ordering is
the design rather than an accident: an interface whose first consumers are
native drivers is a general mechanism something foreign can use later, instead
of a hole shaped like something foreign.

Also: PROT_* and MAP_* moved to include/posix.h. They were defined only in
sys_mem.c, so cmd/fbinfo carried a private copy and this wanted a third --
two copies of a number the kernel compares against, where a mismatch shows up
as a mapping with the wrong permissions rather than an error.

No IOMMU, so this isolates a driver against its own bugs and not against its
own DMA. The plan says so at length and it is still true.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
2026-09-13 15:34:28 +02:00

217 lines
6.5 KiB
C

// fbinfo -- ask /dev/fb0 what it is, and optionally draw on it.
//
// This exists because the framebuffer is the one device whose correctness you
// cannot check by reading kernel source: the numbers have to come back from
// the hardware the bootloader actually programmed. Three places in the kernel
// used to assume the framebuffer lived at 0xfd000000 and was 800x600x32; they
// agreed with QEMU's stdvga by luck, and nothing would have told us when the
// luck ran out. Now `fbinfo` would.
//
// fbinfo print the geometry
// fbinfo fill draw a gradient over the whole screen through mmap
// fbinfo bars draw colour bars through write(), no mmap involved
//
// The two drawing modes take different paths on purpose. If `fill` works and
// `bars` does not, the mapping is right and the device's read/write is wrong;
// if it is the other way round, the reverse. Having both makes a failure say
// which half broke.
#include "types.h"
#include "user.h"
#include "fcntl.h"
#include "fb.h"
#include "posix.h" /* PROT_*, MAP_* -- one definition, shared */
static void
puts(const char *s)
{
int n = 0;
while (s[n]) n++;
write(1, (void*)s, n);
}
// Unsigned decimal. No printf in lib/, and pulling one in for six numbers
// would be a worse trade than fourteen lines.
static void
putu(unsigned long v)
{
char buf[24];
int i = 0;
if (v == 0) {
write(1, "0", 1);
return;
}
while (v > 0 && i < (int)sizeof(buf)) {
buf[i++] = (char)('0' + (v % 10));
v /= 10;
}
while (i > 0) {
i--;
write(1, &buf[i], 1);
}
}
static void
puthex(unsigned long v)
{
static const char d[] = "0123456789abcdef";
char buf[16];
int i = 0;
write(1, "0x", 2);
if (v == 0) {
write(1, "0", 1);
return;
}
while (v > 0 && i < (int)sizeof(buf)) {
buf[i++] = d[v & 0xf];
v >>= 4;
}
while (i > 0) {
i--;
write(1, &buf[i], 1);
}
}
// Name, then spaces out to a fixed column. Padding inside each string
// literal is what produced "bits_per_pixel32": the longest name happened to
// be exactly as wide as the column, so it got no separator at all and the
// value ran straight into it.
static void
label(const char *name)
{
int n = 0;
while (name[n]) n++;
write(1, (void*)name, n);
while (n < 16) { write(1, " ", 1); n++; }
}
static void
field(const char *name, unsigned long v)
{
label(name);
putu(v);
puts("\n");
}
static void
chanline(const char *name, struct fb_bitfield *b)
{
label(name);
puts("offset ");
putu(b->offset);
puts(", length ");
putu(b->length);
if (b->length == 0)
puts(" (absent)");
puts("\n");
}
int
main(int argc, char *argv[])
{
struct fb_var_screeninfo var;
struct fb_fix_screeninfo fix;
int fd;
fd = open("/dev/fb0", O_RDWR);
if (fd < 0) {
puts("fbinfo: cannot open /dev/fb0\n");
return 1;
}
if (ioctl(fd, FBIOGET_VSCREENINFO, &var) < 0) {
puts("fbinfo: FBIOGET_VSCREENINFO failed\n");
return 1;
}
if (ioctl(fd, FBIOGET_FSCREENINFO, &fix) < 0) {
puts("fbinfo: FBIOGET_FSCREENINFO failed\n");
return 1;
}
if (argc < 2) {
label("id");
puts(fix.id);
puts("\n");
field("xres", var.xres);
field("yres", var.yres);
field("bits_per_pixel", var.bits_per_pixel);
label("smem_start");
puthex(fix.smem_start);
puts("\n");
field("smem_len", fix.smem_len);
field("line_length", fix.line_length);
// The stride is the number worth staring at. A card is free to pad
// each row, and a program that computes row starts as
// y * xres * bytes_per_pixel instead of y * line_length draws a
// picture that shears progressively down the screen.
if (fix.line_length != var.xres * (var.bits_per_pixel / 8)) {
label("note");
puts("stride is padded; rows are not xres*bpp/8 apart\n");
}
chanline("red", &var.red);
chanline("green", &var.green);
chanline("blue", &var.blue);
chanline("transp", &var.transp);
return 0;
}
if (argv[1][0] == 'f') { // fill
unsigned char *fb = mmap(0, fix.smem_len,
PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (fb == 0 || (long)fb < 0) {
puts("fbinfo: mmap failed\n");
return 1;
}
unsigned bpp = var.bits_per_pixel / 8;
for (unsigned y = 0; y < var.yres; y++) {
unsigned char *row = fb + (unsigned long)y * fix.line_length;
for (unsigned x = 0; x < var.xres; x++) {
unsigned r = (x * 255) / (var.xres ? var.xres : 1);
unsigned g = (y * 255) / (var.yres ? var.yres : 1);
unsigned long px = ((unsigned long)r << var.red.offset) |
((unsigned long)g << var.green.offset) |
((unsigned long)0x80 << var.blue.offset);
unsigned char *p = row + (unsigned long)x * bpp;
for (unsigned b = 0; b < bpp; b++)
p[b] = (unsigned char)(px >> (b * 8));
}
}
puts("fbinfo: gradient drawn through mmap\n");
return 0;
}
if (argv[1][0] == 'b') { // bars
// One scanline at a time through write(). No mapping: this is the
// path that used to be devsw's nullwrite, i.e. a silent no-op.
static unsigned char row[8192];
unsigned bpp = var.bits_per_pixel / 8;
unsigned len = fix.line_length;
if (len > sizeof(row))
len = sizeof(row);
for (unsigned y = 0; y < var.yres; y++) {
unsigned band = (y * 8) / (var.yres ? var.yres : 1);
unsigned long px =
((unsigned long)((band & 1) ? 255 : 0) << var.red.offset) |
((unsigned long)((band & 2) ? 255 : 0) << var.green.offset) |
((unsigned long)((band & 4) ? 255 : 0) << var.blue.offset);
for (unsigned x = 0; x * bpp + bpp <= len; x++)
for (unsigned b = 0; b < bpp; b++)
row[x * bpp + b] = (unsigned char)(px >> (b * 8));
if (write(fd, row, len) != (int)len) {
puts("fbinfo: short write to /dev/fb0\n");
return 1;
}
}
puts("fbinfo: colour bars drawn through write()\n");
return 0;
}
puts("usage: fbinfo [fill|bars]\n");
return 1;
}