物理设备和队列族
选择物理设备
在通过 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++;
}
太棒了,这就是我们现在找到合适的物理设备所需的一切!下一步是创建逻辑设备以与之交互。