diff --git a/src/hwprobe/core/common/pcie_link.py b/src/hwprobe/core/common/pcie_link.py new file mode 100644 index 0000000..9e79ac8 --- /dev/null +++ b/src/hwprobe/core/common/pcie_link.py @@ -0,0 +1,25 @@ +from typing import Optional + +from hwprobe.models.gpu_models import PCIeLinkInfo, PCIeLinkValue + + +def build_pcie_link( + *, + max_gen: int, + current_gen: int, + max_width: int, + current_width: int +) -> Optional[PCIeLinkInfo]: + gen = None + width = None + + if max_gen > 0 and current_gen > 0: + gen = PCIeLinkValue(max=max_gen, current=current_gen) + + if max_width > 0 and current_width > 0: + width = PCIeLinkValue(max=max_width, current=current_width) + + if gen is None and width is None: + return None + + return PCIeLinkInfo(gen=gen, width=width) \ No newline at end of file diff --git a/src/hwprobe/core/mac/graphics.py b/src/hwprobe/core/mac/graphics.py index d26e8ff..4d4c0a5 100644 --- a/src/hwprobe/core/mac/graphics.py +++ b/src/hwprobe/core/mac/graphics.py @@ -1,3 +1,4 @@ +from hwprobe.core.common.pcie_link import build_pcie_link from hwprobe.models.gpu_models import AppleExtendedGPUInfo, GPUInfo, GraphicsInfo from hwprobe.models.size_models import Megabyte from hwprobe.models.status_models import StatusType @@ -65,13 +66,21 @@ def fetch_graphics_info() -> GraphicsInfo: if gpu.pci_path: module.pci_path = gpu.pci_path - # VRAM for non-Apple-Silicon GPUs + # VRAM & PCIe Link for non-Apple-Silicon GPUs # Apple Silicon GPUs can use the entire system memory as VRAM - if not gpu.is_apple_silicon and gpu.vram_mb: - module.vram = Megabyte(capacity=gpu.vram_mb) + if not gpu.is_apple_silicon: + module.pcie_link = build_pcie_link( + max_gen=gpu.capable_pcie_gen, + max_width=gpu.capable_pcie_width, + current_gen=gpu.negotiated_pcie_gen, + current_width=gpu.negotiated_pcie_width + ) + + if gpu.vram_mb: + module.vram = Megabyte(capacity=gpu.vram_mb) elif not gpu.is_apple_silicon: # Non-Apple Silicon GPU, and yet VRAM is reported as 0 MB. - graphics_info.status.make_partial(f"Could not get VRAM for non-Apple-Silicon GPU: {module.name}") + graphics_info.status.make_partial(f"Could not get VRAM/PCIe info for non-Apple-Silicon GPU: {module.name}") # Apple Silicon extended info if gpu.is_apple_silicon: diff --git a/src/hwprobe/interops/mac/bindings/gpu_info.py b/src/hwprobe/interops/mac/bindings/gpu_info.py index 2c5759e..814f4eb 100644 --- a/src/hwprobe/interops/mac/bindings/gpu_info.py +++ b/src/hwprobe/interops/mac/bindings/gpu_info.py @@ -44,11 +44,15 @@ class _GPUProperties(ctypes.Structure): ("name", ctypes.c_char * 256), ("vendor_id", ctypes.c_uint32), ("device_id", ctypes.c_uint32), - ("is_apple_silicon", ctypes.c_int), - ("apple_gpu", _AppleGPUProperties), ("acpi_path", ctypes.c_char * 512), ("pci_path", ctypes.c_char * 512), ("vram_mb", ctypes.c_uint64), + ("capable_pcie_gen", ctypes.c_uint32), + ("capable_pcie_width", ctypes.c_uint32), + ("negotiated_pcie_gen", ctypes.c_uint32), + ("negotiated_pcie_width", ctypes.c_uint32), + ("is_apple_silicon", ctypes.c_int), + ("apple_gpu", _AppleGPUProperties), ] @@ -82,11 +86,15 @@ class GPUProperties: name: str vendor_id: int device_id: int - is_apple_silicon: bool - apple_gpu: Optional[AppleGPUProperties] # None for non-Apple GPUs acpi_path: Optional[str] pci_path: Optional[str] vram_mb: int + capable_pcie_gen: int + capable_pcie_width: int + negotiated_pcie_gen: int + negotiated_pcie_width: int + is_apple_silicon: bool + apple_gpu: Optional[AppleGPUProperties] # None for non-Apple GPUs def __str__(self) -> str: lines = [ @@ -118,7 +126,7 @@ def get_gpu_info() -> list[GPUProperties]: if count < 0: raise RuntimeError("get_gpu_info() failed (C library returned -1)") - result = [] + result: list[GPUProperties] = [] for i in range(count): raw = buf[i] apple = None @@ -137,11 +145,15 @@ def get_gpu_info() -> list[GPUProperties]: name=raw.name.decode("utf-8", errors="replace"), vendor_id=raw.vendor_id, device_id=raw.device_id, - is_apple_silicon=bool(raw.is_apple_silicon), - apple_gpu=apple, acpi_path=acpi, pci_path=pci, vram_mb=raw.vram_mb, + capable_pcie_gen=raw.capable_pcie_gen, + capable_pcie_width=raw.capable_pcie_width, + negotiated_pcie_gen=raw.negotiated_pcie_gen, + negotiated_pcie_width=raw.negotiated_pcie_width, + is_apple_silicon=bool(raw.is_apple_silicon), + apple_gpu=apple, ) ) return result diff --git a/src/hwprobe/interops/mac/bindings/libdevice_info.dylib b/src/hwprobe/interops/mac/bindings/libdevice_info.dylib index 06cba71..3421754 100755 Binary files a/src/hwprobe/interops/mac/bindings/libdevice_info.dylib and b/src/hwprobe/interops/mac/bindings/libdevice_info.dylib differ diff --git a/src/hwprobe/interops/mac/include/gpu_info.h b/src/hwprobe/interops/mac/include/gpu_info.h index 3723228..eb77448 100644 --- a/src/hwprobe/interops/mac/include/gpu_info.h +++ b/src/hwprobe/interops/mac/include/gpu_info.h @@ -19,11 +19,19 @@ typedef struct { char name[256]; uint32_t vendor_id; uint32_t device_id; - int is_apple_silicon; - AppleGPUProperties apple_gpu; + + // non-apple-silicon properties char acpi_path[512]; char pci_path[512]; uint64_t vram_mb; + uint32_t capable_pcie_gen; + uint32_t capable_pcie_width; + uint32_t negotiated_pcie_gen; + uint32_t negotiated_pcie_width; + + // exclusively apple-silicon properties + int is_apple_silicon; + AppleGPUProperties apple_gpu; } GPUProperties; // Fills `out` with GPU entries. Returns number of GPUs found, or -1 on error. diff --git a/src/hwprobe/interops/mac/main.cpp b/src/hwprobe/interops/mac/main.cpp index 95f1ae2..090e131 100644 --- a/src/hwprobe/interops/mac/main.cpp +++ b/src/hwprobe/interops/mac/main.cpp @@ -34,6 +34,11 @@ int main() { std::cout << " PCI Path: " << g.pci_path << "\n"; if (g.vram_mb > 0) std::cout << " VRAM: " << g.vram_mb << " MB\n"; + + std::cout << " PCIe Gen M: " << g.capable_pcie_gen << "\n"; + std::cout << " PCIe Width M: " << g.capable_pcie_width << "\n"; + std::cout << " PCIe Gen S: " << g.negotiated_pcie_gen << "\n"; + std::cout << " PCIe Width S: " << g.negotiated_pcie_width << "\n"; std::cout << "\n"; } } diff --git a/src/hwprobe/interops/mac/src/gpu_info.cpp b/src/hwprobe/interops/mac/src/gpu_info.cpp index 27341ee..ea0591e 100644 --- a/src/hwprobe/interops/mac/src/gpu_info.cpp +++ b/src/hwprobe/interops/mac/src/gpu_info.cpp @@ -1,6 +1,7 @@ #include "gpu_info.h" #include "iokit_helpers.h" +#include #include #include #include @@ -10,6 +11,9 @@ #include #include +#define RESOLVE_PCIE_SPEED_PROPERTY(capability) (capability & 0xF) +#define RESOLVE_PCIE_WIDTH_PROPERTY(capability) ((capability >> 4) & 0x3F) + // ---- Internal helpers ---- static int getAppleGpuProperty(CFDictionaryRef gpuConfig, CFStringRef key) { @@ -180,6 +184,31 @@ static std::string parseAcpiPath(CFDictionaryRef props) { return result; } +static void processPCIeInformationForGpu(GPUProperties *gpu, uint32_t capabilities, uint32_t negotiated) { + gpu->capable_pcie_gen = RESOLVE_PCIE_SPEED_PROPERTY(capabilities); + gpu->capable_pcie_width = RESOLVE_PCIE_WIDTH_PROPERTY(capabilities); + + gpu->negotiated_pcie_gen = RESOLVE_PCIE_SPEED_PROPERTY(negotiated); + gpu->negotiated_pcie_width = RESOLVE_PCIE_WIDTH_PROPERTY(negotiated); +} + +static bool readRecursiveUInt32Property(io_service_t service, CFStringRef key, uint32_t *value) { + if (!service || !value) { + return false; + } + + CFTypeRef ref = IORegistryEntrySearchCFProperty( + service, kIOServicePlane, key, + kCFAllocatorDefault, kIORegistryIterateRecursively); + if (!ref) { + return false; + } + + *value = static_cast(readCFTypeAsUInt64(ref)); + CFRelease(ref); + return true; +} + // ---- VRAM helper ---- static uint64_t getDiscreteVramMB(io_service_t service) { @@ -245,6 +274,22 @@ int get_gpu_info(GPUProperties *out, int max_count) { gpu.vendor_id = readUInt32(props, CFSTR("vendor-id")); gpu.device_id = readUInt32(props, CFSTR("device-id")); + if (!is_arm) { + uint32_t capabilities = 0; + uint32_t negotiated = 0; + bool hasCapabilities = readRecursiveUInt32Property( + service, CFSTR("IOPCIExpressLinkCapabilities"), &capabilities); + bool hasNegotiated = readRecursiveUInt32Property( + service, CFSTR("IOPCIExpressLinkStatus"), &negotiated); + + std::cout << "Capable PCIe Link: " << capabilities << "\n"; + std::cout << "Negotiated PCIe Link: " << negotiated << "\n"; + + if (hasCapabilities && hasNegotiated) { + processPCIeInformationForGpu(&gpu, capabilities, negotiated); + } + } + CFTypeRef modelRef = CFDictionaryGetValue(props, CFSTR("model")); if (modelRef) { if (CFGetTypeID(modelRef) == CFStringGetTypeID()) { diff --git a/src/hwprobe/interops/mac/src/iokit_helpers.cpp b/src/hwprobe/interops/mac/src/iokit_helpers.cpp index cbc88f9..a110ccc 100644 --- a/src/hwprobe/interops/mac/src/iokit_helpers.cpp +++ b/src/hwprobe/interops/mac/src/iokit_helpers.cpp @@ -18,18 +18,40 @@ std::string readCFString(CFStringRef cfStr) { return {}; } -uint32_t readUInt32(const CFDictionaryRef dict, const CFStringRef key) { - const CFTypeRef ref = CFDictionaryGetValue(dict, key); - if (!ref || CFGetTypeID(ref) != CFDataGetTypeID()) +uint32_t readUInt32(CFDictionaryRef dict, CFStringRef key) { + CFTypeRef ref = CFDictionaryGetValue(dict, key); + if (!ref) { return 0; - uint32_t value = 0; - CFIndex len = CFDataGetLength(static_cast(ref)); - if (len > 0) { - CFDataGetBytes(static_cast(ref), - CFRangeMake(0, std::min(static_cast(sizeof(value)), len)), - reinterpret_cast(&value)); } - return value; + + CFTypeID typeId = CFGetTypeID(ref); + + if (typeId == CFNumberGetTypeID()) { + uint32_t value = 0; + if (CFNumberGetValue(static_cast(ref), kCFNumberSInt32Type, &value)) { + return value; + } + return 0; + } + + if (typeId == CFDataGetTypeID()) { + auto data = static_cast(ref); + CFIndex len = CFDataGetLength(data); + if (len <= 0) { + return 0; + } + + uint32_t value = 0; + CFDataGetBytes( + data, + CFRangeMake(0, std::min(len, sizeof(value))), + reinterpret_cast(&value) + ); + + return value; + } + + return 0; } uint64_t readCFTypeAsUInt64(CFTypeRef ref) { diff --git a/src/hwprobe/models/gpu_models.py b/src/hwprobe/models/gpu_models.py index 3ff85b8..009cf73 100644 --- a/src/hwprobe/models/gpu_models.py +++ b/src/hwprobe/models/gpu_models.py @@ -19,6 +19,17 @@ class AppleExtendedGPUInfo(BaseModel): gpu_gen: Optional[int] = None +class PCIeLinkValue(BaseModel): + """The max/current value distinction for a PCIe Link""" + max: Optional[int] = None + current: Optional[int] = None + +class PCIeLinkInfo(BaseModel): + """Information about the PCIe Link for this GPU (gen/width)""" + gen: Optional[PCIeLinkValue] = None + width: Optional[PCIeLinkValue] = None + + class GPUInfo(BaseModel): """Information for one GPU is stored here""" @@ -47,10 +58,7 @@ class GPUInfo(BaseModel): pci_path: Optional[str] = None #: Number of lanes that the GPU occupies on the PCIe bus. - pcie_width: Optional[int] = None - - #: PCIe generation supported by the GPU. - pcie_gen: Optional[int] = None + pcie_link: Optional[PCIeLinkInfo] = None #: Total VRAM available on the GPU. vram: Optional[StorageSize] = None