65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Vulkan API 为 Android 移动游戏开发实现高性能计算机图形

starIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

1.00/5 (2投票s)

2020年4月8日

CPOL

7分钟阅读

viewsIcon

21304

在本文中,我们将简要介绍两个使用 Vulkan 最大化游戏图形性能的示例。

作为一名 Android 游戏开发者,您有两个图形 API 可供选择:OpenGL ES 和 Vulkan。在本文中,我们将重点介绍 Vulkan。Vulkan 专为希望在移动设备上推动实时 3D 图形最前沿的开发人员而设计,它充当一个非常轻薄的抽象层,让您可以获得更多控制权、更低的 CPU 开销、更小的内存占用以及更高的稳定性。

我们将引导您了解几个关键的 Vulkan 性能示例,这些示例演示了在移动游戏中应遵循的常见优化和最佳实践,因此您可以通过 Vulkan API 的强大功能,榨干设备上的每一丝性能,并为您的粉丝提供他们绝对需要玩的游戏。

最大性能。最小开销。

Vulkan 实现高性能、跨平台图形的原理很简单:“能力越大,责任越大”。为了让开发人员能够最大限度地发挥设备上的图形性能,Vulkan 允许比 OpenGL ES 更高的硬件资源控制权,但需要更多的显式内存管理和操作。为了实现更低的 CPU 开销,Vulkan API 支持多线程,并利用主流移动设备内置的四到八核。

有关更多详细信息, Vulkan 基础知识是一个很好的资源,其中深入解释了 Vulkan 的工作原理。

Vulkan API 示例和教程

有大量优秀的资源和示例可供学习如何使用 Vulkan API。我们将要介绍的两个示例是渲染通道等待空闲,它们展示了您可以在自己的移动游戏中利用的一些最有用的优化。这些性能示例展示了使用 Vulkan API 增强性能的推荐最佳实践,并提供实时性能分析信息,帮助您识别和理解应用程序中的瓶颈。您可以在这里找到 Arm 开源并由 Khronos Group 管理的完整示例和教程。

本文假定您熟悉 3D 渲染管线和 Vulkan API 基础知识。如果您是 Vulkan 的新手,那么这份 Vulkan 指南入门教程将帮助您渲染出第一个三角形。有关其他示例,请参阅这些涵盖 HDR、实例化、纹理加载和细分等主题的 API 示例

必备组件

要使用 Vulkan 示例,您需要具备正确的工具和依赖项。对于 Android,您可以查看构建指南 Android 部分

主要先决条件是

  • CMake v3.10 或更高版本
  • JDK 8 或更高版本
  • Android NDK r18 或更高版本
  • Android SDK
  • Gradle 5 或更高版本
  • 示例 3D 模型

正确使用渲染通道附件

渲染通道附件是 Vulkan 跟踪您的输入和输出渲染目标的方式。可以将其视为颜色或深度缓冲区的引用。优化配置它们是在渲染通道期间获得宝贵毫秒时间的简单但有效的方法。

让我们首先看看这个性能教程示例代码

如果您运行此示例,您将看到一个应用程序在一个通道中渲染 3D 场景,以及一个显示渲染统计信息和用于切换颜色附件的加载操作和深度附件的存储操作选项的 GUI。

知道附件缓冲区的 [内容] 是否需要被清除颜色、读取或写入,可以极大地影响绘制性能,因为您可以进行设置以最大程度地减少读/写操作次数。

例如,由于您不需要读取最终绘制到屏幕的颜色缓冲区的 [内容],因此在 Vulkan 中,您可以将附件描述的加载操作设置为 VK_ATTACHMENT_LOAD_OP_DONT_CARE,从而加快您的渲染通道。

您可以通过选择颜色附件加载操作的 Load 来测试这一点,然后查看 External Read Bytes 值如何增加,因为它会准备您的颜色缓冲区,以便不仅绘制场景,还能在此通道中读取其 [内容]。

