NoiseFloor 发表于 2022-12-11 09:42

Unity 多线程渲染概述

感谢大家的支持和兄弟萌的提醒,文章已修改,去掉了展示源码,请见谅。1. 多线程渲染简介

现代渲染 API 相比于上一代渲染 API,最明显的改变就是去掉渲染上下文(Context)这个中心化的设计,转而使用去中心化的命令队列(CommandQueue)来提供多线程渲染的能力,因此多线程渲染已经成为现代游戏引擎的一项必备内容。
由于个别指令的调用需要耗时较多,导致主线程 CPU 出现等待,以致画面卡顿,这也就是多线程渲染主要解决的问题。为了充分利用现代渲染 API 来解决画面卡顿问题,引擎层面也要实现一套基于多线程的渲染框架,做到逻辑更新和渲染指令提交这两个过程解耦到不同线程,使这两个过程互相之间的影响减到最小。
前文详解了 UE4 的多线程源码,也就是通过 TaskGraph 系统将渲染命令打包成任务提交到渲染线程。
其实多线程渲染框架的思想大体上都差不多,基本上都是主线程生成一系列引擎自定义的渲染命令,并写入渲染队列,渲染线程读取渲染队列里面的渲染命令,通过渲染 API 提交到 GPU。这个流程可以看下图。


Unity 引擎实现了一种不同于虚幻的另外一种多线程渲染。
2. 多线程策略

从主线程往渲染线程提交渲染命令是一个非常频繁操作。举个最简单的例子,渲染一个模型需要至少提交以下命令到渲染线程:SetShader、SetTexture、SetWorldMatrix、SetPiplineState、Draw 等,复杂的例子将会更多,而且一个场景里在同一帧会有千千万万个物体需要渲染,这么大量的数据在线程间传递,如果使用有锁的渲染队列,将会有非常大的消耗。
为了避免锁的消耗,UE4 和 Unity 都使用了无锁的思想来优化执行效率。前文我们知道 UE4 使用的是 LockFreeList 这个无锁队列,通过 CAS(Compare And Swap)实现的无锁编程。而 Unity 使用的是循环队列(RingBuffer)来实现的无锁队列。
这两种无锁多线程的策略各有优缺点。
UE4Unity优点● TaskGraph 系统运行有多个生产者和多个消费者,能根据生产者调用的先后,顺序执行任务。
● 任务数据量大小对性能没有太大影响。● RingBuffer 足够大或渲染足够快的情况下,不容易出现线程阻塞。
● 代码结构简单,易于维护。缺点● CAS 由于本身是一种乐观锁,这样的无锁队列实际上每次 Enqueue 和 Dequeue 都实现了一个自旋锁,所以悲观情况下可能会有自旋的等待。
● 代码结构复杂,比如要处理 TaskGraph 任务间的依赖关系。● RingBuffer 决定了只能有一个生产者和一个消费者。
● 每条命令序列化后的数据大小不宜太大循环队列的实现,实际上是靠 Head 指针和 Tail 指针的原子操作来实现的。也就是说,生产者线程写入循环队列时,会原子地去后移 Tail 指针;消费者线程读取循环队列时,会原子地去后移 Head 指针。当 Tail 指针再次超过 Head 指针,则说明队列满了,则需要阻塞生产者线程。


3. 多线程渲染流程概述

Unity 由于历史原因,渲染框架还是用的类似上下文(Context)的方式进行设计的,兼容基于上下文的单线程渲染 API,也兼容基于渲染命令的多线程渲染 API,类似于D3D的Device。
为了实现多线程之间的命令传递,主线程里有一个上下文对象,渲染线程里也有一个上下文对象,它们的命令函数一一对应。例如主线程上下文里面有一个Draw接口,渲染线程里面也有一个Draw接口。主线程里面的Draw接口不真正负责绘制,仅仅产生一个渲染命令,通过RingBuffer传递给渲染线程;渲染线程接到渲染命令后,传递给渲染线程上下文的对应Draw接口,这时候才会真正的绘制。
经过知乎兄弟萌提醒,担心有法律风险,这里不再详解源码,请见谅。4. 主线程和渲染线程的同步

由于 Unity 使用 RingBuffer 来实现命令队列,从上文分析可知:

[*]RingBuffer 在写满的时候会阻塞主线程,等待渲染线程。
[*]RingBuffer 在读空的时候会阻塞渲染线程,等待主线程。
通过这种简单的方式就实现了两个线程间的同步,也就无需再用 CPU Fence 来特别处理线程同步了。
5. 总结

Unity 的多线程渲染框架,由于使用了 RingBuffer 的策略,使得代码非常清晰且简单,学习起来非常容易,但是这也导致了它只能有一个生产者和一个消费者,复杂的扩展会很受限。
另外,Unity 没有像虚幻一样的 RHI 线程,多 RHI 线程的扩展更是无从说起。而且部分“heavy API call”是直接从主线程提交到 GPU 的,比如各种 Buffer 的创建,这会导致这些处理过程中主线程还是有几率等待 GPU 的。
不过这些在实际项目中都不会引起特别大的问题,作为一个商业游戏引擎,代码还是很干净的,且逻辑清晰好维护算是其特色 ^^。

mypro334 发表于 2022-12-11 09:44

Unity gfx这套并行程度总归是受限制。还是从dx12或者vk这种底层api理解多线程渲染更容易一些

johnsoncodehk 发表于 2022-12-11 09:49

历史原因,加改造成本太高了

kirin77 发表于 2022-12-11 09:54

你这样容易被unity告的,有个同事这么写已经被开除了

jquave 发表于 2022-12-11 09:55

多谢提醒,看来得改一下

kyuskoj 发表于 2022-12-11 10:03

老哥,unity源码不能公开贴代码讨论,还是留着自己备忘比较好

Ylisar 发表于 2022-12-11 10:10

多谢提醒

xiaozongpeng 发表于 2022-12-11 10:14

想请问下哪些算是“Heavy api”

rustum 发表于 2022-12-11 10:15

生成纹理、compileShader、linkProgram等,很多
页: [1]
查看完整版本: Unity 多线程渲染概述