Drawing a triangle/Setup/Physical devices and queue families

文章介绍了在Vulkan应用开发中如何选择合适的物理设备,包括基于VkInstance初始化后列举设备,通过`pickPhysicalDevice`函数选择第一款适合的设备,以及进行设备适配性检查。适配性检查涉及设备类型、Vulkan版本、可选功能等。此外,还提到了队列家族的概念,强调了寻找支持图形命令的队列家族的重要性。
摘要由CSDN通过智能技术生成

目录

Selecting a physical device

Base device suitability checks

Queue families


Selecting a physical device

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

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

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

void pickPhysicalDevice() {

}

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

VkPhysicalDevice physicalDevice = VK_NULL_HANDLE;

列出显卡与列出扩展插件非常相似,并从查询数字开始。

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

如果有0个设备支持Vulkan,那么就没有必要再进一步了。

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!");
}

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

Base device suitability checks

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

VkPhysicalDeviceProperties deviceProperties;
vkGetPhysicalDeviceProperties(device, &deviceProperties);

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

VkPhysicalDeviceFeatures deviceFeatures;
vkGetPhysicalDeviceFeatures(device, &deviceFeatures);

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

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

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,则会退回到集成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;
}

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

Queue families

之前曾有人简要提到,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++;
}

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

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++;
}

太好了,这就是我们现在所需要的找到正确的物理设备!下一步是创建一个逻辑设备与之连接。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值