feat: devices via caps (BootInfo, init assigns devices, drivers map MMIO/DMA themselves, IRQ-driven input); drop boot device wiring and info pages; headless display fallback

This commit is contained in:
2026-10-27 12:28:39 +01:00
parent 8bc5402a29
commit 694c675a40
39 changed files with 731 additions and 387 deletions

View File

@@ -15,9 +15,18 @@ const VIRTIO_IRQ_BASE: u16 = 1;
const VIRTIO_SLOT_SIZE: usize = 0x1000;
const VIRTIO_SLOTS: usize = VIRTIO_SIZE / VIRTIO_SLOT_SIZE;
pub const BOARD_DEVICES: usize = 1 + VIRTIO_SLOTS;
/// What each entry of `devices()` is, for init's BootInfo.
pub fn kinds() -> [u64; BOARD_DEVICES] {
let mut kinds = [types::bootinfo::KIND_VIRTIO; BOARD_DEVICES];
kinds[0] = types::bootinfo::KIND_UART_16550;
kinds
}
/// Every device userspace may drive: the UART and each VirtIO MMIO slot.
/// The PLIC, CLINT and RAM stay the kernel's.
pub fn devices() -> Option<[kernel::BoardDevice; 1 + VIRTIO_SLOTS]> {
pub fn devices() -> Option<[kernel::BoardDevice; BOARD_DEVICES]> {
use types::{address::PhysicalAddress, bounded::PageCount, id::IrqId};
let one = PageCount::new(1)?;
let device = |base: usize, irq: u16| kernel::BoardDevice {
@@ -25,7 +34,7 @@ pub fn devices() -> Option<[kernel::BoardDevice; 1 + VIRTIO_SLOTS]> {
pages: one,
irq: Some(IrqId::from_raw(irq)),
};
let mut all = [device(UART_BASE, UART_IRQ); 1 + VIRTIO_SLOTS];
let mut all = [device(UART_BASE, UART_IRQ); BOARD_DEVICES];
for (i, d) in all.iter_mut().skip(1).enumerate() {
*d = device(
VIRTIO_BASE + i * VIRTIO_SLOT_SIZE,

View File

@@ -1,81 +0,0 @@
use super::{dma_buffer, info_page, kern, map_mmio};
use crate::hw::uart::Uart;
use crate::hw::virtio_gpu::VirtioGpu;
use types::display::Dimension;
use types::id::ProcessId;
const GPU_VA: usize = 0x20000;
const VQUEUE_VA: usize = 0x21000;
const CMDBUF_VA: usize = 0x23000;
const PHYSINFO_VA: usize = 0x24000;
const FB_VA: usize = 0x100000;
const DEFAULT_WIDTH: Dimension = Dimension::new(640);
const DEFAULT_HEIGHT: Dimension = Dimension::new(480);
/// The framebuffer alone is ~1000 pages at 1280x800.
const DISPLAY_PAGE_LIMIT: usize = 2048;
pub fn configure(uart: &Uart, display_pid: ProcessId) {
let gpu = match VirtioGpu::probe() {
Some(g) => g,
None => {
uart.write_str("k-os: no GPU (headless)\n");
return;
}
};
kern()
.set_page_limit(display_pid, DISPLAY_PAGE_LIMIT)
.unwrap_or_else(|_| crate::halt(uart, "display quota"));
map_mmio(uart, display_pid, GPU_VA, gpu.base(), 1);
let vq_phys = dma_buffer(uart, display_pid, VQUEUE_VA, 2);
let cmd_phys = dma_buffer(uart, display_pid, CMDBUF_VA, 1);
gpu.init_device(vq_phys);
let queue = hw_virtio::queue::VirtioQueue::new(hw_virtio::queue::QueueAddrs::from_base(
crate::mmu::phys_to_virt(vq_phys),
));
let dma = hw_virtio::gpu::GpuDmaAddrs {
cmd_pa: types::address::PhysicalAddress::from_raw(cmd_phys),
cmd_va: types::address::VirtualAddress::from_raw(crate::mmu::phys_to_virt(cmd_phys)),
resp_pa: types::address::PhysicalAddress::from_raw(cmd_phys + 512),
};
let (width, height) = match hw_virtio::gpu::get_display_info(gpu.mmio(), &queue, &dma) {
Some((w, h)) => {
uart.write_str("k-os: GPU display: ");
uart.write_dec(w as usize);
uart.put_char(b'x');
uart.write_dec(h as usize);
uart.put_char(b'\n');
(Dimension::new(w as u16), Dimension::new(h as u16))
}
None => {
uart.write_str("k-os: GPU display: using default 640x480\n");
(DEFAULT_WIDTH, DEFAULT_HEIGHT)
}
};
let fb_pages = (width.raw() as usize * height.raw() as usize * 4).div_ceil(4096);
let fb_phys = dma_buffer(uart, display_pid, FB_VA, fb_pages);
info_page(
uart,
display_pid,
PHYSINFO_VA,
&[
cmd_phys as u64,
(cmd_phys + 512) as u64,
fb_phys as u64,
width.raw() as u64,
height.raw() as u64,
],
);
if gpu.setup_framebuffer(cmd_phys, vq_phys, fb_phys, width, height) {
uart.write_str("k-os: VirtIO GPU configured\n");
} else {
uart.write_str("k-os: VirtIO GPU setup failed (display will be headless)\n");
}
}

View File

@@ -1,35 +0,0 @@
use super::{dma_buffer, info_page, map_mmio};
use crate::board;
use crate::hw::uart::Uart;
use types::id::ProcessId;
const UART_VA: usize = 0x20000;
const INPUT_MMIO_VA: usize = 0x25000;
const INPUT_VQUEUE_VA: usize = 0x26000;
const INPUT_EVBUF_VA: usize = 0x28000;
const INPUT_PHYSINFO_VA: usize = 0x29000;
pub fn configure(uart: &Uart, input_pid: ProcessId) {
// Always map UART for fallback
map_mmio(uart, input_pid, UART_VA, board::UART_BASE, 1);
// Try to find VirtIO input device
let first = crate::mmu::phys_to_virt(hw_virtio::mmio::MMIO_FIRST);
if let Some(mmio) = hw_virtio::mmio::scan_for_device(first, hw_virtio::mmio::DEVICE_ID_INPUT) {
let base = crate::mmu::virt_to_phys(mmio.base());
map_mmio(uart, input_pid, INPUT_MMIO_VA, base, 1);
let vq_phys = dma_buffer(uart, input_pid, INPUT_VQUEUE_VA, 2);
let evbuf_phys = dma_buffer(uart, input_pid, INPUT_EVBUF_VA, 1);
info_page(
uart,
input_pid,
INPUT_PHYSINFO_VA,
&[evbuf_phys as u64, vq_phys as u64],
);
hw_virtio::mmio::init_legacy_device(&mmio, vq_phys, 32);
uart.write_str("k-os: VirtIO keyboard configured\n");
} else {
uart.write_str("k-os: UART mapped for input\n");
}
}

View File

@@ -1,18 +1,17 @@
pub mod gpu_setup;
pub mod input_setup;
pub mod storage_setup;
// What boot still sets up for userspace, all through the kernel's mapping
// API: a buffer init and storage share, the display's page quota, and the
// BootInfo page that tells init where its device capabilities are. Drivers
// set up their own devices with the capabilities init hands them.
use crate::hw::uart::Uart;
use types::address::{PhysicalAddress, VirtualAddress};
use types::bounded::PageCount;
use types::address::VirtualAddress;
use types::bootinfo::{BOOT_INFO_VA, BootDevice, BootInfo, MAGIC, MAX_BOOT_DEVICES, NO_SLOT};
use types::bounded::{CapSlot, PageCount};
use types::id::ProcessId;
use types::mapping::PagePerms;
// Boot-time device wiring. Everything here goes through the kernel's
// mapping API so the kernel knows about every page a driver can touch.
// Device regions come from the board table and belong to init, which
// also holds capabilities to them; moving drivers onto those caps is
// userspace work.
/// The framebuffer alone is ~1000 pages at 1280x800.
const DISPLAY_PAGE_LIMIT: usize = 2048;
fn kern() -> &'static mut kernel::Kernel<crate::platform::RiscVPlatform> {
unsafe { &mut *crate::kernel_ptr() }
@@ -25,41 +24,6 @@ fn pages(uart: &Uart, n: usize) -> PageCount {
.unwrap_or_else(|| crate::halt(uart, "bad page count"))
}
/// Maps the board device region at `pa` into `pid` at `va`. The region
/// belongs to init (see `kernel::Kernel::register_board`); `n` must match
/// its size.
pub(crate) fn map_mmio(uart: &Uart, pid: ProcessId, va: usize, pa: usize, n: usize) {
let id = kern()
.device_region_at(PhysicalAddress::from_raw(pa))
.unwrap_or_else(|| crate::halt(uart, "no such board device"));
if kern().region_pages(id).map(|p| p.value() as usize) != Some(n) {
crate::halt(uart, "device size mismatch");
}
kern()
.map_region(id, pid, VirtualAddress::from_raw(va), PagePerms::ReadWrite)
.unwrap_or_else(|_| crate::halt(uart, "device map failed"));
}
/// Physically contiguous, zeroed DMA memory for `owner`, mapped read-write
/// at `va`. Returns its physical base for programming the device.
pub(crate) fn dma_buffer(uart: &Uart, owner: ProcessId, va: usize, n: usize) -> usize {
let id = kern()
.create_pool_region(owner, pages(uart, n))
.unwrap_or_else(|_| crate::halt(uart, "DMA region failed"));
kern()
.map_region(
id,
owner,
VirtualAddress::from_raw(va),
PagePerms::ReadWrite,
)
.unwrap_or_else(|_| crate::halt(uart, "DMA map failed"));
kern()
.region_base(id)
.unwrap_or_else(|| crate::halt(uart, "DMA region vanished"))
.raw()
}
/// Shares a fresh region between `owner` and `other` at the same `va`.
pub(crate) fn shared_buffer(uart: &Uart, owner: ProcessId, other: ProcessId, va: usize, n: usize) {
let id = kern()
@@ -72,20 +36,77 @@ pub(crate) fn shared_buffer(uart: &Uart, owner: ProcessId, other: ProcessId, va:
}
}
/// Writes `words` into a fresh page mapped at `va` in `pid`, so a driver
/// can learn the physical addresses of its DMA buffers.
pub(crate) fn info_page(uart: &Uart, pid: ProcessId, va: usize, words: &[u64]) {
/// Lets the display server hold a framebuffer.
pub(crate) fn display_quota(uart: &Uart, display: ProcessId) {
kern()
.set_page_limit(display, DISPLAY_PAGE_LIMIT)
.unwrap_or_else(|_| crate::halt(uart, "display quota"));
}
/// Tells init, in a read-only page at BOOT_INFO_VA, which of its slots hold
/// each board device's capabilities.
pub(crate) fn boot_info(
uart: &Uart,
init: ProcessId,
board: &[kernel::BoardDevice],
kinds: &[u64],
placed: &[Option<kernel::BoardCaps>],
system_control: CapSlot,
) {
let empty = BootDevice {
kind: 0,
phys_base: 0,
pages: 0,
irq: 0,
region_slot: NO_SLOT,
irq_slot: NO_SLOT,
};
let mut info = BootInfo {
magic: MAGIC,
device_count: 0,
system_control_slot: system_control.value() as u64,
devices: [empty; MAX_BOOT_DEVICES],
};
for (((d, kind), caps), out) in board
.iter()
.zip(kinds)
.zip(placed)
.zip(info.devices.iter_mut())
{
let Some(caps) = caps else { continue };
*out = BootDevice {
kind: *kind,
phys_base: d.base.raw() as u64,
pages: d.pages.value() as u64,
irq: d.irq.map_or(0, |i| i.raw() as u64),
region_slot: caps.region.value() as u64,
irq_slot: caps.irq.map_or(NO_SLOT, |s| s.value() as u64),
};
info.device_count += 1;
}
// SAFETY: BootInfo is repr(C) and made only of u64s.
let words = unsafe {
core::slice::from_raw_parts(
&info as *const BootInfo as *const u64,
core::mem::size_of::<BootInfo>() / 8,
)
};
write_page(uart, init, BOOT_INFO_VA, words);
}
/// Writes `words` into a fresh read-only page mapped at `va` in `pid`.
fn write_page(uart: &Uart, pid: ProcessId, va: usize, words: &[u64]) {
let va = VirtualAddress::from_raw(va);
kern()
.map_fresh_range(pid, va, 1, PagePerms::ReadOnly)
.unwrap_or_else(|_| crate::halt(uart, "info page failed"));
.unwrap_or_else(|_| crate::halt(uart, "boot info page failed"));
let pa = kern()
.user_page_frame(pid, va)
.unwrap_or_else(|| crate::halt(uart, "info page vanished"));
let info = crate::mmu::phys_to_virt(pa.raw()) as *mut u64;
for (i, &w) in words.iter().enumerate() {
.unwrap_or_else(|| crate::halt(uart, "boot info page vanished"));
let page = crate::mmu::phys_to_virt(pa.raw()) as *mut u64;
for (i, &w) in words.iter().take(crate::mmu::PAGE_SIZE / 8).enumerate() {
// SAFETY: `pa` is a pool frame just mapped for `pid`, reachable at
// `phys_to_virt`, and `words` fits in one page.
unsafe { core::ptr::write_volatile(info.add(i), w) };
// `phys_to_virt`, and at most one page is written.
unsafe { core::ptr::write_volatile(page.add(i), w) };
}
}

View File

@@ -1,36 +0,0 @@
use super::{dma_buffer, info_page, map_mmio, shared_buffer};
use crate::hw::uart::Uart;
use types::id::ProcessId;
const VIRTIO_VA: usize = 0x20000;
const VQUEUE_VA: usize = 0x21000;
const REQHDR_VA: usize = 0x23000;
const PHYSINFO_VA: usize = 0x24000;
const SHARED_BUF_VA: usize = 0x30000;
const SHARED_BUF_PAGES: usize = 16;
pub fn configure(uart: &Uart, init_pid: ProcessId, storage_pid: ProcessId, virtio_base: usize) {
map_mmio(uart, storage_pid, VIRTIO_VA, virtio_base, 1);
let vqueue_phys = dma_buffer(uart, storage_pid, VQUEUE_VA, 2);
let reqhdr_phys = dma_buffer(uart, storage_pid, REQHDR_VA, 1);
info_page(
uart,
storage_pid,
PHYSINFO_VA,
&[
reqhdr_phys as u64,
(reqhdr_phys + 16) as u64,
(reqhdr_phys + 32) as u64,
],
);
reinit_device(virtio_base, vqueue_phys);
uart.write_str("k-os: VirtIO configured for storage\n");
shared_buffer(uart, init_pid, storage_pid, SHARED_BUF_VA, SHARED_BUF_PAGES);
uart.write_str("k-os: shared buffer mapped\n");
}
fn reinit_device(base: usize, vqueue_phys: usize) {
let mmio = hw_virtio::mmio::DirectMmio::new(crate::mmu::phys_to_virt(base));
hw_virtio::mmio::init_legacy_device(&mmio, vqueue_phys, hw_virtio::queue::QUEUE_SIZE as u32);
}

View File

@@ -2,4 +2,3 @@ pub mod plic;
pub mod timer;
pub mod uart;
pub mod virtio_blk;
pub mod virtio_gpu;

View File

@@ -57,11 +57,6 @@ impl VirtioBlk {
Some(Self { mmio, queue, dma })
}
/// Physical address of the device's MMIO window.
pub fn base(&self) -> usize {
virt_to_phys(self.mmio.base())
}
pub fn read_sector(&mut self, sector: u32, buf: &mut [u8; SECTOR_SIZE]) -> bool {
block::read_sector(&self.mmio, &self.queue, &self.dma, sector, buf)
}

View File

@@ -1,57 +0,0 @@
use crate::mmu::{phys_to_virt, virt_to_phys};
use hw_virtio::gpu::{self, GpuDmaAddrs};
use hw_virtio::mmio::{self, DirectMmio};
use hw_virtio::queue::{QUEUE_SIZE, QueueAddrs, VirtioQueue};
use types::address::{PhysicalAddress, VirtualAddress};
use types::display::Dimension;
pub struct VirtioGpu {
mmio: DirectMmio,
}
impl VirtioGpu {
pub fn probe() -> Option<Self> {
let mmio = mmio::scan_for_device(phys_to_virt(mmio::MMIO_FIRST), mmio::DEVICE_ID_GPU)?;
Some(Self { mmio })
}
/// Physical address of the device's MMIO window.
pub fn base(&self) -> usize {
virt_to_phys(self.mmio.base())
}
pub fn mmio(&self) -> &DirectMmio {
&self.mmio
}
pub fn init_device(&self, vqueue_phys: usize) {
mmio::init_legacy_device(&self.mmio, vqueue_phys, QUEUE_SIZE as u32);
}
pub fn setup_framebuffer(
&self,
cmd_phys: usize,
vq_phys: usize,
fb_phys: usize,
width: Dimension,
height: Dimension,
) -> bool {
let queue = VirtioQueue::new(QueueAddrs::from_base(phys_to_virt(vq_phys)));
let dma = GpuDmaAddrs {
cmd_pa: PhysicalAddress::from_raw(cmd_phys),
cmd_va: VirtualAddress::from_raw(phys_to_virt(cmd_phys)),
resp_pa: PhysicalAddress::from_raw(cmd_phys + 512),
};
let ok1 = gpu::create_2d(&self.mmio, &queue, &dma, 1, width, height);
let ok2 = gpu::attach_backing(
&self.mmio,
&queue,
&dma,
1,
PhysicalAddress::from_raw(fb_phys),
width.raw() as u32 * height.raw() as u32 * 4,
);
let ok3 = gpu::set_scanout(&self.mmio, &queue, &dma, 0, 1, width, height);
ok1 && ok2 && ok3
}
}

View File

@@ -107,6 +107,7 @@ boot_page_table:
);
const SHARED_BUF_VA: usize = 0x30000;
const SHARED_BUF_PAGES: usize = 16;
#[unsafe(no_mangle)]
extern "C" fn kmain(hart: usize) -> ! {
@@ -127,20 +128,33 @@ extern "C" fn kmain(hart: usize) -> ! {
let storage = boot::loader::load_server(&uart, &mut blk, b"storage.elf", "storage");
let display = boot::loader::load_server(&uart, &mut blk, b"display.elf", "display");
let input = boot::loader::load_server(&uart, &mut blk, b"input.elf", "input");
let virtio_base = blk.base();
let board = board::devices().unwrap_or_else(|| halt(&uart, "bad board table"));
kern_mut()
.register_board(init.pid, &board)
.unwrap_or_else(|_| halt(&uart, "board registration failed"));
devices::storage_setup::configure(&uart, init.pid, storage.pid, virtio_base);
devices::gpu_setup::configure(&uart, display.pid);
devices::input_setup::configure(&uart, input.pid);
devices::shared_buffer(
&uart,
init.pid,
storage.pid,
SHARED_BUF_VA,
SHARED_BUF_PAGES,
);
devices::display_quota(&uart, display.pid);
boot::caps::grant_all(&uart, init.pid, storage.pid, display.pid, input.pid);
// After grant_all, so the slots init already relies on stay put.
kern_mut()
.grant_boot_authority(init.pid, &board)
let mut placed = [None; board::BOARD_DEVICES];
let system_control = kern_mut()
.grant_boot_authority(init.pid, &board, &mut placed)
.unwrap_or_else(|_| halt(&uart, "boot authority failed"));
devices::boot_info(
&uart,
init.pid,
&board,
&board::kinds(),
&placed,
system_control,
);
start_other_harts(&uart, hart);
uart.write_str("k-os: entering userspace\n");