跳到主要内容

物理设备和队列族

选择物理设备

创建VkInstance后,我们需要查询系统中的显卡设备,并选择一个满足我们需求的设备进行使用。Vulkan允许我们选择任意数量的显卡设备并同时使用它们,但本教程只使用第一个满足我们需求的显卡设备。

首先添加pickPhysicalDevice函数,并在initVulkan函数中调用它:

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

void pickPhysicalDevice() {

}

我们使用VkPhysicalDevice对象来存储选择的显卡,这个对象在VkInstance销毁时自动被销毁,所以我们不需要在cleanup函数中对它进行清除。

VkPhysicalDevice physicalDevice = VK_NULL_HANDLE;

查询显卡列表和查询扩展列表的操作类似,首先需要查询显卡的数量:

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

如果可用的显卡设备数量为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函数进行查询。

VkPhysicalDeviceProperties deviceProperties;
vkGetPhysicalDeviceProperties(device, &deviceProperties);

纹理压缩、64位浮点和多视口渲染(常用于VR)等特性的支持情况可以通过vkGetPhysicalDeviceFeatures函数查询:

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

除了直接选择第一个满足需求的设备,一种更好的方法是给每一个满足需求的设备,按照特性加权打分,选择分数最高的设备。具体可以这样做:

#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,显然设备列表中的所有设备都满足:

bool isDeviceSuitable(VkPhysicalDevice device) {
return true;
}

在下一节,我们将介绍要检查的第一个需要的特征。

队列族

之前简要提过,几乎所有的Vulkan操作,从绘制到上传纹理,都需要将操作指令提交给一个队列,然后才能执行。Vulkan有多种不同类型的队列,它们属于不同的队列族,每个队列族的队列只允许执行特定的指令的子集。比如,可能存在只允许执行计算相关指令的队列族和只允许执行内存传输相关指令的队列族。

我们需要检测设备支持的队列族,以及哪些队列族支持我们需要使用的指令。为了完成这一目的,我们添加了一个findQueueFamilies函数,这一函数会查找出满足我们需求的队列族。目前而言,我们需要的队列族只需要支持图形指令即可,但在之后的章节,我们可能会有更多的需求。

这一函数会返回满足需求的队列族的索引。这里,我们使用了一个结构体来作为函数的返回结果,索引-1表示没有找到满足需求的队列族:

struct QueueFamilyIndices {
int graphicsFamily = -1;

bool isComplete() {
return graphicsFamily >= 0;
}
};

findQueueFamilies函数的实现:

QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device) {
QueueFamilyIndices indices;

...

return indices;
}

我们首先获取设备的队列族个数,然后分配数组存储队列族VkQueueFamilyProperties对象:

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.queueCount > 0 && queueFamily.queueFlags & VK_QUEUE_GRAPHICS_BIT) {
indices.graphicsFamily = i;
}

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

i++;
}

现在,我们可以在isDeviceSuitable函数中调用它来确保我们选择的设备可以执行我们需要的指令:

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

return indices.isComplete();
}

太好了!我们已经完成了查找合适的物理设备这一工作,接下来,让我们创建逻辑设备来使用它!

本章节代码:C++代码