[Vulkan教程]绘制一个三角形/设置/物理设备和队列簇(Physical devices and queue families)

选择一个物理设备

通过VkInstance初始化Vulkan库之后,我们需要在系统中查找和选择一个支持我们所需特性的显卡。我们可以选择任意数量的显卡同时使用,但在本教程中,我们只用一张。

我们创建一个函数pickPhysicalDevice,并在initVulkan函数中调用。

  1. void initVulkan() {
  2. createInstance();
  3. setupDebugMessenger();
  4. pickPhysicalDevice();
  5. }
  6. void pickPhysicalDevice() {
  7. }
  8. 123456789

创建一个新的VkPhysicalDevice类型的类成员来保存我们选择的显卡句柄。这个对象会在VkInstance销毁时隐式销毁,所以我们不需要在cleanup函数中做什么。

  1. VkPhysicalDevice physicalDevice = VK_NULL_HANDLE;
  2. 1

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

  1. uint32_t deviceCount = 0;
  2. vkEnumeratePhysicalDevices(instance, &deviceCount, nullptr);
  3. 12

如果支持Vulkan的设备数量为0,那就没有必要继续运行了:

  1. if (deviceCount == 0) {
  2. throw std::runtime_error("failed to find GPUs with Vulkan support!");
  3. }
  4. 123

如果有可用设备,我们先创建一个数组来保存所有VkPhysicalDevice

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

并不是所有显卡都是一样的,所以我们现在需要评估它们中的每一个并检查它们是否适合我们想要执行的操作。为此,我们需要引入一个函数:

  1. bool isDeviceSuitable(VkPhysicalDevice device) {
  2. return true;
  3. }
  4. 123

我们找到一个符合要求的误了设备:

  1. for (const auto& device : devices) {
  2. if (isDeviceSuitable(device)) {
  3. physicalDevice = device;
  4. break;
  5. }
  6. }
  7. if (physicalDevice == VK_NULL_HANDLE) {
  8. throw std::runtime_error("failed to find a suitable GPU!");
  9. }
  10. 12345678910

下一节,我们会在isDeviceSuitable函数中检查第一个要求。之后还会用更多Vulkan功能,到时候我们再做更多检查。

基本的设备能力检查

我们先通过vkGetPhysicalDeviceProperties函数获取名称、类型和支持的Vulkan版本等细节信息:

  1. VkPhysicalDeviceProperties deviceProperties;
  2. vkGetPhysicalDeviceProperties(device, &deviceProperties);
  3. 12

通过vkGetPhysicalDeviceFeatures函数可以获取设备对纹理压缩、64位浮点数和多视口渲染(对VR有用)等可选功能的支持:

  1. VkPhysicalDeviceFeatures deviceFeatures;
  2. vkGetPhysicalDeviceFeatures(device, &deviceFeatures);
  3. 12

之后我们还会讨论关于设备内存和队列簇(下一章)等更多设备细节。

例如,假设我们的程序仅可用于支持几何着色器的专用显卡,那么isDeviceSuitable可能长这样:

  1. bool isDeviceSuitable(VkPhysicalDevice device) {
  2. VkPhysicalDeviceProperties deviceProperties;
  3. VkPhysicalDeviceFeatures deviceFeatures;
  4. vkGetPhysicalDeviceProperties(device, &deviceProperties);
  5. vkGetPhysicalDeviceFeatures(device, &deviceFeatures);
  6. return deviceProperties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU &&
  7. deviceFeatures.geometryShader;
  8. }
  9. 123456789

不仅仅是检查设备能力,我们可以给每个设备打个分,然后选择分最高的那个。当然,如果能力都达不到,就没必要评分了:

  1. #include <map>
  2. ...
  3. void pickPhysicalDevice() {
  4. ...
  5. // Use an ordered map to automatically sort candidates by increasing score
  6. std::multimap<int, VkPhysicalDevice> candidates;
  7. for (const auto& device : devices) {
  8. int score = rateDeviceSuitability(device);
  9. candidates.insert(std::make_pair(score, device));
  10. }
  11. // Check if the best candidate is suitable at all
  12. if (candidates.rbegin()->first > 0) {
  13. physicalDevice = candidates.rbegin()->second;
  14. } else {
  15. throw std::runtime_error("failed to find a suitable GPU!");
  16. }
  17. }
  18. int rateDeviceSuitability(VkPhysicalDevice device) {
  19. ...
  20. int score = 0;
  21. // Discrete GPUs have a significant performance advantage
  22. if (deviceProperties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) {
  23. score += 1000;
  24. }
  25. // Maximum possible size of textures affects graphics quality
  26. score += deviceProperties.limits.maxImageDimension2D;
  27. // Application can't function without geometry shaders
  28. if (!deviceFeatures.geometryShader) {
  29. return 0;
  30. }
  31. return score;
  32. }
  33. 12345678910111213141516171819202122232425262728293031323334353637383940414243

这里只是让你了解如何设计选择设备过程,你可以不像本教程一样实现。你也可以显示一个列表让用户来选择使用哪个设备。

