From 817d666068a6a4a504f1638072460d233296b46c Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Sat, 15 Aug 2026 17:04:10 +0200 Subject: [PATCH] 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 --- Makefile | 3 +- docs/drivers.md | 8 +- include/defs.h | 51 +- include/gfx.h | 78 +++ include/theme.h | 2 +- include/user.h | 2 +- kernel/arch/x86_64/vm.c | 3 +- kernel/core/console.c | 94 +-- kernel/core/main.c | 28 +- kernel/core/panic.c | 47 +- kernel/dev/fb.c | 27 +- kernel/dev/gfx.c | 444 ++++++++++++++ kernel/dev/mouse.c | 9 +- kernel/dev/vga.c | 569 ------------------ {include => kernel/drivers/bus/xhci}/xhci.h | 0 kernel/drivers/gpu/vga/vga_fb.c | 216 +++++++ kernel/drivers/gpu/vga/vga_pci.c | 7 +- .../drivers/gpu/virtio_gpu}/virtio.h | 6 + kernel/drivers/gpu/virtio_gpu/virtio_gpu.c | 81 ++- kernel/drivers/registry.c | 9 +- kernel/drivers/usb/hid/usb_hid.c | 5 +- kernel/sys/sys_mem.c | 5 +- 22 files changed, 942 insertions(+), 752 deletions(-) create mode 100644 include/gfx.h create mode 100644 kernel/dev/gfx.c delete mode 100644 kernel/dev/vga.c rename {include => kernel/drivers/bus/xhci}/xhci.h (100%) create mode 100644 kernel/drivers/gpu/vga/vga_fb.c rename {include => kernel/drivers/gpu/virtio_gpu}/virtio.h (93%) diff --git a/Makefile b/Makefile index f7b075e..d6987c1 100644 --- a/Makefile +++ b/Makefile @@ -25,7 +25,7 @@ KERN_CORE = $(addprefix $(BUILD)/kernel/core/, \ KERN_DEV = $(addprefix $(BUILD)/kernel/dev/, \ bio.o cdev.o device.o disk.o fb.o ide.o input.o irq.o kbd.o mouse.o \ - pci.o text.o ttf.o uart.o vga.o ramdisk.o procfs.o) + pci.o text.o ttf.o uart.o gfx.o ramdisk.o procfs.o) # Drivers for hardware that has to be *found* rather than assumed. Anything # discovered by walking a bus belongs here; the fixed-address legacy devices @@ -39,6 +39,7 @@ KERN_DEV = $(addprefix $(BUILD)/kernel/dev/, \ # the depth costs nothing. KERN_DRIVERS = $(BUILD)/kernel/drivers/registry.o KERN_DRIVERS += $(BUILD)/kernel/drivers/gpu/vga/vga_pci.o +KERN_DRIVERS += $(BUILD)/kernel/drivers/gpu/vga/vga_fb.o KERN_DRIVERS += $(BUILD)/kernel/drivers/gpu/virtio_gpu/virtio_gpu.o KERN_DRIVERS += $(BUILD)/kernel/drivers/net/e1000/e1000.o KERN_DRIVERS += $(BUILD)/kernel/drivers/bus/xhci/xhci.o diff --git a/docs/drivers.md b/docs/drivers.md index 541a761..8567d81 100644 --- a/docs/drivers.md +++ b/docs/drivers.md @@ -121,9 +121,15 @@ existing mechanisms for keeping that true mechanically rather than by intention. | class | vtable | consumer | |---|---|---| - | `DEVCLASS_DISPLAY` | `struct display_ops` ([display.h](../include/display.h)) | the compositor, the console | + | `DEVCLASS_DISPLAY` | `struct display_ops` ([display.h](../include/display.h)) | the compositor | | `DEVCLASS_NET` | `struct netdev_ops` ([netdev.h](../include/netdev.h)) | the TCP/IP stack | + A display driver that owns a linear framebuffer also publishes it with + `gfx_attach_scanout()` ([gfx.h](../include/gfx.h)). That is what the text + console and the crash screen draw on, and it is separate from the display + class on purpose: the class is about *frames* — acquire, present, vblank — + and the crash screen has to work when nothing else in the system does. + A driver that fills these in never mentions its consumer, and its consumer never mentions it. Getting this wrong has a characteristic shape: the e1000 shipped with `.ops = 0` and the network stack called `e1000_transmit()` diff --git a/include/defs.h b/include/defs.h index 84fbe27..acb6406 100644 --- a/include/defs.h +++ b/include/defs.h @@ -491,32 +491,10 @@ void uartinit(void); void uartintr(void); void uartputc(int); -// vga.c -void vga_init(uint64); -void vga_draw_pixel(int, int, uint32); -void vga_draw_char(int, int, uchar, uint32); -void vga_puts(int, int, char*, uint32); -void vga_putc(int, uint32); -void vga_clear(uint32); -void vga_screen_reset(void); -void vga_set_bg(uint32); -// The mode actually in use. The resolution is a boot-time choice now, so no -// caller may hardcode it. -uint32 vga_get_width(void); -uint32 vga_get_height(void); -// Crash-screen primitives: the panic handler lays itself out rather than -// printing. See kernel/core/panic.c. -int vga_ready(void); -void vga_dims(int *w, int *h); -int vga_char_w(void); -int vga_char_h(void); -// Framebuffer geometry. vga.c reads the VBE mode info, so it is the only -// thing that knows where the framebuffer is; nobody else may assume. -uint64 vga_fb_phys(void); -uint64 vga_fb_size(void); -uint32 vga_fb_stride(void); -uint32 vga_fb_bpp(void); -volatile uchar *vga_fb_ptr(void); +// Drawing, the text console and the framebuffer geometry are NOT declared +// here: they live in include/gfx.h, and the display drivers that back them in +// include/display.h. Declaring vga_fill_rect() here is what let the crash +// screen draw on one named driver. // fb.c -- /dev/fb0, major 2. void fbclose(struct inode*); @@ -554,22 +532,17 @@ struct netif *net_eth_netif(int i); struct device *net_eth_device(int i); // registry.c -void platform_register_devices(void); +// Platform devices, and the boot framebuffer. Takes the framebuffer address +// the loader handed over, because bringing that up is the first thing it does. +void platform_register_devices(uint64 fb_addr); +// drivers/gpu/vga -- the VBE framebuffer the loader programmed. Called only +// from registry.c; everything else draws through include/gfx.h. +void vga_fb_init(uint64 phys_addr); +uint64 vga_fb_phys(void); +uint64 vga_fb_size(void); void e820_init(void); int acpi_init(void); -void vga_fill_rect(int x, int y, int w, int h, uint32); -void vga_fill_circle(int cx, int cy, int r, uint32); -void vga_text_at(int, int, const char*, uint32); -void vga_draw_logo(int x, int y, uint32 color, uint32 bg); -void vga_logo_centered(int cy, uint32 color, uint32 bg); -// Cursor addressing and erasing, for the console's ANSI escape parser. -int vga_cols(void); -int vga_rows(void); -void vga_cursor_get(int *col, int *row); -void vga_cursor_set(int col, int row); -void vga_erase_display(int mode); -void vga_erase_line(int mode); // console.c / panic.c void cons_panic_mode(void); diff --git a/include/gfx.h b/include/gfx.h new file mode 100644 index 0000000..75c9ebb --- /dev/null +++ b/include/gfx.h @@ -0,0 +1,78 @@ +#pragma once + +// Drawing, and the text console under it, on whatever display is bound. +// +// This was kernel/dev/vga.c, and every consumer called it by that name: +// panic.c drew the crash screen with vga_fill_rect, the console printed with +// vga_putc, /dev/fb0 handed out vga_fb_phys(). None of that code has anything +// to do with a VGA -- it is bitmap drawing over a linear framebuffer, and the +// only VGA-specific part was where the framebuffer came from. +// +// So the driver pushes its scanout in here (gfx_attach_scanout) and everything +// above draws through these calls. A second display driver becomes a second +// caller of one function rather than an edit to the crash screen. This is the +// same arrangement as include/netdev.h, for the same reason. +// +// Not the same thing as the display class in include/display.h. That is the +// compositor's interface -- acquire a back buffer, present a rectangle, wait +// for vblank -- and it is about *frames*. This is the kernel's own drawing +// surface, used by the console and the crash screen, which do not double +// buffer and must keep working when nothing else in the system does. + +// A linear framebuffer, exactly as the hardware presents it. +struct fb_info { + volatile uchar *ptr; // kernel-mapped base, or 0 if there is none + uint64 phys; // physical base, for mmap and for page tables + uint64 size; // bytes, stride * height rather than w * h * bpp + uint32 stride; // bytes per row, which may exceed width * bpp/8 + uint32 bpp; + uint32 width, height; +}; + +// A driver publishing the framebuffer it owns. Called once, early: the console +// draws long before any bus is enumerated, so whoever owns the display the +// loader left running has to say so before then. +void gfx_attach_scanout(const struct fb_info *); + +// The scanout as attached, for the few callers that need the memory itself -- +// /dev/fb0 maps it, and fork copies the mapping. Returns 0 if none. +const struct fb_info *gfx_scanout(void); + +int gfx_ready(void); +void gfx_dims(int *w, int *h); +uint32 gfx_width(void); +uint32 gfx_height(void); +uint64 gfx_fb_phys(void); +uint64 gfx_fb_size(void); +uint32 gfx_fb_stride(void); +uint32 gfx_fb_bpp(void); +volatile uchar *gfx_fb_ptr(void); + +// Pixels and shapes. +void gfx_draw_pixel(int x, int y, uint32 color); +void gfx_clear(uint32 color); +void gfx_fill_rect(int x, int y, int w, int h, uint32 color); +void gfx_fill_circle(int cx, int cy, int r, uint32 color); +void gfx_draw_logo(int x, int y, uint32 color, uint32 bg); +void gfx_logo_centered(int cy, uint32 color, uint32 bg); + +// The built-in 8x16 font: glyphs at a position, and text that leaves the +// cursor alone. The crash screen lays itself out rather than printing, which +// is what needs the second one. +void gfx_draw_char(int x, int y, uchar c, uint32 color); +void gfx_text_at(int x, int y, const char *s, uint32 color); +int gfx_char_w(void); +int gfx_char_h(void); + +// The text console: a cursor, scrolling, and the erase operations an ANSI +// escape parser needs. See kernel/core/console.c. +void gfx_putc(int c, uint32 color); +void gfx_puts(int x, int y, char *s, uint32 color); +void gfx_set_bg(uint32 color); +void gfx_screen_reset(void); +int gfx_cols(void); +int gfx_rows(void); +void gfx_cursor_get(int *col, int *row); +void gfx_cursor_set(int col, int row); +void gfx_erase_display(int mode); +void gfx_erase_line(int mode); diff --git a/include/theme.h b/include/theme.h index 5474e95..94f3bbf 100644 --- a/include/theme.h +++ b/include/theme.h @@ -18,7 +18,7 @@ // types, no code — because stage2 is compiled 32-bit against a different // world than the kernel. // -// Colours are 0xRRGGBB. vga_draw_pixel packs them for whatever depth the +// Colours are 0xRRGGBB. gfx_draw_pixel packs them for whatever depth the // framebuffer actually is. // --- the product --------------------------------------------------------- diff --git a/include/user.h b/include/user.h index 7d35ceb..61ad465 100644 --- a/include/user.h +++ b/include/user.h @@ -34,7 +34,7 @@ int getpid(void); char* sbrk(uint64); int sleep(int); int uptime(void); -int vga_sync(void*); +int vga_sync(void*); // legacy no-op syscall; the name is ABI, not this layer int getmouse(struct mouse_data*); int reboot(void); int shutdown(void); diff --git a/kernel/arch/x86_64/vm.c b/kernel/arch/x86_64/vm.c index 1798450..06057e6 100644 --- a/kernel/arch/x86_64/vm.c +++ b/kernel/arch/x86_64/vm.c @@ -1,6 +1,7 @@ #include "param.h" #include "types.h" #include "defs.h" +#include "gfx.h" #include "x86.h" #include "memlayout.h" #include "mmu.h" @@ -429,7 +430,7 @@ copy_vma_range(pml4e_t *dstpgdir, pml4e_t *srcpgdir, uint64 va, uint64 length, i if (share) { // Point the child at the very same physical pages, whatever they are. // - // This used to special-case the framebuffer and map vga_fb_phys() + // This used to special-case the framebuffer and map gfx_fb_phys() // explicitly. Reading the parent's own PTE instead covers device memory // and shared memory segments with one rule, and cannot disagree with // what the parent actually has mapped -- which the explicit version diff --git a/kernel/core/console.c b/kernel/core/console.c index 481eca5..35a3925 100644 --- a/kernel/core/console.c +++ b/kernel/core/console.c @@ -1,6 +1,7 @@ #include #include "types.h" #include "defs.h" +#include "gfx.h" #include "theme.h" #include "irq.h" #include "param.h" @@ -35,13 +36,13 @@ int panicked = 0; // and the display genuinely belongs to the console. The F8 verbose boot never // sets it, so "show me everything" still works. int cons_serial_only = 1; -extern void vga_screen_reset(void); +extern void gfx_screen_reset(void); // Text colour used when drawing to the framebuffer. Changed by SGR escapes // (see sgr() below); reset to the default by ESC[0m and ESC c. // // This was initialised to 7, a leftover from when it was a VGA text-mode -// attribute byte. vga_putc takes a 32-bit RGB value, so 7 meant "very dark +// attribute byte. gfx_putc takes a 32-bit RGB value, so 7 meant "very dark // blue" -- it looked black against the cream background purely by accident. static uint32 cons_fg = SCH_FG; @@ -166,7 +167,7 @@ console_takeover(void) if (!cons_serial_only) return; // verbose boot: the console never left cons_serial_only = 0; - vga_screen_reset(); + gfx_screen_reset(); } // Foreground process group of the console, i.e. the controlling terminal. @@ -197,6 +198,11 @@ int tty_fg_pgrp = 1; // consoleinit() used to program these into the VGA DAC with set_vga_palette(), // which does nothing at all in a 32bpp linear mode -- there is no palette to // index. The colours had to live somewhere real, and this is it. +// +// That DAC code is now gone rather than merely unused, along with the blink +// disable next to it: both were writes to VGA hardware registers made from the +// console, which is neither a driver nor allowed to know it was running on a +// VGA. Nothing observable changed, because nothing observable depended on it. static const uint32 ansi_rgb[16] = { 0x000000, 0xAA0000, 0x00AA00, 0xAA5500, // black red green yellow(brown) 0x0000AA, 0xAA00AA, 0x00AAAA, 0xAAAAAA, // blue magenta cyan white @@ -324,7 +330,7 @@ sgr(void) cons_bg = ansi_rgb[(p - 100) + 8]; } } - vga_set_bg(cons_bg); + gfx_set_bg(cons_bg); } // One final byte of a CSI sequence, with esc_parm[] already filled in. @@ -335,32 +341,32 @@ csi_dispatch(int final) int n = esc_nparm > 0 ? esc_parm[0] : 0; int n1 = n ? n : 1; // most sequences default to 1 - vga_cursor_get(&col, &row); + gfx_cursor_get(&col, &row); switch (final) { - case 'A': vga_cursor_set(col, row - n1); break; - case 'B': vga_cursor_set(col, row + n1); break; - case 'C': vga_cursor_set(col + n1, row); break; - case 'D': vga_cursor_set(col - n1, row); break; - case 'E': vga_cursor_set(0, row + n1); break; - case 'F': vga_cursor_set(0, row - n1); break; - case 'G': vga_cursor_set(n1 - 1, row); break; - case 'd': vga_cursor_set(col, n1 - 1); break; + case 'A': gfx_cursor_set(col, row - n1); break; + case 'B': gfx_cursor_set(col, row + n1); break; + case 'C': gfx_cursor_set(col + n1, row); break; + case 'D': gfx_cursor_set(col - n1, row); break; + case 'E': gfx_cursor_set(0, row + n1); break; + case 'F': gfx_cursor_set(0, row - n1); break; + case 'G': gfx_cursor_set(n1 - 1, row); break; + case 'd': gfx_cursor_set(col, n1 - 1); break; case 'H': case 'f': { int r = esc_nparm > 0 && esc_parm[0] ? esc_parm[0] : 1; int c = esc_nparm > 1 && esc_parm[1] ? esc_parm[1] : 1; - vga_cursor_set(c - 1, r - 1); // ANSI counts from 1 + gfx_cursor_set(c - 1, r - 1); // ANSI counts from 1 break; } - case 'J': vga_erase_display(n); break; - case 'K': vga_erase_line(n); break; + case 'J': gfx_erase_display(n); break; + case 'K': gfx_erase_line(n); break; case 'm': sgr(); break; case 's': saved_col = col; saved_row = row; break; - case 'u': vga_cursor_set(saved_col, saved_row); break; + case 'u': gfx_cursor_set(saved_col, saved_row); break; // Cursor visibility (DECTCEM) and the other private modes. There is no // hardware cursor being drawn, so there is nothing to show or hide -- @@ -375,7 +381,7 @@ csi_dispatch(int final) // Feed one output byte to the framebuffer side. Returns nothing; the serial // side has already had the raw byte. static void -vga_feed(int c) +gfx_feed(int c) { switch (esc_state) { case E_ESC: @@ -390,7 +396,7 @@ vga_feed(int c) if (c == 'c') { // RIS -- full reset cons_fg = CONS_DEFAULT_FG; cons_bg = CONS_DEFAULT_BG; - vga_screen_reset(); + gfx_screen_reset(); } return; // other two-byte escapes: ignored @@ -421,23 +427,23 @@ vga_feed(int c) if (u8_need > 0) { if ((c & 0xC0) != 0x80) { // malformed: abandon the sequence u8_need = 0; - vga_putc('?', cons_fg); + gfx_putc('?', cons_fg); return; } u8_cp = (u8_cp << 6) | (c & 0x3F); if (--u8_need > 0) return; int g = glyph_for(u8_cp); - vga_putc(g ? g : '?', cons_fg); + gfx_putc(g ? g : '?', cons_fg); return; } if (c >= 0xF0) { u8_cp = c & 0x07; u8_need = 3; return; } else if (c >= 0xE0) { u8_cp = c & 0x0F; u8_need = 2; return; } else if (c >= 0xC0) { u8_cp = c & 0x1F; u8_need = 1; return; } - else if (c >= 0x80) { vga_putc('?', cons_fg); return; } // stray continuation + else if (c >= 0x80) { gfx_putc('?', cons_fg); return; } // stray continuation - vga_putc(c, cons_fg); + gfx_putc(c, cons_fg); } void @@ -451,13 +457,13 @@ consputc(int c) if (c == BACKSPACE) { uartputc('\b'); uartputc(' '); uartputc('\b'); if (!cons_serial_only) - vga_putc(c, cons_fg); + gfx_putc(c, cons_fg); return; } uartputc(c); if (!cons_serial_only) - vga_feed(c & 0xff); + gfx_feed(c & 0xff); } #define INPUT_BUF 256 @@ -747,25 +753,6 @@ consolewrite(struct inode *ip, uint64 off, char *buf, int n) return n; } -void -disable_blink(void) -{ - inb(0x3DA); - outb(0x3C0, 0x10 | 0x20); - uchar dummy = inb(0x3C1); - outb(0x3C0, dummy & ~0x08); -} - -static void -set_vga_palette(uchar index, uchar r, uchar g, uchar b) -{ - outb(0x3C6, 0xFF); - outb(0x3C8, index); - outb(0x3C9, r & 0x3F); - outb(0x3C9, g & 0x3F); - outb(0x3C9, b & 0x3F); -} - // The keyboard handler lives in kbd.c, but the console is what owns the // keyboard as an input source, so this is where the line gets claimed. static void @@ -787,24 +774,5 @@ consoleinit(void) devsw[CONSOLE].read = consoleread; cons.locking = 1; - disable_blink(); - - set_vga_palette(0, 0, 0, 0); // Black - set_vga_palette(1, 0, 0, 42); // Blue - set_vga_palette(2, 0, 42, 0); // Green - set_vga_palette(3, 0, 42, 42); // Cyan - set_vga_palette(4, 42, 0, 0); // Red - set_vga_palette(5, 42, 0, 42); // Magenta - set_vga_palette(6, 42, 21, 0); // Brown - set_vga_palette(7, 42, 42, 42); // Light Gray - set_vga_palette(8, 21, 21, 21); // Dark Gray - set_vga_palette(9, 21, 21, 63); // Light Blue - set_vga_palette(10, 21, 63, 21); // Light Green - set_vga_palette(11, 21, 63, 63); // Light Cyan - set_vga_palette(12, 63, 21, 21); // Light Red - set_vga_palette(13, 21, 21, 63); // Light Magenta - set_vga_palette(14, 63, 63, 21); // Yellow - set_vga_palette(15, 63, 63, 63); // White - irq_register(IRQ_KBD, kbd_irq, 0, "ps2-kbd", 0); } diff --git a/kernel/core/main.c b/kernel/core/main.c index ab6a0cf..01ee806 100644 --- a/kernel/core/main.c +++ b/kernel/core/main.c @@ -74,20 +74,22 @@ kmain(uint32 fb_addr) if (boot_verbose) cprintf("verbose boot: uartinit done\n"); mouseinit(); if (boot_verbose) cprintf("verbose boot: mouseinit done\n"); - vga_init(fb_addr); - if (boot_verbose) cprintf("verbose boot: vga_init done (fb=0x%x)\n", fb_addr); - // No splash drawing here on purpose. The loader owns the splash, logo and - // all, and vga_init() deliberately does not clear -- so it is still on the - // display right through to console_takeover(). Painting it again from here - // would land in the last few hundred milliseconds of boot and change nothing. - // After vga_init, because the display driver cross-checks the framebuffer - // address it finds on the bus against the one VBE reported -- and it can - // only do that once vga.c has read the VBE block. // The devices that are simply known to be on a PC rather than enumerated by - // any bus. Registered before pci_init() so that /proc/devices lists them - // first, which puts the machine's fixed furniture above whatever happened to - // be plugged into it. - platform_register_devices(); + // any bus, and the framebuffer the loader left running. Registered before + // pci_init() so that /proc/devices lists them first, which puts the machine's + // fixed furniture above whatever happened to be plugged into it -- and + // because the display has to work long before any bus is walked. + // + // main() names no display driver. Whichever one owns the boot framebuffer + // publishes it there as this kernel's scanout, and the console and crash + // screen draw on that; see include/gfx.h. + // + // No splash drawing here on purpose. The loader owns the splash, logo and + // all, and bringing the framebuffer up deliberately does not clear it -- so + // it stays on the display right through to console_takeover(). Painting it + // again from here would land in the last few hundred milliseconds of boot + // and change nothing. + platform_register_devices(fb_addr); pci_init(); if (boot_verbose) cprintf("verbose boot: pci_init done\n"); pinit(); diff --git a/kernel/core/panic.c b/kernel/core/panic.c index 4a60f6b..75588bc 100644 --- a/kernel/core/panic.c +++ b/kernel/core/panic.c @@ -27,6 +27,7 @@ #include "types.h" #include "defs.h" +#include "gfx.h" #include "param.h" #include "memlayout.h" #include "mmu.h" @@ -49,7 +50,7 @@ extern int panicked; /* */ /* cprintf goes to the console, which during a panic is also the screen */ /* being painted over -- so the panel builds its strings itself and */ -/* places them with vga_text_at. */ +/* places them with gfx_text_at. */ /* ------------------------------------------------------------------ */ static char * @@ -130,7 +131,7 @@ draw_paren(int cx, int cy, int h, int thick, uint32 color) int dx = isqrt(r * r - dy * dy); // Narrow it: a true half-circle reads as a bracket, not a parenthesis. int x = cx - (dx * 45) / 100; - vga_fill_circle(x, cy + dy, thick / 2, color); + gfx_fill_circle(x, cy + dy, thick / 2, color); } } @@ -141,8 +142,8 @@ draw_sad_face(int x, int y, int h, uint32 color) int gap = h / 4; // The colon. - vga_fill_circle(x + dot, y + h / 2 - gap, dot, color); - vga_fill_circle(x + dot, y + h / 2 + gap, dot, color); + gfx_fill_circle(x + dot, y + h / 2 - gap, dot, color); + gfx_fill_circle(x + dot, y + h / 2 + gap, dot, color); // The parenthesis, a little to the right of it. draw_paren(x + dot * 2 + h / 2, y + h / 2, h, dot * 2, color); @@ -380,15 +381,15 @@ draw_qr_panel(int sw, int sh, int ch, char *reason, addr_t *pcs, int npcs) int x0 = sw - card - sw / 24; int y0 = sh / 8; - vga_fill_rect(x0, y0, card, card, PSOGM_CARD); + gfx_fill_rect(x0, y0, card, card, PSOGM_CARD); for (int y = 0; y < panic_qr.size; y++) for (int x = 0; x < panic_qr.size; x++) if (panic_qr.mod[y][x]) - vga_fill_rect(x0 + (x + quiet) * scale, y0 + (y + quiet) * scale, + gfx_fill_rect(x0 + (x + quiet) * scale, y0 + (y + quiet) * scale, scale, scale, PSOGM_INK); - vga_text_at(x0, y0 + card + ch, "Scan for a text copy", PSOGM_DIM); - vga_text_at(x0, y0 + card + ch * 2 + 2, "of this report.", PSOGM_DIM); + gfx_text_at(x0, y0 + card + ch, "Scan for a text copy", PSOGM_DIM); + gfx_text_at(x0, y0 + card + ch * 2 + 2, "of this report.", PSOGM_DIM); return card + sw / 24; } @@ -410,19 +411,19 @@ static void paint(char *reason, addr_t *pcs, int npcs) { int sw, sh; - vga_dims(&sw, &sh); + gfx_dims(&sw, &sh); - int ch = vga_char_h(); - int cw = vga_char_w(); + int ch = gfx_char_h(); + int cw = gfx_char_w(); // The whole display, one colour. An earlier version dimmed the previous // screen and floated a panel over it, which looked good and was wrong: the // thing is called a Purple Screen, and half a screen of somebody's abandoned // work behind the report is a distraction at the worst possible moment. - vga_clear(PSOGM_BG); + gfx_clear(PSOGM_BG); // Glyphs are drawn with their background filled, so the console's cream has // to become the background or every line of text arrives in its own box. - vga_set_bg(PSOGM_BG); + gfx_set_bg(PSOGM_BG); // The symbol goes first, because it decides how much width the text has. It // is drawn before anything can fail: if the encoder refuses the payload the @@ -439,10 +440,10 @@ paint(char *reason, addr_t *pcs, int npcs) // Left-aligned, not centred. Centred text reads as a poster; this is a // report, and a report is read down the left edge. - vga_text_at(left, y, "SchOS ran into a problem and needs to restart.", + gfx_text_at(left, y, "SchOS ran into a problem and needs to restart.", PSOGM_FG); y += ch + ch / 2; - vga_text_at(left, y, "Nothing on your disks was touched by this.", + gfx_text_at(left, y, "Nothing on your disks was touched by this.", PSOGM_DIM); // A rule in the boot splash's accent, the same width as the one under the @@ -452,25 +453,25 @@ paint(char *reason, addr_t *pcs, int npcs) // the line above to read as an underline of that sentence rather than as a // divider before the facts. y += ch * 2; - vga_fill_rect(left, y, 120, 2, SCH_ACCENT); + gfx_fill_rect(left, y, 120, 2, SCH_ACCENT); y += ch; // Facts: quiet labels down the left, bright values in a second column. int vx = left + 14 * cw; for (int i = 0; i < nfields; i++) { - vga_text_at(left, y, fields[i].label, PSOGM_DIM); - vga_text_at(vx, y, fields[i].value, PSOGM_FG); + gfx_text_at(left, y, fields[i].label, PSOGM_DIM); + gfx_text_at(vx, y, fields[i].value, PSOGM_FG); y += ch + 2; } y += ch; - vga_text_at(left, y, "Call chain", PSOGM_DIM); + gfx_text_at(left, y, "Call chain", PSOGM_DIM); y += ch + 2; for (int i = 0; i < npcs && pcs[i]; i++) { char line[48]; trace_line(line, sizeof(line), i, pcs[i]); - vga_text_at(vx - 3 * cw, y, line, PSOGM_FG); + gfx_text_at(vx - 3 * cw, y, line, PSOGM_FG); y += ch; if (y > sh - ch * 5) break; @@ -479,9 +480,9 @@ paint(char *reason, addr_t *pcs, int npcs) // Two closing lines, bottom left: what to do with the numbers above, and // what this screen is called, so it can be searched for by name. Shortened // from the full command line, which at 62 characters ran under the QR card. - vga_text_at(left, sh - ch * 3, + gfx_text_at(left, sh - ch * 3, "Resolve: addr2line -e kernel
", PSOGM_DIM); - vga_text_at(left, sh - ch * 3 + ch + 4, + gfx_text_at(left, sh - ch * 3 + ch + 4, "PSOGM -- Purple Screen of Guru Meditation", PSOGM_DIM); } @@ -534,7 +535,7 @@ panic(char *s) gather(s, pcs, 10); report_serial(s, pcs, 10); - if (vga_ready()) + if (gfx_ready()) paint(s, pcs, 10); panicked = 1; diff --git a/kernel/dev/fb.c b/kernel/dev/fb.c index 9abe8de..ee84d4f 100644 --- a/kernel/dev/fb.c +++ b/kernel/dev/fb.c @@ -13,6 +13,7 @@ #include "types.h" #include "defs.h" +#include "gfx.h" #include "param.h" #include "mmu.h" #include "proc.h" @@ -35,7 +36,7 @@ chan(struct fb_bitfield *f, unsigned int offset, unsigned int length) f->msb_right = 0; } -// Pixel layout for the depths vga_draw_pixel knows how to write. Keeping the +// Pixel layout for the depths gfx_draw_pixel knows how to write. Keeping the // two in step matters: this is the kernel telling userspace how to build a // pixel, and if it disagrees with how the kernel builds one, every colour // drawn by a program is wrong in a way that looks like a hardware fault. @@ -52,7 +53,7 @@ pixel_format(struct fb_var_screeninfo *v, unsigned int bpp) chan(&v->transp, 0, 0); break; case 16: - // RGB565, matching the packing vga_draw_pixel does for this depth. + // RGB565, matching the packing gfx_draw_pixel does for this depth. chan(&v->red, 11, 5); chan(&v->green, 5, 6); chan(&v->blue, 0, 5); chan(&v->transp, 0, 0); break; @@ -66,8 +67,8 @@ pixel_format(struct fb_var_screeninfo *v, unsigned int bpp) int fbread(struct inode *ip, uint64 off, char *dst, int n) { - volatile uchar *fb = vga_fb_ptr(); - uint64 size = vga_fb_size(); + volatile uchar *fb = gfx_fb_ptr(); + uint64 size = gfx_fb_size(); if (!fb) return -ENODEV; @@ -100,8 +101,8 @@ fb_mapped(void) int fbwrite(struct inode *ip, uint64 off, char *src, int n) { - volatile uchar *fb = vga_fb_ptr(); - uint64 size = vga_fb_size(); + volatile uchar *fb = gfx_fb_ptr(); + uint64 size = gfx_fb_size(); if (!fb) return -ENODEV; @@ -137,7 +138,7 @@ fbclose(struct inode *ip) // indistinguishable from a bug -- and this is the only place in the system // that wipes the display without being asked to. cprintf("fb0: released by pid %d, console restored\n", myproc() ? myproc()->pid : -1); - vga_screen_reset(); + gfx_screen_reset(); } // FBIO* handling, called from sys_ioctl once it has established that the fd @@ -151,10 +152,10 @@ fb_ioctl(int request, addr_t arg) { int w, h; - if (!vga_fb_ptr()) + if (!gfx_fb_ptr()) return -ENODEV; - vga_dims(&w, &h); + gfx_dims(&w, &h); switch (request) { case FBIOGET_VSCREENINFO: { @@ -165,7 +166,7 @@ fb_ioctl(int request, addr_t arg) memset(v, 0, sizeof(*v)); v->xres = v->xres_virtual = (unsigned int)w; v->yres = v->yres_virtual = (unsigned int)h; - v->bits_per_pixel = vga_fb_bpp(); + v->bits_per_pixel = gfx_fb_bpp(); v->activate = FB_ACTIVATE_NOW; pixel_format(v, v->bits_per_pixel); // height/width (physical millimetres) and every timing field stay zero: @@ -180,11 +181,11 @@ fb_ioctl(int request, addr_t arg) return -EFAULT; memset(f, 0, sizeof(*f)); safestrcpy(f->id, "schosfb", sizeof(f->id)); - f->smem_start = (unsigned long)vga_fb_phys(); - f->smem_len = (unsigned int)vga_fb_size(); + f->smem_start = (unsigned long)gfx_fb_phys(); + f->smem_len = (unsigned int)gfx_fb_size(); f->type = FB_TYPE_PACKED_PIXELS; f->visual = FB_VISUAL_TRUECOLOR; - f->line_length = vga_fb_stride(); + f->line_length = gfx_fb_stride(); return 0; } diff --git a/kernel/dev/gfx.c b/kernel/dev/gfx.c new file mode 100644 index 0000000..d42be91 --- /dev/null +++ b/kernel/dev/gfx.c @@ -0,0 +1,444 @@ +// Drawing and the text console, on whatever display is bound. See include/gfx.h +// for why this is not a driver and does not name one. +// +// It was kernel/dev/vga.c, and the name was the problem: panic.c called +// gfx_fill_rect, console.c called gfx_putc, /dev/fb0 called gfx_fb_phys. None +// of this code is VGA-specific -- it is bitmap drawing over a linear +// framebuffer -- and the one part that genuinely was is now the driver, in +// kernel/drivers/gpu/vga/. +// +// The framebuffer arrives through gfx_attach_scanout() rather than being read +// out of the boot block here. That is the whole of the inversion: a driver +// publishes what it owns, and this file draws on it without knowing what it is. + +#include "types.h" +#include "x86.h" +#include "memlayout.h" +#include "defs.h" +#include "theme.h" +#include "font.h" +#include "logo.h" +#include "device.h" +#include "display.h" +#include "gfx.h" + +#define BACKSPACE 0x100 +#define CHAR_HEIGHT 16 +#define CHAR_WIDTH 8 +#define COLOR_BG SCH_BG + +// The scanout, as published by whichever driver owns the display. Zeroed until +// one does, and every entry point checks `fb` before touching memory -- a +// kernel that panics before any display driver has run still has to be able to +// call in here and do nothing gracefully. +static struct fb_info scanout; +static volatile uchar *fb; + +static int cursor_x = 0; +static int cursor_y = 0; +// Current fill color for glyph backgrounds, backspace erase, and scroll -- +// normally COLOR_BG, but panic() switches this to paint the whole console a +// solid color (e.g. PSOGM's purple) instead of leaving cream-colored boxes +// around each character. +static uint32 gfx_bg = COLOR_BG; + +void +gfx_attach_scanout(const struct fb_info *info) +{ + scanout = *info; + fb = info->ptr; +} + +const struct fb_info * +gfx_scanout(void) +{ + return fb ? &scanout : 0; +} + +void +gfx_set_bg(uint32 color) +{ + gfx_bg = color; +} + + +// The mode that was actually set. Exported because the resolution is chosen at +// boot now (press R -- see boot/bios/stage2.S), so nothing above may assume it. +uint32 +gfx_width(void) +{ + return scanout.width; +} + +uint32 +gfx_height(void) +{ + return scanout.height; +} + +void +gfx_draw_pixel(int x, int y, uint32 color) +{ + if (!fb || x < 0 || x >= scanout.width || y < 0 || y >= scanout.height) + return; + + uint32 offset = (y * scanout.stride) + (x * (scanout.bpp / 8)); + + if (scanout.bpp == 32) { + *(volatile uint32*)(fb + offset) = color; + } else if (scanout.bpp == 24) { + fb[offset] = color & 0xFF; + fb[offset + 1] = (color >> 8) & 0xFF; + fb[offset + 2] = (color >> 16) & 0xFF; + } else if (scanout.bpp == 16) { + uint16 c16 = (((color >> 19) & 0x1F) << 11) | (((color >> 10) & 0x3F) << 5) | ((color >> 3) & 0x1F); + *(volatile uint16*)(fb + offset) = c16; + } +} + +void +gfx_clear(uint32 color) +{ + for (int y = 0; y < scanout.height; y++) { + for (int x = 0; x < scanout.width; x++) { + gfx_draw_pixel(x, y, color); + } + } +} + +void +gfx_draw_char(int x, int y, uchar c, uint32 color) +{ + // No `c > 127` rejection any more: font_data holds 256 Latin-1 glyphs, and + // a uchar cannot index past them. + if (!fb) return; + + for (int row = 0; row < CHAR_HEIGHT; row++) { + uchar row_data = font_data[c][row]; + for (int col = 0; col < CHAR_WIDTH; col++) { + if (row_data & (0x80 >> col)) { + gfx_draw_pixel(x + col, y + row, color); + } else { + gfx_draw_pixel(x + col, y + row, gfx_bg); + } + } + } +} + +void +gfx_puts(int x, int y, char *s, uint32 color) +{ + while (*s) { + gfx_putc((uchar)*s, color); + s++; + } +} + +void +gfx_putc(int c, uint32 color) +{ + if (!fb) return; + + if (c == BACKSPACE || c == 0x08 || c == '\b' || c == 127) { + if (cursor_x >= CHAR_WIDTH) { + cursor_x -= CHAR_WIDTH; + for (int i = 0; i < CHAR_HEIGHT; i++) { + for (int j = 0; j < CHAR_WIDTH; j++) { + gfx_draw_pixel(cursor_x + j, cursor_y + i, gfx_bg); + } + } + } + return; + } + + if (c == '\n') { + cursor_x = 0; + cursor_y += CHAR_HEIGHT; + } else if (c == '\r') { + cursor_x = 0; + } else if (c == '\t') { + // Advance to the next multiple of eight columns, erasing as it goes so a + // tab overwrites rather than skipping over stale characters. + // + // There was no tab case at all, so '\t' fell through to gfx_draw_char and + // was drawn as glyph 9 -- which in CP437 is a hollow circle. Pressing Tab + // printed a circle. Same for every other control character: the font has + // a picture for each of them, and none of those pictures is what the + // character means. + int col = cursor_x / CHAR_WIDTH; + int stop = (col + 8) & ~7; + while (col < stop && cursor_x < (int)scanout.width) { + for (int i = 0; i < CHAR_HEIGHT; i++) + for (int j = 0; j < CHAR_WIDTH; j++) + gfx_draw_pixel(cursor_x + j, cursor_y + i, gfx_bg); + cursor_x += CHAR_WIDTH; + col++; + } + } else if (c < 32) { + return; // any other control character: not a glyph + } else { + gfx_draw_char(cursor_x, cursor_y, (uchar)c, color); + cursor_x += CHAR_WIDTH; + } + + if (cursor_x >= scanout.width) { + cursor_x = 0; + cursor_y += CHAR_HEIGHT; + } + + if (cursor_y > (scanout.height - CHAR_HEIGHT)) { + uint32 line_size = scanout.stride * CHAR_HEIGHT; + uint32 total_size = scanout.stride * scanout.height; + uint32 copy_size = total_size - line_size; + + memmove((void*)fb, (void*)(fb + line_size), copy_size); + + for (int y = scanout.height - CHAR_HEIGHT; y < scanout.height; y++) { + for (int x = 0; x < scanout.width; x++) { + gfx_draw_pixel(x, y, gfx_bg); + } + } + cursor_y = scanout.height - CHAR_HEIGHT; + } +} + +void +gfx_screen_reset(void) +{ + gfx_bg = COLOR_BG; + gfx_clear(COLOR_BG); + cursor_x = 0; + cursor_y = 0; +} + +/* ------------------------------------------------------------------ */ +/* Drawing primitives for the crash screen */ +/* */ +/* The crash screen lays itself out rather than printing, which needs */ +/* two things the console never did: filled shapes, and text at an */ +/* absolute position that leaves the cursor alone. */ +/* ------------------------------------------------------------------ */ + +int +gfx_ready(void) +{ + return fb != 0; +} + +void +gfx_dims(int *w, int *h) +{ + if (w) *w = (int)scanout.width; + if (h) *h = (int)scanout.height; +} + +int gfx_char_w(void) { return CHAR_WIDTH; } +int gfx_char_h(void) { return CHAR_HEIGHT; } + +/* ------------------------------------------------------------------ */ +/* Framebuffer geometry */ +/* */ +/* Whatever the attached scanout says, rather than anything read from */ +/* the boot block here -- the driver that owns the display is the only */ +/* thing that knows where the pixels are. See kernel/dev/fb.c for the */ +/* device these back. */ +/* ------------------------------------------------------------------ */ + +uint64 gfx_fb_phys(void) { return scanout.phys; } + +// 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 gfx_fb_size(void) +{ + if (!fb) + return 0; + return (uint64)scanout.stride * (uint64)scanout.height; +} + +uint32 gfx_fb_stride(void) { return scanout.stride; } +uint32 gfx_fb_bpp(void) { return scanout.bpp; } + +volatile uchar *gfx_fb_ptr(void) { return fb; } + +/* ------------------------------------------------------------------ */ +/* Cursor addressing and erasing */ +/* */ +/* What an ANSI escape parser needs and gfx_putc alone cannot give it: */ +/* the ability to put the cursor somewhere specific and to erase a */ +/* region without writing spaces over it. See kernel/core/console.c. */ +/* ------------------------------------------------------------------ */ + +int gfx_cols(void) { return scanout.width ? (int)(scanout.width / CHAR_WIDTH) : 0; } +int gfx_rows(void) { return scanout.height ? (int)(scanout.height / CHAR_HEIGHT) : 0; } + +void +gfx_cursor_get(int *col, int *row) +{ + if (col) *col = cursor_x / CHAR_WIDTH; + if (row) *row = cursor_y / CHAR_HEIGHT; +} + +// Clamped rather than validated: an escape sequence is untrusted input that +// arrives from any program that can write to the terminal, and "CUP to row +// 9999" should put the cursor on the last row, not off the end of the +// framebuffer. +void +gfx_cursor_set(int col, int row) +{ + int maxc = gfx_cols(), maxr = gfx_rows(); + + if (maxc <= 0 || maxr <= 0) + return; + if (col < 0) col = 0; + if (row < 0) row = 0; + if (col >= maxc) col = maxc - 1; + if (row >= maxr) row = maxr - 1; + + cursor_x = col * CHAR_WIDTH; + cursor_y = row * CHAR_HEIGHT; +} + +// Fill a run of character cells with the background colour. +static void +erase_cells(int col, int row, int count) +{ + int maxc = gfx_cols(); + + while (count-- > 0) { + int x = col * CHAR_WIDTH, y = row * CHAR_HEIGHT; + for (int i = 0; i < CHAR_HEIGHT; i++) + for (int j = 0; j < CHAR_WIDTH; j++) + gfx_draw_pixel(x + j, y + i, gfx_bg); + if (++col >= maxc) { col = 0; row++; } + } +} + +// ED -- erase in display. 0: cursor to end, 1: start to cursor, 2: all. +void +gfx_erase_display(int mode) +{ + int col, row, maxc = gfx_cols(), maxr = gfx_rows(); + + gfx_cursor_get(&col, &row); + switch (mode) { + case 0: erase_cells(col, row, (maxr - row) * maxc - col); break; + case 1: erase_cells(0, 0, row * maxc + col + 1); break; + default: gfx_clear(gfx_bg); break; + } +} + +// EL -- erase in line. Same three modes, bounded to the current row. +void +gfx_erase_line(int mode) +{ + int col, row, maxc = gfx_cols(); + + gfx_cursor_get(&col, &row); + switch (mode) { + case 0: erase_cells(col, row, maxc - col); break; + case 1: erase_cells(0, row, col + 1); break; + default: erase_cells(0, row, maxc); break; + } +} + +// Row pointer computed once per row rather than per pixel. The crash screen +// paints a solid card behind the QR symbol, and at 8 pixels per module that is +// tens of thousands of writes going through gfx_draw_pixel's bounds check and +// multiply. +void +gfx_fill_rect(int x0, int y0, int w, int h, uint32 color) +{ + if (!fb) + return; + if (x0 < 0) { w += x0; x0 = 0; } + if (y0 < 0) { h += y0; y0 = 0; } + if (x0 + w > (int)scanout.width) w = (int)scanout.width - x0; + if (y0 + h > (int)scanout.height) h = (int)scanout.height - y0; + if (w <= 0 || h <= 0) + return; + + // Only the 32bpp case gets the fast path; anything else is rare enough that + // correctness through gfx_draw_pixel is the better trade. + if (scanout.bpp != 32) { + for (int y = y0; y < y0 + h; y++) + for (int x = x0; x < x0 + w; x++) + gfx_draw_pixel(x, y, color); + return; + } + for (int y = y0; y < y0 + h; y++) { + volatile uint32 *row = (volatile uint32 *)(fb + y * scanout.stride) + x0; + for (int x = 0; x < w; x++) + row[x] = color; + } +} + +void +gfx_fill_circle(int cx, int cy, int r, uint32 color) +{ + for (int dy = -r; dy <= r; dy++) + for (int dx = -r; dx <= r; dx++) + if (dx * dx + dy * dy <= r * r) + gfx_draw_pixel(cx + dx, cy + dy, color); +} + +// Text at an absolute position, leaving the console cursor alone. The crash +// screen lays itself out, so gfx_putc's cursor tracking and scrolling are +// exactly what it does not want. +void +gfx_text_at(int x, int y, const char *s, uint32 color) +{ + for (int i = 0; s[i]; i++) + gfx_draw_char(x + i * CHAR_WIDTH, y, (uchar)s[i], color); +} + +// --- Logo ------------------------------------------------------------------ +// +// The boot splash's logo is drawn by the *loader*, not here -- see splash() in +// boot/bios/stage2main.c. It has to be, because the kernel does not reach gfx_init() +// until roughly the last 300ms of a 2.8s boot: drawing it here put the logo on +// screen for one frame before console_takeover() wiped it, which is worse than +// not having one. +// +// What lives here is the same drawing code against the same mask, for the +// things that want the mark *after* boot -- the crash screen, and eventually +// whatever schinit shows. Keeping one copy of the compositing logic is why this +// is not simply deleted. + +// Blend `fg` over `bg` by coverage a (0..255). Per-channel, integer only: this +// runs before any floating point is safe to touch. +static uint32 +blend(uint32 bg, uint32 fg, uint a) +{ + uint32 r = (((fg >> 16) & 0xFF) * a + ((bg >> 16) & 0xFF) * (255 - a)) / 255; + uint32 g = (((fg >> 8) & 0xFF) * a + ((bg >> 8) & 0xFF) * (255 - a)) / 255; + uint32 b = (((fg ) & 0xFF) * a + ((bg ) & 0xFF) * (255 - a)) / 255; + return (r << 16) | (g << 8) | b; +} + +void +gfx_draw_logo(int x, int y, uint32 color, uint32 bg) +{ + if (!fb) + return; + for (int dy = 0; dy < LOGO_H; dy++) { + for (int dx = 0; dx < LOGO_W; dx++) { + uint a = LOGO_INK(dy * LOGO_W + dx); + // Skip the background rather than painting it. The mask is a square but + // the mark is a circle, so most of it is empty -- and drawing that empty + // area in bg would stamp a visible box over whatever is underneath. + if (a == 0) + continue; + gfx_draw_pixel(x + dx, y + dy, a == 255 ? color : blend(bg, color, a)); + } + } +} + +// Centred, for callers that own the whole screen. +void +gfx_logo_centered(int cy, uint32 color, uint32 bg) +{ + if (!fb || scanout.width < LOGO_W) + return; + gfx_draw_logo(((int)scanout.width - LOGO_W) / 2, cy - LOGO_H / 2, color, bg); +} diff --git a/kernel/dev/mouse.c b/kernel/dev/mouse.c index 05f90b0..5fdd320 100644 --- a/kernel/dev/mouse.c +++ b/kernel/dev/mouse.c @@ -1,5 +1,6 @@ #include "types.h" #include "defs.h" +#include "gfx.h" #include "irq.h" #include "x86.h" #include "traps.h" @@ -71,8 +72,8 @@ mouse_move_locked(int dx, int dy) // It is a boot-time choice now (press R), and at 1280x800 the pointer simply // stopped two thirds of the way across -- which reads as the cursor drifting // away from the host's, rather than as a clamp. - int maxx = (int)vga_get_width(); - int maxy = (int)vga_get_height(); + int maxx = (int)gfx_width(); + int maxy = (int)gfx_height(); if(maxx <= 0) maxx = 800; if(maxy <= 0) maxy = 600; @@ -104,8 +105,8 @@ mouse_inject(int dx, int dy, int buttons) void mouse_set_abs(int x, int y, int buttons) { - int maxx = (int)vga_get_width(); - int maxy = (int)vga_get_height(); + int maxx = (int)gfx_width(); + int maxy = (int)gfx_height(); if(maxx <= 0) maxx = 800; if(maxy <= 0) maxy = 600; diff --git a/kernel/dev/vga.c b/kernel/dev/vga.c deleted file mode 100644 index 9dce0e4..0000000 --- a/kernel/dev/vga.c +++ /dev/null @@ -1,569 +0,0 @@ -#include "types.h" -#include "x86.h" -#include "memlayout.h" -#include "defs.h" -#include "theme.h" -#include "font.h" -#include "logo.h" -#include "device.h" -#include "display.h" -#include "dma.h" - -#define BACKSPACE 0x100 -#define CHAR_HEIGHT 16 -#define CHAR_WIDTH 8 -#define COLOR_BG SCH_BG - -volatile uchar *vga_mem = 0; -// Physical base of the linear framebuffer, as reported by VBE. Every other -// part of the kernel that needs to map the framebuffer -- fork's VMA copy, -// mmap of /dev/fb0 -- asks for this rather than assuming a value. Three -// places used to hardcode 0xfd000000, which is only where QEMU's stdvga -// happens to put it. -static uint64 vga_phys = 0; -static uint32 vga_stride = 0; -static uint32 vga_bpp = 0; -static uint32 vga_width = 800; -static uint32 vga_height = 600; -static int cursor_x = 0; -static int cursor_y = 0; -// Current fill color for glyph backgrounds, backspace erase, and scroll — -// normally COLOR_BG, but panic() switches this to paint the whole console -// a solid color (e.g. PSOGM's purple) instead of leaving cream-colored boxes -// around each character. -static uint32 vga_bg = COLOR_BG; - -void -vga_set_bg(uint32 color) -{ - vga_bg = color; -} - -// Written by boot/bios/stage2.S's VBE call and read by boot/bios/stage2main.c under -// the same name (SCHBOOT_VBE_INFO) — not 0x9000, since stage2's own code now -// runs up to nearly 0x9000 once the boot-menu/progress-bar logic is -// included, which would otherwise clobber a mode info block placed there. -#include "schboot.h" /* the hand-off block */ - -void -vga_init(uint64 phys_addr) { - if (phys_addr == 0) { - phys_addr = *(uint32*)(P2V(SCHBOOT_VBE_LFB)); - } - vga_mem = (volatile uchar*)P2V(phys_addr); - vga_phys = phys_addr; - - vga_stride = *(ushort*)(P2V(SCHBOOT_VBE_STRIDE)); - vga_width = *(ushort*)(P2V(SCHBOOT_VBE_WIDTH)); - vga_height = *(ushort*)(P2V(SCHBOOT_VBE_HEIGHT)); - vga_bpp = *(uchar*)(P2V(SCHBOOT_VBE_BPP)); - - if (vga_width == 0) vga_width = 800; - if (vga_height == 0) vga_height = 600; - if (vga_stride == 0) vga_stride = vga_width * (vga_bpp / 8); - - cprintf("VGA: %dx%d, %d bpp, stride %d, LFB: 0x%x\n", vga_width, vga_height, vga_bpp, vga_stride, (uint32)phys_addr); - - // Deliberately does NOT clear the screen. - // - // The bootloader 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 kernel log lines. The splash now stays up - // for the whole of boot, and console_takeover() clears it exactly once -- - // when userspace starts and the display changes owner. -} - -// The mode that was actually set. Exported because the resolution is chosen at -// boot now (press R -- see boot/bios/stage2.S), so nothing above may assume it. -uint32 -vga_get_width(void) -{ - return vga_width; -} - -uint32 -vga_get_height(void) -{ - return vga_height; -} - -void -vga_draw_pixel(int x, int y, uint32 color) -{ - if (!vga_mem || x < 0 || x >= vga_width || y < 0 || y >= vga_height) - return; - - uint32 offset = (y * vga_stride) + (x * (vga_bpp / 8)); - - if (vga_bpp == 32) { - *(volatile uint32*)(vga_mem + offset) = color; - } else if (vga_bpp == 24) { - vga_mem[offset] = color & 0xFF; - vga_mem[offset + 1] = (color >> 8) & 0xFF; - vga_mem[offset + 2] = (color >> 16) & 0xFF; - } else if (vga_bpp == 16) { - uint16 c16 = (((color >> 19) & 0x1F) << 11) | (((color >> 10) & 0x3F) << 5) | ((color >> 3) & 0x1F); - *(volatile uint16*)(vga_mem + offset) = c16; - } -} - -void -vga_clear(uint32 color) -{ - for (int y = 0; y < vga_height; y++) { - for (int x = 0; x < vga_width; x++) { - vga_draw_pixel(x, y, color); - } - } -} - -void -vga_draw_char(int x, int y, uchar c, uint32 color) -{ - // No `c > 127` rejection any more: font_data holds 256 Latin-1 glyphs, and - // a uchar cannot index past them. - if (!vga_mem) return; - - for (int row = 0; row < CHAR_HEIGHT; row++) { - uchar row_data = font_data[c][row]; - for (int col = 0; col < CHAR_WIDTH; col++) { - if (row_data & (0x80 >> col)) { - vga_draw_pixel(x + col, y + row, color); - } else { - vga_draw_pixel(x + col, y + row, vga_bg); - } - } - } -} - -void -vga_puts(int x, int y, char *s, uint32 color) -{ - while (*s) { - vga_putc((uchar)*s, color); - s++; - } -} - -void -vga_putc(int c, uint32 color) -{ - if (!vga_mem) return; - - if (c == BACKSPACE || c == 0x08 || c == '\b' || c == 127) { - if (cursor_x >= CHAR_WIDTH) { - cursor_x -= CHAR_WIDTH; - for (int i = 0; i < CHAR_HEIGHT; i++) { - for (int j = 0; j < CHAR_WIDTH; j++) { - vga_draw_pixel(cursor_x + j, cursor_y + i, vga_bg); - } - } - } - return; - } - - if (c == '\n') { - cursor_x = 0; - cursor_y += CHAR_HEIGHT; - } else if (c == '\r') { - cursor_x = 0; - } else if (c == '\t') { - // Advance to the next multiple of eight columns, erasing as it goes so a - // tab overwrites rather than skipping over stale characters. - // - // There was no tab case at all, so '\t' fell through to vga_draw_char and - // was drawn as glyph 9 -- which in CP437 is a hollow circle. Pressing Tab - // printed a circle. Same for every other control character: the font has - // a picture for each of them, and none of those pictures is what the - // character means. - int col = cursor_x / CHAR_WIDTH; - int stop = (col + 8) & ~7; - while (col < stop && cursor_x < (int)vga_width) { - for (int i = 0; i < CHAR_HEIGHT; i++) - for (int j = 0; j < CHAR_WIDTH; j++) - vga_draw_pixel(cursor_x + j, cursor_y + i, vga_bg); - cursor_x += CHAR_WIDTH; - col++; - } - } else if (c < 32) { - return; // any other control character: not a glyph - } else { - vga_draw_char(cursor_x, cursor_y, (uchar)c, color); - cursor_x += CHAR_WIDTH; - } - - if (cursor_x >= vga_width) { - cursor_x = 0; - cursor_y += CHAR_HEIGHT; - } - - if (cursor_y > (vga_height - CHAR_HEIGHT)) { - uint32 line_size = vga_stride * CHAR_HEIGHT; - uint32 total_size = vga_stride * vga_height; - uint32 copy_size = total_size - line_size; - - memmove((void*)vga_mem, (void*)(vga_mem + line_size), copy_size); - - for (int y = vga_height - CHAR_HEIGHT; y < vga_height; y++) { - for (int x = 0; x < vga_width; x++) { - vga_draw_pixel(x, y, vga_bg); - } - } - cursor_y = vga_height - CHAR_HEIGHT; - } -} - -void -vga_screen_reset(void) -{ - vga_bg = COLOR_BG; - vga_clear(COLOR_BG); - cursor_x = 0; - cursor_y = 0; -} - -/* ------------------------------------------------------------------ */ -/* Drawing primitives for the crash screen */ -/* */ -/* The crash screen lays itself out rather than printing, which needs */ -/* two things the console never did: filled shapes, and text at an */ -/* absolute position that leaves the cursor alone. */ -/* ------------------------------------------------------------------ */ - -int -vga_ready(void) -{ - return vga_mem != 0; -} - -void -vga_dims(int *w, int *h) -{ - if (w) *w = (int)vga_width; - if (h) *h = (int)vga_height; -} - -int vga_char_w(void) { return CHAR_WIDTH; } -int vga_char_h(void) { return CHAR_HEIGHT; } - -/* ------------------------------------------------------------------ */ -/* Framebuffer geometry */ -/* */ -/* vga.c is the only place that reads the VBE mode info, so it is the */ -/* only place that knows where the framebuffer actually is. Everything */ -/* else asks. See kernel/dev/fb.c for the device these back. */ -/* ------------------------------------------------------------------ */ - -uint64 vga_fb_phys(void) { return vga_phys; } - -// 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 (!vga_mem) - return 0; - return (uint64)vga_stride * (uint64)vga_height; -} - -uint32 vga_fb_stride(void) { return vga_stride; } -uint32 vga_fb_bpp(void) { return vga_bpp; } - -volatile uchar *vga_fb_ptr(void) { return vga_mem; } - -/* ------------------------------------------------------------------ */ -/* Cursor addressing and erasing */ -/* */ -/* What an ANSI escape parser needs and vga_putc alone cannot give it: */ -/* the ability to put the cursor somewhere specific and to erase a */ -/* region without writing spaces over it. See kernel/core/console.c. */ -/* ------------------------------------------------------------------ */ - -int vga_cols(void) { return vga_width ? (int)(vga_width / CHAR_WIDTH) : 0; } -int vga_rows(void) { return vga_height ? (int)(vga_height / CHAR_HEIGHT) : 0; } - -void -vga_cursor_get(int *col, int *row) -{ - if (col) *col = cursor_x / CHAR_WIDTH; - if (row) *row = cursor_y / CHAR_HEIGHT; -} - -// Clamped rather than validated: an escape sequence is untrusted input that -// arrives from any program that can write to the terminal, and "CUP to row -// 9999" should put the cursor on the last row, not off the end of the -// framebuffer. -void -vga_cursor_set(int col, int row) -{ - int maxc = vga_cols(), maxr = vga_rows(); - - if (maxc <= 0 || maxr <= 0) - return; - if (col < 0) col = 0; - if (row < 0) row = 0; - if (col >= maxc) col = maxc - 1; - if (row >= maxr) row = maxr - 1; - - cursor_x = col * CHAR_WIDTH; - cursor_y = row * CHAR_HEIGHT; -} - -// Fill a run of character cells with the background colour. -static void -erase_cells(int col, int row, int count) -{ - int maxc = vga_cols(); - - while (count-- > 0) { - int x = col * CHAR_WIDTH, y = row * CHAR_HEIGHT; - for (int i = 0; i < CHAR_HEIGHT; i++) - for (int j = 0; j < CHAR_WIDTH; j++) - vga_draw_pixel(x + j, y + i, vga_bg); - if (++col >= maxc) { col = 0; row++; } - } -} - -// ED -- erase in display. 0: cursor to end, 1: start to cursor, 2: all. -void -vga_erase_display(int mode) -{ - int col, row, maxc = vga_cols(), maxr = vga_rows(); - - vga_cursor_get(&col, &row); - switch (mode) { - case 0: erase_cells(col, row, (maxr - row) * maxc - col); break; - case 1: erase_cells(0, 0, row * maxc + col + 1); break; - default: vga_clear(vga_bg); break; - } -} - -// EL -- erase in line. Same three modes, bounded to the current row. -void -vga_erase_line(int mode) -{ - int col, row, maxc = vga_cols(); - - vga_cursor_get(&col, &row); - switch (mode) { - case 0: erase_cells(col, row, maxc - col); break; - case 1: erase_cells(0, row, col + 1); break; - default: erase_cells(0, row, maxc); break; - } -} - -// Row pointer computed once per row rather than per pixel. The crash screen -// paints a solid card behind the QR symbol, and at 8 pixels per module that is -// tens of thousands of writes going through vga_draw_pixel's bounds check and -// multiply. -void -vga_fill_rect(int x0, int y0, int w, int h, uint32 color) -{ - if (!vga_mem) - return; - if (x0 < 0) { w += x0; x0 = 0; } - if (y0 < 0) { h += y0; y0 = 0; } - if (x0 + w > (int)vga_width) w = (int)vga_width - x0; - if (y0 + h > (int)vga_height) h = (int)vga_height - y0; - if (w <= 0 || h <= 0) - return; - - // Only the 32bpp case gets the fast path; anything else is rare enough that - // correctness through vga_draw_pixel is the better trade. - if (vga_bpp != 32) { - for (int y = y0; y < y0 + h; y++) - for (int x = x0; x < x0 + w; x++) - vga_draw_pixel(x, y, color); - return; - } - for (int y = y0; y < y0 + h; y++) { - volatile uint32 *row = (volatile uint32 *)(vga_mem + y * vga_stride) + x0; - for (int x = 0; x < w; x++) - row[x] = color; - } -} - -void -vga_fill_circle(int cx, int cy, int r, uint32 color) -{ - for (int dy = -r; dy <= r; dy++) - for (int dx = -r; dx <= r; dx++) - if (dx * dx + dy * dy <= r * r) - vga_draw_pixel(cx + dx, cy + dy, color); -} - -// Text at an absolute position, leaving the console cursor alone. The crash -// screen lays itself out, so vga_putc's cursor tracking and scrolling are -// exactly what it does not want. -void -vga_text_at(int x, int y, const char *s, uint32 color) -{ - for (int i = 0; s[i]; i++) - vga_draw_char(x + i * CHAR_WIDTH, y, (uchar)s[i], color); -} - -// --- Logo ------------------------------------------------------------------ -// -// The boot splash's logo is drawn by the *loader*, not here -- see splash() in -// boot/bios/stage2main.c. It has to be, because the kernel does not reach vga_init() -// until roughly the last 300ms of a 2.8s boot: drawing it here put the logo on -// screen for one frame before console_takeover() wiped it, which is worse than -// not having one. -// -// What lives here is the same drawing code against the same mask, for the -// things that want the mark *after* boot -- the crash screen, and eventually -// whatever schinit shows. Keeping one copy of the compositing logic is why this -// is not simply deleted. - -// Blend `fg` over `bg` by coverage a (0..255). Per-channel, integer only: this -// runs before any floating point is safe to touch. -static uint32 -blend(uint32 bg, uint32 fg, uint a) -{ - uint32 r = (((fg >> 16) & 0xFF) * a + ((bg >> 16) & 0xFF) * (255 - a)) / 255; - uint32 g = (((fg >> 8) & 0xFF) * a + ((bg >> 8) & 0xFF) * (255 - a)) / 255; - uint32 b = (((fg ) & 0xFF) * a + ((bg ) & 0xFF) * (255 - a)) / 255; - return (r << 16) | (g << 8) | b; -} - -void -vga_draw_logo(int x, int y, uint32 color, uint32 bg) -{ - if (!vga_mem) - return; - for (int dy = 0; dy < LOGO_H; dy++) { - for (int dx = 0; dx < LOGO_W; dx++) { - uint a = LOGO_INK(dy * LOGO_W + dx); - // Skip the background rather than painting it. The mask is a square but - // the mark is a circle, so most of it is empty -- and drawing that empty - // area in bg would stamp a visible box over whatever is underneath. - if (a == 0) - continue; - vga_draw_pixel(x + dx, y + dy, a == 255 ? color : blend(bg, color, a)); - } - } -} - -// Centred, for callers that own the whole screen. -void -vga_logo_centered(int cy, uint32 color, uint32 bg) -{ - if (!vga_mem || vga_width < LOGO_W) - return; - vga_draw_logo(((int)vga_width - LOGO_W) / 2, cy - LOGO_H / 2, color, bg); -} - -// --- display class implementation ------------------------------------------ -// -// See include/display.h. This is the VBE linear framebuffer presenting itself -// as a generic display, so a compositor can be written against the class and -// keep working when virtio-gpu arrives. - -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) -{ - m->width = (int)vga_width; - m->height = (int)vga_height; - m->bpp = (int)vga_bpp; - m->stride = vga_stride; -} - -static void * -vga_disp_acquire(struct device *d, uint32 *stride_out) -{ - if (stride_out) - *stride_out = vga_stride; - if (back) - return back; - if (!vga_mem || vga_height == 0 || vga_stride == 0) - return 0; - - back_bytes = (uint64)vga_stride * vga_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) -{ - if (!back || !vga_mem) - return; - - if (x < 0) { w += x; x = 0; } - if (y < 0) { h += y; y = 0; } - if (x + w > (int)vga_width) w = (int)vga_width - x; - if (y + h > (int)vga_height) h = (int)vga_height - y; - if (w <= 0 || h <= 0) - return; - - int bytes = w * (int)(vga_bpp / 8); - uint32 off = (uint32)x * (vga_bpp / 8); - for (int row = y; row < y + h; row++) - memmove((void *)(vga_mem + row * vga_stride + off), - back + row * vga_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; - - 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, -}; diff --git a/include/xhci.h b/kernel/drivers/bus/xhci/xhci.h similarity index 100% rename from include/xhci.h rename to kernel/drivers/bus/xhci/xhci.h diff --git a/kernel/drivers/gpu/vga/vga_fb.c b/kernel/drivers/gpu/vga/vga_fb.c new file mode 100644 index 0000000..fe36660 --- /dev/null +++ b/kernel/drivers/gpu/vga/vga_fb.c @@ -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, +}; diff --git a/kernel/drivers/gpu/vga/vga_pci.c b/kernel/drivers/gpu/vga/vga_pci.c index 3596b89..c2eab97 100644 --- a/kernel/drivers/gpu/vga/vga_pci.c +++ b/kernel/drivers/gpu/vga/vga_pci.c @@ -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 diff --git a/include/virtio.h b/kernel/drivers/gpu/virtio_gpu/virtio.h similarity index 93% rename from include/virtio.h rename to kernel/drivers/gpu/virtio_gpu/virtio.h index 6719f75..5cbff3f 100644 --- a/include/virtio.h +++ b/kernel/drivers/gpu/virtio_gpu/virtio.h @@ -2,6 +2,12 @@ // 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. diff --git a/kernel/drivers/gpu/virtio_gpu/virtio_gpu.c b/kernel/drivers/gpu/virtio_gpu/virtio_gpu.c index 6727a06..728332f 100644 --- a/kernel/drivers/gpu/virtio_gpu/virtio_gpu.c +++ b/kernel/drivers/gpu/virtio_gpu/virtio_gpu.c @@ -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, }; diff --git a/kernel/drivers/registry.c b/kernel/drivers/registry.c index 90c2705..e7b0a9e 100644 --- a/kernel/drivers/registry.c +++ b/kernel/drivers/registry.c @@ -99,10 +99,17 @@ usb_bind_all(struct usb_dev *dev) // is visible in one place, which is the same reasoning as the driver arrays // above. void -platform_register_devices(void) +platform_register_devices(uint64 fb_addr) { struct device *d; + // The framebuffer first, before anything that might want to draw. It is a + // driver call, and this file is where driver names belong -- main() used to + // call vga_init() directly, which put one display driver's name in the + // kernel's startup path and would have needed editing to boot on a machine + // whose framebuffer came from anywhere else. + vga_fb_init(fb_addr); + if ((d = device_add("kbd", DEVBUS_PLATFORM, DEVCLASS_INPUT, "ps2kbd"))) device_set_desc(d, "PS/2 keyboard"); if ((d = device_add("mouse", DEVBUS_PLATFORM, DEVCLASS_INPUT, "ps2mouse"))) diff --git a/kernel/drivers/usb/hid/usb_hid.c b/kernel/drivers/usb/hid/usb_hid.c index 9d42b14..8e542d4 100644 --- a/kernel/drivers/usb/hid/usb_hid.c +++ b/kernel/drivers/usb/hid/usb_hid.c @@ -14,6 +14,7 @@ #include "types.h" #include "defs.h" +#include "gfx.h" #include "param.h" #include "spinlock.h" #include "dma.h" @@ -219,8 +220,8 @@ tablet_report(struct hid *h, const uchar *rep, int len) int rawx = rep[1] | (rep[2] << 8); int rawy = rep[3] | (rep[4] << 8); - int w = (int)vga_get_width(); - int h_px = (int)vga_get_height(); + int w = (int)gfx_width(); + int h_px = (int)gfx_height(); if(w <= 0) w = 800; if(h_px <= 0) h_px = 600; diff --git a/kernel/sys/sys_mem.c b/kernel/sys/sys_mem.c index a81f7da..06e13a9 100644 --- a/kernel/sys/sys_mem.c +++ b/kernel/sys/sys_mem.c @@ -1,5 +1,6 @@ #include "types.h" #include "defs.h" +#include "gfx.h" #include "param.h" #include "stat.h" #include "memlayout.h" @@ -159,8 +160,8 @@ sys_mmap(void) // than the screen holds handed out user-writable PTEs to whatever // MMIO sits above the framebuffer, and any machine that put the LFB // somewhere else got a window onto the wrong device entirely. - uint64 fb = vga_fb_phys(); - uint64 fbsz = PGROUNDUP(vga_fb_size()); + uint64 fb = gfx_fb_phys(); + uint64 fbsz = PGROUNDUP(gfx_fb_size()); if (fb == 0 || fbsz == 0) return -ENODEV; if (length > fbsz)