reorg: extract library/device/pci — the claimed-function view

New pci logic module (library/device/pci/pci.zig): a device driver's view of
the one PCI function it has claimed — Function.map (config space = resource 0),
vendorId/deviceId/command/status, enableMemoryAndBusMaster, mapBar (BAR decode
+ resource correlation + mmio_map, cached), and a capabilities() iterator. The
generic PCI mechanics every leaf PCI driver used to re-derive inline.

The config-space layout it needs — header offsets, the command MEM|bus-master
bits, the status capabilities-list bit, the capability-pointer mask, and the
BAR bit fields — is named in the pci-class data module (a "Configuration-space
layout" section), so the bus enumerator can share the same constants later.

virtio-gpu is the first consumer: its inline mapBar + walkCapabilities + config
header reads are gone, replaced by pci.Function; only the virtio-specific
cfg_type dispatch (and the virtio common-config cfgRead/cfgWrite, which are NOT
PCI config space) stay in the driver. The generic display driver will use the
same module. pci-bus's enumerator (arbitrary-function probing) is untouched.

zig build + test green; virtio-gpu, display-native, display-reattach, pci-scan pass.
This commit is contained in:
Daniel Samson
2026-07-22 20:43:47 +01:00
parent f86f2987d5
commit a7f0c1a450
4 changed files with 182 additions and 78 deletions
+14
View File
@@ -436,6 +436,19 @@ pub fn build(b: *std.Build) void {
.root_source_file = b.path("library/mmio/mmio.zig"), .root_source_file = b.path("library/mmio/mmio.zig"),
}); });
// A device driver's view of its claimed PCI function: config-space header fields, BAR
// decode + map, and the capability walk (library/device/pci/pci.zig). The generic PCI
// mechanics every leaf PCI driver used to re-derive inline. Imports runtime (device
// access) + mmio + the pci-class data module (config-space layout constants).
const pci_module = b.addModule("pci", .{
.root_source_file = b.path("library/device/pci/pci.zig"),
.imports = &.{
.{ .name = "runtime", .module = runtime_module },
.{ .name = "mmio", .module = mmio_module },
.{ .name = "pci-class", .module = pci_class_module },
},
});
// Keyboard layouts compiled from the X11 xkeyboard-config database into native Zig // Keyboard layouts compiled from the X11 xkeyboard-config database into native Zig
// (keycode + modifiers -> keysym/character). The `layouts` tables are generated by // (keycode + modifiers -> keysym/character). The `layouts` tables are generated by
// tools/make-xkeyboard-config.py; `xkeyboard-config` is the hand-written API over them. // tools/make-xkeyboard-config.py; `xkeyboard-config` is the hand-written API over them.
@@ -550,6 +563,7 @@ pub fn build(b: *std.Build) void {
const display_exe = addThreadedUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "display", "system/services/display/display.zig"); const display_exe = addThreadedUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "display", "system/services/display/display.zig");
const display_demo_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "display-demo", "system/services/display-demo/display-demo.zig"); const display_demo_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "display-demo", "system/services/display-demo/display-demo.zig");
const virtio_gpu_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "virtio-gpu", "system/drivers/virtio-gpu/virtio-gpu.zig"); const virtio_gpu_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "virtio-gpu", "system/drivers/virtio-gpu/virtio-gpu.zig");
programModule(virtio_gpu_exe).addImport("pci", pci_module); // library/device/pci — the claimed-function view
const shared_memory_server_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "shared-memory-server", "system/services/shared-memory-server/shared-memory-server.zig"); const shared_memory_server_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "shared-memory-server", "system/services/shared-memory-server/shared-memory-server.zig");
const shared_memory_client_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "shared-memory-client", "system/services/shared-memory-client/shared-memory-client.zig"); const shared_memory_client_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "shared-memory-client", "system/services/shared-memory-client/shared-memory-client.zig");
const fat_test_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "fat-test", "system/services/fat/fat-test.zig"); const fat_test_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "fat-test", "system/services/fat/fat-test.zig");
+29
View File
@@ -41,6 +41,35 @@ pub const ClassCode = struct {
} }
}; };
// --- Configuration-space layout ---------------------------------------------------------
// The offsets and bit layouts of the PCI configuration header (PCI spec; see
// https://wiki.osdev.org/PCI). Pure data — named here so both a device driver's view of
// its own claimed function (library/device/pci/pci.zig) and the bus enumerator name the
// same bytes instead of scattering bare 0x04/0x34/0xFFFF_FFF0 magic across the tree.
/// Header field offsets (byte offsets into the 256-byte configuration space).
pub const config_vendor_id: usize = 0x00;
pub const config_device_id: usize = 0x02;
pub const config_command: usize = 0x04;
pub const config_status: usize = 0x06;
pub const config_capabilities_pointer: usize = 0x34;
pub const config_bar0: usize = 0x10; // BAR0; BAR n is at config_bar0 + n*4
/// Command register: Memory-Space enable (bit 1) | Bus-Master enable (bit 2).
pub const command_memory_and_bus_master: u16 = 0x06;
/// Status register bit 4: a capability list is present at config_capabilities_pointer.
pub const status_capabilities_list: u16 = 0x10;
/// Capability pointers are dword-aligned; the low two bits are reserved.
pub const capability_pointer_mask: u8 = 0xFC;
/// BAR bit layout: bit 0 selects I/O (1) vs memory (0) space; for a memory BAR, bits 2:1
/// give the type (00 = 32-bit, 10 = 64-bit spanning the next BAR), and the base address is
/// the dword with the low 4 flag bits masked off.
pub const bar_io_space: u32 = 0x1;
pub const bar_type_mask: u32 = 0x6;
pub const bar_type_64bit: u32 = 0x4;
pub const bar_memory_base_mask: u32 = 0xFFFF_FFF0;
/// Base class (config byte 0x0B). Non-exhaustive: an unlisted code is a real but /// Base class (config byte 0x0B). Non-exhaustive: an unlisted code is a real but
/// unnamed class, decoded as "Unknown" rather than rejected. /// unnamed class, decoded as "Unknown" rather than rejected.
pub const BaseClass = enum(u8) { pub const BaseClass = enum(u8) {
+109
View File
@@ -0,0 +1,109 @@
//! library/device/pci/pci.zig — a device driver's view of the ONE PCI function it has
//! claimed. Config space is mapped as resource 0; this gives header-field accessors, BAR
//! decode + map, and a capability-list iterator, so a driver never re-derives the
//! config-space layout by hand.
//!
//! This is the *device-owned* view: read my own function's live config, map my own BARs.
//! The bus enumerator's view — probing arbitrary, not-yet-claimed functions and sizing
//! their BARs — is a different mechanism and lives in the pci-bus driver. The pure
//! config-space layout both need (offsets, BAR bit fields) is named once in the `pci-class`
//! data module; this logic module adds the parts that need `mmio` + `runtime.device`.
const std = @import("std");
const runtime = @import("runtime");
const mmio = @import("mmio");
const pci_class = @import("pci-class");
const device = runtime.device;
/// A claimed PCI function whose configuration space is mapped (resource 0). `descriptor`
/// must outlive the Function — the driver's `device.enumerate` buffer does, for the whole
/// bring-up. Header reads and the capability walk hit live config space; `mapBar` caches.
pub const Function = struct {
device_id: u64,
descriptor: *const device.DeviceDescriptor,
config: usize, // virtual base of mapped resource 0
bar_virtual: [6]usize = .{ 0, 0, 0, 0, 0, 0 }, // per-BAR mmio_map cache
/// Map config space (resource 0) of the already-claimed `device_id`. null if the map
/// fails (not claimed, or no config resource).
pub fn map(device_id: u64, descriptor: *const device.DeviceDescriptor) ?Function {
const base = device.mmioMap(device_id, 0) orelse return null;
return .{ .device_id = device_id, .descriptor = descriptor, .config = base };
}
pub fn vendorId(self: *const Function) u16 {
return mmio.read(u16, self.config + pci_class.config_vendor_id);
}
pub fn deviceId(self: *const Function) u16 {
return mmio.read(u16, self.config + pci_class.config_device_id);
}
pub fn command(self: *const Function) u16 {
return mmio.read(u16, self.config + pci_class.config_command);
}
pub fn status(self: *const Function) u16 {
return mmio.read(u16, self.config + pci_class.config_status);
}
/// Set Memory-Space + Bus-Master enable in the command register. Firmware often leaves
/// a secondary display's decode off; a bus-mastering device must enable both.
pub fn enableMemoryAndBusMaster(self: *const Function) void {
const at = self.config + pci_class.config_command;
mmio.write(u16, at, mmio.read(u16, at) | pci_class.command_memory_and_bus_master);
}
/// Decode BAR `bar` (0..5) and map it: read the BAR register, reject I/O-space BARs,
/// combine the high dword for a 64-bit BAR, mask the base, then correlate that physical
/// base with one of the descriptor's memory resources and `mmio_map` it — a BAR names a
/// *number*, while `mmio_map` takes a *resource index*, and gaps/config-space shift the
/// numbering. Cached per BAR. null if the BAR is I/O-space or is not a mapped resource.
pub fn mapBar(self: *Function, bar: u8) ?usize {
if (bar >= 6) return null;
if (self.bar_virtual[bar] != 0) return self.bar_virtual[bar];
const low = mmio.read(u32, self.config + pci_class.config_bar0 + @as(usize, bar) * 4);
if (low & pci_class.bar_io_space != 0) return null; // an I/O-space BAR
var base: u64 = low & pci_class.bar_memory_base_mask;
if ((low & pci_class.bar_type_mask) == pci_class.bar_type_64bit) { // 64-bit: high half is the next dword
const high = mmio.read(u32, self.config + pci_class.config_bar0 + (@as(usize, bar) + 1) * 4);
base |= @as(u64, high) << 32;
}
for (self.descriptor.resources[0..@intCast(self.descriptor.resource_count)], 0..) |resource, index| {
if (resource.kind == @intFromEnum(device.ResourceKind.memory) and resource.start == base) {
const v = device.mmioMap(self.device_id, index) orelse return null;
self.bar_virtual[bar] = v;
return v;
}
}
return null;
}
/// Iterate the capability list. Empty when the function advertises none.
pub fn capabilities(self: *const Function) CapabilityIterator {
const present = self.status() & pci_class.status_capabilities_list != 0;
const first = if (present)
mmio.read(u8, self.config + pci_class.config_capabilities_pointer) & pci_class.capability_pointer_mask
else
0;
return .{ .config = self.config, .cursor = first };
}
};
/// One capability header. `offset` is the ABSOLUTE virtual address of the header, so the
/// caller reads its body with `mmio.read(T, cap.offset + n)`.
pub const Capability = struct { id: u8, offset: usize };
pub const CapabilityIterator = struct {
config: usize,
cursor: u8,
guard: u32 = 0, // bounds a malformed/looping chain (48 = the 256-byte space in dwords)
pub fn next(self: *CapabilityIterator) ?Capability {
if (self.cursor == 0 or self.guard >= 48) return null;
self.guard += 1;
const at = self.config + self.cursor;
const id = mmio.read(u8, at + 0);
self.cursor = mmio.read(u8, at + 1) & pci_class.capability_pointer_mask;
return .{ .id = id, .offset = at };
}
};
+30 -78
View File
@@ -16,6 +16,7 @@
const std = @import("std"); const std = @import("std");
const runtime = @import("runtime"); const runtime = @import("runtime");
const mmio = @import("mmio"); const mmio = @import("mmio");
const pci = @import("pci");
const device = runtime.device; const device = runtime.device;
const dma = runtime.dma; const dma = runtime.dma;
const shared_memory = runtime.shared_memory; const shared_memory = runtime.shared_memory;
@@ -84,10 +85,6 @@ var notify_base: usize = 0;
var notify_multiplier: u32 = 0; var notify_multiplier: u32 = 0;
var notify_addr: usize = 0; var notify_addr: usize = 0;
// Per-BAR mapping cache: several capabilities usually share one BAR, and mmio_map must not
// be asked to map the same resource twice.
var bar_virtual: [6]usize = .{ 0, 0, 0, 0, 0, 0 };
// DMA memory: the virtqueue rings and the command scratch. // DMA memory: the virtqueue rings and the command scratch.
var ring: dma.Region = undefined; var ring: dma.Region = undefined;
var command: dma.Region = undefined; var command: dma.Region = undefined;
@@ -121,75 +118,6 @@ fn orStatus(bit: u8) void {
cfgWrite(u8, "device_status", cfgRead(u8, "device_status") | bit); cfgWrite(u8, "device_status", cfgRead(u8, "device_status") | bit);
} }
// --- PCI config-space capability walk (config space is resource 0) ---------------------
/// Map the BAR numbered `bar` (0..5) and return its virtual base, correlating the BAR's
/// physical address (read from config space) with one of our device resources — because a
/// virtio capability names a BAR *number*, while `mmio_map` takes a *resource index* (and
/// resource 0 is config space, so BAR resources are re-numbered and gaps skipped).
fn mapBar(config: usize, descriptor: *const device.DeviceDescriptor, bar: u8) ?usize {
if (bar >= 6) return null;
if (bar_virtual[bar] != 0) return bar_virtual[bar];
const low = mmio.read(u32, config + 0x10 + @as(usize, bar) * 4);
if (low & 0x1 != 0) return null; // an I/O-space BAR — virtio structures are in memory BARs
var base: u64 = low & 0xFFFF_FFF0;
if ((low & 0x6) == 0x4) { // 64-bit memory BAR: the high half is the next dword
const high = mmio.read(u32, config + 0x10 + (@as(usize, bar) + 1) * 4);
base |= @as(u64, high) << 32;
}
for (descriptor.resources[0..@intCast(descriptor.resource_count)], 0..) |resource, index| {
if (resource.kind == @intFromEnum(device.ResourceKind.memory) and resource.start == base) {
const v = device.mmioMap(device_id, index) orelse return null;
bar_virtual[bar] = v;
return v;
}
}
std.log.info("BAR {d} (physical 0x{x}) is not a mapped resource", .{ bar, base });
return null;
}
/// Walk the PCI capability list from mapped config space, recording the common-config and
/// notify structures (the only two V3 needs). Returns false if either is missing.
fn walkCapabilities(config: usize, descriptor: *const device.DeviceDescriptor) bool {
if (mmio.read(u16, config + 0x06) & 0x10 == 0) { // Status bit 4: capabilities list present
std.log.info("device has no PCI capability list", .{});
return false;
}
var cap: u8 = @as(u8, @truncate(mmio.read(u8, config + 0x34))) & 0xFC;
var guard: u32 = 0;
while (cap != 0 and guard < 48) : (guard += 1) {
const at = config + cap;
const id = mmio.read(u8, at + 0);
const next = mmio.read(u8, at + 1) & 0xFC;
// Only map BARs for the structures V3 uses (common + notify). The other virtio
// capabilities (isr, device, and especially the cfg_pci back-door, which carries a
// placeholder bar=0/offset=0) reference BARs we never touch, so mapping them would
// just log spurious "not a mapped resource" noise.
if (id == vp.pci_cap_vendor) {
const cfg_type = mmio.read(u8, at + 3);
if (cfg_type == vp.cfg_common or cfg_type == vp.cfg_notify) {
const bar = mmio.read(u8, at + 4);
const offset = mmio.read(u32, at + 8);
if (mapBar(config, descriptor, bar)) |bar_base| {
if (cfg_type == vp.cfg_common) {
common_base = bar_base + offset;
} else {
notify_base = bar_base + offset;
notify_multiplier = mmio.read(u32, at + 16); // virtio_pci_notify_cap tail
}
}
}
}
cap = next;
}
if (common_base == 0 or notify_base == 0) {
std.log.info("missing common-config or notify capability", .{});
return false;
}
return true;
}
// --- the control virtqueue ------------------------------------------------------------- // --- the control virtqueue -------------------------------------------------------------
@@ -283,19 +211,43 @@ fn initialise(endpoint: ipc.Handle) bool {
// Config space is resource 0. Confirm it really is a virtio-gpu, then enable memory-space // Config space is resource 0. Confirm it really is a virtio-gpu, then enable memory-space
// decode + bus mastering (the device DMAs the ring and backing out of RAM); pci-bus only // decode + bus mastering (the device DMAs the ring and backing out of RAM); pci-bus only
// preserves whatever the firmware left, and a secondary display is often left disabled. // preserves whatever the firmware left, and a secondary display is often left disabled.
const config = device.mmioMap(device_id, 0) orelse { var function = pci.Function.map(device_id, descriptor) orelse {
std.log.info("config-space map failed", .{}); std.log.info("config-space map failed", .{});
return false; return false;
}; };
const vendor = mmio.read(u16, config + 0x00); const vendor = function.vendorId();
const dev = mmio.read(u16, config + 0x02); const dev = function.deviceId();
if (vendor != virtio_vendor or dev != virtio_gpu_device) { if (vendor != virtio_vendor or dev != virtio_gpu_device) {
std.log.info("not a virtio-gpu (vendor 0x{x} device 0x{x})", .{ vendor, dev }); std.log.info("not a virtio-gpu (vendor 0x{x} device 0x{x})", .{ vendor, dev });
return false; return false;
} }
mmio.write(u16, config + 0x04, mmio.read(u16, config + 0x04) | 0x06); // MEM + bus master function.enableMemoryAndBusMaster();
if (!walkCapabilities(config, descriptor)) return false; // Walk the capability list for the virtio common-config and notify structures (V3 needs
// only those two). The generic PCI mechanics — header fields, BAR decode, the capability
// chain — are library/device/pci; the virtio cfg_type dispatch stays here. We map only the
// common/notify BARs (the other virtio caps, and the cfg_pci back-door's placeholder
// bar=0/offset=0, reference BARs we never touch, so mapping them would only log noise).
var caps = function.capabilities();
while (caps.next()) |cap| {
if (cap.id != vp.pci_cap_vendor) continue;
const cfg_type = mmio.read(u8, cap.offset + 3);
if (cfg_type != vp.cfg_common and cfg_type != vp.cfg_notify) continue;
const bar = mmio.read(u8, cap.offset + 4);
const offset = mmio.read(u32, cap.offset + 8);
if (function.mapBar(bar)) |bar_base| {
if (cfg_type == vp.cfg_common) {
common_base = bar_base + offset;
} else {
notify_base = bar_base + offset;
notify_multiplier = mmio.read(u32, cap.offset + 16); // virtio_pci_notify_cap tail
}
}
}
if (common_base == 0 or notify_base == 0) {
std.log.info("missing common-config or notify capability", .{});
return false;
}
// Reset, then the modern feature handshake: acknowledge, take driver ownership, require // Reset, then the modern feature handshake: acknowledge, take driver ownership, require
// VERSION_1 and offer nothing else, and confirm the device accepts that. // VERSION_1 and offer nothing else, and confirm the device accepts that.