Metal: use the IORegistry to get the PCI ids

MTLDevice.registryID points to an entry in the IORegistry from which we
are able to find the entry for the actual GPU device, which contains
properties for the PCI device and vendor ids.

But MTLDevice.registryID is introduced from macOS 10.13. For macOS <=
10.12, we get the PCI ids by matching them with MTLDevice.name.

BUG=dawn:120, dawn:184

Change-Id: If4040c623fc61115c88d453eee0f2d05ed17a546
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/8920
Reviewed-by: Corentin Wallez <cwallez@chromium.org>
Reviewed-by: Austin Eng <enga@chromium.org>
Commit-Queue: Hao Li <hao.x.li@intel.com>
diff --git a/src/dawn_native/metal/BackendMTL.mm b/src/dawn_native/metal/BackendMTL.mm
index 7e1be79..e09976d 100644
--- a/src/dawn_native/metal/BackendMTL.mm
+++ b/src/dawn_native/metal/BackendMTL.mm
@@ -14,76 +14,53 @@
 
 #include "dawn_native/metal/BackendMTL.h"
 
+#include "common/Constants.h"
 #include "dawn_native/Instance.h"
 #include "dawn_native/MetalBackend.h"
 #include "dawn_native/metal/DeviceMTL.h"
 
-#include <IOKit/graphics/IOGraphicsLib.h>
+#import <IOKit/IOKitLib.h>
 
 namespace dawn_native { namespace metal {
 
     namespace {
-        // Since CGDisplayIOServicePort was deprecated in macOS 10.9, we need create
-        // an alternative function for getting I/O service port from current display.
-        io_service_t GetDisplayIOServicePort() {
-            // The matching service port (or 0 if none can be found)
-            io_service_t servicePort = 0;
+        struct PCIIDs {
+            uint32_t vendorId;
+            uint32_t deviceId;
+        };
 
-            // Create matching dictionary for display service
-            CFMutableDictionaryRef matchingDict = IOServiceMatching("IODisplayConnect");
-            if (matchingDict == nullptr) {
-                return 0;
-            }
+        struct Vendor {
+            const char* trademark;
+            uint32_t vendorId;
+        };
 
-            io_iterator_t iter;
-            // IOServiceGetMatchingServices look up the default master ports that match a
-            // matching dictionary, and will consume the reference on the matching dictionary,
-            // so we don't need to release the dictionary, but the iterator handle should
-            // be released when its iteration is finished.
-            if (IOServiceGetMatchingServices(kIOMasterPortDefault, matchingDict, &iter) !=
-                kIOReturnSuccess) {
-                return 0;
-            }
+        const Vendor kVendors[] = {{"AMD", kVendorID_AMD},
+                                   {"Radeon", kVendorID_AMD},
+                                   {"Intel", kVendorID_Intel},
+                                   {"Geforce", kVendorID_Nvidia},
+                                   {"Quadro", kVendorID_Nvidia}};
 
-            // Vendor number and product number of current main display
-            const uint32_t displayVendorNumber = CGDisplayVendorNumber(kCGDirectMainDisplay);
-            const uint32_t displayProductNumber = CGDisplayModelNumber(kCGDirectMainDisplay);
-
-            io_service_t serv;
-            while ((serv = IOIteratorNext(iter)) != IO_OBJECT_NULL) {
-                CFDictionaryRef displayInfo =
-                    IODisplayCreateInfoDictionary(serv, kIODisplayOnlyPreferredName);
-
-                CFNumberRef vendorIDRef, productIDRef;
-                Boolean success;
-                // The ownership of CF object follows the 'Get Rule', we don't need to
-                // release these values
-                success = CFDictionaryGetValueIfPresent(displayInfo, CFSTR(kDisplayVendorID),
-                                                        (const void**)&vendorIDRef);
-                success &= CFDictionaryGetValueIfPresent(displayInfo, CFSTR(kDisplayProductID),
-                                                         (const void**)&productIDRef);
-                if (success) {
-                    CFIndex vendorID = 0, productID = 0;
-                    CFNumberGetValue(vendorIDRef, kCFNumberSInt32Type, &vendorID);
-                    CFNumberGetValue(productIDRef, kCFNumberSInt32Type, &productID);
-
-                    if (vendorID == displayVendorNumber && productID == displayProductNumber) {
-                        // Check if vendor id and product id match with current display's
-                        // If it does, we find the desired service port
-                        servicePort = serv;
-                        CFRelease(displayInfo);
-                        break;
-                    }
+        // Find vendor ID from MTLDevice name.
+        MaybeError GetVendorIdFromVendors(id<MTLDevice> device, PCIIDs* ids) {
+            uint32_t vendorId = 0;
+            const char* deviceName = [device.name UTF8String];
+            for (const auto& it : kVendors) {
+                if (strstr(deviceName, it.trademark) != nullptr) {
+                    vendorId = it.vendorId;
+                    break;
                 }
-
-                CFRelease(displayInfo);
-                IOObjectRelease(serv);
             }
-            IOObjectRelease(iter);
-            return servicePort;
+
+            if (vendorId == 0) {
+                return DAWN_CONTEXT_LOST_ERROR("Failed to find vendor id with the device");
+            }
+
+            // Set vendor id with 0
+            *ids = PCIIDs{vendorId, 0};
+            return {};
         }
 
-        // Get integer property from registry entry.
+        // Extracts an integer property from a registry entry.
         uint32_t GetEntryProperty(io_registry_entry_t entry, CFStringRef name) {
             uint32_t value = 0;
 
@@ -93,19 +70,82 @@
                 entry, kIOServicePlane, name, kCFAllocatorDefault,
                 kIORegistryIterateRecursively | kIORegistryIterateParents));
 
-            if (data != nullptr) {
-                const uint32_t* valuePtr =
-                    reinterpret_cast<const uint32_t*>(CFDataGetBytePtr(data));
-                if (valuePtr) {
-                    value = *valuePtr;
-                }
-
-                CFRelease(data);
+            if (data == nullptr) {
+                return value;
             }
 
+            // CFDataGetBytePtr() is guaranteed to return a read-only pointer
+            value = *reinterpret_cast<const uint32_t*>(CFDataGetBytePtr(data));
+
+            CFRelease(data);
             return value;
         }
 
+        // Queries the IO Registry to find the PCI device and vendor IDs of the MTLDevice.
+        // The registry entry correponding to [device registryID] doesn't contain the exact PCI ids
+        // because it corresponds to a driver. However its parent entry corresponds to the device
+        // itself and has uint32_t "device-id" and "registry-id" keys. For example on a dual-GPU
+        // MacBook Pro 2017 the IORegistry explorer shows the following tree (simplified here):
+        //
+        //  - PCI0@0
+        //  | - AppleACPIPCI
+        //  | | - IGPU@2 (type IOPCIDevice)
+        //  | | | - IntelAccelerator (type IOGraphicsAccelerator2)
+        //  | | - PEG0@1
+        //  | | | - IOPP
+        //  | | | | - GFX0@0 (type IOPCIDevice)
+        //  | | | | | - AMDRadeonX4000_AMDBaffinGraphicsAccelerator (type IOGraphicsAccelerator2)
+        //
+        // [device registryID] is the ID for one of the IOGraphicsAccelerator2 and we can see that
+        // their parent always is an IOPCIDevice that has properties for the device and vendor IDs.
+        MaybeError GetDeviceIORegistryPCIInfo(id<MTLDevice> device, PCIIDs* ids) {
+            // Get a matching dictionary for the IOGraphicsAccelerator2
+            CFMutableDictionaryRef matchingDict = IORegistryEntryIDMatching([device registryID]);
+            if (matchingDict == nullptr) {
+                return DAWN_CONTEXT_LOST_ERROR("Failed to create the matching dict for the device");
+            }
+
+            // IOServiceGetMatchingService will consume the reference on the matching dictionary,
+            // so we don't need to release the dictionary.
+            io_registry_entry_t acceleratorEntry =
+                IOServiceGetMatchingService(kIOMasterPortDefault, matchingDict);
+            if (acceleratorEntry == IO_OBJECT_NULL) {
+                return DAWN_CONTEXT_LOST_ERROR(
+                    "Failed to get the IO registry entry for the accelerator");
+            }
+
+            // Get the parent entry that will be the IOPCIDevice
+            io_registry_entry_t deviceEntry = IO_OBJECT_NULL;
+            if (IORegistryEntryGetParentEntry(acceleratorEntry, kIOServicePlane, &deviceEntry) !=
+                kIOReturnSuccess) {
+                IOObjectRelease(acceleratorEntry);
+                return DAWN_CONTEXT_LOST_ERROR(
+                    "Failed to get the IO registry entry for the device");
+            }
+
+            ASSERT(deviceEntry != IO_OBJECT_NULL);
+            IOObjectRelease(acceleratorEntry);
+
+            uint32_t vendorId = GetEntryProperty(deviceEntry, CFSTR("vendor-id"));
+            uint32_t deviceId = GetEntryProperty(deviceEntry, CFSTR("device-id"));
+
+            *ids = PCIIDs{vendorId, deviceId};
+
+            IOObjectRelease(deviceEntry);
+
+            return {};
+        }
+
+        MaybeError GetDevicePCIInfo(id<MTLDevice> device, PCIIDs* ids) {
+            // [device registryID] is introduced on macOS 10.13+, otherwise workaround to get vendor
+            // id by vendor name on old macOS
+            if ([NSProcessInfo.processInfo isOperatingSystemAtLeastVersion:{10, 13, 0}]) {
+                return GetDeviceIORegistryPCIInfo(device, ids);
+            } else {
+                return GetVendorIdFromVendors(device, ids);
+            }
+        }
+
         bool IsMetalSupported() {
             // Metal was first introduced in macOS 10.11
             NSOperatingSystemVersion macOS10_11 = {10, 11, 0};
@@ -120,16 +160,12 @@
         Adapter(InstanceBase* instance, id<MTLDevice> device)
             : AdapterBase(instance, BackendType::Metal), mDevice([device retain]) {
             mPCIInfo.name = std::string([mDevice.name UTF8String]);
-            // Gather the PCI device and vendor IDs based on which device is rendering to the
-            // main display. This is obviously wrong for systems with multiple devices.
-            // TODO(cwallez@chromium.org): Once Chromium has the macOS 10.13 SDK rolled, we
-            // should use MTLDevice.registryID to gather the information.
-            io_registry_entry_t entry = GetDisplayIOServicePort();
-            if (entry != IO_OBJECT_NULL) {
-                mPCIInfo.vendorId = GetEntryProperty(entry, CFSTR("vendor-id"));
-                mPCIInfo.deviceId = GetEntryProperty(entry, CFSTR("device-id"));
-                IOObjectRelease(entry);
-            }
+
+            PCIIDs ids;
+            if (!instance->ConsumedError(GetDevicePCIInfo(device, &ids))) {
+                mPCIInfo.vendorId = ids.vendorId;
+                mPCIInfo.deviceId = ids.deviceId;
+            };
 
             if ([device isLowPower]) {
                 mDeviceType = DeviceType::IntegratedGPU;