|
什么是Vulkan?
Vulkan是Khronos在2016年的GDC上提出的新一代的图形和计算 API。它提供对现代 GPU 的高效、跨平台访问,这些现代 GPU 用于从 PC 和主机到手机等各种设备。然而Vulkan诞生的最重要的理由是性能,更具体的说,是优化CPU上图形驱动相关的性能。
Vulkan是个显式的API,也就是说,几乎所有的事情你都需要亲自负责。驱动程序是一个软件,用于接收API调用传递过来的指令和数据,并将它们进行转换,使得硬件可以理解。在传统图形API(例如OpenGL)里,驱动程序会跟踪大量对象的状态,自动管理内存和同步,以及在程序运行时检查错误。这对开发人员非常友好,但是在应用程序经过调试并且正确运行时,会消耗宝贵的CPU性能。Vulkan解决这个问题的方式是,将状态跟踪、同步和内存管理交给了应用程序开发人员,同时将正确性检查交给各个Layer进行代理,而要想使用这些Layer必须手动启用。这些Layer在正常情况下不会在应用程序里执行。
由于这些原因,Vulkan难以使用,并且在一定程度上很不稳定。你需要做大量的前置工作来保证Vulkan运行正常,并且API的错误使用经常会导致渲染错乱甚至程序崩溃,而在传统的图形API里你通常会提前收到用于帮助解决问题的错误消息。以此为代价,Vulkan提供了对设备的更多控制、清晰的线程模型以及比传统API高得多的性能。
Vulkan设计思想
简单来说就是以下几点
- 强调程序员自己决定一切: Vulkan不会在驱动程序后台中做隐式的优化 "意图"。让程序员有更大空间去发挥,自由的调度流程和优化以及内存分配和释放。程序员需要自己去完成优化方案,而不是像传统的API一样在驱动程序当中通过调用API模式来揣测并且推断所有操作的优化方法。
- 多线程友好: 确保Vulkan是多线程友好的图形API,利用好现在的多核CPU架构来提高增加性能。抛弃传统API中依赖绑定到基于某个线程上的Context概念。而现在的图形API(Vulkan,Metal,Dx12)全部是通过Queue的方式向GPU提交任务,同时将同步的责任放在开发人员的肩膀上,以允许更高层次的同步。
- 强调复用: 减少相应在申请内存的开销,提高性能。Vulkan的多个组件可以复用。
Vulkan和传统图形API的对比
这有一张表是对于Vulkan和OpenGL ES(其实和OpenGL差距不大)的对比图,如下所示
这里还有两张图也是很好的对比了Vulkan和OpenGL的差异
OpenGL示意图
Vulkan示意图
State Management
OpenGL以及OpenGL ES使用全局的状态控制,并且必须为每个DrawCall重新创建必要的Render State和Resource Bind Table。这里隐含一个问题,那就是当场景特别复杂的时候(几何多,材质复杂,参数多)。这个设置渲染状态以及资源绑定的过程所需要的时间也就不能忽略了。但是场景复杂,大量几何,材质复杂等,正是现代高质量渲染所需的。
Vulkan 使用基于对象的状态(称为描述符),允许应用程序提前预打包已使用状态的组合。编译后的管道对象结合了所有相关状态,允许更可预测地应用基于Shader的优化,从而降低运行时成本。这些更改的影响是显着降低了图形驱动程序的 CPU 开销,代价是要求应用程序预先确定它需要的状态,以便构建状态对象并从减少的开销中受益。
关于Vlukan在这方面设计理念可以在PipeLine的设置当中体现。一个Pipeline包含了传统API中大部分的状态和属性。只不过Pipeline是需要事先创建好的,这样所有的状态组合的验证和编译都可以在初始化的时候完成,运行时不会再因为这些操作有任何性能上的浪费。但正是因为这一点,如果你不同RenderPass需要不同的状态,你需要预先创造多个不同的Pipeline。然而我们不能把所有渲染需要的信息全都丢进pipeline中,一个Pipeline应该是可以通过绑定不同的资源而复用的。可以从下图看出
但是Vulkan也为PipeLine 提供了一些动态状态,这些状态仍然可以被改变,而不会导致Shader重新编译。因此PipeLine不需要重新Bake。更加灵活。
API execution model
传统图形API使用同步(Synchronous)渲染模型,这意味着 API 调用必须表现得好像所有先前的API 调用都已被处理。实际上,没有现代 GPU 以这种方式工作,渲染工作是异步处理的,同步模型是由设备驱动程序维护的精心制作的假象。为了保持这种错觉,驱动程序必须跟踪队列中的每个渲染操作读取或写入了哪些资源,确保所有任务以合法顺序运行以避免渲染错乱,并确保需要数据资源的 API 调用阻塞并等待资源是安全可用的。
那么这种传统图形API执行方式会造成什么问题呢?,那么当你在操作管线的时候,那么驱动程序可能会去做编译Shader,加载资源或者绑定等等操作。但是对于程序员来说,这些操作什么时候会去做,是不是已经做完了。这些在API层面都无从感知。可能造成的结果就是你的CPU卡顿,但是你却不知道卡顿是发生在什么时候。卡顿也许是你第一次给PipeLine绑定特定的Shader,VBO或者Blend Mode,Render Target的时候。由于不同的硬件厂商的驱动处理这些工作的方式都不一样,所以App在不同显卡上运行的症状以及优化的方式都会有差别,优化也是无从下手。
Vulkan 使用异步(ASynchronous)渲染模型,反映了现代 GPU 的工作方式。应用程序将渲染命令塞入Queue,使用显式调度依赖关系来控制渲染任务执行顺序,并使用显式同步原语来对齐依赖的 CPU 和 GPU 处理。
这些更改的影响是显着降低了图形驱动程序的 CPU 开销,但代价是要求程序员自己处理依赖关系和同步,加重了开发者的心智负担(这也就是上面提到的Vulkan是一个显式的API,一切都要自己控制)。
这一张图更加能够体现OpenGL和AZDO(Approaching Zero Driver Overhead) OpenGL,以及Vulkan的区别和特点。OpenGL就像一个预先已经组装好的玩具车,开箱即用,没有太多定制的空间。到了 AZDO OpenGL就像一个乐高一样。你可以自由的去建造,配有大量有用的预制件。Vulkan只有最基础的配件,你必须自己先雕刻成你想要的样子,并且动力系统也得自己去搓出来。
这很符合上面说的说的内容,在OpenGL中驱动程序帮助我们做了很多的东西,但同时也带来了很多限制。虽然对于十分的易用,但是没有定制的空间。而Vulkan是将之前OpenGL在驱动程序中做的那些操作,更多的开放给开发者自己来管理。包括资源加载,以及CPU和GPU之间的同步以及依赖关系管理等等。这给了我们极大的定制空间,解放图形驱动程序CPU侧的开销。性能更佳,但是同时心智负担来到了开发者这边。
API threading model
传统的图形API(包括OpenGL,以及OpenGL ES) 对多线程不友好,传统图形API都包含一个Context概念。Context包括当前渲染管线中的所有状态,绑定的Shader,Render Target等。在OpenGL中Context和单一线程是绑定的,所以所有需要作用于Context的操作,例如改变渲染状态,绑定Shader,调用Draw Call,都只能在单一线程上进行。如下所示
主线程任务繁重
其他线程在休息
但是现在多核系统可谓是相当的普遍。但是传统图形API却不能利用这多核CPU资源。当我们发现是主要是CPU瓶颈的时候,通过CPU多线程应该可以很好的解决性能问题。
Vulkan是通过Queue的方式向Gpu提交任务,多个线程可以向一个Queue提交任务。不需要为了提交任务而 "绑定一个Context,Queue通过CommandBuffer的提交来驱动GPU工作。
尤其是在复杂场景的情况下,这也是Vulkan相比传统API最能体现提高的情况。那就是并行的在不同线程上生成场景不同部分的渲染任务,并且生成自己的Command Buffer,不用任何线程间的Synchronization。最后,不同的线程可以将Command Buffer的Handle传给主线程然后由主线程将它们写入Queue中,也可以直接写入子线程中的per-thread Queue递交给GPU。这样的模式达到了计算资源利用的最大化,多个CPU核都参与了场景的渲染,并且有大量的渲染任务同时递交给GPU最大化了GPU的吞吐量。如下图所示。能够充分利用多线程,并且提高GPU的吞吐量。
API error checking
OpenGL ES 是一个严格指定的 API,具有大量的运行时错误检查。许多错误是由编程错误导致的,这些错误只会在开发过程中发生,并且在运行时无法有效处理,但运行时检查仍然必须进行,这增加了所有应用程序的发布版本中的驱动程序开销。
Vulkan 是围绕最小化驱动开销的理念设计的,这一目标的表现之一就是API中默认的错误检查非常有限。即使是将枚举设置为不正确的值或将空指针传递给所需的参数这样简单的错误通常也不会被明确处理,而会导致崩溃或未定义行为。因为Vulkan要求你对你所做的一切都要非常明确。
然而,这并不意味着这些检查不能被添加到API中。Vulkan 提供了一个框架,允许在应用程序和本机 Vulkan 驱动程序之间插入Validation layers。Validation layers可以实现错误检查和其他调试功能,并且具有可以在不需要时将其删除的主要优点。 这些更改的影响是减少驱动程序 CPU 负载,但代价是使许多错误无法检测到,除非使用Validation layers
Render pass abstraction
传统图形API大都没有Render Pass的概念,但它们对于Tile Base渲染的基本功能至关重要。因此,驱动程序必须在运行中推断出哪些渲染命令构成一个单独的RenderPass。该任务需要一些处理时间并且依赖于可能不准确的启发式方法。
Vulkan API 是围绕RendPass概念构建的,另外还包括单个Pass中的SubPass的概念,可以在Tile Base渲染器中自动转换为Tile Base Shading。这种显式编码消除了对启发式的需求,并进一步减少了驱动程序的负载,因为可以预先构建RenderPass结构。
Memory allocation
传统的图形API大都使用类似客户端-服务器内存模型。该模型明确划分了可在CPU和GPU上访问的资源,并提供了在两者之间移动数据的传输函数。其实也就是说在传统图形API对于资源的创建以及资源的使用都是一对一的映射,很明显这不是最佳的资源管理模式。这有两个主要的副作用。
- 首先,开发者不能直接分配或管理支持GPU端资源的内存。驱动程序将使用内部内存分配器单独管理所有这些资源,不利于性能的最大化。
- 在CPU和GPU之间同步资源是有成本的,特别是在API的同步渲染要求和异步处理现实之间存在冲突的情况下。
Vulkan是为现代硬件设计的,并假定CPU和GPU可见的内存设备之间存在某种程度的硬件支持的内存一致性。这使得API能够让开发者更直接地控制内存资源,如何分配它们,以及如何更新它们。对内存一致性的支持允许缓冲区在应用地址空间中保持持久的映射,避免了OpenGL ES为注入手动一致性操作而需要的连续映射-解映射周期。
根据Vulkan的设计思想。当然对于内存管理这块,Vulkan同时会把这部分的控制权交由开发者,让开发者自己来管理。更加依赖于程序员自身的资源理解,来做到更好的优化。所有的内存的分配和释放控制权全部在开发者。
这些变化的影响是降低了驱动程序的CPU负载,使开发者对内存管理有更多的控制。应可以进一步减少CPU负载,例如,将具有相同寿命的对象分组到一个分配中并进行跟踪,而不是单独跟踪它们。
Vulkan甚至允许讲一个Buffer对象的不同区间划分给格式以及用途不同的子Buffer,例如索引和顶点Buffer可以共享同一个Buffer,只要在绑定的时候指定不同的偏移量即可。这也是最优的做法,它既减少了内存分配的频率,也减少了Buffer绑定的频率。这种做法也符合了Vulkan关于资源复用的设计思想。如下图所示。
至于如何组合这些资源和Buffer的组合以及Layout,那就是需要开发者自己去思考。找到最好的数据/资源布局以及相应的更新频率,绑定等等。不需要像传统图形API那种在驱动中的优化。开发者也最清楚这些数据/资源的更新频率。开发者能以最正确的方式进行更新,永远都能比驱动分析的结果更加准确。以到达更高的性能。所以将这个任务交给程序本身其实也是非常合理的。最好的分配者就是你自己!!
Memory usage
OpenGL ES使用了大量的类型化对象模型,它将逻辑资源与支持它的物理内存紧密结合起来。这在使用上非常简单,但是意味着很多中间存储(例如帧缓冲器的附件)只用于一个帧的子集。
Vulkan将资源的概念,如图像,与支持它的物理内存分开。这使得在渲染管道的不同点上为多个不同的资源重复使用相同的物理内存成为可能。
这种复用内存资源的能力可以用来减少应用程序的总内存占用,通过回收相同的物理内存在一个帧的不同点上用于多种用途。复用和内存突变性会对驱动端优化带来一些限制,尤其是可以改变内存布局的优化,如帧缓冲区压缩。
能从Vulkan获得什么?
Vulkan 是一个相当底层API,它为开发者提供了很多优化事物的能力,但作为回报,它也将很多责任推给了开发者以正确的方式做事。在开始您的 Vulkan 之旅之前,值得考虑一下它带来的好处以及您必须付出的代价;它是一个专家才能玩转 API,并不总是适合每个项目的正确选择。
使用 Vulkan 时要记住的最重要的一点是,它不一定会给您带来性能提升。 GPU 硬件是相同的,Vulkan 提供的渲染功能几乎与传统的图形API相同。如果您的应用程序受到 GPU 渲染性能的限制,那么 Vulkan 不太可能为您提供更多性能上的提升。
优点
Vulkan 带来的最大优势是减少了驱动程序和应用程序渲染逻辑中的 CPU 侧负载。这是通过简化 API 接口和多线程应用程序的能力来实现的。这可以提高 CPU 受限应用程序的性能,并提高整体系统能效。 第二个优点是由于中间内存资源的帧内回收,减少了应用程序的内存占用。虽然这在高端设备中很少出现问题,但它可以在带有较小 RAM 的大众市场设备中有更好的效果。
缺点
Vulkan的优点有时同样会变成缺点,我们减少了CPU侧的负载。但是相应的我们需要去管理在传统图形API当中被隐藏起来的细节,包括内存分配、任务依赖管理和 CPU-GPU 同步。开发者需要负责的东西更多了。虽然这可以实现高度的控制和微调,但是同样在不当的编程下变成一种负优化。还值得注意的是,较薄的抽象级别意味着 Vulkan 可以对底层 GPU 硬件的差异更加敏感,从而降低了性能的可移植性,因为驱动程序无法帮助隐藏硬件差异。
总结
Vulkan 是一个底层 API,它赋予开发者高度的控制权和责任感,并通过非常低的 CPU 开销通过瘦抽象提供对 GPU 硬件和图形资源的访问。很好地使用它的应用程序可以受益于减少 CPU 负载和内存占用,以及更平滑的渲染,减少因更厚的驱动程序抽象对应用程序进行第二次猜测而导致的故障。需要注意的是,Vulkan 很少提升 GPU 渲染性能。毕竟硬件与OpenGL ES下的硬件相同。如果你的程序的瓶颈是在GPU或者程序在CPU端很难多线程并行化的话,那么Vulkan也很难帮助你在效率上有所提升。可以用下面这一张图,来表明什么时机适合使用Vulkan
References
https://zhuanlan.zhihu.com/p/20712354
http://behindthepixels.io/assets/files/High-performance, Low-Overhead Rendering with OpenGL and Vulkan - Edward Liu.pdf
https://developer.download.nvidia.com/gameworks/events/GDC2016/mschott_lbishop_gl_vulkan.pdf
https://developer.download.nvidia.com/gameworks/events/GDC2016/Vulkan_Essentials_GDC16_tlorach.pdf
https://github.com/KhronosGroup/Vulkan-Guide/blob/master/chapters/what_is_vulkan.adoc
https://github.com/KhronosGroup/Vulkan-Samples/blob/master/samples/vulkan_basics.md#what-to-expect |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|