生态系统工具与GPU兼容性

      +

      简介

      在本章中,我们将探索Vulkan开发的重要生态系统工具,并学习如何调整代码以支持更广泛的GPU。随着Vulkan不断演进并推出新版本和功能,了解以下内容非常重要:

      1. 发现不同GPU支持的功能

      2. 修改代码以保持与旧硬件的兼容性

      3. 在可用时有条件地使用高级功能

      这些知识对于开发能够在各种硬件上运行的Vulkan应用程序至关重要,从最新的高端GPU到较旧或功能有限的设备。

      Vulkan硬件数据库(GPUInfo.org)

      GPUInfo.org简介

      Vulkan硬件数据库(GPUInfo.org)是Vulkan开发者的宝贵资源。这个社区驱动的数据库收集并展示了各种GPU和设备上的Vulkan支持信息。

      GPUInfo.org提供以下详细信息:

      • 支持的Vulkan版本

      • 可用的扩展

      • 功能支持

      • 实现限制

      • 格式属性

      • 队列族属性

      这些信息来自运行Vulkan硬件能力查看器工具的用户,该工具将GPU的能力报告给数据库。

      使用GPUInfo.org进行开发

      在开发Vulkan应用程序时,GPUInfo.org可以帮助您:

      1. 确定最低要求:了解需要针对哪些Vulkan版本和扩展以支持您期望的硬件范围。

      2. 检查功能可用性:验证特定功能(如动态渲染、时间线信号量或光线追踪)是否广泛支持。

      3. 识别实现限制:发现不同硬件上各种Vulkan功能的实际限制。

      4. 比较供应商和设备:了解NVIDIA、AMD、Intel和移动GPU供应商之间Vulkan支持的差异。

      让我们看一些使用GPUInfo.org的实际示例:

      示例:检查Vulkan版本支持

      要确定Vulkan 1.3(引入了动态渲染)的支持范围:

      1. 访问GPUInfo.org

      2. 导航到“核心版本支持”

      3. 检查支持Vulkan 1.3的设备百分比

      您会发现,虽然较新的GPU支持Vulkan 1.3+,但仍有许多设备仅限于Vulkan 1.0、1.1或1.2。

      示例:检查扩展支持

      如果您考虑使用特定扩展:

      1. 访问“扩展”部分

      2. 搜索您感兴趣的扩展

      3. 检查其在不同供应商中的支持百分比

      这有助于您决定是否需要该扩展或提供备用路径。

      示例:使用Vulkan配置工具

      Vulkan配置工具(在所有平台上可执行文件名为`vkconfig`)包含在Vulkan SDK中,提供了一种方便的方式来配置系统上的Vulkan设置。以下是使用方法:

      1. 启动Vulkan配置工具

        • 在Windows上:建议从开始菜单启动“Vulkan Configurator”,因为从命令行运行`vkconfig.exe`仅显示有限选项

        • 在其他平台上:打开终端并运行`vkconfig`

          注意,可执行文件在所有平台上都称为`vkconfig`。
      2. 配置验证层

        • 导航到“Layers”选项卡

        • 根据调试需求启用或禁用特定验证层

        • 例如,在开发期间启用`VK_LAYER_KHRONOS_validation`以捕获API使用错误

      3. 管理环境变量

        • 转到“Settings”选项卡

        • 设置环境变量,如`VK_LAYER_PATH`或`VK_ICD_FILENAMES`

        • 这些设置可以应用于整个系统或当前会话

      4. 配置驱动程序特定选项

        • 一些GPU供应商提供额外的配置选项

        • 可以通过供应商特定选项卡访问这些选项

      5. 导出配置

        • 保存配置以供以后使用或与团队成员共享

        • 这确保开发机器上的Vulkan环境一致

      使用Vulkan配置工具在以下情况下特别有帮助: - 使用不同验证层配置调试Vulkan应用程序 - 在不修改代码的情况下测试应用程序的不同Vulkan设置 - 设置具有特定Vulkan要求的开发环境

      使用Vulkan配置工具代替代码进行验证层管理

      在许多Vulkan应用程序中,验证层在实例创建期间以编程方式启用,通常仅在调试构建中启用。以下是常见的做法:

      // 定义验证层
      const std::vector validationLayers = {
          "VK_LAYER_KHRONOS_validation"
      };
      
      // 仅在调试构建中启用
      #ifdef NDEBUG
      constexpr bool enableValidationLayers = false;
      #else
      constexpr bool enableValidationLayers = true;
      #endif
      
      void createInstance() {
          // 检查验证层是否可用
          if (enableValidationLayers && !checkValidationLayerSupport()) {
              throw std::runtime_error("validation layers requested, but not available!");
          }
      
          // 应用程序信息...
      
          // 在调试模式下启用验证层
          std::vector<char const *> enabledLayers;
          if (enableValidationLayers) {
              enabledLayers.assign(validationLayers.begin(), validationLayers.end());
          }
      
          // 创建带有验证层的实例
          vk::InstanceCreateInfo createInfo{
              .pApplicationInfo        = &appInfo,
              .enabledLayerCount       = static_cast<uint32_t>(enabledLayers.size()),
              .ppEnabledLayerNames     = enabledLayers.data(),
              // ... 其他参数
          };
      
          instance = vk::raii::Instance(context, createInfo);
      }

      虽然这种方法有效,但它有几个缺点:

      1. 需要修改和重新编译代码以启用/禁用验证

      2. 难以尝试不同的验证层配置

      3. 增加了代码库的复杂性

      更好的方法是使用Vulkan配置工具从外部管理验证层。以下是修改代码以利用此方法的示例:

      void createInstance() {
          // 应用程序信息...
      
          // 创建实例而不显式启用验证层
          vk::InstanceCreateInfo createInfo{
              .pApplicationInfo        = &appInfo,
              // ... 其他参数
          };
      
          instance = vk::raii::Instance(context, createInfo);
      }

      使用这种方法:

      1. 从应用程序中删除所有验证层特定的代码

      2. 使用Vulkan配置工具在需要时启用验证层

      3. 可以在不重新编译的情况下切换验证配置

      要使用Vulkan配置工具启用验证层:

      1. 启动Vulkan配置工具(在Windows上从开始菜单启动,或在终端中运行`vkconfig` - 可执行文件在所有平台上都称为`vkconfig`)

      2. 转到“Layers”选项卡

      3. 启用`VK_LAYER_KHRONOS_validation`层

      4. 应用设置

      此配置将应用于在该环境中运行的所有Vulkan应用程序,从而无需更改代码即可轻松切换验证。

      这种方法的优点包括:

      • 代码更简洁:应用程序代码不需要处理验证层

      • 灵活性:无需重新编译即可更改验证设置

      • 一致性:在多个应用程序中应用相同的验证设置

      • 实验性:轻松尝试不同的验证配置

      其他有用的生态系统工具

      除了GPUInfo.org,还有其他几个工具可以帮助您开发和调试Vulkan应用程序:

      • Vulkan SDK工具

        • vulkaninfo:显示本地系统的Vulkan能力

        • vkconfig(Vulkan配置工具):用于管理Vulkan设置的配置工具(详见示例:使用Vulkan配置工具

        • 验证层:帮助识别API使用错误

        • RenderDoc:图形调试工具

      • 供应商特定工具

        • NVIDIA Nsight Graphics

        • AMD Radeon GPU Profiler

      支持旧GPU

      现在我们已经了解了如何发现GPU能力,让我们探索如何修改代码以支持不支持Vulkan 1.3/1.4功能(如动态渲染)的旧GPU。

      检测可用功能

      第一步是检测用户GPU上可用的功能。这是在设备创建期间完成的:

      // 检查是否支持动态渲染
      bool dynamicRenderingSupported = false;
      
      // 检查Vulkan 1.3支持
      if (deviceProperties.apiVersion >= VK_VERSION_1_3) {
          dynamicRenderingSupported = true;
      } else {
          // 在旧版Vulkan上检查扩展
          for (const auto& extension : availableExtensions) {
              if (strcmp(extension.extensionName, VK_KHR_DYNAMIC_RENDERING_EXTENSION_NAME) == 0) {
                  dynamicRenderingSupported = true;
                  break;
              }
          }
      }
      
      // 存储此信息以供以后使用
      appInfo.dynamicRenderingSupported = dynamicRenderingSupported;

      动态渲染的替代方案:传统渲染通道

      如果动态渲染不可用,我们需要使用传统的渲染通道和帧缓冲区。以下是实现此替代方法的方法:

      创建渲染通道

      void createRenderPass() {
          if (appInfo.dynamicRenderingSupported) {
              // 动态渲染不需要渲染通道
              return;
          }
      
          // 颜色附件描述
          vk::AttachmentDescription colorAttachment{
              .format = swapChainImageFormat,
              .samples = vk::SampleCountFlagBits::e1,
              .loadOp = vk::AttachmentLoadOp::eClear,
              .storeOp = vk::AttachmentStoreOp::eStore,
              .stencilLoadOp = vk::AttachmentLoadOp::eDontCare,
              .stencilStoreOp = vk::AttachmentStoreOp::eDontCare,
              .initialLayout = vk::ImageLayout::eUndefined,
              .finalLayout = vk::ImageLayout::ePresentSrcKHR
          };
      
          // 子传递对颜色附件的引用
          vk::AttachmentReference colorAttachmentRef{
              .attachment = 0,
              .layout = vk::ImageLayout::eColorAttachmentOptimal
          };
      
          // 子传递描述
          vk::SubpassDescription subpass{
              .pipelineBindPoint = vk::PipelineBindPoint::eGraphics,
              .colorAttachmentCount = 1,
              .pColorAttachments = &colorAttachmentRef
          };
      
          // 依赖以确保正确的图像布局转换
          vk::SubpassDependency dependency{
              .srcSubpass = VK_SUBPASS_EXTERNAL,
              .dstSubpass = 0,
              .srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput,
              .dstStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput,
              .srcAccessMask = vk::AccessFlagBits::eNone,
              .dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite
          };
      
          // 创建渲染通道
          vk::RenderPassCreateInfo renderPassInfo{
              .attachmentCount = 1,
              .pAttachments = &colorAttachment,
              .subpassCount = 1,
              .pSubpasses = &subpass,
              .dependencyCount = 1,
              .pDependencies = &dependency
          };
      
          renderPass = device.createRenderPass(renderPassInfo);
      }

      创建帧缓冲区

      void createFramebuffers() {
          if (appInfo.dynamicRenderingSupported) {
              // 动态渲染不需要帧缓冲区
              return;
          }
      
          swapChainFramebuffers.resize(swapChainImageViews.size());
      
          for (size_t i = 0; i < swapChainImageViews.size(); i++) {
              vk::ImageView attachments[] = {
                  swapChainImageViews[i]
              };
      
              vk::FramebufferCreateInfo framebufferInfo{
                  .renderPass = renderPass,
                  .attachmentCount = 1,
                  .pAttachments = attachments,
                  .width = swapChainExtent.width,
                  .height = swapChainExtent.height,
                  .layers = 1
              };
      
              swapChainFramebuffers[i] = device.createFramebuffer(framebufferInfo);
          }
      }

      修改管线创建

      在创建图形管线时,如果动态渲染不可用,我们需要指定渲染通道:

      void createGraphicsPipeline() {
          // ... 现有的着色器阶段和固定功能设置 ...
      
          vk::GraphicsPipelineCreateInfo pipelineInfo{};
      
          if (appInfo.dynamicRenderingSupported) {
              // 使用动态渲染
              vk::PipelineRenderingCreateInfo pipelineRenderingCreateInfo{
                  .colorAttachmentCount = 1,
                  .pColorAttachmentFormats = &swapChainImageFormat
              };
      
              pipelineInfo.pNext = &pipelineRenderingCreateInfo;
              pipelineInfo.renderPass = nullptr;
          } else {
              // 使用传统渲染通道
              pipelineInfo.pNext = nullptr;
              pipelineInfo.renderPass = renderPass;
              pipelineInfo.subpass = 0;
          }
      
          // ... 管线创建的其余部分 ...
      }

      调整命令缓冲区记录

      最后,我们需要修改记录命令缓冲区的方式:

      void recordCommandBuffer(vk::CommandBuffer commandBuffer, uint32_t imageIndex) {
          // ... 开始命令缓冲区 ...
      
          if (appInfo.dynamicRenderingSupported) {
              // 开始动态渲染
              vk::RenderingAttachmentInfo colorAttachment{
                  .imageView = swapChainImageViews[imageIndex],
                  .imageLayout = vk::ImageLayout::eAttachmentOptimal,
                  .loadOp = vk::AttachmentLoadOp::eClear,
                  .storeOp = vk::AttachmentStoreOp::eStore,
                  .clearValue = clearColor
              };
      
              vk::RenderingInfo renderingInfo{
                  .renderArea = {{0, 0}, swapChainExtent},
                  .layerCount = 1,
                  .colorAttachmentCount = 1,
                  .pColorAttachments = &colorAttachment
              };
      
              commandBuffer.beginRendering(renderingInfo);
          } else {
              // 开始传统渲染通道
              vk::RenderPassBeginInfo renderPassInfo{
                  .renderPass = renderPass,
                  .framebuffer = swapChainFramebuffers[imageIndex],
                  .renderArea = {{0, 0}, swapChainExtent},
                  .clearValueCount = 1,
                  .pClearValues = &clearColor
              };
      
              commandBuffer.beginRenderPass(renderPassInfo, vk::SubpassContents::eInline);
          }
      
          // ... 绑定管线和绘制 ...
      
          if (appInfo.dynamicRenderingSupported) {
              commandBuffer.endRendering();
          } else {
              commandBuffer.endRenderPass();
          }
      
          // ... 结束命令缓冲区 ...
      }

      处理其他Vulkan 1.3/1.4功能

      动态渲染只是可能无法在旧GPU上使用的功能的一个示例。以下是您可能需要提供替代方案的其他Vulkan 1.3/1.4功能:

      时间线信号量

      时间线信号量(在Vulkan 1.2中引入)提供了比二进制信号量更灵活的同步机制。如果不可用,您需要使用二进制信号量和栅栏:

      bool timelineSemaphoresSupported = false;
      
      // 检查Vulkan 1.2支持或扩展
      if (deviceProperties.apiVersion >= VK_VERSION_1_2) {
          timelineSemaphoresSupported = true;
      } else {
          // 检查扩展
          for (const auto& extension : availableExtensions) {
              if (strcmp(extension.extensionName, VK_KHR_TIMELINE_SEMAPHORE_EXTENSION_NAME) == 0) {
                  timelineSemaphoresSupported = true;
                  break;
              }
          }
      }
      
      // 创建适当的同步原语
      if (timelineSemaphoresSupported) {
          // 创建时间线信号量
          vk::SemaphoreTypeCreateInfo timelineCreateInfo{
              .semaphoreType = vk::SemaphoreType::eTimeline,
              .initialValue = 0
          };
      
          vk::SemaphoreCreateInfo semaphoreInfo{
              .pNext = &timelineCreateInfo
          };
      
          timelineSemaphore = device.createSemaphore(semaphoreInfo);
      } else {
          // 创建二进制信号量和栅栏
          vk::SemaphoreCreateInfo semaphoreInfo{};
          vk::FenceCreateInfo fenceInfo{.flags = vk::FenceCreateFlagBits::eSignaled};
      
          for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
              imageAvailableSemaphores[i] = device.createSemaphore(semaphoreInfo);
              renderFinishedSemaphores[i] = device.createSemaphore(semaphoreInfo);
              inFlightFences[i] = device.createFence(fenceInfo);
          }
      }

      Synchronization2

      Synchronization2功能(Vulkan 1.3)简化了管线屏障和内存依赖。如果不可用,请使用原始同步命令:

      bool synchronization2Supported = false;
      
      // 检查Vulkan 1.3支持或扩展
      if (deviceProperties.apiVersion >= VK_VERSION_1_3) {
          synchronization2Supported = true;
      } else {
          // 检查扩展
          for (const auto& extension : availableExtensions) {
              if (strcmp(extension.extensionName, VK_KHR_SYNCHRONIZATION_2_EXTENSION_NAME) == 0) {
                  synchronization2Supported = true;
                  break;
              }
          }
      }
      
      // 使用适当的屏障命令
      if (synchronization2Supported) {
          // 使用Synchronization2 API
          vk::ImageMemoryBarrier2 barrier{
              .srcStageMask = vk::PipelineStageFlagBits2::eTopOfPipe,
              .srcAccessMask = vk::AccessFlagBits2::eNone,
              .dstStageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput,
              .dstAccessMask = vk::AccessFlagBits2::eColorAttachmentWrite,
              .oldLayout = vk::ImageLayout::eUndefined,
              .newLayout = vk::ImageLayout::eAttachmentOptimal,
              .image = swapChainImages[i],
              .subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}
          };
      
          vk::DependencyInfo dependencyInfo{
              .imageMemoryBarrierCount = 1,
              .pImageMemoryBarriers = &barrier
          };
      
          commandBuffer.pipelineBarrier2(dependencyInfo);
      } else {
          // 使用原始同步API
          vk::ImageMemoryBarrier barrier{
              .srcAccessMask = vk::AccessFlagBits::eNone,
              .dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite,
              .oldLayout = vk::ImageLayout::eUndefined,
              .newLayout = vk::ImageLayout::eColorAttachmentOptimal,
              .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
              .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
              .image = swapChainImages[i],
              .subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}
          };
      
          commandBuffer.pipelineBarrier(
              vk::PipelineStageFlagBits::eTopOfPipe,
              vk::PipelineStageFlagBits::eColorAttachmentOutput,
              vk::DependencyFlagBits::eByRegion,
              {},
              {},
              { barrier }
          );
      }

      跨GPU兼容性的最佳实践

      根据我们所学,以下是开发适用于各种GPU的Vulkan应用程序的一些最佳实践:

      1. 在运行时检查功能可用性:不要仅基于Vulkan版本假设功能可用。始终检查特定功能和扩展。

      2. 提供备用路径:为现代功能不可用时实现替代代码路径。

      3. 使用功能结构:在创建逻辑设备时,使用适当的功能结构仅启用您需要且可用的功能。

      4. 在各种硬件上测试:使用GPUInfo.org识别常见硬件配置并在代表性样本上测试您的应用程序。

      5. 优雅降级:设计应用程序在功能较弱的硬件上运行时优雅地降低视觉质量或功能。

      6. 记录要求:清楚地记录应用程序的最低和推荐Vulkan版本和扩展要求。

      结论

      了解Vulkan生态系统工具并知道如何调整代码以适应不同的GPU能力是Vulkan开发者的基本技能。通过遵循本章概述的方法,您可以创建在广泛硬件上运行的应用程序,同时在可用时仍能利用最新功能。