docs/plan/hybrid.md's first eviction. cmd/vgpud is the virtio-gpu driver as a
process: it finds the device by vendor and device id, asks the kernel for it,
and does the whole virtio 1.0 bring-up from outside -- reset, feature
negotiation, a virtqueue in DMA memory, GET_DISPLAY_INFO, RESOURCE_CREATE_2D,
ATTACH_BACKING, SET_SCANOUT, TRANSFER and FLUSH.
vgpud: virtio-gpu at 00:03.0, currently held by virtio-gpu
virtio-gpu: released to userspace; screen is the VBE framebuffer
vgpud: claimed
vgpud: display is 1280x800
vgpud: scanout is ours, framebuffer at physical 0x3eceb000
vgpud: drew a test pattern from userspace
Verified by screendumping the virtio-gpu head: the pattern is there, with a
checkerboard that would shear if the stride were wrong. The console carries on
in VBE on the other head at the same time, which is the fallback rule from the
plan working rather than being described.
No syscall knows this is a GPU. The kernel handed over a PCI device and the
process decided what it was -- which is the property that makes this a general
mechanism, and the reason the evictions come before anything foreign.
Four things the first real driver needed that the test program had not:
- PCI config space (PCIIOC_CFG). A virtio device says where its registers
are in vendor capabilities, which live in config space, so a driver that
cannot read it cannot find its own registers.
- a way for the incumbent to let go: struct pci_driver gained a release
hook. A driver without one keeps its device, which is always safe and for
anything on the path to a shell is also correct.
- the outgoing driver freeing its DMA. Releasing the device but keeping its
4 MB framebuffer is half a handover -- the incoming driver asks for one and
is told there is no room.
- a 16 MB DMA arena. 4 MB was exactly one framebuffer at this resolution,
and either side of a handover there are two.
And one bug it reproduced on its first working run: vgpud polls the used ring,
so it registered no interrupt handler, and a virtio device interrupts on every
completion. The storm detector said so immediately --
irq 11: 77780 interrupts/second -- a device on this line is not
acknowledging it. handler 'e1000' is attached
-- naming the line, the rate and the neighbour it was drowning. One line fixes
it (VRING_AVAIL_F_NO_INTERRUPT before the queue is enabled), and that detector
was worth every minute it cost.
include/virtio.h moved out of the driver directory. Its own comment said it
should when a second reader appeared; one has.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
54 lines
2.6 KiB
C
54 lines
2.6 KiB
C
#pragma once
|
|
|
|
// Physically contiguous memory for devices that read and write it themselves.
|
|
//
|
|
// kalloc() hands out 4 KB pages from a free list, in whatever order they were
|
|
// released. That is exactly what a page table wants and exactly what a device
|
|
// cannot use: an xHCI command ring, a NIC's descriptor array or a virtio
|
|
// queue is a single physically contiguous object that the hardware walks
|
|
// without any idea that paging exists. Two kalloc'd pages are adjacent in the
|
|
// kernel's address space only by luck.
|
|
//
|
|
// So DMA memory comes from an arena carved out of the top of physical memory
|
|
// before kalloc ever sees it, and is handed out contiguous and aligned.
|
|
//
|
|
// On x86 this memory needs no special cacheability. The PCI bus is cache
|
|
// coherent: a device's write invalidates the corresponding cache line, and a
|
|
// CPU write is snooped out of the store buffer before the device sees stale
|
|
// data. Architectures without that guarantee need explicit cache maintenance
|
|
// around every descriptor access, and porting this file is where that would
|
|
// go. What is *not* free even here is ordering -- the compiler and the CPU
|
|
// will happily reorder stores to a ring, so a driver still needs a barrier
|
|
// between filling a descriptor and telling the device about it.
|
|
|
|
// 16 MB, not 4.
|
|
//
|
|
// 4 MB was one framebuffer at 1280x800, and that was fine while exactly one
|
|
// driver wanted one. It stopped being fine the day a display driver could be
|
|
// handed to userspace (docs/plan/hybrid.md): for the moment either side of the
|
|
// handover both drivers exist, and the incoming one has to allocate its
|
|
// framebuffer before the outgoing one is gone.
|
|
//
|
|
// It is carved off the top of physical memory at boot and never grows, so the
|
|
// cost of being wrong in this direction is 12 MB and the cost of being wrong
|
|
// in the other is a driver that cannot start.
|
|
#define DMA_ARENA_BYTES (16*1024*1024)
|
|
|
|
void dma_init(void *vstart, void *vend);
|
|
|
|
// Zeroed, physically contiguous, at least `align`-aligned (align must be a
|
|
// power of two). Returns a kernel virtual address, or 0.
|
|
void *dma_alloc(uint64 size, uint64 align);
|
|
|
|
// `size` must be the size the allocation was made with, as in Linux's
|
|
// dma_free_coherent(). Tracking it here would mean a second table whose only
|
|
// job is to remember something the caller already knows.
|
|
void dma_free(void *p, uint64 size);
|
|
|
|
// The address to give the device. Kernel memory is direct-mapped, so this is
|
|
// just V2P -- but drivers should say what they mean, because on a machine
|
|
// with an IOMMU these stop being the same number.
|
|
addr_t dma_phys(const void *p);
|
|
|
|
uint64 dma_free_bytes(void);
|
|
uint64 dma_total_bytes(void);
|