diff --git a/README.md b/README.md index d00c38c..644220e 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ View Metal GPU information from the command-line. $ metalgpu Index: 0 Name: Apple M1 Max - Registry ID: 4294969670 + Registry ID: 4294969661 Location: Built-in Characteristics: Unified Memory Features: @@ -20,16 +20,16 @@ Index: 0 Barycentric Coordinates: Supported Dynamic Libraries: Supported Function Pointers: Supported - Function Pointers from Render: Supported - Primitive Motion Blur: Supported - Pull Model Interopolation: Supported + Programmable Sample Position: Supported + Pull Model Interpolation: Supported Query Texture LOD: Supported - Ray Tracing: Supported - Ray Tracing from Render: Supported + Raster Order Groups: Supported + Shader Barycentric Coordinates: Supported Recommended Maximum Memory Size: 42.67 GB Max Buffer Length: 32 GB Max Threads per Thread Group: (Width: 1024, Height: 1024, Depth: 1024) Max Thread Group Memory Size: 32 KB + Sparse Tile Size: 16 KB ``` ## Usage diff --git a/Sources/metalgpu/MetalGpuInfo.swift b/Sources/metalgpu/MetalGpuInfo.swift new file mode 100644 index 0000000..afc84c3 --- /dev/null +++ b/Sources/metalgpu/MetalGpuInfo.swift @@ -0,0 +1,199 @@ +// +// MetalGpuInfo.swift +// metalgpu +// +// Created by Kenneth Endfinger on 11/15/21. +// + +import Foundation +import Metal + +struct MetalGpuInfo: Codable { + let index: Int? + let name: String + let registryID: UInt64 + let location: Location + let characteristics: [Characteristic] + let features: [Feature] + let maxTransferRateBytesPerSecond: UInt64? + let recommendedMaxMemorySizeBytes: UInt64? + let maxBufferLengthInBytes: UInt64? + let maxThreadGroupMemorySize: UInt64? + let sparseTileSizeInBytes: UInt64? + let maxThreadsPerThreadGroup: Size + + enum Location: String, Codable { + case builtIn = "Built-in" + case external = "External" + case slot = "Slot" + case unspecified = "Unspecified" + } + + enum Characteristic: String, Codable { + case lowPower = "Low Power" + case headless = "Headless" + case removable = "Removable" + case unifiedMemory = "Unified Memory" + } + + enum FeatureKey: String, Codable { + case rayTracing = "Ray Tracing" + case msaa32Bit = "32-Bit MSAA" + case dynamicLibraries = "Dynamic Libraries" + case functionPointers = "Function Pointers" + case queryTextureLOD = "Query Texture LOD" + case floatFiltering32Bit = "32-Bit Float Filtering" + case pullModelInterpolation = "Pull Model Interpolation" + case bcTextureCompression = "BC Texture Compression" + case shaderBarycentricCoordinates = "Shader Barycentric Coordinates" + case barycentricCoordinates = "Barycentric Coordinates" + case rasterOrderGroups = "Raster Order Groups" + case programmableSamplePosition = "Programmable Sample Position" + } + + struct Feature: Codable { + let name: String + let supported: Bool + + public init(name: String, supported: Bool) { + self.name = name + self.supported = supported + } + } + + struct Size: Codable { + let width: Int + let height: Int + let depth: Int + } +} + +@available(macOS 10.15, *) +extension MTLDeviceLocation { + func asMetalGpuLocation() -> MetalGpuInfo.Location { + switch self { + case .builtIn: return .builtIn + case .external: return .external + case .slot: return .slot + case .unspecified: return .unspecified + @unknown default: + fatalError("Unknown GPU Location") + } + } +} + +extension MTLDevice { + func collectMetalGpuInfo(_ index: Int? = nil) -> MetalGpuInfo { + var recommendedMaxMemorySize: UInt64? + if #available(macOS 10.12, *) { + recommendedMaxMemorySize = self.recommendedMaxWorkingSetSize + } + + var registryID: UInt64 = 0 + var maxThreadGroupMemorySize: UInt64? + if #available(macOS 10.13, *) { + registryID = self.registryID + maxThreadGroupMemorySize = UInt64(self.maxThreadgroupMemoryLength) + } + + var maxBufferMemorySize: UInt64? + if #available(macOS 10.14, *) { + maxBufferMemorySize = UInt64(self.maxBufferLength) + } + + var location: MetalGpuInfo.Location = .unspecified + var maxTransferRate: UInt64? + if #available(macOS 10.15, *) { + location = self.location.asMetalGpuLocation() + + if location != .builtIn { + maxTransferRate = self.maxTransferRate + } + } + + var sparseTileSizeInBytes: UInt64? + if #available(macOS 11.0, *) { + sparseTileSizeInBytes = UInt64(self.sparseTileSizeInBytes) + } + + return MetalGpuInfo( + index: index, + name: name, + registryID: registryID, + location: location, + characteristics: collectGpuCharacteristics(), + features: collectGpuFeatures().map { + MetalGpuInfo.Feature( + name: $0.key.rawValue, + supported: $0.value + ) + }.sorted { + $0.name.compare($1.name) == .orderedAscending + }, + maxTransferRateBytesPerSecond: maxTransferRate, + recommendedMaxMemorySizeBytes: recommendedMaxMemorySize, + maxBufferLengthInBytes: maxBufferMemorySize, + maxThreadGroupMemorySize: maxThreadGroupMemorySize, + sparseTileSizeInBytes: sparseTileSizeInBytes, + maxThreadsPerThreadGroup: maxThreadsPerThreadgroup.asMetalGpuSize() + ) + } + + func collectGpuFeatures() -> [MetalGpuInfo.FeatureKey: Bool] { + var features: [MetalGpuInfo.FeatureKey: Bool] = [:] + if #available(macOS 10.13, *) { + features[.rasterOrderGroups] = areRasterOrderGroupsSupported + features[.programmableSamplePosition] = areProgrammableSamplePositionsSupported + } + + if #available(macOS 10.15, *) { + features[.shaderBarycentricCoordinates] = supportsShaderBarycentricCoordinates + features[.barycentricCoordinates] = areBarycentricCoordsSupported + } + + if #available(macOS 11.0, *) { + features[.pullModelInterpolation] = supportsPullModelInterpolation + features[.bcTextureCompression] = supportsBCTextureCompression + features[.floatFiltering32Bit] = supports32BitFloatFiltering + features[.queryTextureLOD] = supportsQueryTextureLOD + features[.functionPointers] = supportsFunctionPointers + features[.dynamicLibraries] = supportsDynamicLibraries + features[.msaa32Bit] = supports32BitMSAA + } + return features + } + + func collectGpuCharacteristics() -> [MetalGpuInfo.Characteristic] { + var characteristics: [MetalGpuInfo.Characteristic] = [] + if isLowPower { + characteristics.append(.lowPower) + } + + if isHeadless { + characteristics.append(.headless) + } + + if #available(macOS 10.13, *) { + if isRemovable { + characteristics.append(.removable) + } + } + + if #available(macOS 10.15, *) { + if hasUnifiedMemory { + characteristics.append(.unifiedMemory) + } + } + return characteristics + } +} + +extension MTLSize { + func asMetalGpuSize() -> MetalGpuInfo.Size { + MetalGpuInfo.Size( + width: width, + height: height, + depth: depth + ) + } +} diff --git a/Sources/metalgpu/MetalGpuTool.swift b/Sources/metalgpu/MetalGpuTool.swift index b4d0400..b7f1961 100644 --- a/Sources/metalgpu/MetalGpuTool.swift +++ b/Sources/metalgpu/MetalGpuTool.swift @@ -21,6 +21,9 @@ struct MetalGpuTool: ParsableCommand { @Option(name: [.customShort("i"), .customLong("index")], help: "View GPU of Specified Index") var onlySelectedIndex: Int? + @Flag(name: [.customShort("j"), .customLong("json")], help: "Enable JSON Output") + var json: Bool = false + func run() throws { var gpus: [MTLDevice] = [] if isDefaultOnly { @@ -37,148 +40,63 @@ struct MetalGpuTool: ParsableCommand { gpus = [gpu] } - for (index, gpu) in gpus.enumerated() { - printGpuInfo(gpu, index: index) - if index != gpus.count - 1 { - print() - } - } - } - - func printGpuInfo(_ gpu: MTLDevice, index: Int? = nil) { - let characteristics = collectGpuCharacteristics(gpu) - let features = collectFeatureSupport(gpu).sorted(by: { - $0.key.compare($1.key) == .orderedAscending - }) - - if index != nil { - print("Index: \(index!)") - } - - print(" Name: \(gpu.name)") - if #available(macOS 10.13, *) { - print(" Registry ID: \(gpu.registryID)") - } - - if #available(macOS 10.15, *) { - print(" Location: \(locationAsString(gpu.location))") - } - print(" Characteristics: \(joinedOrEmpty(characteristics, "(None)"))") - print(" Features:") - for (name, supported) in features { - print(" \(name): \(supported ? "Supported" : "Unsupported")") - } - - if #available(macOS 10.15, *) { - if gpu.location != .builtIn { - print(" Max Transfer Rate: \(byteCountString(Int64(gpu.maxTransferRate)))/sec") - } - } - - if #available(macOS 10.12, *) { - print(" Recommended Maximum Memory Size: \(byteCountString(Int64(gpu.recommendedMaxWorkingSetSize)))") - } - - if #available(macOS 10.14, *) { - print(" Max Buffer Length: \(byteCountString(Int64(gpu.maxBufferLength)))") - } - - print(" Max Threads per Thread Group: \(sizeToString(gpu.maxThreadsPerThreadgroup))") - - if #available(macOS 10.13, *) { - print(" Max Thread Group Memory Size: \(byteCountString(Int64(gpu.maxThreadgroupMemoryLength)))") - } - - if #available(macOS 11.0, *) { - print(" Sparse Tile Size: \(byteCountString(Int64(gpu.sparseTileSizeInBytes)))") - } - } - - func collectGpuCharacteristics(_ gpu: MTLDevice) -> [String] { - var characteristics: [String] = [] - if gpu.isLowPower { - characteristics.append("Low Power") + let metalGpuInfos: [MetalGpuInfo] = gpus.enumerated().map { + $0.element.collectMetalGpuInfo($0.offset) } - if gpu.isHeadless { - characteristics.append("Headless") - } - - if #available(macOS 10.13, *) { - if gpu.isRemovable { - characteristics.append("Removable") - } - } - - if #available(macOS 10.15, *) { - if gpu.hasUnifiedMemory { - characteristics.append("Unified Memory") + if json { + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + let encoded = try encoder.encode(metalGpuInfos) + print(String(data: encoded, encoding: .utf8)!) + } else { + for gpu in metalGpuInfos { + printGpuInfo(gpu) + if gpu.index != gpus.count - 1 { + print() + } } } - return characteristics } - func collectFeatureSupport(_ gpu: MTLDevice) -> [String: Bool] { - var features: [String: Bool] = [:] - if #available(macOS 11.0, *) { - features["Ray Tracing"] = gpu.supportsRaytracing + func printGpuInfo(_ info: MetalGpuInfo) { + let features = info.features.sorted { + $0.name.compare($1.name) == .orderedAscending } - if #available(macOS 11.0, *) { - features["32-Bit MSAA"] = gpu.supports32BitMSAA + if let index = info.index { + print("Index: \(index)") } - if #available(macOS 11.0, *) { - features["Dynamic Libraries"] = gpu.supportsDynamicLibraries - } - - if #available(macOS 11.0, *) { - features["Function Pointers"] = gpu.supportsFunctionPointers - } - - if #available(macOS 11.0, *) { - features["Query Texture LOD"] = gpu.supportsQueryTextureLOD - } - - if #available(macOS 11.0, *) { - features["32-Bit Float Filtering"] = gpu.supports32BitFloatFiltering + print(" Name: \(info.name)") + print(" Registry ID: \(info.registryID)") + print(" Location: \(info.location.rawValue)") + print(" Characteristics: \(joinedOrEmpty(info.characteristics.map(\.rawValue), "(None)"))") + print(" Features:") + for feature in features { + print(" \(feature.name): \(feature.supported ? "Supported" : "Unsupported")") } - if #available(macOS 11.0, *) { - features["Pull Model Interopolation"] = gpu.supportsPullModelInterpolation + if let maxTransferRateBytesPerSecond = info.maxTransferRateBytesPerSecond { + print(" Max Transfer Rate: \(byteCountString(Int64(maxTransferRateBytesPerSecond)))/sec") } - if #available(macOS 11.0, *) { - features["BC Texture Compression"] = gpu.supportsBCTextureCompression + if let recommendedMaxMemorySizeBytes = info.recommendedMaxMemorySizeBytes { + print(" Recommended Maximum Memory Size: \(byteCountString(Int64(recommendedMaxMemorySizeBytes)))") } - if #available(macOS 10.15, *) { - features["Shader Barycentric Coordinates"] = gpu.supportsShaderBarycentricCoordinates + if let maxBufferLengthInBytes = info.maxBufferLengthInBytes { + print(" Max Buffer Length: \(byteCountString(Int64(maxBufferLengthInBytes)))") } - if #available(macOS 10.15, *) { - features["Barycentric Coordinates"] = gpu.areBarycentricCoordsSupported - } + print(" Max Threads per Thread Group: \(sizeToString(info.maxThreadsPerThreadGroup))") - if #available(macOS 10.13, *) { - features["Raster Order Groups"] = gpu.areRasterOrderGroupsSupported + if let maxThreadGroupMemorySize = info.maxThreadGroupMemorySize { + print(" Max Thread Group Memory Size: \(byteCountString(Int64(maxThreadGroupMemorySize)))") } - if #available(macOS 10.13, *) { - features["Programmable Sample Position"] = gpu.areProgrammableSamplePositionsSupported - } - return features - } - - @available(macOS 10.15, *) - func locationAsString(_ location: MTLDeviceLocation) -> String { - switch location { - case .builtIn: return "Built-in" - case .external: return "External" - case .slot: return "Slot" - case .unspecified: return "Unspecified" - @unknown default: - fatalError("Unknown GPU Location") + if let sparseTileSizeInBytes = info.sparseTileSizeInBytes { + print(" Sparse Tile Size: \(byteCountString(Int64(sparseTileSizeInBytes)))") } } @@ -194,7 +112,7 @@ struct MetalGpuTool: ParsableCommand { ByteCountFormatter.string(fromByteCount: value, countStyle: .binary) } - func sizeToString(_ value: MTLSize) -> String { + func sizeToString(_ value: MetalGpuInfo.Size) -> String { "(Width: \(value.width), Height: \(value.height), Depth: \(value.depth))" }