FrameGraph Extensible Rendering Architecture in Frostbite

基于 Unity SRP 的 Render Graph 来满足多个在研Unity项目多平台、高质量、多风格的渲染要求。

目录

简介&历史

WorldRender

Frame Graph

高级FrameGraph

lambdas

RenderMode

Transient resource system: The back-bone of FrameGraph


简介&历史

2007时是DICE的引擎,用于未来的battleField的产品,多平台DX9&10。10年后,2017年Frostbite成为EA的标准引擎,多平台DX12,在15款游戏以及未来的EA游戏中使用。

07年的rendering system

10年后,加了很多新特性,新平台,解决了很多新问题。

简化的rendering system。最底下是Platform Graphics API,上面是Render Contex作为API的抽象,在上面Shading system 07年介绍过,用于渲染整个世界,而且是根据美术在ShaderGraphs中的连线构造的Data-Driven Architecture。ShaderGraph定义了surface properties,对光照进行了解耦,来帮助我们创建一批shader可以用于deferred/forward;

shading system被渲染features驱动,比如地表渲染,mesh scattering,rigid geometry,一些渲染features会直接使用Render Contex,大部分是一些全屏处理,比如lightng,pp等,RC是code-driven的架构;最上层有一个庞然大物world render,来管理整个世界。

这是一个code-driven的架构,world render知道所有的渲染特性,views & passes,它分配出资源(RT,buffer),在不同的system中设置并调度它们。

BF4中使用的rendering pass(直接作用于Render Contex的features),很多产品项目组的程序美术会添加各种各样的new feature进来。

WorldRender

World Render的挑战:

  • 显式的立即的模式进行渲染;
  • 显式的资源管理,各个产品项目组都会定制、手工的管理ESRAM,对资源的统一性和merge造成了影响;
  • 和渲染框架耦合严重,因为显式的定制渲染和资源,所以很多模块都受到对方的影响;
  • 局限的扩展新;产品组必须fork来定制;10年间,从4k行代码涨到15k行,非常困难去维护、扩充、整合。

WorldRender的目标:

  • 想知道高层整帧的概况;
  • 更好的扩展性,解耦并组合渲染模块,自动化的资源管理,更好的可视化和分析工具。

为了达到目标,引入了两个新的架构组件:Frame Graph以及Transient Resource System

Frame Graph

Frame Graph用来高度抽象表示render pass和资源,它了解一帧之内所有的信息;Transient Resources来帮助FG分配资源,管理memory aliasing。

FG的目标:

  • 构建高层对整帧的了解,简化资源管理、简化渲染管线配置、简化async compute以及resource barriers;
  • 允许我们写独立的高性能渲染模块;
  • 可以可视化以及debug复杂的渲染管线。

这是一个Deferred Shading pipeline,橙色代表render passes,蓝色代表资源,箭头代表render pass之间的依赖关系,红色是write操作,绿色是read操作。

BF4的一帧,大概有几百个render passes和resources,一个可以线性分析的可视化工具,可以顺序的显示render passes和resources。

 最上面是render passes,包括graphic和async compute;当鼠标移到某个render pass上方时,可以看到那些resources是需要被当前rp读写的(绿色/红色);

当移动到resource上时,可以看到对它进行读写的render passes

最下方可以看到这些数据的物理内存分布,由transient resource system计算。

移除了立即模式的渲染,新的rendering代码可以分割为passes,rendering分为了3个阶段:

  1. setup设置使用哪些render passes,哪些resources(定义每个passes的inputs和outputs资源);
  2. compile负责resources的lifetime,并且给他们分配空间;
  3. execute就是执行每个render pass。

 Render passes通过resources来建立互相的关系,这儿resources是handles并没有内存分配,rp必须定义所有用到的资源,包括读写和创建;有些永久资源直接通过API创建,它们会被导入到rp中一起进行管理,比如TAA的history buffer/backbuffer等。

在setup阶段,Flow类似于IM渲染,但在此阶段我们不会生成任何GPU命令。只是建立有关帧渲染操作的信息。在图形构建过程中,所有资源都是虚拟的。渲染过程的输入和输出是使用虚拟资源句柄声明的。

 我们也可以读或者写之前创建的资源,这儿rp读一些资源并且写入一些资源;我们写入的资源之前老的handle会被invalidated,在写操作后我们会重命名这个资源,写入invalid handle的资源会造成runtime error,这会帮助我们找到一些之前老代码就存在的数据竞争与依赖。

高级FrameGraph

  •  延迟创建资源,很早的声明资源,直到使用时才创建,根据使用自动设置创建的desc;
  • 推导资源参数,可以根据input的格式/size来推导参数,比如在resolve pass需要一个downsample chain,简单的可以自动直接推导出创建资源。
  • 移动子资源,可以帮助我们直接在多个pass复用资源,自动的创建子资源或者资源别名。

 有Deferred Shading,最终会输出2D RT;另外我们还有一个Reflection模块用来做一些convolution,他需要一些cubemap作为input,我们可以使用move操作把lighting buffer输出给cubemap的faces,所以lightbuffer会使用cubemap的subresource view来代替整个2D rtview,复用了lighting buffer的资源。这帮助我们解耦了各个模块,ds和refl模块之间不需要互相了解任何信息。

Compile阶段

