创建了表面之后我们接下来就可以在系统中选取一张显卡,事实上我们可以选取多张显卡并同时使用它们,但是在本文中我们只用一张显卡作为示范

还是一样,我们先添加一个私有成员变量

VkPhysicalDevice physicalDevice = VK_NULL_HANDLE;

然后同样添加一个成员函数 pickPhysicalDevice 用于选取物理设备,并在 initVulkan 函数中调用它

void initVulkan() {
    createInstance();
    pickPhysicalDevice();
}

接下来我们开始填充这个函数

void pickPhysicalDevice() {
    // 获取设备数量
    uint32_t deviceCount = 0;
    vkEnumeratePhysicalDevices(instance, &deviceCount, nullptr);
    if (deviceCount == 0)
        throw std::runtime_error("failed to find GPUs with Vulkan support!");
    // 获取所有可用的设备
    std::vector<VkPhysicalDevice> devices(deviceCount);
    vkEnumeratePhysicalDevices(instance, &deviceCount, devices.data());
}

可以看到上面的代码先是调用一个函数获取了物理设备的数量,然后根据设备数量开缓冲区,接着再次调用这个函数把设备获取出来扔进缓冲区里。这是 Vulkan 的特色,不要问我为什么,后面的文章中你们也会看到许多类似的用法

于是我们就可以遍历所有的物理设备,选一个合适的来进行后面的操作。简单起见,本文选择了一张集成显卡,感兴趣的可以选自己的独显试试

// 选取一个合适的设备
for (const auto &device : devices) {
    // 查询物理设备属性
    VkPhysicalDeviceProperties deviceProperties;
    vkGetPhysicalDeviceProperties(device, &deviceProperties);
    bool integratedGraphicsCard = deviceProperties.deviceType == VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU;
    // 选择集显
    if (integratedGraphicsCard) {
        physicalDevice = device;
        break;
    }
}

if (physicalDevice == VK_NULL_HANDLE)
    throw std::runtime_error("failed to find a suitable GPU!");

在遍历物理设备的寻找集显的时候,我们还需要检测显卡是否支持我们需要的功能

检测队列族

在此之前,我们先引入一个队列族的概念。Vulkan 中的所有操作都需要把命令提交到一个队列中,比如绘制、传输贴图等。然后会有许多个属于不同队列族的不同队列,并且一种队列族只会允许某一类命令。举个例子,可能有的队列族只允许提交计算命令而有的队列族只允许提交内存传递相关的命令

在这里,我们需要检测显卡是否具有支持图形命令的队列族,这类队列族可以传入顶点数据进行图形计算并将结果绘制到帧缓冲区中。

然后还需要检测显卡是否具有支持我们的窗体表面的队列族,我们可以通过这类队列族将帧缓冲区中的绘制结果通过窗体表面显示在屏幕上

而事实上,可能会有一个队列族同时支持这两种功能

简单起见,我们就直接寻找一个同时支持图形命令且支持我们的窗体表面的队列族,于是添加一个成员函数来获取它

std::optional<uint32_t> findQueueFamily(VkPhysicalDevice device) {
    // 获取所有的队列族
    uint32_t queueFamilyCount = 0;
    vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, nullptr);
    std::vector<VkQueueFamilyProperties> queueFamilies(queueFamilyCount);
    vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, queueFamilies.data());
    // 选取队列族
    for (int i = 0; i < queueFamilies.size(); i++) {
        // 支持表面
        VkBool32 presentSupport = false;
        vkGetPhysicalDeviceSurfaceSupportKHR(device, i, surface, &presentSupport);
        // 支持图形命令
        VkBool32 graphicsSupport = queueFamilies[i].queueFlags & VK_QUEUE_GRAPHICS_BIT;
        if (graphicsSupport && presentSupport)
            return i;
    }
    return std::optional<uint32_t>();
}

注意到 uint32_t 的任何取值都有可能是一个真实存在的队列族索引,因此我们使用 std::optional 来处理获取失败的情况

然后我们修改 pickPhysicalDevice 中遍历物理设备并选择集显的那部分代码,在其中尝试获取这个显卡支持的我们需要的队列族,并判断是否获取成功

这部分的完整代码如下

// 选取一个合适的设备
for (const auto &device : devices) {
    // 查询物理设备属性
    VkPhysicalDeviceProperties deviceProperties;
    vkGetPhysicalDeviceProperties(device, &deviceProperties);
    bool integratedGraphicsCard = deviceProperties.deviceType == VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU;
    // 查询合适的队列族
    std::optional<uint32_t> queueFamily = findQueueFamily(device);
    bool queueFamilySupported = queueFamily.has_value();
    // 选择具有合适的队列族的集显
    if (integratedGraphicsCard && queueFamilySupported) {
        physicalDevice = device;
        break;
    }
}

if (physicalDevice == VK_NULL_HANDLE)
    throw std::runtime_error("failed to find a suitable GPU!");

交换链支持

Vulkan 没有默认缓冲区这样的概念,因此我们在把图像展示到屏幕之前,需要一个东西来存放用于渲染这些图像的缓冲区。这个东西就是交换链,且它必须被显式的创建

并不是所有的显卡都能够把图像直接展示到屏幕上,比如有些显卡为服务器设计并不需要输出。因此我们需要查询显卡是否支持交换链扩展

此外,我们还需要查询一下交换链细节,包括物理设备支持的表面格式、表面展示模式

关于交换链的具体知识会在后续的文章中讨论,我们可以暂时不管

于是我们在之前的基础上继续修改这段代码
为了避免混乱,还是把修改过后的完整代码贴出来

// 选取一个合适的设备
for (const auto &device : devices) {
    // 查询物理设备属性
    VkPhysicalDeviceProperties deviceProperties;
    vkGetPhysicalDeviceProperties(device, &deviceProperties);
    bool integratedGraphicsCard = deviceProperties.deviceType == VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU;
    // 查询合适的队列族
    std::optional<uint32_t> queueFamily = findQueueFamily(device);
    bool queueFamilySupported = queueFamily.has_value();
    // 查询交换链支持
    std::string swapchainExtensionName = VK_KHR_SWAPCHAIN_EXTENSION_NAME;
    uint32_t extensionCount;
    vkEnumerateDeviceExtensionProperties(device, nullptr, &extensionCount, nullptr);
    std::vector<VkExtensionProperties> availableExtensions(extensionCount);
    vkEnumerateDeviceExtensionProperties(device, nullptr, &extensionCount, availableExtensions.data());
    bool swapchainExtensionSupported = false;
    for (auto properties : availableExtensions)
        if (properties.extensionName == swapchainExtensionName)
            swapchainExtensionSupported = true;
    bool swapchainSupported = false;
    if (swapchainExtensionSupported) {
        uint32_t formatCount;
        uint32_t presentModeCount;
        vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, nullptr);
        vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, nullptr);
        swapchainSupported = formatCount && presentModeCount;
    }
    // 选择具有合适的队列族且支持交换链的集显
    if (integratedGraphicsCard && queueFamilySupported && swapchainSupported) {
        physicalDevice = device;
        break;
    }
}

if (physicalDevice == VK_NULL_HANDLE)
    throw std::runtime_error("failed to find a suitable GPU!");

最后说一下物理设备会在 Vulkan 实例被销毁时隐式地销毁掉,因此我们不需要在 cleanup 中做任何操作

另外,可能有的读者会觉得随便选张显卡就行了,其实一般也就两块显卡,但直接进入下一步可能会因为显卡踩到巨坑,因此本文就还是简单说了一下显卡的支持检测