逻辑设备和队列

      +

      介绍

      选择要使用的物理设备后,我们需要设置一个*逻辑设备*来与之交互。逻辑设备的创建过程类似于实例创建过程,并描述了我们想要使用的功能。既然我们已经查询了哪些队列族可用,我们还需要指定要创建的队列。如果您有不同的需求,甚至可以从同一个物理设备创建多个逻辑设备。

      首先添加一个新的类成员来存储逻辑设备句柄。

      vk::raii::Device device;

      接下来,添加一个从 initVulkan 调用的 createLogicalDevice 函数。

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

      指定要创建的队列

      创建逻辑设备涉及再次在结构体中指定一堆细节,其中第一个将是 VkDeviceQueueCreateInfo。此结构描述了我们为单个队列族想要的队列数量。现在我们只对具有图形功能的队列感兴趣。

      std::vector<vk::QueueFamilyProperties> queueFamilyProperties = physicalDevice.getQueueFamilyProperties();
      
      vk::DeviceQueueCreateInfo deviceQueueCreateInfo { .queueFamilyIndex = graphicsIndex };

      当前可用的驱动程序只允许您为每个队列族创建少量队列,而您实际上不需要超过一个。这是因为您可以在多个线程上创建所有命令缓冲区,然后在主线程上通过一次低开销调用一次性提交它们。

      Vulkan 允许您为队列分配优先级,以使用 0.01.0 之间的浮点数影响命令缓冲区执行的调度。即使只有一个队列,这也是必需的:

      float queuePriority = 0.0f;
      vk::DeviceQueueCreateInfo deviceQueueCreateInfo { .queueFamilyIndex = graphicsIndex, .queueCount = 1, .pQueuePriorities = &queuePriority };

      指定使用的设备功能

      下一个要指定的信息是我们要使用的设备功能集。这些是我们在上一个章节中使用 vkGetPhysicalDeviceFeatures 查询支持的功能,如几何着色器。现在我们不需要任何特殊的东西,所以我们可以简单地定义它并将所有内容保留为 VK_FALSE。一旦我们开始用 Vulkan 做更有趣的事情,我们将回到这个结构。

      vk::PhysicalDeviceFeatures deviceFeatures;

      启用额外的设备功能

      Vulkan 设计为向后兼容,这意味着默认情况下,您只能访问 Vulkan 1.0 中可用的基本功能。要使用新功能,您需要在设备创建期间明确请求它们。

      在 Vulkan 中,功能根据引入时间或相关功能组织到不同的结构中。例如: - 基本功能在 vk::PhysicalDeviceFeatures 中 - Vulkan 1.3 功能在 vk::PhysicalDeviceVulkan13Features 中 - 扩展特定功能在它们自己的结构中(如 vk::PhysicalDeviceExtendedDynamicStateFeaturesEXT

      要启用多组功能,Vulkan 使用称为“结构链”的概念。每个功能结构都有一个 pNext 字段,可以指向另一个结构,创建一系列功能请求。

      C++ Vulkan API 提供了一个名为 vk::StructureChain 的辅助模板,使这个过程更容易。让我们看看如何使用它:

      // 创建一个功能结构链
      vk::StructureChain<vk::PhysicalDeviceFeatures2, vk::PhysicalDeviceVulkan13Features, vk::PhysicalDeviceExtendedDynamicStateFeaturesEXT> featureChain = {
          {},                               // vk::PhysicalDeviceFeatures2(现在为空)
          {.dynamicRendering = true },      // 启用 Vulkan 1.3 的动态渲染
          {.extendedDynamicState = true }   // 启用扩展的动态状态
      };

      这段代码中发生了什么:

      1. 我们创建了一个 vk::StructureChain,其中包含三个不同的功能结构。

      2. 对于链中的每个结构,我们提供了一个初始化器:

        • 第一个结构(vk::PhysicalDeviceFeatures2)用 {} 留空

        • 在第二个结构中,我们启用了 Vulkan 1.3 的 dynamicRendering 功能

        • 在第三个结构中,我们启用了扩展的 extendedDynamicState 功能

      vk::StructureChain 模板通过设置它们之间的 pNext 指针自动连接这些结构。这使我们不必手动将每个结构链接到下一个结构。

      当我们稍后创建逻辑设备时,我们将传递指向此链中第一个结构的指针,这将允许 Vulkan 看到我们想要启用的所有功能。

      指定设备扩展

      为了使我们的应用程序正常工作,我们需要启用某些设备扩展。这些扩展提供了我们将在教程后面需要的额外功能。

      std::vector<const char*> deviceExtensions = {
          vk::KHRSwapchainExtensionName,
          vk::KHRSpirv14ExtensionName,
          vk::KHRSynchronization2ExtensionName,
          vk::KHRCreateRenderpass2ExtensionName
      };

      VK_KHR_swapchain 扩展是将渲染的图像呈现到窗口所必需的。其他扩展提供了我们将在教程后面部分使用的额外功能。

      创建逻辑设备

      准备好所有必要的信息后,我们现在可以创建逻辑设备。我们需要填写 vk::DeviceCreateInfo 结构并将我们的功能链连接到它:

      vk::DeviceCreateInfo deviceCreateInfo{
          .pNext = &featureChain.get<vk::PhysicalDeviceFeatures2>(),
          .queueCreateInfoCount = 1,
          .pQueueCreateInfos = &deviceQueueCreateInfo,
          .enabledExtensionCount = static_cast<uint32_t>(deviceExtensions.size()),
          .ppEnabledExtensionNames = deviceExtensions.data()
      };

      回顾我们如何将功能链连接到设备创建过程:

      1. featureChain.get<vk::PhysicalDeviceFeatures2>() 方法检索我们链中第一个结构(vk::PhysicalDeviceFeatures2 结构)的引用。

      2. 我们将此引用分配给 deviceCreateInfo 结构的 pNext 字段。

      3. 由于我们功能链中的所有结构都已连接(感谢 vk::StructureChain),Vulkan 将通过跟随 pNext 指针链看到我们想要启用的所有功能。

      这种方法使我们能够以干净有序的方式请求多组功能。Vulkan 将在设备创建期间处理链中的每个结构并启用请求的功能。

      其余的信息类似于 VkInstanceCreateInfo 结构,并要求您指定扩展和验证层。不同的是,这次这些是设备特定的。

      设备特定扩展的一个例子是 VK_KHR_swapchain,它允许您将该设备渲染的图像呈现到窗口。系统中可能存在不支持此功能的 Vulkan 设备,例如因为它们仅支持计算操作。我们将在交换链章节中回到这个扩展。

      Vulkan 的先前实现在实例和设备特定验证层之间进行了区分,但这是 不再如此。 这意味着 VkDeviceCreateInfoenabledLayerCountppEnabledLayerNames 字段被最新实现忽略。

      如前所述,我们需要几个设备特定的扩展才能使我们的应用程序正常工作。

      device = vk::raii::Device( physicalDevice, deviceCreateInfo );

      参数是要交互的物理设备,我们刚刚指定的使用信息,可选的分配回调指针和存储逻辑设备句柄的变量指针。与实例创建函数类似,此调用可能会基于启用不存在的扩展或指定不支持功能的所需使用而抛出错误。

      逻辑设备不直接与实例交互,这就是为什么它不作为参数包含在内。

      检索队列句柄

      队列是与逻辑设备一起自动创建的,但我们还没有一个句柄来与它们交互。 首先,添加一个类成员来存储图形队列的句柄:

      vk::raii::Queue graphicsQueue;

      设备队列在设备销毁时隐式清理,因此我们不需要在 cleanup 中做任何事情。

      我们可以使用 vkGetDeviceQueue 函数检索每个队列族的队列句柄。参数是逻辑设备、队列族、队列索引和存储队列句柄的变量指针。因为我们只从这个族创建一个队列,所以我们将简单地使用索引 0

      graphicsQueue = vk::raii::Queue( device, graphicsIndex, 0 );

      有了逻辑设备和队列句柄,我们现在实际上可以开始使用显卡来做事情了!在 接下来的几章 中,我们将设置资源以将结果呈现到窗口系统。