diff --git a/build.zig b/build.zig index 13f9dda..5ac47a1 100644 --- a/build.zig +++ b/build.zig @@ -436,6 +436,19 @@ pub fn build(b: *std.Build) void { .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 // (keycode + modifiers -> keysym/character). The `layouts` tables are generated by // 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_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"); + 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_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"); diff --git a/library/device/pci/pci-class.zig b/library/device/pci/pci-class.zig index 32a6c17..3a903ea 100644 --- a/library/device/pci/pci-class.zig +++ b/library/device/pci/pci-class.zig @@ -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 /// unnamed class, decoded as "Unknown" rather than rejected. pub const BaseClass = enum(u8) { diff --git a/library/device/pci/pci.zig b/library/device/pci/pci.zig new file mode 100644 index 0000000..bb494b3 --- /dev/null +++ b/library/device/pci/pci.zig @@ -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 }; + } +}; diff --git a/system/drivers/virtio-gpu/virtio-gpu.zig b/system/drivers/virtio-gpu/virtio-gpu.zig index b1ee785..86a55f2 100644 --- a/system/drivers/virtio-gpu/virtio-gpu.zig +++ b/system/drivers/virtio-gpu/virtio-gpu.zig @@ -16,6 +16,7 @@ const std = @import("std"); const runtime = @import("runtime"); const mmio = @import("mmio"); +const pci = @import("pci"); const device = runtime.device; const dma = runtime.dma; const shared_memory = runtime.shared_memory; @@ -84,10 +85,6 @@ var notify_base: usize = 0; var notify_multiplier: u32 = 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. var ring: 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); } -// --- 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 ------------------------------------------------------------- @@ -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 // 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. - const config = device.mmioMap(device_id, 0) orelse { + var function = pci.Function.map(device_id, descriptor) orelse { std.log.info("config-space map failed", .{}); return false; }; - const vendor = mmio.read(u16, config + 0x00); - const dev = mmio.read(u16, config + 0x02); + const vendor = function.vendorId(); + const dev = function.deviceId(); if (vendor != virtio_vendor or dev != virtio_gpu_device) { std.log.info("not a virtio-gpu (vendor 0x{x} device 0x{x})", .{ vendor, dev }); 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 // VERSION_1 and offer nothing else, and confirm the device accepts that.