| // |
| // Copyright 2026 The ANGLE Project Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| // |
| // vulkan_icd_unittest.cpp: Unit tests for ChoosePhysicalDevice. |
| // |
| // ChoosePhysicalDevice takes the properties query as a function pointer and |
| // treats the device handles as opaque, so these tests run without a Vulkan |
| // loader, an ICD or a GPU. |
| // |
| |
| #include <gtest/gtest.h> |
| |
| #include <algorithm> |
| #include <limits> |
| #include <string_view> |
| #include <vector> |
| |
| #include "common/vulkan/vulkan_icd.h" |
| |
| namespace angle |
| { |
| namespace |
| { |
| // All MIG slices of one GPU report the same PCI IDs, which is what makes UUID |
| // matching the only way to tell them apart. |
| constexpr uint32_t kVendorID = 0x10DE; |
| constexpr uint32_t kDeviceID = 0x2900; |
| |
| constexpr uint32_t kOtherVendorID = 0x8086; |
| constexpr uint32_t kOtherDeviceID = 0x9A49; |
| |
| // With ICD::Default and ANGLE_PREFERRED_DEVICE unset, the ICD filter compares |
| // the empty string against the device name. An empty name would therefore |
| // match every device and the first device would always win. |
| constexpr char kDeviceName[] = "ANGLE Test Device"; |
| |
| // What GetFilterForICD looks for when ICD::SwiftShader is requested. Repeated |
| // here because those constants are private to vulkan_icd.cpp. |
| constexpr uint32_t kSwiftShaderVendorID = 0x1AE0; |
| constexpr uint32_t kSwiftShaderDeviceID = 0xC0DE; |
| constexpr char kSwiftShaderDeviceName[] = "SwiftShader Device"; |
| |
| constexpr size_t kNoDevice = std::numeric_limits<size_t>::max(); |
| |
| // A physical device as reported by the stub properties function. |
| struct TestDevice |
| { |
| uint32_t vendorID = kVendorID; |
| uint32_t deviceID = kDeviceID; |
| VkPhysicalDeviceType deviceType = VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU; |
| // Every byte of the UUIDs is set to the seed, so distinct seeds give |
| // distinct UUIDs. |
| uint8_t deviceUUIDSeed = 0; |
| uint8_t driverUUIDSeed = 0; |
| const char *deviceName = kDeviceName; |
| }; |
| |
| // vkGetPhysicalDeviceProperties2 carries no user data, so the device table has |
| // to be reachable from file scope. |
| const std::vector<TestDevice> *gDevices = nullptr; |
| |
| // Handles are opaque to ChoosePhysicalDevice, so an index biased by one serves |
| // as a handle; the bias keeps them distinct from VK_NULL_HANDLE. |
| VkPhysicalDevice HandleForIndex(size_t index) |
| { |
| return reinterpret_cast<VkPhysicalDevice>(index + 1); |
| } |
| |
| size_t IndexForHandle(VkPhysicalDevice handle) |
| { |
| return handle == VK_NULL_HANDLE ? kNoDevice : reinterpret_cast<uintptr_t>(handle) - 1; |
| } |
| |
| void FillUUID(uint8_t (&uuid)[VK_UUID_SIZE], uint8_t seed) |
| { |
| std::fill(std::begin(uuid), std::end(uuid), seed); |
| } |
| |
| VKAPI_ATTR void VKAPI_CALL GetPhysicalDeviceProperties2Stub(VkPhysicalDevice physicalDevice, |
| VkPhysicalDeviceProperties2 *properties) |
| { |
| const TestDevice &device = (*gDevices)[IndexForHandle(physicalDevice)]; |
| |
| properties->properties.apiVersion = vk::kMinimumVulkanAPIVersion; |
| properties->properties.vendorID = device.vendorID; |
| properties->properties.deviceID = device.deviceID; |
| properties->properties.deviceType = device.deviceType; |
| const std::string_view deviceName(device.deviceName); |
| std::fill(std::begin(properties->properties.deviceName), |
| std::end(properties->properties.deviceName), '\0'); |
| std::copy(deviceName.begin(), deviceName.end(), std::begin(properties->properties.deviceName)); |
| |
| for (VkBaseOutStructure *next = static_cast<VkBaseOutStructure *>(properties->pNext); |
| next != nullptr; next = next->pNext) |
| { |
| if (next->sType == VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ID_PROPERTIES) |
| { |
| VkPhysicalDeviceIDProperties *idProperties = |
| reinterpret_cast<VkPhysicalDeviceIDProperties *>(next); |
| FillUUID(idProperties->deviceUUID, device.deviceUUIDSeed); |
| FillUUID(idProperties->driverUUID, device.driverUUIDSeed); |
| } |
| } |
| } |
| |
| class ChoosePhysicalDeviceTest : public testing::Test |
| { |
| protected: |
| void TearDown() override { gDevices = nullptr; } |
| |
| // Returns the index of the device ChoosePhysicalDevice settled on. |
| size_t choose(uint32_t preferredVendorID, |
| uint32_t preferredDeviceID, |
| const uint8_t *preferredDeviceUUID) |
| { |
| gDevices = &mDevices; |
| |
| std::vector<VkPhysicalDevice> handles; |
| for (size_t index = 0; index < mDevices.size(); ++index) |
| { |
| handles.push_back(HandleForIndex(index)); |
| } |
| |
| VkPhysicalDevice chosen = VK_NULL_HANDLE; |
| VkPhysicalDeviceProperties2 properties = {}; |
| VkPhysicalDeviceIDProperties idProperties = {}; |
| VkPhysicalDeviceDriverProperties driverProperties = {}; |
| |
| vk::ChoosePhysicalDevice(GetPhysicalDeviceProperties2Stub, handles, mICD, preferredVendorID, |
| preferredDeviceID, preferredDeviceUUID, nullptr, |
| static_cast<VkDriverId>(0), &chosen, &properties, &idProperties, |
| &driverProperties); |
| |
| return IndexForHandle(chosen); |
| } |
| |
| std::vector<TestDevice> mDevices; |
| vk::ICD mICD = vk::ICD::Default; |
| }; |
| |
| // A UUID names one exact device, so a PCI match on an earlier device must not |
| // preempt it. This is the MIG case: every slice carries the same PCI IDs. |
| TEST_F(ChoosePhysicalDeviceTest, UUIDMatchIsNotPreemptedByEarlierPCIMatch) |
| { |
| mDevices = { |
| TestDevice{kVendorID, kDeviceID, VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU, 1, 0}, |
| TestDevice{kVendorID, kDeviceID, VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU, 2, 0}, |
| TestDevice{kVendorID, kDeviceID, VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU, 3, 0}, |
| }; |
| |
| uint8_t requestedUUID[VK_UUID_SIZE]; |
| FillUUID(requestedUUID, 3); |
| |
| EXPECT_EQ(2u, choose(kVendorID, kDeviceID, requestedUUID)); |
| } |
| |
| // The ICD filter names one device too, whether through ANGLE_PREFERRED_DEVICE |
| // or through a request for the mock or SwiftShader device, so a PCI match on |
| // an earlier device must not preempt it either. |
| TEST_F(ChoosePhysicalDeviceTest, ICDFilterMatchIsNotPreemptedByEarlierPCIMatch) |
| { |
| mDevices = { |
| TestDevice{kVendorID, kDeviceID, VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU, 1, 0, kDeviceName}, |
| TestDevice{kSwiftShaderVendorID, kSwiftShaderDeviceID, VK_PHYSICAL_DEVICE_TYPE_CPU, 2, 0, |
| kSwiftShaderDeviceName}, |
| }; |
| mICD = vk::ICD::SwiftShader; |
| |
| EXPECT_EQ(1u, choose(kVendorID, kDeviceID, nullptr)); |
| } |
| |
| // Without a UUID, the PCI IDs still select the first device that matches them. |
| TEST_F(ChoosePhysicalDeviceTest, PCIIDsSelectTheFirstMatchingDevice) |
| { |
| mDevices = { |
| TestDevice{kOtherVendorID, kOtherDeviceID, VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU, 1, 0}, |
| TestDevice{kVendorID, kDeviceID, VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU, 2, 0}, |
| }; |
| |
| EXPECT_EQ(1u, choose(kVendorID, kDeviceID, nullptr)); |
| } |
| |
| // Without PCI IDs, the UUID selects its device wherever it sits. |
| TEST_F(ChoosePhysicalDeviceTest, UUIDSelectsItsDevice) |
| { |
| mDevices = { |
| TestDevice{kVendorID, kDeviceID, VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU, 1, 0}, |
| TestDevice{kVendorID, kDeviceID, VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU, 2, 0}, |
| }; |
| |
| uint8_t requestedUUID[VK_UUID_SIZE]; |
| FillUUID(requestedUUID, 2); |
| |
| EXPECT_EQ(1u, choose(0, 0, requestedUUID)); |
| } |
| |
| // An unmatched UUID falls back to the first discrete device, as the extension |
| // permits. Pinned here so that the precedence change above does not quietly |
| // turn into a policy change. |
| TEST_F(ChoosePhysicalDeviceTest, UnmatchedUUIDFallsBackToTheFirstDiscreteDevice) |
| { |
| mDevices = { |
| TestDevice{kVendorID, kDeviceID, VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU, 1, 0}, |
| TestDevice{kVendorID, kDeviceID, VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU, 2, 0}, |
| }; |
| |
| uint8_t requestedUUID[VK_UUID_SIZE]; |
| FillUUID(requestedUUID, 9); |
| |
| EXPECT_EQ(1u, choose(0, 0, requestedUUID)); |
| } |
| } // namespace |
| } // namespace angle |