更改深度附件的存储操作对 External Write Bytes 具有类似的影响,因为您正在指示是否要花费时间将深度信息保存到缓冲区。

以下是您在代码中绘制 3D 场景时优化使用渲染通道附件的典型设置

VkAttachmentDescription attachments[
  2 ];
  
  //
  Color attachment
  attachments[ 0 ].format = colorFormat;
  attachments[ 0 ].samples = VK_SAMPLE_COUNT_1_BIT;
  attachments[ 0 ].loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
  attachments[ 0 ].storeOp = VK_ATTACHMENT_STORE_OP_STORE;
  attachments[ 0 ].stencilLoadOp =
  VK_ATTACHMENT_LOAD_OP_DONT_CARE;
  attachments[ 0 ].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
  attachments[ 0 ].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
  attachments[ 0 ].finalLayout =
  VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
  
  //
  Depth attachment
  attachments[ 1 ].format = depthFormat;
  attachments[ 1 ].samples = VK_SAMPLE_COUNT_1_BIT;
  attachments[ 1 ].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
  attachments[ 1 ].storeOp =
  VK_ATTACHMENT_STORE_OP_DONT_CARE;
  attachments[ 1 ].stencilLoadOp =
  VK_ATTACHMENT_LOAD_OP_DONT_CARE;
  attachments[ 1 ].stencilStoreOp =
  VK_ATTACHMENT_STORE_OP_DONT_CARE;
  attachments[ 1 ].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
  attachments[ 1 ].finalLayout =
  VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
  
  VkAttachmentReference colorReference = {};
  colorReference.attachment = 0;
  colorReference.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
  
  VkAttachmentReference depthReference = {};
  depthReference.attachment = 1;
  depthReference.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
  
  VkSubpassDescription subpass = {};
  subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
  subpass.colorAttachmentCount = 1;
  subpass.pColorAttachments = &colorReference;
  subpass.pDepthStencilAttachment = &depthReference;
  
  VkRenderPassCreateInfo renderPassInfo = {};
  renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
  renderPassInfo.attachmentCount = 2;
  renderPassInfo.pAttachments = attachments;
  renderPassInfo.subpassCount = 1;
  renderPassInfo.pSubpasses = &subpass;
  
  vkCreateRenderPass( g_device, &renderPassInfo, nullptr,
  &renderPass );

此示例中演示的最后一个选项是 Use vkCmdClear 复选框,它将显式清除颜色附件,并演示这样做如何对性能产生负面影响。使用加载操作重置整个缓冲区效率更高,因此使用此显式清除函数最好保留用于其他场景,例如当您需要指定要清除的矩形区域时。

例如,如果您想保留 10px 的边框不变,可以在命令缓冲区中添加如下内容

VkClearAttachment clearAttachment =
  {};
  clearAttachment.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
  clearAttachment.clearValue.color = 0;
  clearAttachment.colorAttachment = 0;
  
  VkClearRect clearRect = {};
  clearRect.layerCount = 1;
  clearRect.rect.offset = { 10, 10 };
  clearRect.rect.extent = { width - 20, height - 20 };
  
  vkCmdClearAttachments( g_cmdBuffer, 1,
  &clearAttachment, 1, &clearRect );

优化技巧: 准确识别您如何使用每个渲染通道附件将有助于确保获得最佳的读/写吞吐量。记住,当您需要清除渲染目标时,请使用 VK_ATTACHMENT_LOAD_OP_CLEAR。当您不需要读取附件的 [内容] 时,请设置 VK_ATTACHMENT_LOAD_OP_DONT_CARE 以避免不必要的操作。

同步 CPU 和 GPU

Vulkan 的渲染管线计算有些在 CPU 上完成,例如创建命令缓冲区,有些在 GPU 上完成,例如着色器和渲染目标。以正确的顺序处理它们意味着 CPU 和 GPU 需要以适当的时序协同工作。

使用 Vulkan API 实现这一目标的一种简单可靠的方法是使用 vkQueueWaitIdle,在 CPU 添加新命令交由 GPU 处理之前,简单地等待当前队列为空。然而,您的渲染吞吐量可以获得的最大收益之一是确保您的 GPU 和 CPU 不会长时间处于微观等待状态,并且可以立即着手准备下一帧。

您可以在等待空闲性能教程示例代码中看到这其中的差异。

运行此示例会显示一个带有 Wait IdleFences 两个选项的场景,以及显示帧时间(渲染帧的平均时间)的文本。此示例演示了高效地排队下一帧(或更复杂的通道的下一个命令缓冲区)如何提高性能。

当您运行示例时,您会注意到选择 Wait Idle 选项时帧时间要长得多,而选择 Fences 选项时帧时间要短。

以下是您可以在代码中设置渲染循环以使用 fence 的方法

void render()
  {
      vkWaitForFences( g_device, 1, &g_renderFence, VK_TRUE,
  UINT64_MAX );
      vkResetFences( g_device, 1, &g_renderFence );
  
      // Update frame with new commands
      setCmdBuffer( g_cmdBuffer );
  
      uint32_t imageIndex;
      vkAcquireNextImageKHR( g_device, g_swapchain, UINT64_MAX, 
  g_imageSemaphore,
  VK_NULL_HANDLE, &imageIndex );
  
      VkSubmitInfo submitInfo = { VK_STRUCTURE_TYPE_SUBMIT_INFO };
      submitInfo.waitSemaphoreCount = 1;
      submitInfo.pWaitSemaphores = &g_imageSemaphore;
      submitInfo.commandBufferCount = 1;
      submitInfo.pCommandBuffers = &g_cmdBuffer;
  
      vkQueueSubmit( g_queue, 1, &submitInfo, g_renderFence );
  
      VkPresentInfoKHR presentInfo = 
  {
  VK_STRUCTURE_TYPE_PRESENT_INFO_KHR };
      presentInfo.waitSemaphoreCount = 1;
      presentInfo.pWaitSemaphores = &g_renderSemaphore;
      presentInfo.swapChainCount = 1;
      presentInfo.pSwapchains = &g_swapchain;
      presentInfo.pImageIndices = &imageIndex;
      vkQueuePresentKHR( g_queue, &presentInfo );
  }

优化技巧: 避免使用 vkQueueWaitIdlevkDeviceWaitIdle,而是使用 VkFence 对象和 vkWaitForFences 来保持您的渲染队列的流畅。您需要确保每个 fence 独立工作,没有重叠(例如,分开渲染帧)。此外,如果您的 GPU 在单帧中有多个命令,但不需要与 CPU 同步,则可以考虑使用 VkSemaphore 对象。

要查看更详细的 CPU 和 GPU 同步示例,您还可以查看此 Vulkan 教程,了解飞行中的帧

后续步骤

我们简要介绍了使用 Vulkan 最大化游戏图形性能的两个示例。Vulkan 提供了一些低级优化,要求您在更细粒度的级别上管理应用程序中的进程。但正如您所见,实现一些独立的 Vulkan API 可以让入门更容易,并能立即带来性能红利。

这仅仅是开始。有许多开源教程和示例可在此处获取,以帮助您优化多边形绘制并在移动游戏中进一步使用渲染通道。

以下是我们为使用 Vulkan 进行 Android 设备开发的您推荐的一些其他性能示例:

  • 多个渲染通道优于子通道的好处
  • 启用 AFBC(Arm 帧缓冲区压缩)
  • 高效使用管线屏障

以下是一些其他有用的资源:

  • PerfDoc - 验证应用程序最佳实践的 Vulkan 工具
  • Mali GPU 最佳实践 - Arm Mali GPU 的最佳实践指南
  • Android NDK Vulkan 图形 API 指南
© . All rights reserved.