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 <noreply@anthropic.com>
This commit is contained in:
Nexoniarz 2026-08-15 17:04:10 +02:00
commit 817d666068
22 changed files with 942 additions and 752 deletions

View file

@ -0,0 +1,161 @@
#pragma once
// eXtensible Host Controller Interface -- the register and data-structure
// definitions from the xHCI 1.2 specification.
//
// Why xHCI and not UHCI/OHCI/EHCI: xHCI is the only host controller interface
// that speaks every USB speed, so one driver covers USB 1.1 through 3.x. The
// older controllers would each need their own, and a machine with all of them
// needs the companion-controller handover dance on top. There is no hardware
// worth supporting that has no xHCI.
// ---------------------------------------------------------------------------
// Capability registers -- at the start of BAR0, read-only
// ---------------------------------------------------------------------------
#define XHCI_CAPLENGTH 0x00 // byte: where the operational regs begin
#define XHCI_HCIVERSION 0x02 // word
#define XHCI_HCSPARAMS1 0x04
#define XHCI_HCSPARAMS2 0x08
#define XHCI_HCSPARAMS3 0x0C
#define XHCI_HCCPARAMS1 0x10
#define XHCI_DBOFF 0x14 // doorbell array offset from BAR0
#define XHCI_RTSOFF 0x18 // runtime register offset from BAR0
#define HCS1_MAXSLOTS(x) ((x) & 0xFF)
#define HCS1_MAXINTRS(x) (((x) >> 8) & 0x7FF)
#define HCS1_MAXPORTS(x) (((x) >> 24) & 0xFF)
// Scratchpad buffer count is split across two fields, high part first.
#define HCS2_SPB_HI(x) (((x) >> 21) & 0x1F)
#define HCS2_SPB_LO(x) (((x) >> 27) & 0x1F)
#define HCC1_AC64 (1 << 0) // 64-bit addressing
#define HCC1_CSZ (1 << 2) // contexts are 64 bytes, not 32
#define HCC1_XECP(x) (((x) >> 16) & 0xFFFF) // extended caps, in dwords
// ---------------------------------------------------------------------------
// Operational registers -- at BAR0 + CAPLENGTH
// ---------------------------------------------------------------------------
#define XHCI_USBCMD 0x00
#define XHCI_USBSTS 0x04
#define XHCI_PAGESIZE 0x08
#define XHCI_DNCTRL 0x14
#define XHCI_CRCR 0x18 // 64-bit
#define XHCI_DCBAAP 0x30 // 64-bit
#define XHCI_CONFIG 0x38
#define XHCI_PORTSC(p) (0x400 + 0x10 * ((p) - 1)) // ports are 1-based
#define CMD_RS (1 << 0) // run/stop
#define CMD_HCRST (1 << 1) // host controller reset
#define CMD_INTE (1 << 2) // interrupter enable
#define CMD_HSEE (1 << 3)
#define STS_HCH (1 << 0) // halted
#define STS_HSE (1 << 2) // host system error
#define STS_EINT (1 << 3) // event interrupt (write 1 to clear)
#define STS_PCD (1 << 4) // port change detect (write 1 to clear)
#define STS_CNR (1 << 11) // controller not ready
#define CRCR_RCS (1 << 0) // ring cycle state
// PORTSC. The change bits are write-1-to-clear, and so is PED -- writing back
// a value read from this register clears whatever happened to be set. Every
// write has to mask them off first.
#define PORT_CCS (1 << 0) // current connect status
#define PORT_PED (1 << 1) // port enabled (RW1C!)
#define PORT_OCA (1 << 3)
#define PORT_PR (1 << 4) // port reset
#define PORT_PLS(x) (((x) >> 5) & 0xF)
#define PORT_PP (1 << 9) // port power
#define PORT_SPEED(x) (((x) >> 10) & 0xF)
#define PORT_CSC (1 << 17)
#define PORT_PEC (1 << 18)
#define PORT_PRC (1 << 21)
#define PORT_RW1C 0x00FE0002u // change bits 17..23, plus PED
// PORTSC speed encoding. These are the same numbers as USB_SPEED_* in usb.h,
// which is not a coincidence -- the core uses this encoding directly so that
// nothing has to be translated on its way to a slot context.
#define XSPEED_FULL USB_SPEED_FULL
#define XSPEED_LOW USB_SPEED_LOW
#define XSPEED_HIGH USB_SPEED_HIGH
#define XSPEED_SUPER USB_SPEED_SUPER
// ---------------------------------------------------------------------------
// Runtime registers -- at BAR0 + RTSOFF. Interrupter n is at 0x20 + 32*n.
// ---------------------------------------------------------------------------
#define XHCI_MFINDEX 0x00
#define XHCI_IR0 0x20
#define IR_IMAN 0x00
#define IR_IMOD 0x04
#define IR_ERSTSZ 0x08
#define IR_ERSTBA 0x10 // 64-bit
#define IR_ERDP 0x18 // 64-bit
#define IMAN_IP (1 << 0) // interrupt pending (RW1C)
#define IMAN_IE (1 << 1) // interrupt enable
#define ERDP_EHB (1 << 3) // event handler busy (RW1C)
// ---------------------------------------------------------------------------
// Transfer Request Blocks
// ---------------------------------------------------------------------------
struct trb {
uint64 param;
uint32 status;
uint32 control;
} __attribute__((packed));
#define TRB_TYPE(t) (((t) & 0x3F) << 10)
#define TRB_GET_TYPE(c) (((c) >> 10) & 0x3F)
#define TRB_C (1 << 0) // cycle bit
#define TRB_TC (1 << 1) // toggle cycle (link TRBs)
#define TRB_ISP (1 << 2)
#define TRB_CH (1 << 4) // chain
#define TRB_IOC (1 << 5) // interrupt on completion
#define TRB_IDT (1 << 6) // immediate data
#define TRB_SLOT(s) ((uint32)(s) << 24)
#define TRB_GET_SLOT(c) (((c) >> 24) & 0xFF)
#define TRB_GET_EPID(c) (((c) >> 16) & 0x1F) // transfer events
// Transfer TRBs
#define TRB_NORMAL 1
#define TRB_SETUP 2
#define TRB_DATA 3
#define TRB_STATUS 4
#define TRB_LINK 6
// Command TRBs
#define TRB_ENABLE_SLOT 9
#define TRB_DISABLE_SLOT 10
#define TRB_ADDRESS_DEV 11
#define TRB_CONFIG_EP 12
#define TRB_EVAL_CTX 13
#define TRB_RESET_EP 14
#define TRB_NOOP_CMD 23
// Event TRBs
#define TRB_XFER_EVENT 32
#define TRB_CMD_COMPLETE 33
#define TRB_PORT_STATUS 34
#define TRB_CC(status) (((status) >> 24) & 0xFF)
#define CC_SUCCESS 1
#define CC_SHORT_PACKET 13
// Setup Stage transfer type, and Data Stage direction.
#define TRT_NONE 0
#define TRT_OUT 2
#define TRT_IN 3
#define DIR_IN (1 << 16)
// Endpoint types in an endpoint context. Direction is part of the type, and
// the numbering is not arbitrary: OUT types are 1-3, control is 4, and the IN
// types are the OUT ones plus four.
#define EPTYPE_ISOCH_OUT 1
#define EPTYPE_BULK_OUT 2
#define EPTYPE_INT_OUT 3
#define EPTYPE_CONTROL 4
#define EPTYPE_ISOCH_IN 5
#define EPTYPE_BULK_IN 6
#define EPTYPE_INT_IN 7
// Doorbell targets: 0 is the command ring; for a device slot, DCI 1 is EP0.
#define DCI_EP0 1

