物理设备和队列族

      +

      选择物理设备

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

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

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

      我们最终选择的显卡将存储在一个 VkPhysicalDevice 句柄中,作为新的类成员添加。

      vk::raii::PhysicalDevice physicalDevice;

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

      auto devices = instance.enumeratePhysicalDevices()

      如果没有支持 Vulkan 的设备,那么继续下去就没有意义了。

      if (devices.empty()) {
          throw std::runtime_error("failed to find GPUs with Vulkan support!");
      }

      现在我们需要评估每个设备,并检查它们是否适合我们想要执行的操作,因为并非所有显卡都是相同的。我们将检查是否有任何物理设备满足我们将添加到该函数中的要求。

      for (const auto& device : devices) {
          physicalDevice = std::make_unique<vk::raii::PhysicalDevice>(device);
          break;
      }

      基本设备适用性检查

      要评估设备的适用性,我们可以从查询一些细节开始。基本的设备属性,如名称、类型和支持的 Vulkan 版本,可以使用 vkGetPhysicalDeviceProperties 查询。

      auto deviceProperties = device.getProperties();

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

      auto deviceFeatures = device.getFeatures();

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

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

      bool isDeviceSuitable(VkPhysicalDevice device) {
          auto deviceProperties = device.getProperties();
          auto deviceFeatures = device.getFeatures();
          if (deviceProperties.deviceType == vk::PhysicalDeviceType::eDiscreteGpu &&
                 deviceFeatures.geometryShader) {
              physicalDevice = std::make_unique<vk::raii::PhysicalDevice>(device);
              break;
         }
      }

      您不仅可以检查设备是否合适并选择第一个合适的设备,还可以为每个设备打分并选择得分最高的设备。这样,您可以通过给专用显卡更高的分数来优先选择它,但如果只有集成 GPU 可用,则回退到集成 GPU。您可以实现类似以下的内容:

      #include <map>
      
      ...
      
      void pickPhysicalDevice() {
          auto devices = vk::raii::PhysicalDevices( *instance );
          if (devices.empty()) {
              throw std::runtime_error( "failed to find GPUs with Vulkan support!" );
          }
          // 使用有序映射自动按分数递增排序候选设备
          std::multimap<int, vk::raii::PhysicalDevice> candidates;
      
          for (const auto& device : devices) {
              auto deviceProperties = device.getProperties();
              auto deviceFeatures = device.getFeatures();
              uint32_t score = 0;
      
              // 专用 GPU 具有显著的性能优势
              if (deviceProperties.deviceType == vk::PhysicalDeviceType::eDiscreteGpu) {
                  score += 1000;
              }
      
              // 纹理的最大可能尺寸影响图形质量
              score += deviceProperties.limits.maxImageDimension2D;
      
              // 应用程序无法在没有几何着色器的情况下运行
              if (!deviceFeatures.geometryShader) {
                 continue;
              }
              candidates.insert(std::make_pair(score, device));
          }
      
          // 检查最佳候选设备是否完全合适
          if (candidates.rbegin()->first > 0) {
              physicalDevice = std::make_unique<vk::raii::PhysicalDevice>(candidates.rbegin()->second);
          } else {
              throw std::runtime_error("failed to find a suitable GPU!");
          }
      }

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

      因为我们刚开始,Vulkan 1.3 支持是我们唯一需要的,因此我们将搜索它以及我们实际要演示的扩展:

      std::vector<const char*> deviceExtensions = {
          vk::KHRSwapchainExtensionName,
          vk::KHRSpirv14ExtensionName,
          vk::KHRSynchronization2ExtensionName,
          vk::KHRCreateRenderpass2ExtensionName
      };
      
      void pickPhysicalDevice() {
          std::vector<vk::raii::PhysicalDevice> devices = instance.enumeratePhysicalDevices();
          const auto devIter = std::ranges::find_if(devices,
          [&](auto const & device) {
                  auto queueFamilies = device.getQueueFamilyProperties();
                  bool isSuitable = device.getProperties().apiVersion >= VK_API_VERSION_1_3;
                  const auto qfpIter = std::ranges::find_if(queueFamilies,
                  []( vk::QueueFamilyProperties const & qfp )
                          {
                              return (qfp.queueFlags & vk::QueueFlagBits::eGraphics) != static_cast<vk::QueueFlags>(0);
                          } );
                  isSuitable = isSuitable && ( qfpIter != queueFamilies.end() );
                  auto extensions = device.enumerateDeviceExtensionProperties( );
                  bool found = true;
                  for (auto const & extension : deviceExtensions) {
                      auto extensionIter = std::ranges::find_if(extensions, [extension](auto const & ext) {return strcmp(ext.extensionName, extension) == 0;});
                      found = found &&  extensionIter != extensions.end();
                  }
                  isSuitable = isSuitable && found;
                  printf("\n");
                  if (isSuitable) {
                      physicalDevice = device;
                  }
                  return isSuitable;
          });
          if (devIter == devices.end()) {
              throw std::runtime_error("failed to find a suitable GPU!");
          }
      }

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

      队列族

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

      我们需要检查设备支持哪些队列族,以及其中哪些支持我们想要使用的命令。现在我们只寻找支持图形命令的队列,因此代码可能如下所示:

      uint32_t findQueueFamilies(VkPhysicalDevice device) {
          // 找到第一个支持图形的队列族的索引
          std::vector<vk::QueueFamilyProperties> queueFamilyProperties = physicalDevice->getQueueFamilyProperties();
      
          // 获取 queueFamilyProperties 中第一个支持图形的索引
          auto graphicsQueueFamilyProperty =
            std::find_if( queueFamilyProperties.begin(),
                          queueFamilyProperties.end(),
                          []( vk::QueueFamilyProperties const & qfp ) { return qfp.queueFlags & vk::QueueFlagBits::eGraphics; } );
      
          return static_cast<uint32_t>( std::distance( queueFamilyProperties.begin(), graphicsQueueFamilyProperty ) );
      }

      很好,这就是我们现在选择正确的物理设备所需的全部内容!下一步是 创建一个逻辑设备 来与之交互。