物理设备和队列族

选择物理设备

通过 VkInstance 初始化 Vulkan 库后,我们需要查找并选择系统中支持我们所需功能的显卡。 实际上,我们可以选择任意数量的显卡并同时使用它们,但在本教程中,我们将只选择第一块满足我们需求的显卡。

我们将添加一个函数 pickPhysicalDevice,并在 initVulkan 函数中添加对其的调用。

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

void pickPhysicalDevice() {

}

我们最终选择的显卡将存储在 VkPhysicalDevice 句柄中,该句柄将添加为新的类成员。 此对象将在销毁 VkInstance 时隐式销毁,因此我们不需要在 cleanup 函数中执行任何新操作。

VkPhysicalDevice physicalDevice = VK_NULL_HANDLE;

列出显卡与列出扩展非常相似,首先查询数量。

uint32_t deviceCount = 0;
vkEnumeratePhysicalDevices(instance, &deviceCount, nullptr);

如果 Vulkan 支持的设备数为 0,则没有必要再继续。

if (deviceCount == 0) {
    throw std::runtime_error("failed to find GPUs with Vulkan support!");
}

否则,我们现在可以分配一个数组来保存所有 VkPhysicalDevice 句柄。

std::vector<VkPhysicalDevice> devices(deviceCount);
vkEnumeratePhysicalDevices(instance, &deviceCount, devices.data());

现在我们需要评估每个设备,并检查它们是否适合我们想要执行的操作,因为并非所有显卡都是相同的。为此,我们将引入一个新函数

bool isDeviceSuitable(VkPhysicalDevice device) {
    return true;
}

我们将检查是否有任何物理设备满足我们将添加到该函数中的要求。

for (const auto& device : devices) {
    if (isDeviceSuitable(device)) {
        physicalDevice = device;
        break;
    }
}

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

下一节将介绍我们将在 isDeviceSuitable 函数中检查的第一个要求。随着我们在后面的章节中开始使用更多的 Vulkan 功能,我们还将扩展此函数以包含更多检查。

基本设备适用性检查

为了评估设备的适用性,我们可以首先查询一些详细信息。可以使用 vkGetPhysicalDeviceProperties 查询基本设备属性,如名称、类型和支持的 Vulkan 版本。

VkPhysicalDeviceProperties deviceProperties;
vkGetPhysicalDeviceProperties(device, &deviceProperties);

可以使用 vkGetPhysicalDeviceFeatures 查询对纹理压缩、64 位浮点数和多视口渲染(对 VR 有用)等可选功能的支持情况

VkPhysicalDeviceFeatures deviceFeatures;
vkGetPhysicalDeviceFeatures(device, &deviceFeatures);

还有更多可以从设备查询的详细信息,我们将在以后讨论有关设备内存和队列族(请参阅下一节)。

例如,假设我们认为我们的应用程序只适用于支持几何着色器的独立显卡。那么 isDeviceSuitable 函数将如下所示

bool isDeviceSuitable(VkPhysicalDevice device) {
    VkPhysicalDeviceProperties deviceProperties;
    VkPhysicalDeviceFeatures deviceFeatures;
    vkGetPhysicalDeviceProperties(device, &deviceProperties);
    vkGetPhysicalDeviceFeatures(device, &deviceFeatures);

    return deviceProperties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU &&
           deviceFeatures.geometryShader;
}

您也可以为每个设备评分并选择得分最高的设备,而不是仅仅检查设备是否适用并选择第一个设备。这样,您可以通过给它更高的分数来偏爱独立显卡,但如果它是唯一可用的显卡,则回退到集成 GPU。您可以按如下方式实现类似的功能

#include <map>

...

void pickPhysicalDevice() {
    ...

    // Use an ordered map to automatically sort candidates by increasing score
    std::multimap<int, VkPhysicalDevice> candidates;

    for (const auto& device : devices) {
        int score = rateDeviceSuitability(device);
        candidates.insert(std::make_pair(score, device));
    }

    // Check if the best candidate is suitable at all
    if (candidates.rbegin()->first > 0) {
        physicalDevice = candidates.rbegin()->second;
    } else {
        throw std::runtime_error("failed to find a suitable GPU!");
    }
}

int rateDeviceSuitability(VkPhysicalDevice device) {
    ...

    int score = 0;

    // Discrete GPUs have a significant performance advantage
    if (deviceProperties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) {
        score += 1000;
    }

    // Maximum possible size of textures affects graphics quality
    score += deviceProperties.limits.maxImageDimension2D;

    // Application can't function without geometry shaders
    if (!deviceFeatures.geometryShader) {
        return 0;
    }

    return score;
}