View file

@ -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,
};

View file

@ -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

View file

@ -0,0 +1,180 @@
#pragma once
// 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.
//
// Why virtio-gpu at all, when there is already a framebuffer: the VBE
// framebuffer is a flat block of memory the loader happened to be told about.
// It has no mode setting, no notion of a frame being complete, and no way to
// ask the host to present anything -- so a compositor drawing into it is
// racing the scanout with no way to synchronise. virtio-gpu has resources,
// explicit transfer and an explicit flush, which is the model every real GPU
// driver has. It is the smallest device that teaches the right shape.
// --- PCI transport ---------------------------------------------------------
#define VIRTIO_VENDOR 0x1AF4
// Modern (1.0) device IDs are 0x1040 + the virtio device type. GPU is type 16.
#define VIRTIO_DEV_GPU 0x1050
// Vendor-specific PCI capability, which is how a virtio device says where in
// its BARs each configuration structure lives.
#define PCI_CAP_ID_VNDR 0x09
#define VIRTIO_PCI_CAP_COMMON_CFG 1
#define VIRTIO_PCI_CAP_NOTIFY_CFG 2
#define VIRTIO_PCI_CAP_ISR_CFG 3
#define VIRTIO_PCI_CAP_DEVICE_CFG 4
// Offsets within the common configuration structure. Written out rather than
// declared as a struct: the fields are little-endian by specification and of
// mixed width, and a struct invites the compiler to pad or a caller to forget
// that these are MMIO and must be accessed at exactly the stated width.
#define VCOMMON_DFEAT_SEL 0x00 // le32
#define VCOMMON_DFEAT 0x04 // le32
#define VCOMMON_GFEAT_SEL 0x08 // le32
#define VCOMMON_GFEAT 0x0C // le32
#define VCOMMON_MSIX_CFG 0x10 // le16
#define VCOMMON_NUM_QUEUES 0x12 // le16
#define VCOMMON_STATUS 0x14 // u8
#define VCOMMON_CFG_GEN 0x15 // u8
#define VCOMMON_Q_SELECT 0x16 // le16
#define VCOMMON_Q_SIZE 0x18 // le16
#define VCOMMON_Q_MSIX 0x1A // le16
#define VCOMMON_Q_ENABLE 0x1C // le16
#define VCOMMON_Q_NOTIFY_OFF 0x1E // le16
#define VCOMMON_Q_DESC 0x20 // le64
#define VCOMMON_Q_DRIVER 0x28 // le64
#define VCOMMON_Q_DEVICE 0x30 // le64
// device_status bits, set in this order during bring-up.
#define VSTATUS_ACK 1
#define VSTATUS_DRIVER 2
#define VSTATUS_DRIVER_OK 4
#define VSTATUS_FEATURES_OK 8
#define VSTATUS_FAILED 0x80
#define VIRTIO_F_VERSION_1 32 // feature bit: this is a 1.0 device
// --- split virtqueue -------------------------------------------------------
#define VRING_DESC_F_NEXT 1
#define VRING_DESC_F_WRITE 2 // device writes it; i.e. this is our buffer
struct vring_desc {
uint64 addr;
uint32 len;
uint16 flags;
uint16 next;
} __attribute__((packed));
struct vring_avail {
uint16 flags;
uint16 idx;
uint16 ring[];
} __attribute__((packed));
struct vring_used_elem {
uint32 id;
uint32 len;
} __attribute__((packed));
struct vring_used {
uint16 flags;
uint16 idx;
struct vring_used_elem ring[];
} __attribute__((packed));
// --- virtio-gpu, 2D --------------------------------------------------------
#define VIRTIO_GPU_CMD_GET_DISPLAY_INFO 0x0100
#define VIRTIO_GPU_CMD_RESOURCE_CREATE_2D 0x0101
#define VIRTIO_GPU_CMD_RESOURCE_UNREF 0x0102
#define VIRTIO_GPU_CMD_SET_SCANOUT 0x0103
#define VIRTIO_GPU_CMD_RESOURCE_FLUSH 0x0104
#define VIRTIO_GPU_CMD_TRANSFER_TO_HOST_2D 0x0105
#define VIRTIO_GPU_CMD_RESOURCE_ATTACH_BACKING 0x0106
#define VIRTIO_GPU_RESP_OK_NODATA 0x1100
#define VIRTIO_GPU_RESP_OK_DISPLAY_INFO 0x1101
#define VIRTIO_GPU_MAX_SCANOUTS 16
// B8G8R8X8, which is what a 32-bit little-endian 0x00RRGGBB word looks like in
// memory -- the same layout vga.c already draws into, so a framebuffer can be
// handed over without a conversion pass.
#define VIRTIO_GPU_FORMAT_B8G8R8X8_UNORM 2
struct virtio_gpu_ctrl_hdr {
uint32 type;
uint32 flags;
uint64 fence_id;
uint32 ctx_id;
uint32 padding;
} __attribute__((packed));
struct virtio_gpu_rect {
uint32 x, y, width, height;
} __attribute__((packed));
struct virtio_gpu_display_one {
struct virtio_gpu_rect r;
uint32 enabled;
uint32 flags;
} __attribute__((packed));
struct virtio_gpu_resp_display_info {
struct virtio_gpu_ctrl_hdr hdr;
struct virtio_gpu_display_one pmodes[VIRTIO_GPU_MAX_SCANOUTS];
} __attribute__((packed));
struct virtio_gpu_resource_create_2d {
struct virtio_gpu_ctrl_hdr hdr;
uint32 resource_id;
uint32 format;
uint32 width;
uint32 height;
} __attribute__((packed));
struct virtio_gpu_mem_entry {
uint64 addr;
uint32 length;
uint32 padding;
} __attribute__((packed));
struct virtio_gpu_resource_attach_backing {
struct virtio_gpu_ctrl_hdr hdr;
uint32 resource_id;
uint32 nr_entries;
} __attribute__((packed));
struct virtio_gpu_set_scanout {
struct virtio_gpu_ctrl_hdr hdr;
struct virtio_gpu_rect r;
uint32 scanout_id;
uint32 resource_id;
} __attribute__((packed));
struct virtio_gpu_transfer_to_host_2d {
struct virtio_gpu_ctrl_hdr hdr;
struct virtio_gpu_rect r;
uint64 offset;
uint32 resource_id;
uint32 padding;
} __attribute__((packed));
struct virtio_gpu_resource_flush {
struct virtio_gpu_ctrl_hdr hdr;
struct virtio_gpu_rect r;
uint32 resource_id;
uint32 padding;
} __attribute__((packed));

View file

@ -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,
};

View file

@ -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")))

View file

@ -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;