3

RenderGraph review | mikialex

 2 years ago
source link: https://mikialex.github.io/2020/08/14/render-graph-2020/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

RenderGraph review

Posted on Aug 14, 2020

一个图形框架主要分为两部分,描述画什么以及怎么画,前者往往抽象为场景,而后者就是我们要讨论的主题。

我们目前见到的绝大多数渲染框架,都主要集中在资源的角度来进行抽象,比如geometry, material,这些抽象为库的使用者减少和抹去了管理创建资源,以及渲染的实现的负担。但是这些库并没有在如何组织渲染流程上提供太多的抽象。在面对复杂的后处理,多个pass间的依赖关系时,这些依赖一般都是手工维护,rendertarget的复用也是需要手工维护,当这些依赖关系需要由多项配置决定时,整个事情就非常难做了,以至于变成工程上不可能的事情。

去年我在artgl里通过一些业余实践实现了一套渲染流程上的框架,称之为rendergraph: 用户通过一套声明式的api构建pass/target依赖的有向无环图,框架完成依赖解析,fbo重用,等工作。最后可以说解决的非常干净漂亮。这套体系经过定制修改目前已经集成于实际工程项目,一举解决了诸多维护性上的问题。

现在回想整个设计过程和解决方案,给我带来几个关键的认识:

尽可能从data flow的角度思考解决问题,而不是传统的control flow。

如果没有graph,所有的数据依赖处理,都是分散在各个pass的类里,由大量的配合外部配置项的if else所决定,通过控制流来决定一步一步该做什么,控制流的代码是写死的,写死在各个类的方法里。

在graph下,核心其实变成了数据流,api直接描述数据间的依赖关系,框架来组织什么数据在什么时候需要给谁。而原本的控制流即实际的依赖关系,或者说流程,是通过运行时计算出的,而不是靠控制流写死在代码里。

流程即是数据,数据即是流程

这个基本上就是代码即是数据,数据即是代码的一个小的体现。

前面说到 原本的控制流即实际的依赖关系,或者说流程,是通过运行时计算出的。这个计算结果简单说比如就是拓扑排序的结果,这个结果就包含了这个流程完整的内容,不多不少。就是一个array,就是数据。这个数据,这个实际的流程,我们也是直接cache的。对于任意一个能影响图结构的配置项,直接计算出流程或者从cache得到。将代码/算法/流程直接变成数据。

理论上如果我们知道会用到的配置组合,那么从这个数据直接生成流程的代码也是可行的,不过没什么意义。只是计算到底是编译期还是运行期的问题。

没有graph,执行流程是写死在代码里超集,通过配置和控制流代码来决定哪些部分真的要执行,直接造成了实现上的灾难

graph这个设计是通用做法,正常做法

我后来了解到很多桌面端的引擎,都采取了graph结构。我觉得他们不得不用,为什么,因为比如vulkan,要是不用图我觉得代码就没法写了。vulkan 是要你自己完全控制各个pass间的依赖的,这包括但不仅限于你要在target和pass依赖的点,各种数据的依赖点自己加semaphore,内存屏障,各种同步源语,不然你根本不知道什么时候gpu真正执行到哪个pass,不知道什么时候可以拿到正确的数据。

事实上,在有的游戏引擎中,整个流程,不仅是渲染部分也使用了graph的架构,rendergraph,解决的是渲染数据流的问题,而相同的思想用来解决gameplay部分的数据依赖完全是没有问题的,整个游戏引擎,整个世界更新逻辑就是一个超大的graph,渲染只是其中一部分。

基于graph,我们还能做更多优化

为了解决依赖问题,拓扑排序就够了。但是有了graph我们能做更多事情。

就 fbo 重用来说,目前很简单就是按需重用,没有主动优化。什么意思呢:一个graph,满足拓扑顺序的结果有很多,如果真的要好的fbo重用,我们应该找到这个解的集合中fbo并行量最小的一个。所以fbo重用就变成了一个图优化的问题。

又比如,现在对于每个effect,假设需要depth,那么我自己做图构建的时候,还是要手工的重用depth对应pass的node,比如从外层传入这个node。ok,事情变复杂了以后我怎么保证我能靠人肉充分复用某些pass的计算呢。从理想的角度,我希望我不要考虑这些事情,让框架自动找到可复用的pass计算。这种重用本质上是流程的重用,而本质上是另一个图优化的问题,如何识别和复用图中的子结构的问题。

那么state切换方面的优化是不是图的问题呢,我想过其实也使得,不过这个是scenegraph,就不展开了。

Rendering engines are becoming compilers

比如你手工的提取出可以复用的depth pass计算,和本质上你在一个函数里手工提出重复计算并无不同。而事实上编译器的确是能优化重复计算的,优化重复计算也是把代码转化成计算图解决图优化的问题。

如果说整个3d框架,渲染引擎,流程上本质就是生成,优化,执行一个数据流图,那和一个compiler后端有什么区别呢?

我现在搞了套很漂亮的api来做图构建,本质上也太低级,那似乎搞一套dsl也是没啥问题的,正好对应compiler前端,设计一套更高层次的流程描述出来?

其实如果将renderengine视为compiler的,那不是那种静态的AOT的,而是JIT的,意思是对于执行结果,其实是有feedback的,既然有feedback,那就可以做优化的。我认为render engine as complier最终极的方向,就是compiler能根据实际的场景数据执行情况,分析出场景特点,分析出绘制瓶颈,动态实现各种优化行为。比如配合其他子系统比如场景,动态优化调整输入。配合降级体系,自动挑选最优的效果降级方案,这种降级不是关掉某个pass那么简单,而是能实时获得底层gpu情况,精确的调整某些sample count,size这类。

当然这个脑洞目前很大,工程上做出来也很有挑战,我相信一些成熟引擎已经开始这个方向的转变了,很有意思。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK