适合高级用户的低级图形
Vulkan 是 Khronos Group 最新的 3D 渲染 API。它是一个低级 API,旨在通过设备驱动程序提供的最低程度的抽象,将 GPU 暴露给应用程序开发者。这使得 Vulkan 应用程序能够享受更低的 CPU 开销、更低的内存占用和更高的性能稳定性。然而,与 OpenGL ES 相比,较低的抽象级别将更多责任推给了应用程序开发者。
本文对 OpenGL ES 和 Vulkan 进行了比较,并概述了开发人员在使用 Vulkan 时应该(和不应该)期待什么。
OpenGL ES 与 Vulkan
对于 Android 移动开发者来说,OpenGL ES 和 Vulkan 是两种 API 的选择,因此,首先比较一下这两个 API,看看它们的主要区别在哪里,会很有帮助。下表给出了概要,并在下方更详细地探讨了每个功能。
| 特征 | OpenGL ES | Vulkan |
|---|---|---|
| 状态管理 | 全局状态 | 状态对象 |
| API 执行模型 | 同步 | 异步 |
| API 线程模型 | 单线程 | 多线程 |
| API 错误检查 | 广泛的运行时检查 | 仅通过层 |
| 渲染过程抽象 | 推断渲染过程 | 显式渲染过程 |
| 内存分配 | 客户端 - 服务器池 | 共享内存池 |
| 内存使用情况 | 类型分配 | 类型化视图 |
状态管理
OpenGL ES 使用单一全局状态,并且必须为每次绘制调用重新创建必要的渲染状态和资源绑定表。所使用的状态组合仅在绘制时才可知,这意味着某些优化操作会比较困难且/或成本高昂。
Vulkan 使用基于对象的状态(称为描述符),允许应用程序预先打包已使用状态的组合。编译后的管线对象会组合所有相关状态,从而更可预测地应用基于着色器的优化,从而降低运行时成本。
这些变化的影响是显著减少图形驱动程序的 CPU 开销,但代价是要求应用程序预先确定所需的状态,以便构建状态对象并从减少的开销中受益。
API 执行模型
OpenGL ES 使用同步渲染模型,这意味着 API 调用必须表现得如同所有先前的 API 调用都已处理完毕。实际上,现代 GPU 均未采用这种机制,渲染工作负载是异步处理的,同步模型只是设备驱动程序精心维护的假象。为了维持这种假象,驱动程序必须跟踪队列中每个渲染操作读取或写入的资源,确保工作负载按合法顺序运行以避免渲染损坏,并确保需要数据资源的 API 调用被阻塞并等待,直到该资源安全可用。
Vulkan 采用异步渲染模型,这与现代 GPU 的工作方式相符。应用程序将渲染命令排入队列,使用显式调度依赖项来控制工作负载的执行顺序,并使用显式同步原语来协调相互依赖的 CPU 和 GPU 处理。
这些变化的影响是显著减少图形驱动程序的 CPU 开销,但代价是要求应用程序处理依赖管理和同步。
API 线程模型
OpenGL ES 使用单线程渲染模型,这严重限制了应用程序在主渲染管道中使用多个 CPU 内核的能力。
Vulkan 使用多线程渲染模型,允许应用程序在多个 CPU 核心上并行执行渲染操作。
这些变化的影响在于,它使应用程序能够从多核系统中受益。值得注意的是,基于 Arm 的系统通常采用一种名为“big.LITTLE”的异构多核技术,该技术将高性能“大”CPU 核心与速度较慢但更高效的“小”CPU 核心相结合,以减轻工作负载。将工作负载拆分到多个核心上可以降低每个核心的负载,并可能允许工作负载从单个“大”核心迁移到多个“小”核心。这可以显著降低系统功耗,并释放热预算,以便将其重新分配给有用的渲染工作负载。
API 错误检查
OpenGL ES 是一个严格规范的 API,具有丰富的运行时错误检查功能。许多错误是由编程错误引起的,这些错误只会在开发过程中发生,无法在运行时进行有效处理,但运行时检查仍然必须进行,这会增加所有应用程序发布版本中的驱动程序开销。
Vulkan 由核心规范严格定义,但不要求驱动程序实现运行时错误检查。API 的不当使用可能会导致渲染损坏,甚至导致应用程序崩溃。作为始终启用错误检查的替代方案,Vulkan 提供了一个框架,允许在应用程序和原生 Vulkan 驱动程序之间插入层驱动程序。这些层可以实现错误检查和其他调试功能,并且其主要优势在于,它们可以在不需要时被移除。
这些变化的影响是减少驱动程序 CPU 负载,但代价是除非使用层驱动程序,否则许多错误将无法检测到。
渲染过程抽象
OpenGL ES API 没有渲染通道对象的概念,但它对于基于图块的渲染器(例如 Mali)的基本功能至关重要。因此,驱动程序必须动态推断哪些渲染命令构成单个通道,这项任务需要一些处理时间,并且依赖于可能不准确的启发式方法。
Vulkan API 围绕渲染通道的概念构建,此外还包含单个通道内的子通道概念,这些子通道可以在基于图块的渲染器中自动转换为图块内着色操作。这种显式编码无需启发式算法,并且由于可以预先构建渲染通道结构,因此进一步降低了驱动程序的负载。
内存分配
OpenGL ES 使用客户端 - 服务器内存模型。该模型明确划分了客户端(CPU)和服务器(GPU)可访问的资源,并提供了在两者之间移动数据的传输函数。这主要有两个副作用:
-
首先,应用程序无法直接分配或管理服务器端内存资源。驱动程序将使用内部内存分配器单独管理所有这些资源,而不会察觉任何可以利用的更高级别的关系来降低成本。
-
其次,客户端和服务器之间的资源同步是有成本的,特别是在 API 的同步渲染要求和异步处理的现实存在冲突的情况下。
Vulkan 专为现代硬件设计,并假设 CPU 和 GPU 可见的内存设备之间存在一定程度的硬件支持的内存一致性。这使得 API 能够让应用程序更直接地控制内存资源、内存分配方式以及更新方式。内存一致性支持允许缓冲区在应用程序地址空间中保持持久映射,从而避免 OpenGL ES 需要手动注入一致性操作而进行的持续映射 - 取消映射循环。
这些变更旨在降低驱动程序的 CPU 负载,并赋予应用程序更强的内存管理控制权。应用程序还可以进一步降低 CPU 负载,例如,将具有相同生命周期的对象分组到单个分配中并进行跟踪,而不是分别跟踪所有对象。
内存使用情况
OpenGL ES 使用重类型对象模型,将逻辑资源与支持它的物理内存紧密耦合。这种模型使用起来非常简单,但这意味着许多中间存储(例如用于帧缓冲区附件)仅用于帧的子集。
Vulkan 将资源(例如图像)的概念与支持它的物理内存分离。这使得可以在渲染管线的不同位置将同一物理内存复用给多个不同的资源。
内存资源别名功能可用于通过在一帧的不同时间点回收相同的物理内存以供多次使用来减少应用程序的总内存占用。别名和内存可变性可能会对驱动程序端优化造成一些限制,尤其是那些可以更改内存布局的优化,例如帧缓冲区压缩。
预期结果
Vulkan 是一个低级 API,它赋予应用程序强大的优化能力,但同时也将许多责任推给了应用程序,以确保其以正确的方式运行。在开始 Vulkan 之旅之前,值得考虑一下它带来的好处以及您需要付出的代价;它是一个面向高级用户的 API,并不一定适合所有项目。
中性的
使用 Vulkan 时,需要牢记的一点是,它不一定能带来性能提升。GPU 硬件相同,Vulkan 提供的渲染功能与 OpenGL ES 几乎完全相同。如果您的应用程序受限于 GPU 渲染性能,那么 Vulkan 不太可能带来更好的性能。
| + 降低 CPU 负载可以释放 GPU 的热预算,从而允许使用更高的 GPU 频率,因此在某些平台上可能会间接提高性能。 |
优势
Vulkan 带来的最大优势是降低了驱动程序和应用程序渲染逻辑中的 CPU 负载。这是通过精简 API 接口和多线程功能实现的。这可以提升 CPU 受限应用程序的性能,并提高整体系统能效。
第二个优势是由于帧内回收中间内存资源,减少了应用程序的内存占用需求。虽然这在高端设备中很少成为问题,但它可以在配备较小 RAM 的大众市场设备中启用新的用例。
缺点
Vulkan 的主要缺点是它将许多任务推给了应用程序,包括内存分配、工作负载依赖关系管理以及 CPU-GPU 同步。虽然这可以实现高度的控制和微调,但也增加了应用程序执行某些操作不理想并导致性能下降的风险。
值得注意的是,更薄的抽象层意味着 Vulkan 对底层 GPU 硬件的差异更加敏感,从而降低了性能的可移植性,因为驱动程序无法隐藏硬件差异。例如,OpenGL ES 依赖项完全由设备驱动程序处理,因此可以假设这样做是正确的,但对于 Vulkan 来说,它们是由应用程序控制的。有些渲染通道依赖项在传统的立即模式渲染器上工作良好,但对于基于图块的渲染器来说却过于保守,因此会导致调度泡沫,导致部分 GPU 空闲。
结论
Vulkan 是一个低级 API,它赋予应用程序高度的控制权和责任,并通过一个 CPU 开销极低的薄抽象层提供对 GPU 硬件和图形资源的访问。善加利用 Vulkan 的应用程序可以受益于更低的 CPU 负载和内存占用,以及更流畅的渲染体验,并减少因更厚的驱动程序抽象层对应用程序的预测而导致的卡顿。需要注意的是,Vulkan 很少能提升 GPU 渲染性能;毕竟,其硬件与 OpenGL ES 底层的硬件相同……