找回密码
 立即注册
查看: 488|回复: 8

Unity 多线程渲染概述

[复制链接]
发表于 2022-12-11 09:42 | 显示全部楼层 |阅读模式
感谢大家的支持和兄弟萌的提醒,文章已修改,去掉了展示源码,请见谅。
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 的。
不过这些在实际项目中都不会引起特别大的问题,作为一个商业游戏引擎,代码还是很干净的,且逻辑清晰好维护算是其特色 ^^。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2022-12-11 09:44 | 显示全部楼层
Unity gfx这套并行程度总归是受限制。还是从dx12或者vk这种底层api理解多线程渲染更容易一些
发表于 2022-12-11 09:49 | 显示全部楼层
历史原因,加改造成本太高了
发表于 2022-12-11 09:54 | 显示全部楼层
你这样容易被unity告的,有个同事这么写已经被开除了
发表于 2022-12-11 09:55 | 显示全部楼层
多谢提醒,看来得改一下
发表于 2022-12-11 10:03 | 显示全部楼层
老哥,unity源码不能公开贴代码讨论,还是留着自己备忘比较好
发表于 2022-12-11 10:10 | 显示全部楼层
多谢提醒
发表于 2022-12-11 10:14 | 显示全部楼层
想请问下哪些算是“Heavy api”
发表于 2022-12-11 10:15 | 显示全部楼层
生成纹理、compileShader、linkProgram等,很多
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-15 22:29 , Processed in 0.224372 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表