Files
RedBear-OS/local/recipes/drivers/redox-driver-sys/source/src/pci.rs
T
vasilito 678980521c feat: T1.1-T2.2 MSI subsystem — kernel MSI, vectors, affinity, validation
Kernel:
- T1.1 msi.rs: MSI message composition (MsiMessage), validation
  (is_valid_msi_address, is_valid_msi_vector), capability parsing
  (MsiCapability, MsixCapability) with bounds-safe .get() access
- T1.2 vector.rs: per-CPU bitmatrix vector allocation/deallocation
- T1.3 IRQ scheme: MSI vector validation gate at irq_trigger,
  iommu_validate_msi_irq hook, msi_vector_is_valid helper
- mod.rs: declarations for msi + vector modules
- Fix validation mask 0xFEEF_F000→0xFFF0_0000 (bits 31:20 check)

T2.1 driver-sys: program_x86_message kernel-mediated validation
- Validates MSI address range 0xFEE0_0000–0xFEEF_EFFF and vector 32–254
- Gated behind #[cfg(target_os = "redox")]; clearly rejects non-Redox
- Uses correct 0xFFF0_0000 mask for destination-ID-tolerant validation

T2.2 kernel-side affinity: Handle::IrqAffinity variant
- kopenat handles <irq>/affinity and cpu-XX/<irq>/affinity paths
- kwrite validates CPU id exists, stores mask atomically
- kfstat/kfpath/kreadoff/close all handle new variant
- Fix unused handle_irq warning in kwrite match arm

T2.3 driver-sys: MsiAllocation struct + irq_set_affinity helper
- MsiAllocation with round-robin CPU allocation via alloc_cpu_id
- irq_set_affinity uses scheme:irq/<irq>/affinity write path
- IrqFd type alias in pci.rs for file descriptor tracking

IOMMU T3.1: InterruptRemapTable, IRTE encode/decode, IrqRemapManager
- IRTE (16-byte) encoding/decoding for AMD-Vi interrupt remapping
- InterruptRemapTable with program/invalidate/find_free
- IrqRemapManager with multi-table remap and validate_msi gate
- Remove arbitrary .min(256) bound on find_free

P8-msi.patch: 281-line durable kernel patch, wired in recipe.toml
2026-05-04 18:00:15 +01:00

1059 lines
30 KiB
Rust