这个阶段不会给program user暴露,自动运行。把没有引用的res和passes cull掉,这样在setup阶段可以草率一些,更容易解耦,简化了可选的passes以及debug rendering等;计算res的生命周期;根据生命周期和bind flag分配固定的GPU资源,对于async compute会延长其生命周期。

graph culling的重要性:DS会有一个debug模块,我们就一直加上他,不需要知道它是否开关,正常渲染时会被cull掉。

Culling algorithm
Simple graph flood-fill from unreferenced resources.
Compute initial resource and pass reference counts
    renderPass.refCount++ for every resource write
    resource.refCount++ for every resource read
Identify resources with refCount == 0 and push them on a stack
    While stack is non-empty
        Pop a resource and decrement ref count of its producer
        If producer .refCount == 0, decrement ref counts of resources that it 
            reads
             Add them to the stack when their refCount == 0

执行阶段

执行每个rp的callback函数,里面的代码和没有FG之前的一样:包括使用RC、设置state等,dispatch和draw;唯一不同的是这儿需要根据handle去devirtualize真正的GPU资源。

异步计算

SSAO读取depth输出到raw AO上,filter把raw AO处理为AO,之后作为输入给Lighting pass,AO变为async compute,这样rawAO的声明周期就延长至lighting pass(main queue中第一次使用其output依赖的地方)

 

 

lambdas

重要的是在C++中怎么声明RP:可以对每个RP声明一个class,但是这样就会破坏code
flow(每个class分割了procedure的代码),需要一堆样板参数,对于现有代码难移植;使用了lambdas来解决,可以保证线性的代码流,最小化改变已有代码(把原有代码包进lambda中,加入资源的使用声明)

第一段包含rp使用的resources声明;接下来是setup phase的lambda,声明了resources如何使用;最后是execution phase的lambda,会在之后才执行,也许会被cull掉不执行;lambda能帮助我们自动捕获需要的变量,对于setup phase逻辑上应该可进行读写设置,捕捉引用&,对于execute phase逻辑上应该是延迟执行,捕捉使用值传递=。

addCallbackPass()是一个模板函数,它在后台创建一个由PassData和executelambda参数化的渲染过程类。Setuplambda是在addMyPass()中内联的,但executelambda是deferred的。Setuplambda可以通过引用捕获所有内容,但executelambda必须通过值捕获。

按值捕获数据有点危险,因为可能会意外捕获在执行阶段之前释放的指针。也有可能意外地capture huge structures by value。幸运的是,我们可以强制executelambda的大小在编译时低于一定的大小(我们确定了1KB的限制)。

RenderMode

Render modules:2种渲染模块:1)Free-standing stateless function

第二种是类似TAA的history buffer这种,生命周期大于一帧。WorldRenderer依然在高层协调整个渲染,但是它不会分配任何GPU资源,仅仅kick渲染模块,更加简单去扩展,代码量从15k行变为5k行。

 

之前渲染模块之间显式的通过参数和返回值传递数据,这种机制非常难以扩展因为需要改变函数声明或者结构定义。新的模块通过一种storage(hash table) component传递数据,key是typeid,这样的耦合是可控的。我们想让模块之间可以传递数据,但是又不行暴露给外界;举个例子:tonemap模块需要blur模块的数据,blur模块通过blackboard.add把相应数据加入hashtable,tonemap模块从blackboard中get出blur模块里的数据,这样blur模块不需要知道是否有tonemap的存在。

Transient resource system: The back-bone of FrameGraph

 

对于一帧之内临时的资源,我们希望最小化它们的生命周期。在真正使用时才分配资源,可以在渲染系统的叶子结点模块分配资源,尽快的回收,更容易的写独立包含的代码,不需要考虑其他模块/全局的资源传递创建等。它对于FG是非常重要的。

 

这是实际ps4中的memory map,x轴是时间,y轴是virtual address。Ps4中的trs首先会申请一大段virtual memory(例如几个G)但是没有物理内存的分配,在每帧的执行期间当我们需要一个新的rt时,用户申请数段chunks of memory并且真正分配物理空间,资源的handle会指向申请的内存,等待使用完毕我们可以复用它的virtual memory,可以在一帧开始计算需要多少GPU内存,事先分配好,在execute的时候使用。坏处是可能会造成内存碎片的浪费,因为使用greedy算法来分配回收内存,例如图示中的AO buffer回收后会造成不连续的碎片,但实际中不是太大的问题。

 在DX12中有些不一样,GPU的分配会抽象为resource heap,分配之前需要找到满足条件的resource pool,使用placedResource(DX12 API)来创建heap中的资源,track heap中的资源来复用。这不仅会有ps4碎片的缺点,而且会带来更多per heap的细小内存碎片,但实际上使用了aliasing的管理还是比不使用任何内存管理效果好得多。

需要仔细考虑:必须小心,首先需要确定资源的metadata state(fastclear/discard/diable),其次要确定资源的生命周期,比想象中的要难很多,要考虑render和compute,保证在使用前已经分配了真正的物理内存页。

 SUMMARY

知道每帧全局的信息有很多好处(复用内存,半自动化的async compute,简单的管线配置,可视化和debug工具);图表pipeline的表示非常吊,有非常直观的感受,类似cpu job graphs或者shader graphs,C++的新特性减少了重构的麻烦。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值