您无需为本教程实现所有这些,但这只是为了让您了解如何设计设备选择过程。当然,您也可以只显示选项的名称,并允许用户选择。

因为我们才刚刚开始,Vulkan 支持是我们唯一需要的东西,因此我们将只选择任何 GPU

bool isDeviceSuitable(VkPhysicalDevice device) {
    return true;
}

在下一节中,我们将讨论要检查的第一个真正需要的功能。

队列族

之前已经简要提及,Vulkan 中几乎每个操作,从绘制到上传纹理,都需要将命令提交到队列。有不同类型的队列来自不同的队列族,并且每个队列族只允许命令的一个子集。例如,可能有一个队列族只允许处理计算命令,或者一个只允许与内存传输相关的命令。

我们需要检查设备支持哪些队列族,以及其中哪些队列族支持我们要使用的命令。为此,我们将添加一个新函数 findQueueFamilies 来查找我们需要的所有队列族。

现在我们只查找支持图形命令的队列,因此该函数可能如下所示

uint32_t findQueueFamilies(VkPhysicalDevice device) {
    // Logic to find graphics queue family
}

但是,在接下来的章节中,我们将查找另一个队列,因此最好为此做好准备,并将索引捆绑到一个结构中

struct QueueFamilyIndices {
    uint32_t graphicsFamily;
};

QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device) {
    QueueFamilyIndices indices;
    // Logic to find queue family indices to populate struct with
    return indices;
}

但是,如果队列族不可用怎么办? 我们可以在 findQueueFamilies 中抛出一个异常,但是这个函数并不是做出关于设备适用性决定的合适位置。 例如,我们可能更喜欢具有专用传输队列族的设备,但不是必须的。 因此,我们需要某种方式来指示是否找到了特定的队列族。

实际上,使用一个魔法值来表示队列族不存在是不可能的,因为理论上任何 uint32_t 的值都可能是一个有效的队列族索引,包括 0。幸运的是,C++17 引入了一种数据结构来区分值存在或不存在的情况。

#include <optional>

...

std::optional<uint32_t> graphicsFamily;

std::cout << std::boolalpha << graphicsFamily.has_value() << std::endl; // false

graphicsFamily = 0;

std::cout << std::boolalpha << graphicsFamily.has_value() << std::endl; // true

std::optional 是一个包装器,在您向其赋值之前,它不包含任何值。在任何时候,您都可以通过调用其 has_value() 成员函数来查询它是否包含值。这意味着我们可以将逻辑更改为:

#include <optional>

...

struct QueueFamilyIndices {
    std::optional<uint32_t> graphicsFamily;
};

QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device) {
    QueueFamilyIndices indices;
    // Assign index to queue families that could be found
    return indices;
}

现在我们可以开始实际实现 findQueueFamilies 了。

QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device) {
    QueueFamilyIndices indices;

    ...

    return indices;
}

检索队列族列表的过程正如您所预期的那样,并且使用了 vkGetPhysicalDeviceQueueFamilyProperties

uint32_t queueFamilyCount = 0;
vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, nullptr);

std::vector<VkQueueFamilyProperties> queueFamilies(queueFamilyCount);
vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, queueFamilies.data());

VkQueueFamilyProperties 结构体包含有关队列族的一些详细信息,包括支持的操作类型以及基于该族可以创建的队列数量。我们需要找到至少一个支持 VK_QUEUE_GRAPHICS_BIT 的队列族。

int i = 0;
for (const auto& queueFamily : queueFamilies) {
    if (queueFamily.queueFlags & VK_QUEUE_GRAPHICS_BIT) {
        indices.graphicsFamily = i;
    }

    i++;
}

现在我们有了这个奇特的队列族查找函数,我们可以在 isDeviceSuitable 函数中使用它作为检查,以确保设备可以处理我们想要使用的命令。

bool isDeviceSuitable(VkPhysicalDevice device) {
    QueueFamilyIndices indices = findQueueFamilies(device);

    return indices.graphicsFamily.has_value();
}

为了使这更方便一些,我们还将在结构体本身中添加一个通用检查。

struct QueueFamilyIndices {
    std::optional<uint32_t> graphicsFamily;

    bool isComplete() {
        return graphicsFamily.has_value();
    }
};

...

bool isDeviceSuitable(VkPhysicalDevice device) {
    QueueFamilyIndices indices = findQueueFamilies(device);

    return indices.isComplete();
}

我们现在也可以使用它来提前从 findQueueFamilies 中退出。

for (const auto& queueFamily : queueFamilies) {
    ...

    if (indices.isComplete()) {
        break;
    }

    i++;
}

太棒了,这就是我们现在找到合适的物理设备所需的一切!下一步是创建逻辑设备来与之交互。