use std::io::{Read, Seek, SeekFrom, Write};
use crate::{DriverError, Result};
pub const PCI_VENDOR_ID_AMD: u16 = 0x1002;
pub const PCI_VENDOR_ID_INTEL: u16 = 0x8086;
pub const PCI_VENDOR_ID_NVIDIA: u16 = 0x10DE;
pub const PCI_CLASS_DISPLAY: u8 = 0x03;
pub const PCI_CLASS_DISPLAY_VGA: u8 = 0x00;
pub const PCI_CLASS_DISPLAY_3D: u8 = 0x02;
pub const PCI_HEADER_TYPE_NORMAL: u8 = 0x00;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PciLocation {
pub segment: u16,
pub bus: u8,
pub device: u8,
pub function: u8,
}
impl PciLocation {
pub fn scheme_path(&self) -> String {
format!(
"/scheme/pci/{:04x}--{:02x}--{:02x}.{}",
self.segment, self.bus, self.device, self.function
)
}
pub fn bdf(&self) -> u32 {
((self.bus as u32) << 16)
| ((self.device as u32) & 0x1F) << 11
| ((self.function as u32) & 0x07) << 8
}
pub fn from_bdf(bdf: u32) -> Self {
PciLocation {
segment: 0,
bus: ((bdf >> 16) & 0xFF) as u8,
device: ((bdf >> 11) & 0x1F) as u8,
function: ((bdf >> 8) & 0x07) as u8,
}
}
}
impl std::fmt::Display for PciLocation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{:04x}:{:02x}:{:02x}.{}",
self.segment, self.bus, self.device, self.function
)
}
}
#[derive(Clone, Copy, Debug)]
pub struct PciBarInfo {
pub index: usize,
pub kind: PciBarKind,
pub addr: u64,
pub size: u64,
pub prefetchable: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PciBarKind {
Memory32,
Memory64,
Io,
None,
}
impl PciBarInfo {
pub fn is_memory(&self) -> bool {
matches!(self.kind, PciBarKind::Memory32 | PciBarKind::Memory64)
}
pub fn is_io(&self) -> bool {
self.kind == PciBarKind::Io
}
pub fn memory_info(&self) -> Option<(u64, usize)> {
if self.is_memory() && self.addr != 0 && self.size != 0 {
Some((self.addr, self.size as usize))
} else {
None
}
}
pub fn io_port(&self) -> Option<u16> {
if self.is_io() && self.addr != 0 {
u16::try_from(self.addr).ok()
} else {
None
}
}
}
pub const PCI_CMD_IO_SPACE: u16 = 0x0001;
pub const PCI_CMD_MEMORY_SPACE: u16 = 0x0002;
pub const PCI_CMD_BUS_MASTER: u16 = 0x0004;
pub const PCI_CMD_MEM_WRITE_INVALIDATE: u16 = 0x0010;
pub const PCI_CMD_PARITY_ERROR_RESPONSE: u16 = 0x0040;
pub const PCI_CMD_SERR_ENABLE: u16 = 0x0100;
pub const PCI_CMD_INTX_DISABLE: u16 = 0x0400;
#[derive(Clone, Debug)]
pub struct PciCapability {
pub id: u8,
pub offset: u8,
pub vendor_cap_id: Option<u8>,
}
pub const PCI_CAP_ID_MSI: u8 = 0x05;
pub const PCI_CAP_ID_ATS: u8 = 0x0B;
pub const PCI_CAP_ID_MSIX: u8 = 0x11;
pub const PCI_CAP_ID_PCIE: u8 = 0x10;
pub const PCI_CAP_ID_POWER: u8 = 0x01;
pub const PCI_CAP_ID_VNDR: u8 = 0x09;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum InterruptSupport {
None,
LegacyOnly,
Msi,
MsiX,
}
impl InterruptSupport {
pub fn as_str(self) -> &'static str {
match self {
Self::None => "none",
Self::LegacyOnly => "legacy",
Self::Msi => "msi",
Self::MsiX => "msix",
}
}
}
#[derive(Clone, Debug)]
pub struct MsixCapability {
pub table_bar: u8,
pub table_offset: u32,
pub pba_bar: u8,
pub pba_offset: u32,
pub table_size: u16,
pub masked: bool,
}
#[derive(Clone, Debug)]
pub struct PciDeviceInfo {
pub location: PciLocation,
pub vendor_id: u16,
pub device_id: u16,
pub subsystem_vendor_id: u16,
pub subsystem_device_id: u16,
pub revision: u8,
pub class_code: u8,
pub subclass: u8,
pub prog_if: u8,
pub header_type: u8,
pub irq: Option<u32>,
pub bars: Vec<PciBarInfo>,
pub capabilities: Vec<PciCapability>,
}
pub type IrqFd = std::fs::File;
impl PciDeviceInfo {
pub fn is_gpu(&self) -> bool {
self.class_code == PCI_CLASS_DISPLAY
}
pub fn is_amd_gpu(&self) -> bool {
self.class_code == PCI_CLASS_DISPLAY && self.vendor_id == PCI_VENDOR_ID_AMD
}
pub fn is_intel_gpu(&self) -> bool {
self.class_code == PCI_CLASS_DISPLAY && self.vendor_id == PCI_VENDOR_ID_INTEL
}
pub fn find_capability(&self, id: u8) -> Option<&PciCapability> {
self.capabilities.iter().find(|c| c.id == id)
}
pub fn find_msix(&self) -> Option<MsixCapability> {
self.find_capability(PCI_CAP_ID_MSIX).and_then(|cap| {
let mut dev = PciDevice::from_info(self).ok()?;
dev.parse_msix(cap.offset).ok()
})
}
pub fn find_memory_bar(&self, index: usize) -> Option<&PciBarInfo> {
self.bars.iter().find(|b| b.index == index && b.is_memory())
}
pub fn supports_msi(&self) -> bool {
self.find_capability(PCI_CAP_ID_MSI).is_some()
}
pub fn supports_msix(&self) -> bool {
self.find_capability(PCI_CAP_ID_MSIX).is_some()
}
pub fn interrupt_support(&self) -> InterruptSupport {
let quirks = self.quirks();
let has_legacy = self.irq.is_some();
let has_msi = self.supports_msi() && !quirks.contains(crate::quirks::PciQuirkFlags::NO_MSI);
let has_msix =
self.supports_msix() && !quirks.contains(crate::quirks::PciQuirkFlags::NO_MSIX);
if quirks.contains(crate::quirks::PciQuirkFlags::FORCE_LEGACY_IRQ) {
return if has_legacy {
InterruptSupport::LegacyOnly
} else {
InterruptSupport::None
};
}
if has_msix {
InterruptSupport::MsiX
} else if has_msi {
InterruptSupport::Msi
} else if has_legacy {
InterruptSupport::LegacyOnly
} else {
InterruptSupport::None
}
}
pub fn quirks(&self) -> crate::quirks::PciQuirkFlags {
crate::quirks::lookup_pci_quirks(self)
}
pub fn has_quirk(&self, flag: crate::quirks::PciQuirkFlags) -> bool {
self.quirks().contains(flag)
}
}
pub struct PciDevice {
location: PciLocation,
config_fd: std::fs::File,
}
impl PciDevice {
pub fn open(segment: u16, bus: u8, device: u8, function: u8) -> Result<Self> {
let loc = PciLocation {
segment,
bus,
device,
function,
};
Self::open_location(&loc)
}
pub fn open_location(loc: &PciLocation) -> Result<Self> {
let config_path = format!("{}/config", loc.scheme_path());
let fd = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(&config_path)
.map_err(|e| {
DriverError::Pci(format!("cannot open PCI config at {}: {}", config_path, e))
})?;
Ok(PciDevice {
location: *loc,
config_fd: fd,
})
}
pub fn from_info(info: &PciDeviceInfo) -> Result<Self> {
Self::open_location(&info.location)
}
pub fn location(&self) -> &PciLocation {
&self.location
}
pub fn read_config_dword(&mut self, offset: u64) -> Result<u32> {
self.config_fd.seek(SeekFrom::Start(offset))?;
let mut buf = [0u8; 4];
self.config_fd.read_exact(&mut buf)?;
Ok(u32::from_le_bytes(buf))
}
pub fn read_config_word(&mut self, offset: u64) -> Result<u16> {
self.config_fd.seek(SeekFrom::Start(offset))?;
let mut buf = [0u8; 2];
self.config_fd.read_exact(&mut buf)?;
Ok(u16::from_le_bytes(buf))
}
pub fn read_config_byte(&mut self, offset: u64) -> Result<u8> {
self.config_fd.seek(SeekFrom::Start(offset))?;
let mut buf = [0u8; 1];
self.config_fd.read_exact(&mut buf)?;
Ok(buf[0])
}
pub fn write_config_dword(&mut self, offset: u64, val: u32) -> Result<()> {
self.config_fd.seek(SeekFrom::Start(offset))?;
self.config_fd.write_all(&val.to_le_bytes())?;
Ok(())
}
pub fn write_config_word(&mut self, offset: u64, val: u16) -> Result<()> {
self.config_fd.seek(SeekFrom::Start(offset))?;
self.config_fd.write_all(&val.to_le_bytes())?;
Ok(())
}
pub fn write_config_byte(&mut self, offset: u64, val: u8) -> Result<()> {
self.config_fd.seek(SeekFrom::Start(offset))?;
self.config_fd.write_all(&[val])?;
Ok(())
}
pub fn vendor_id(&mut self) -> Result<u16> {
self.read_config_word(0x00)
}
pub fn device_id(&mut self) -> Result<u16> {
self.read_config_word(0x02)
}
pub fn command(&mut self) -> Result<u16> {
self.read_config_word(0x04)
}
pub fn set_command(&mut self, flags: u16) -> Result<()> {
self.write_config_word(0x04, flags)
}
pub fn enable_device(&mut self) -> Result<()> {
let mut cmd = self.command()?;
cmd |= PCI_CMD_IO_SPACE | PCI_CMD_MEMORY_SPACE | PCI_CMD_BUS_MASTER;
self.set_command(cmd)
}
pub fn set_bus_master(&mut self, enable: bool) -> Result<()> {
let mut cmd = self.command()?;
if enable {
cmd |= 0x0004;
} else {
cmd &= !0x0004;
}
self.set_command(cmd)
}
pub fn set_intx_disable(&mut self, disable: bool) -> Result<()> {
let mut cmd = self.command()?;
if disable {
cmd |= 0x0400;
} else {
cmd &= !0x0400;
}
self.set_command(cmd)
}
pub fn status(&mut self) -> Result<u16> {
self.read_config_word(0x06)
}
pub fn revision(&mut self) -> Result<u8> {
self.read_config_byte(0x08)
}
pub fn class_code(&mut self) -> Result<u8> {
self.read_config_byte(0x0B)
}
pub fn subclass(&mut self) -> Result<u8> {
self.read_config_byte(0x0A)
}
pub fn prog_if(&mut self) -> Result<u8> {
self.read_config_byte(0x09)
}
pub fn header_type(&mut self) -> Result<u8> {
let ht = self.read_config_byte(0x0E)?;
Ok(ht & 0x7F)
}
pub fn is_multi_function(&mut self) -> Result<bool> {
let ht = self.read_config_byte(0x0E)?;
Ok(ht & 0x80 != 0)
}
pub fn irq_line(&mut self) -> Result<u8> {
self.read_config_byte(0x3C)
}
pub fn irq_pin(&mut self) -> Result<u8> {
self.read_config_byte(0x3D)
}
pub fn full_info(&mut self) -> Result<PciDeviceInfo> {
let vendor_id = self.vendor_id()?;
let device_id = self.device_id()?;
let revision = self.revision()?;
let prog_if = self.prog_if()?;
let subclass = self.subclass()?;
let class_code = self.class_code()?;
let header_type = self.header_type()?;
let irq_byte = self.irq_line()?;
let bars = if header_type == PCI_HEADER_TYPE_NORMAL {
self.parse_bars()?
} else {
Vec::new()
};
let capabilities = if header_type == PCI_HEADER_TYPE_NORMAL {
self.parse_capabilities()?
} else {
Vec::new()
};
let (subsystem_vendor_id, subsystem_device_id) = if header_type == PCI_HEADER_TYPE_NORMAL {
(
self.read_config_word(0x2C).unwrap_or(0xFFFF),
self.read_config_word(0x2E).unwrap_or(0xFFFF),
)
} else {
(0xFFFF, 0xFFFF)
};
Ok(PciDeviceInfo {
location: self.location,
vendor_id,
device_id,
subsystem_vendor_id,
subsystem_device_id,
revision,
class_code,
subclass,
prog_if,
header_type,
irq: if irq_byte != 0 && irq_byte != 0xFF {
Some(irq_byte as u32)
} else {
None
},
bars,
capabilities,
})
}
pub fn parse_bars(&mut self) -> Result<Vec<PciBarInfo>> {
let mut bars = Vec::with_capacity(6);
let mut bar_idx = 0usize;
let mut config_offset = 0x10u64;
while bar_idx < 6 && config_offset <= 0x24 {
let val_lo = self.read_config_dword(config_offset)?;
if val_lo == 0 {
bars.push(PciBarInfo {
index: bar_idx,
kind: PciBarKind::None,
addr: 0,
size: 0,
prefetchable: false,
});
bar_idx += 1;
config_offset += 4;
continue;
}
let is_io = (val_lo & 0x01) != 0;
if is_io {
let addr = (val_lo & 0xFFFFFFFC) as u64;
let size = self.probe_bar_size(config_offset)?;
bars.push(PciBarInfo {
index: bar_idx,
kind: PciBarKind::Io,
addr,
size,
prefetchable: false,
});
bar_idx += 1;
config_offset += 4;
} else {
let is_64bit = ((val_lo >> 2) & 0x01) != 0;
let prefetchable = ((val_lo >> 3) & 0x01) != 0;
let addr_lo = (val_lo & 0xFFFFFFF0) as u64;
let (addr, size) = if is_64bit {
let val_hi = self.read_config_dword(config_offset + 4)?;
let full_addr = addr_lo | ((val_hi as u64) << 32);
let full_size = self.probe_bar64_size(config_offset)?;
bars.push(PciBarInfo {
index: bar_idx,
kind: PciBarKind::Memory64,
addr: full_addr,
size: full_size,
prefetchable,
});
bar_idx += 2;
config_offset += 8;
continue;
} else {
let sz = self.probe_bar_size(config_offset)?;
(addr_lo, sz)
};
bars.push(PciBarInfo {
index: bar_idx,
kind: PciBarKind::Memory32,
addr,
size,
prefetchable,
});
bar_idx += 1;
config_offset += 4;
}
}
Ok(bars)
}
fn probe_bar_size(&mut self, offset: u64) -> Result<u64> {
let original = self.read_config_dword(offset)?;
self.write_config_dword(offset, 0xFFFFFFFF)?;
let inverted = self.read_config_dword(offset)?;
self.write_config_dword(offset, original)?;
let is_io = (original & 0x01) != 0;
let mask = if is_io { 0xFFFFFFFC } else { 0xFFFFFFF0 };
let size_val = !(inverted & mask) & mask;
if size_val == 0 {
return Ok(0);
}
Ok(size_val as u64)
}
fn probe_bar64_size(&mut self, offset: u64) -> Result<u64> {
let original_lo = self.read_config_dword(offset)?;
let original_hi = self.read_config_dword(offset + 4)?;
self.write_config_dword(offset, 0xFFFFFFFF)?;
self.write_config_dword(offset + 4, 0xFFFFFFFF)?;
let inverted_lo = self.read_config_dword(offset)?;
let inverted_hi = self.read_config_dword(offset + 4)?;
self.write_config_dword(offset, original_lo)?;
self.write_config_dword(offset + 4, original_hi)?;
let lo = !(inverted_lo & 0xFFFFFFF0) & 0xFFFFFFF0;
let hi = !inverted_hi;
if lo == 0 && hi == 0 {
return Ok(0);
}
let size = ((hi as u64) << 32) | (lo as u64);
Ok(size)
}
pub fn parse_capabilities(&mut self) -> Result<Vec<PciCapability>> {
let status = self.status()?;
if status & 0x0010 == 0 {
return Ok(Vec::new());
}
let mut caps = Vec::new();
let mut cap_ptr = self.read_config_byte(0x34)? as u64;
let mut visited = 0u8;
while cap_ptr >= 0x40 && visited < 48 {
let cap_id = self.read_config_byte(cap_ptr)?;
let next_ptr = self.read_config_byte(cap_ptr + 1)? as u64;
if cap_id == 0 {
break;
}
let vendor_cap_id = if cap_id == PCI_CAP_ID_VNDR {
self.read_config_byte(cap_ptr + 2).ok()
} else {
None
};
caps.push(PciCapability {
id: cap_id,
offset: cap_ptr as u8,
vendor_cap_id,
});
if next_ptr == 0 || next_ptr <= cap_ptr {
break;
}
cap_ptr = next_ptr;
visited += 1;
}
Ok(caps)
}
pub fn parse_msix(&mut self, cap_offset: u8) -> Result<MsixCapability> {
let msg_ctrl = self.read_config_word(cap_offset as u64 + 2)?;
let table_raw = self.read_config_dword(cap_offset as u64 + 4)?;
let pba_raw = self.read_config_dword(cap_offset as u64 + 8)?;
let table_bar = (table_raw & 0x07) as u8;
let table_offset = table_raw & 0xFFFFFFF8;
let pba_bar = (pba_raw & 0x07) as u8;
let pba_offset = pba_raw & 0xFFFFFFF8;
let table_size = (msg_ctrl & 0x07FF) + 1;
let masked = (msg_ctrl & 0x8000) != 0;
Ok(MsixCapability {
table_bar,
table_offset,
pba_bar,
pba_offset,
table_size,
masked,
})
}
/// Enable MSI-X by setting the MSI-X Enable bit (bit 14) in Message Control.
/// Per PCI spec §6.8.2, bit 14 enables MSI-X; bit 15 is Function Mask.
pub fn enable_msix(&mut self, cap_offset: u8) -> Result<()> {
let msg_ctrl = self.read_config_word(cap_offset as u64 + 2)?;
let new_ctrl = msg_ctrl | 0x4000;
self.write_config_word(cap_offset as u64 + 2, new_ctrl)?;
Ok(())
}
/// Disable MSI-X by clearing the MSI-X Enable bit (bit 14) in Message Control.
pub fn disable_msix(&mut self, cap_offset: u8) -> Result<()> {
let msg_ctrl = self.read_config_word(cap_offset as u64 + 2)?;
let new_ctrl = msg_ctrl & !0x4000;
self.write_config_word(cap_offset as u64 + 2, new_ctrl)?;
Ok(())
}
pub fn map_bar(
&mut self,
_bar_index: usize,
phys_addr: u64,
size: usize,
) -> Result<crate::memory::MmioRegion> {
crate::memory::MmioRegion::map(
phys_addr,
size,
crate::memory::CacheType::DeviceMemory,
crate::memory::MmioProt::READ_WRITE,
)
}
}
impl std::io::Write for PciDevice {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.config_fd.write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
self.config_fd.flush()
}
}
fn enumerate_pci_filtered(class: Option<u8>) -> Result<Vec<PciDeviceInfo>> {
let entries = std::fs::read_dir("/scheme/pci")?;
let mut devices = Vec::new();
for entry in entries {
let entry = entry?;
let name = entry.file_name();
let name_str = match name.to_str() {
Some(s) => s,
None => continue,
};
// pcid scheme entries use format: segment--bus--device.function
let location = match parse_scheme_entry(name_str) {
Some(loc) => loc,
None => continue,
};
let config_path = format!("{}/config", location.scheme_path());
if let Ok(data) = std::fs::read(&config_path) {
if let Some(info) = parse_device_info_from_config_space(location, &data)
.filter(|info| class.is_none_or(|class| info.class_code == class))
{
devices.push(info);
}
}
}
log::debug!(
"PCI enumeration{}: found {} devices",
class
.map(|class| format!(" for class {class:#04x}"))
.unwrap_or_default(),
devices.len()
);
Ok(devices)
}
pub fn parse_device_info_from_config_space(location: PciLocation, data: &[u8]) -> Option<PciDeviceInfo> {
if data.len() < 64 {
return None;
}
let class_code = data[0x0b];
let header_type = data[0x0e] & 0x7F;
let capabilities = if header_type == PCI_HEADER_TYPE_NORMAL {
parse_capabilities_from_config_bytes(data)
} else {
Vec::new()
};
let (subsystem_vendor_id, subsystem_device_id) =
if header_type == PCI_HEADER_TYPE_NORMAL && data.len() > 0x2F {
(
u16::from_le_bytes([data[0x2c], data[0x2d]]),
u16::from_le_bytes([data[0x2e], data[0x2f]]),
)
} else {
(0xFFFF, 0xFFFF)
};
let irq_line = data[0x3c];
Some(PciDeviceInfo {
location,
vendor_id: u16::from_le_bytes([data[0x00], data[0x01]]),
device_id: u16::from_le_bytes([data[0x02], data[0x03]]),
subsystem_vendor_id,
subsystem_device_id,
revision: data[0x08],
class_code,
subclass: data[0x0a],
prog_if: data[0x09],
header_type,
irq: if irq_line != 0 && irq_line != 0xff {
Some(irq_line as u32)
} else {
None
},
bars: Vec::new(),
capabilities,
})
}
fn parse_capabilities_from_config_bytes(data: &[u8]) -> Vec<PciCapability> {
if data.len() < 64 {
return Vec::new();
}
let status = u16::from_le_bytes([data[0x06], data[0x07]]);
if status & 0x0010 == 0 {
return Vec::new();
}
let mut caps = Vec::new();
let mut cap_ptr = usize::from(data[0x34]);
let mut visited = 0u8;
while cap_ptr >= 0x40 && cap_ptr + 1 < data.len() && visited < 48 {
let cap_id = data[cap_ptr];
let next_ptr = usize::from(data[cap_ptr + 1]);
if cap_id == 0 {
break;
}
let vendor_cap_id = if cap_id == PCI_CAP_ID_VNDR && cap_ptr + 2 < data.len() {
Some(data[cap_ptr + 2])
} else {
None
};
caps.push(PciCapability {
id: cap_id,
offset: cap_ptr as u8,
vendor_cap_id,
});
if next_ptr == 0 || next_ptr <= cap_ptr {
break;
}
cap_ptr = next_ptr;
visited += 1;
}
caps
}
pub fn enumerate_pci_class(class: u8) -> Result<Vec<PciDeviceInfo>> {
enumerate_pci_filtered(Some(class))
}
pub fn enumerate_pci_all() -> Result<Vec<PciDeviceInfo>> {
enumerate_pci_filtered(None)
}
fn parse_scheme_entry(name: &str) -> Option<PciLocation> {
let parts: Vec<&str> = name.splitn(3, "--").collect();
if parts.len() != 3 {
return None;
}
let segment = u16::from_str_radix(parts[0], 16).ok()?;
let bus = u8::from_str_radix(parts[1], 16).ok()?;
let dev_func: Vec<&str> = parts[2].splitn(2, '.').collect();
if dev_func.len() != 2 {
return None;
}
let device = u8::from_str_radix(dev_func[0], 16).ok()?;
let function = u8::from_str_radix(dev_func[1], 16).ok()?;
if device > 0x1F || function > 0x07 {
return None;
}
Some(PciLocation {
segment,
bus,
device,
function,
})
}
pub fn find_amd_gpus() -> Result<Vec<PciDeviceInfo>> {
let mut all = enumerate_pci_class(PCI_CLASS_DISPLAY)?;
all.retain(|d| d.is_amd_gpu());
Ok(all)
}
pub fn find_intel_gpus() -> Result<Vec<PciDeviceInfo>> {
let mut all = enumerate_pci_class(PCI_CLASS_DISPLAY)?;
all.retain(|d| d.is_intel_gpu());
Ok(all)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pci_location_bdf_round_trip_preserves_bus_device_function() {
let location = PciLocation {
segment: 0,
bus: 0x5a,
device: 0x1c,
function: 0x03,
};
let round_trip = PciLocation::from_bdf(location.bdf());
assert_eq!(round_trip.segment, 0);
assert_eq!(round_trip.bus, location.bus);
assert_eq!(round_trip.device, location.device);
assert_eq!(round_trip.function, location.function);
assert_eq!(location.scheme_path(), "/scheme/pci/0000--5a--1c.3");
}
#[test]
fn io_bar_port_requires_nonzero_u16_address() {
let io_bar = PciBarInfo {
index: 0,
kind: PciBarKind::Io,
addr: 0x3f8,
size: 8,
prefetchable: false,
};
assert_eq!(io_bar.io_port(), Some(0x3f8));
let zero_io_bar = PciBarInfo {
addr: 0,
..io_bar
};
assert_eq!(zero_io_bar.io_port(), None);
let oversized_io_bar = PciBarInfo {
addr: u64::from(u16::MAX) + 1,
..io_bar
};
assert_eq!(oversized_io_bar.io_port(), None);
}
#[test]
fn parse_scheme_entry_rejects_invalid_bdf_components() {
assert!(parse_scheme_entry("0000--00--20.0").is_none());
assert!(parse_scheme_entry("0000--00--1f.8").is_none());
assert!(parse_scheme_entry("not-a-device").is_none());
}
#[test]
fn parse_scheme_entry_accepts_valid_pci_scheme_name() {
let parsed = parse_scheme_entry("0000--80--1f.0").expect("valid PCI entry should parse");
assert_eq!(parsed.segment, 0);
assert_eq!(parsed.bus, 0x80);
assert_eq!(parsed.device, 0x1f);
assert_eq!(parsed.function, 0x00);
}
#[test]
fn parse_capabilities_from_config_bytes_reads_standard_and_vendor_caps() {
let mut data = vec![0u8; 256];
data[0x06] = 0x10;
data[0x34] = 0x50;
data[0x50] = PCI_CAP_ID_MSI;
data[0x51] = 0x60;
data[0x60] = PCI_CAP_ID_VNDR;
data[0x61] = 0x00;
data[0x62] = 0xAB;
let caps = parse_capabilities_from_config_bytes(&data);
assert_eq!(caps.len(), 2);
assert_eq!(caps[0].id, PCI_CAP_ID_MSI);
assert_eq!(caps[0].offset, 0x50);
assert_eq!(caps[0].vendor_cap_id, None);
assert_eq!(caps[1].id, PCI_CAP_ID_VNDR);
assert_eq!(caps[1].offset, 0x60);
assert_eq!(caps[1].vendor_cap_id, Some(0xAB));
}
#[test]
fn parse_capabilities_from_config_bytes_stops_on_backwards_pointer() {
let mut data = vec![0u8; 256];
data[0x06] = 0x10;
data[0x34] = 0x50;
data[0x50] = PCI_CAP_ID_MSI;
data[0x51] = 0x48;
let caps = parse_capabilities_from_config_bytes(&data);
assert_eq!(caps.len(), 1);
assert_eq!(caps[0].id, PCI_CAP_ID_MSI);
}
#[test]
fn parse_device_info_from_config_bytes_includes_capabilities() {
let mut data = vec![0u8; 256];
data[0x00] = 0x86;
data[0x01] = 0x80;
data[0x02] = 0x34;
data[0x03] = 0x12;
data[0x06] = 0x10;
data[0x08] = 0x02;
data[0x09] = 0x01;
data[0x0a] = PCI_CLASS_DISPLAY_VGA;
data[0x0b] = PCI_CLASS_DISPLAY;
data[0x0e] = PCI_HEADER_TYPE_NORMAL;
data[0x2c] = 0x86;
data[0x2d] = 0x80;
data[0x2e] = 0x78;
data[0x2f] = 0x56;
data[0x34] = 0x50;
data[0x3c] = 11;
data[0x50] = PCI_CAP_ID_MSIX;
data[0x51] = 0x00;
let info = parse_device_info_from_config_space(
PciLocation {
segment: 0,
bus: 0,
device: 2,
function: 0,
},
&data,
)
.expect("display device should be parsed");
assert_eq!(info.vendor_id, PCI_VENDOR_ID_INTEL);
assert_eq!(info.device_id, 0x1234);
assert_eq!(info.subsystem_device_id, 0x5678);
assert_eq!(info.irq, Some(11));
assert_eq!(info.capabilities.len(), 1);
assert_eq!(info.capabilities[0].id, PCI_CAP_ID_MSIX);
}
#[test]
fn parse_device_info_from_config_space_rejects_short_config() {
let location = PciLocation {
segment: 0,
bus: 0,
device: 0,
function: 0,
};
assert!(parse_device_info_from_config_space(location, &[0u8; 32]).is_none());
}
#[test]
fn interrupt_support_prefers_msix_over_msi_and_legacy() {
let info = PciDeviceInfo {
location: PciLocation {
segment: 0,
bus: 0,
device: 0,
function: 0,
},
vendor_id: 0x1234,
device_id: 0x5678,
subsystem_vendor_id: 0xffff,
subsystem_device_id: 0xffff,
revision: 0,
class_code: 0,
subclass: 0,
prog_if: 0,
header_type: PCI_HEADER_TYPE_NORMAL,
irq: Some(11),
bars: Vec::new(),
capabilities: vec![
PciCapability {
id: PCI_CAP_ID_MSI,
offset: 0x50,
vendor_cap_id: None,
},
PciCapability {
id: PCI_CAP_ID_MSIX,
offset: 0x60,
vendor_cap_id: None,
},
],
};
assert_eq!(info.interrupt_support(), InterruptSupport::MsiX);
assert_eq!(info.interrupt_support().as_str(), "msix");
}
#[test]
fn interrupt_support_honors_no_msix_quirk() {
let info = PciDeviceInfo {
location: PciLocation {
segment: 0,
bus: 0,
device: 0,
function: 0,
},
vendor_id: 0x1022,
device_id: 0x145C,
subsystem_vendor_id: 0xffff,
subsystem_device_id: 0xffff,
revision: 0,
class_code: 0,
subclass: 0,
prog_if: 0,
header_type: PCI_HEADER_TYPE_NORMAL,
irq: Some(9),
bars: Vec::new(),
capabilities: vec![PciCapability {
id: PCI_CAP_ID_MSIX,
offset: 0x60,
vendor_cap_id: None,
}],
};
assert_eq!(info.interrupt_support(), InterruptSupport::LegacyOnly);
assert_eq!(info.interrupt_support().as_str(), "legacy");
}
}