Unity云原生分布式运行优化方案

元宇宙时代的来临对实时3d引擎提出了诸多要求,unity作为游戏行业应用最广泛的3d实时内容创作引擎,为应对这些新挑战,提出了unity云原生分布式运行时的解决方案。livevideostack 2023上海站邀请到unity中国的解决方案工程师舒润萱,和大家分享该方案的实践案例、面临的问题、解决方式,并介绍了unity目前对其他方案的构想。
文/舒润萱
大家好,我叫舒润萱,现在在unity中国担任解决方案工程师,主要负责开发的项目是unity云原生分布式运行时。
首先介绍一下unity。unity是游戏行业应用最广泛的3d实时内容创作引擎。截止2021年第4季度,70%以上移动平台的游戏是使用unity开发的。
但unity不止是一个游戏引擎,unity的业务目前涉及到汽车行业、建筑行业、航空航天行业、能源行业等等各行各业。
unity的业务在全球都有开展,在18个国家有54个办公室。在中国,在上海、北京、广州都有办公室,在临港也开了一间新的办公室。
unity覆盖的平台是最广泛的,它支持超过20个主流平台,率先支持了apple新发布的vision os。
我今天的分享将从这六个方面进行。
-01-
元宇宙时代的挑战
首先,实时3d引擎在元宇宙时代会遇到什么挑战?
unity认为元宇宙会是下一代的互联网,它将是实时的3d的、交互的、社交性的,并且是持久的。
元宇宙会是一个规模庞大的虚拟世界,这个虚拟世界里有很多参与者,它将会是一个大量用户实时交互的场景。同时,元宇宙它必须是一个持续稳定的虚拟世界。
参与者在这个虚拟世界里对它进行的改变将会随着时间保留下来,并且它是稳定的状态。这就对实时3d交互造成了很多挑战。
首先,因为这个虚拟世界将会是一个大规模的高清世界,里面将会有数量庞大的动态元素、静态元素,所以它对实时3d引擎提出了渲染的挑战。
其次,它伴随着大量的网络传输,对引擎的可扩展性和可伸缩性提出了很高要求,所以对运行平台来说也是一个不小的挑战。
最后,因为这个虚拟世界会有超大规模的物理仿真,用户将会在其中进行大量的实时交互,所以这对运算资源也是一个巨大的挑战。
-02-
unity分布式运行时
为了应对这些挑战,unity提出了分布式运行时的解决方案。
这个方案由两部分组成,第一个部分是unity云原生的分布式渲染,第二个部分是unity云原生分布式计算。
首先介绍分布式渲染。要做一个多人联网的体验,可以从最简单的server-client架构入手。如图所示,在这个架构中有一个中心化的server,它可以服务于多个client。这张图上画了两个client,这是最简单的架构。
为了对应大规模的渲染压力,unity把用户的client端拆成了merger和renderer。在这里面,一个merger对应多个renderer,这些renderer将会负责虚拟世界的渲染。我们把一帧的渲染任务拆分成很多个子任务,分别交给这些render进行。merger负责把renderer渲染的画面组合成最终画面,之后通过webrtc推流的方案推给用户的客户端。要注意,这里除了用户的客户端以外,所有的环境都是运行在云上的。
图示为一个最简单的屏幕空间拆分,即把一个完整的画面在屏幕空间上拆成几个部分。除此之外还有其他拆分方式,例如通过时间拆分:假设有4个renderer,第一个renderer渲染第一帧,第二个renderer渲染第二帧,第三个renderer渲染第三帧……以此类推,再把它们组合成一个完整的序列。
unity现在也在实验一个新的拆分方式:在merger上渲染一个比较高清的近景,再把远景拆分到几个renderer上。renderer的几个画面可以组成一个天空盒推到merger上,这样merger就可以同时拥有细节比较丰富的近景和远景。unity的架构允许开发者根据自己的业务需求定义自己画面的拆分方式。
回到这个架构图,想象在一个大型的虚拟世界里,有成千上万用户连入运行时,通过分布式渲染,把原本一个进程服务对应一个用户的场景,拆分成好几个进程服务于一个用户,因此这个场景就会对server提出巨大的挑战。为了应对这个挑战,unity提出了分布式计算。
这种分布式计算把server拆分成了server和remote,用 remote分担server的一些运算压力。
简要概述一下unity是怎样把计算任务分配给remote的。unity游戏的业务流程可以简单拆分成数据和数据处理器,数据叫做component,数据处理器实际上就是业务逻辑,代码叫system。每个system有自己感兴趣的数据,它会读取这个感兴趣的数据,通过逻辑进行数据更新。
unity把这个system跑在远程的机器上,而不是本地,之后把它感兴趣的数据通过网络发送给远程的机器,它就可以像本地处理一样处理这个数据,再把更新的数据通过网络发回给server。
视频编解码与实时渲染
下面进入今天的正题,视频编解码与实时渲染。
为什么要在分布式渲染中引入视频编解码?
可以看这个简化的分布式渲染架构图。merger和renderer之间的通信是通过网络tcp传输的,即图像是通过tcp传输的。
这个做法有两个问题:
一是会遇到网络带宽瓶颈,在实时3d交互的场景下,一般至少要target到1080p60帧。而如果传的是8 bits rgba的图像格式,需要的带宽是4gbps,远远超出了常见的千兆带宽1 gbps,很多机房没有办法接受。即使对raw data进行yuv420的简单压缩,它也要占用约1.6 gbps的带宽,超过了千兆。
其次是性能问题。因为unity的画面是从gpu渲染出来的,为了通过网络传输这个画面,要先把gpu显存上的图像数据先回传到cpu端。这个回读就会导致需要暂停渲染的流水线,因为目前unity优化较好的实时3d引擎的渲染都是流水线形式的,即cpu产生场景数据,再把这个场景数据交给gpu渲染,同时cpu可以进行下一帧的计算。但是如果数据是反过来从gpu回到cpu,意味着需要把流水线暂停,包括gpu、cpu上的所有任务都要等待回读完成之后才能继续工作。
这里截取了一张unity的回读指令,它消耗的时间是7.22毫秒,在实时交互的场景下,希望做到的是60帧的体验,即16毫秒,因此用7.22毫秒的时间把这张图片读回来是无法接受的。
为了解决这两个问题,unity为分布式渲染的方案开发了unity native render plugin。unity的目标运行环境是云端,linux和vulkan的图形api。云端一般配备的是nvidia gpu,希望采用nvidia gpu进行硬件的视频编解码。在这个方案下面,unity还是负责虚拟世界的渲染和逻辑。ffmpeg负责视频的编解码以及把unity的图像在显存内部拷贝给nvidia的video codec sdk,unity的插件就是连接这两个部分以实现目标。
这几个流程图展示了unity一定要硬件编解码的原因。橙色的框为显存上的数据,蓝色的框为cpu端内存上的数据。
最左边的流程图是最简单的。如果要传输一个raw data的方案,在gpu上把图像渲染完成之后,要通过回读把它读到cpu端,这是非常耗时的操作。在读完这个图像之后,把它通过tcp发送给merger,占用带宽非常大,这是不可接受的。
如果引入的是一个软件编解码的场景,需要图像的数据在cpu端存在。所以回读还是无法避免,操作仍然十分耗时。在软件编解码时进行的编码和解码操作,同样非常耗时。虽然这对视频来说没问题,但是对于60帧的实时场景是不太能接受的。但是这个方案的带宽其实优化了很多,因为视频码率至少比4 gbps、1.5 gbps好很多。这是软件面板的情况,它最多只是优化了带宽。
最后是硬件编解码。硬件编解码是在渲染完成之后,通过显存间的拷贝,把渲染的结果拷贝到media的cuda端,然后做硬件编码和硬件解码。虽然是一次拷贝,但是因为是在一张显卡的显存上,所以拷贝过程非常快。硬件编码完成之后,视频帧数据会自动回到cpu端。虽然其本质上也是一次回读,但是它和流水线无关,相当于是在另一条线上完成的,所以这个回读消耗的只有从pcie到cpu到内存的带宽的速度,回读的不是一个完整的图像,而是压缩好的视频帧,它的数据量也大大减小,所以回读非常快。之后通过tcp发送视频帧,带宽非常小,在merger端解码上传的buffer也比原来小,解码也很快。最后把解码完成后的image,copy 到vulkan texture,这也是在同一块gpu内显存里的操作。
可以看到,硬件编解码的流程优化了很多,每一步都能得到很高的提升。
介绍一下unity是怎样配置ffmpeg的codeccontext的。unity使用了两个codeccontext,video context是真正用来做视频编解码的context,它是一个type cuda的hwdevice。第二个codeccontext用的是vulkan的context,这个context的作用是
和unity的vulkan context进行交互,所以在初始化时不会使用到它默认的api,而是从unity的vulkan instance里面取出了所有必要的信息,把它交给ffmpeg的vulkan context,最后通过这种方式,就可以让ffmpeg的环境和unity的环境存在于同一个vulkan的运行环境中。
video context做初始化时,没有用到它默认的hwdevice context create的api,用到的是create derived的api。这个api需要传入另一个hwdevice context,它能保证做初始化时和另一个hwdevice context使用的是同一个物理gpu,这样才能真正地做显存间拷贝。这里选取的边界码的格式是h.264,8 bit,nv 12。 为什么要选用这个格式呢?其实是受硬件限制,因为目前nvidia硬件不支持10 bit的h.264的解码。h.265则耗时较大,不太满足60帧的需求,所以不得不选取这个格式。同时unity是zero latency的配置,gop的size是0,保证视频每一帧都是intra frame。这有两个好处,一是它的延迟非常低。其次,因为分布式渲染在管理的时候可能会随机丢弃帧,如果有预测帧则不好丢弃,在全是i帧的情况下可以随机丢弃。
-05-
画质与性能优化
接下介绍unity在画质方面和性能方面对插件进行的一些优化。
首先是tone-mapping。unity之所以这样做是因为遇到了color-banding问题,这个问题由两个因素导致:
首先,传输的图像不是一个普通的sdr图像,而是hdr图像;
其次,选取的格式只有8 bit,它导致了颜色精度和范围都是受限的。
为了理解为什么unity传输的是hdr图像,我给大家简单介绍一下渲染里面的一帧是怎么生成的。图示为简化的渲染过程,真正的渲染过程要比这复杂很多。
渲染过程可以简单分为三部分:
第一个部分叫pre-pass,它的作用是生成后续渲染阶段所需要的buffer,在这一阶段里会预先生成depth,即预先生成深度缓冲。深度缓冲预先生成最主要的好处就是可以减少over-draw,很多被遮挡的物体直接被剔除掉,后续步骤中就可以不用画。
第二步是渲染中最主要的一个步骤,我把它叫做lighting pass,主要的作用是计算光照,生成物体的最终颜色。这种pass的实现方式有很多种,例如前向渲染、延迟渲染等。无论是使用哪种实现方式,它的最终目的都是生成一张color buffer,即颜色数据。
最后一个步骤一般成为后处理post-processing。这个步骤的主要作用是对输出的color buffer进行图像处理。经过post-processing产生的图像,颜色比单纯的color buffer自然很多。post-processing里面的效果大部分都是屏幕空间效果,例如要做一个辉光的效果,画面中有一盏灯非常亮,在它的边缘会有一些柔和的光效溢出,这个效果基本上就是通过post-processing实现的。
屏幕空间效果会有什么问题?在分布式渲染中,渲染任务是被拆分开的,所以真正做渲染的render的机器,实际上是没有全屏信息的,它没有办法做后处理。
对此unity的解决方案是将后处理交由merger进行。renderer渲染到color buffer生成之后,就把color buffer传给merger,merger先把这个color buffer合起来,再总体做一次后处理。
那么,为什么这一过程中传的colour buffer是hdr的?因为在渲染中,光照模型一般都是物理真实的,所以为了之后做后处理,colour buffer本身是hdr的。
如图所示,这是把black point和white point分成设置成0和1的效果,但是实际上这张图最高的值可以到90以上。如果是室外的场景,最高值可以到一万多,所以这张图的动态范围是非常高的。
同时,因为传的是动态很高的hdr图像,加上使用的是8bit的编码,所以必须找到一个方法把hdr的corlor buffer映射为sdr的buffer。对于这一映射也有一些要求,其一,它需要可逆;其二,它需要保留更大的表示范围,并且尽量减少精度损失。
unity在这方面采用的是amd提出的fast reversible tone-mapper,它有许多好处: 首先它是unity srp原生支持的,是unity的可编程的渲染管线;其次它是可逆的,如图是它的公式;再次,它保留的数字范围更大,精度损失更小。如图上这一图像,y=x可以理解为原始颜色,经过tone-mapper处理后,它的取值范围永远在0~1之间,同时当它的值越小时,斜率越大,代表它能表示的数字越多,可保留的精度越高。有时在渲染中会遇到颜色值非常高的情况,但这种情况少之又少,更多的还是保留在0~1的范围区间内。因此tone-mapper能够帮助我们在0~1这个区间范围内保留更多的精度;最后它非常快,在amd的gcn架构的显卡下,max rgb会被编译为一个指令,整个运算中只有3个指令,max、加法和除法,所以它是非常快的。
如图,可以对比使用tone-mapping前后的效果。最左边是未经任何处理的原始图像;中间是不使用任何tone-mapping,经过8 bit的编解码、网络传输,再解码回来的情况;最右边是应用了fast reversible tone-mapping的情况。可以看到里面的背景有很多细节纹理,代表其中高频的信息比较多。在高频信息较多的场景下,应用了fast reversible tone-mapping之后的效果和原始图像的效果对比,已经看不出什么区别了。
但是如果场景里低频的信息较多,例如渐变较多,即使运用了tone-mapping,也没有办法完全解决这个问题。可以看到,原始图像上的渐变非常柔和,在无tone-mapping的情况下,色带肉眼可见。但是即使引入了fast reversible tone-mapping,也只能减缓色带问题,比起原始图像而言还是差了很多,目前没有更好的办法解决这一问题。
unity对插件进行性能优化的另一个方式是vulkan同步。因为涉及gpu内部的显存拷贝,而且它是vulkan拷贝到cuda的操作,所以它是一个gpu-gpu的异步操作。异步操作要求开发人员对于操作做好同步,即在编码时,要保证unity渲染完成之后才进行编码,解码时要保证解码完成之后,才能把这张图像copy给unity,让unity把它显示在屏幕上。
unity vulkan native plugin interface提供了一种同步方式。这里主要关注框出的两个flag:
上面的flush command buffer,指在unity调用自定义渲染事件时,unity会先把它已经录制好的渲染指令提交到gpu上,这时gpu就可以开始执行这些指令了。
下面的sync worker threads,指在unity调用自己定义的plugin render event时,会等待gpu上所有的工作全部完成之后才会调用。
这两个方式组合起来确实能满足需求,确实可以做到同步,但是这种方式也打断了渲染流水线。使用这种同步方式,在调用plugin event之前,所有程序要全部停下,等待gpu完成操作。因此这个方式虽然能实现同步,但非常耗时。
如图展示的就是在这个同步方式下的情况。在简单的场景下,它耗时5.6毫秒。所以这种方式虽然能同步,但是性能非常差。
unity只提供了上述的同步方式,因此我们只能转向vulkan自带的同步原语。timeline semaphore是vulkan在1.2版本的sdk里提出的新型的semaphore,它非常灵活,而且是ffmpeg原生支持的。这里是ffmpeg的vulkan context的frame,它通过timeline semaphore同步。在ffmpeg里,它主要被用于gpu-cpu同步,但它也可以用于gpu-gpu同步。
unity提供的同步方式只有上述两个flag,它无法直接使用底层的同步原语,但是它允许我们hook vulkan的任何一个api,即在 hook之后,unity在调用vulkan的api时,它其实调用的是我们自己定义的hook的版本,因此我们使用了vulkan hook介入unity的渲染,在unity把一帧的渲染提交给gpu之前,通过hook提交的api,把ffmpeg的semaphore 塞到提交里面,就可以保证在渲染完成之后会通知timeline semaphore,ffmpeg会等semaphore被通知之后再执行。通过这种方式,这个同步也能达成目的,而且不会打断渲染流水线。
如图所示,上面为5.6毫秒耗时的情况,下面则完全把这一耗时消除了。因为在这个情况下,不需要在调用render event之前就提交渲染指令,而是在gpu上通过timeline semaphore和ffmpeg的command进行同步,因此把这一部分完全省去了。在这个简单场景中大概有5.6毫秒左右的提升,经过测试,在复杂场景中则会有10~20毫秒不等的提升。
另一个性能优化方案是多重缓冲,这是渲染中非常常用的技巧。在渲染中,常常会用多重缓冲来减少画面的卡顿、撕裂等情况,三重缓冲还能够提高帧率的稳定性、提高渲染性能。
多重缓冲的引入会把渲染变成single producer, single consumer的流水线模型,即渲染流水线。正是因为有多重缓冲,才能形成流水线,让gpu往一个缓冲中写入时,cpu可以开始准备下一个缓冲,gpu可以同时往另一缓冲区写入下一帧数据。
在编解码的插件中,unity也引入了多重缓冲来提高性能,使用硬件编解码代替图像上屏操作。渲染的图像没有显示在屏幕上,而是通过网络发走,这是通过多重缓冲的方式实现的,和渲染有同样的效果。在引入多重缓冲后,unity的渲染和编解码器会分别作为producer和consumer进行渲染和编解码的流水线。
-06-
总结与未来展望
在分布式渲染的解决方案中会遇到网络带宽和性能问题。
首先,通过引入视频编解码可以解决了网络带宽的问题,采取硬件编解码避免gpu-cpu的回读,避免打断渲染的流水线。为了实现这个目标,unity开发了unity native rendering plugin来对接unity和ffmpeg底层的vulkan和nvidia codec 的sdk。
因为选取的编解码格式,方案中还遇到了色带问题,因此在方案中我们引入tone-mapping优化画质,通过ffmpeg自带的timeline semaphore,把unity的渲染指令和ffmpeg的拷贝指令和编解码指令进行同步,保证编解码结果正确。
最后,unity通过引入多重缓冲提升性能,减少帧率不稳的情况。
目前unity还在探索一些其他的方案。
首先,unity希望尝试vulkan自己推出的vulkan vider extensions。它在2023年1月左右才真正进入vulkan的sdk,成为一个正式的功能。这个extension非常新,所以到目前为止unity还没有机会进行尝试,但一直在关注。如果使用这一extension,就可以完全避免前文所述得到显存间拷贝、同步等问题。因为这一应用程序没有引入别的gpu端的运行环境,完全在vulkan内部运行,因此我们不需要拷贝,直接使用unity的结果即可。
其次,unity在对接其他gpu的硬件厂商,尝试其他硬件编码。unity目前正在和一些国产的gpu厂商对接,他们表示他们的硬件编解码能力会有所提升,支持的格式不再限制于8 bits了。
最后,unity还希望尝试一些要使用gpudirect rdma、cxl共享内存等特殊硬件的方案。gpudirect rdma允许直接把gpu显存里的东西直接通过网络发走,能够减少回读。cxl共享内存,顾名思义是个共享内存,相当于很多台机器共享一个远程的内存池,因此它的带宽和延迟都是内存级别。这一方案至少允许我们在分布式渲染的环境下不进行视频编解码,可以使用raw data的方式,把raw data存在远程内存中。


PCI Express架构QA问题解答
CAN总线在汽车ECU中的作用
电感电容、谐振器和振荡器的Q值
罗德与施瓦茨和华为共同完成4.5G移动通信下行传输速率测试
安捷伦N9030A频谱分析仪维修按键不灵,不稳定最新案例
Unity云原生分布式运行优化方案
Linux程序之可变参数&&选项那些事
如何解决iphone不能拍照故障
迪文2023全国巡回研讨会武汉、长沙站成功举办
选购高低温冲击试验箱时需要注意什么
Giroptic为iPhone推出iO全景摄像头,支持全景直播
苹果卖不动了?真相是iPhone打破了历史销售记录!
深刻解读动力电池梯级利用技术规范
具二维亚铁磁性石墨烯系统首次合成
数字化医院解决方案
stm32最高工作频率是多少
2019年全球拼接屏增速26% 京东方发力拼接超大尺寸等商显市场
扎根电池产业链用户端采购商,这个展会你值得拥有!
创新力连续四载,再添荣耀!ZLG致远电子荣获24届中国专利奖优秀奖
全球汽车保有量逐年增长, 未来汽车维修服务发展潜力巨大