我们才开始,支持Vulkan的显卡就够我们用了,所以:

  1. bool isDeviceSuitable(VkPhysicalDevice device) {
  2. return true;
  3. }
  4. 123

下一节,我们会讨论我们真正需要检查的特性。

队列簇

我们之前提到的很多内容,包括绘制和上传纹理等,都需要将命令提交到一个队列。有不同的队列簇和不同类型的队列,每个队列簇只允许一个命令子集。例如,有的队列簇只允许处理计算命令,有的队列只允许与内存传输相关的命令。

我们需要检查设备支持哪些队列簇,哪一个支持我们要用的命令。为此,我们添加一个新的函数findiQueueFamilies来查找我们需要的所有队列簇。

现在我们只要需要支持图形命令的队列,所以函数大概这样:

  1. uint32_t findQueueFamilies(VkPhysicalDevice device) {
  2. // Logic to find graphics queue family
  3. }
  4. 123

只后还要用到其它队列,这里准备一个结构体保存队列簇信息:

  1. struct QueueFamilyIndices {
  2. uint32_t graphicsFamily;
  3. };
  4. QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device) {
  5. QueueFamilyIndices indices;
  6. // Logic to find queue family indices to populate struct with
  7. return indices;
  8. }
  9. 123456789

如果队列簇不可用怎么办?我们可以在findQueueFamilies函数中抛出异常,但这里不是判断设备是否可用的最佳位置。例如,我们可能更倾向于使用拥有专用传输队列簇的设备,但它不是必要的。因此,我们需要用某种方式指示我们找到了特定队列簇。

译者认为不需要用std::optional,不需要整这些不是特别必要的东西,这里只是简单翻译一下吧

uint32_t表示索引,不太好用它表示队列簇是否存在。C++17中引入了一种数据结构可以用来区分值是否存在:

  1. #include <optional>
  2. ...
  3. std::optional<uint32_t> graphicsFamily;
  4. std::cout << std::boolalpha << graphicsFamily.has_value() << std::endl; // false
  5. graphicsFamily = 0;
  6. std::cout << std::boolalpha << graphicsFamily.has_value() << std::endl; // true
  7. 1234567891011

std::optional是一个包装器,在赋值之前它不包含任何值。任何时候,你都可以用has_value()函数来判断它是否有值。我们可以把逻辑改成这样:

  1. #include <optional>
  2. ...
  3. struct QueueFamilyIndices {
  4. std::optional<uint32_t> graphicsFamily;
  5. };
  6. QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device) {
  7. QueueFamilyIndices indices;
  8. // Assign index to queue families that could be found
  9. return indices;
  10. }
  11. 12345678910111213

我们现在可以实际实现findQueueFamilies

  1. QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device) {
  2. QueueFamilyIndices indices;
  3. ...
  4. return indices;
  5. }
  6. 1234567

检索队列簇列表的方法也比较简单,使用函数vkGetPhysicalDeviceQueueFamilyProperties就行:

  1. uint32_t queueFamilyCount = 0;
  2. vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, nullptr);
  3. std::vector<VkQueueFamilyProperties> queueFamilies(queueFamilyCount);
  4. vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, queueFamilies.data());
  5. 12345

VkQueueFamilyProperties结构体保存了队列簇的一些详细信息,包括支持的操作类型和可以基于该簇创建的队列的数量。我们需要至少找到一个支持VK_QUQUE_GRAPHICS_BIT的队列簇。

  1. int i = 0;
  2. for (const auto& queueFamily : queueFamilies) {
  3. if (queueFamily.queueFlags & VK_QUEUE_GRAPHICS_BIT) {
  4. indices.graphicsFamily = i;
  5. }
  6. i++;
  7. }
  8. 12345678

现在,我们有了一个奇特的队列簇查找函数,我们可以在isDeviceSuitable函数中使用它,确保设备可以处理我们要使用的命令:

  1. bool isDeviceSuitable(VkPhysicalDevice device) {
  2. QueueFamilyIndices indices = findQueueFamilies(device);
  3. return indices.graphicsFamily.has_value();
  4. }
  5. 12345

为了更方便一点,我们给结构体本身添加一个通用检查函数:

  1. struct QueueFamilyIndices {
  2. std::optional<uint32_t> graphicsFamily;
  3. bool isComplete() {
  4. return graphicsFamily.has_value();
  5. }
  6. };
  7. ...
  8. bool isDeviceSuitable(VkPhysicalDevice device) {
  9. QueueFamilyIndices indices = findQueueFamilies(device);
  10. return indices.isComplete();
  11. }
  12. 123456789101112131415

我们可以通过上面的函数在findQueueFamilies中提前返回:

  1. for (const auto& queueFamily : queueFamilies) {
  2. ...
  3. if (indices.isComplete()) {
  4. break;
  5. }
  6. i++;
  7. }
  8. 123456789

好的,这就是我们现在需要找到合适的物理设备的全部内容!下一步是创建一个逻辑设备来与之交互。