[GDC17] FrameGraph Extensible Rendering Architecture in Frostbite

[GDC17] FrameGraph Extensible Rendering Architecture in Frostbite

來自專欄 Dirty Game EngineFrameGraph Extensible Rendering Architecture in Frostbite?

www.gdcvault.com

大綱,先介紹下歷史,然後Frame Graph,之後細節,最後總結。FG能提升引擎的擴展性,簡化async compute,自動的ESRAM別名管理,節省了大量顯存。

2007時是DICE的引擎,用於未來的battleField的產品,多平台DX9&10。10年後,2017年FB成為EA的標準引擎,多平台DX12,在15款遊戲以及未來的EA遊戲中使用。

使用FB的遊戲

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會直接使用RC,大部分是一些全屏處理,比如lightng,pp等,RC是code-driven的架構;最上層有一個龐然大物world render,來管理整個世界。

這是一個code-driven的架構,world render知道所有的渲染特性,views & passes,它分配出資源(RT,buffer),在不同的system中設置並調度它們。

BF4中使用的rendering pass(直接作用於RC的features),很多產品項目組的程序美術會添加各種各樣的new feature進來。

World Render的挑戰:顯式的立即的模式進行渲染;顯式的資源管理,各個產品項目組都會定製、手工的管理ESRAM,對資源的統一性和merge造成了影響;和渲染框架耦合嚴重,因為顯式的定製渲染和資源,所以很多模塊都受到對方的影響;局限的擴展新;產品組必須fork來定製;10年間,從4k行代碼漲到15k行,非常困難去維護、擴充、整合。

WorldRender的目標:想知道高層整幀的概況;更好的擴展性,解耦並組合渲染模塊,自動化的資源管理,更好的可視化和分析工具。

為了達到目標,引入了兩個新的架構組件:Frame Graph以及Transient Resource System。Frame Graph用來高度抽象表示render pass和資源(render pass的概念和vulkan中的差不多),它了解一幀之內所有的信息;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。

牛X的來了

最上面是render passes,包括graphic和async compute;

當滑鼠移到某個render pass上方時,可以看到那些resources是需要被當前rp讀寫的(綠色/紅色);

當移動到resource上時,可以看到對它進行讀寫的render passes

最下方可以看到這些數據的物理內存分布,由transient resource system計算。

移除了立即模式的渲染,新的rendering代碼可以分割為passes,rendering分為了3個階段:setup設置使用哪些render passes,哪些resources;compile負責resources的lifetime,並且給他們分配空間;execute就是執行每個render pass。不想提前處理好所有的渲染模塊,希望能根據不同的應用場景每幀提前去自定義渲染管線,比如每幀可以變化解析度,根據是否在播放動畫腳本/望遠鏡/在水下等環境去build pipeline features;code-driven的架構。

Setup階段:定義使用的render/compute passes;定義每個passes的inputs和outputs資源;整個代碼流和之前的渲染模式一樣,調試時就可以簡單的single step代碼了。

Render passes通過resources來建立互相的關係,這兒resources是handles並沒有內存分配,rp必須定義所有用到的資源,包括讀寫和創建;有些永久資源直接通過API創建,它們會被導入到rp中一起進行管理,比如TAA的history buffer/backbuffer等。

一個rp的build過程,定義資源和之前沒啥區別,主要是初始狀態需要指定。

我們也可以讀或者寫之前創建的資源,這兒rp讀一些資源並且寫入一些資源;我們寫入的資源之前老的handle會被invalidated,在寫操作後我們會重命名這個資源,寫入invalid handle的資源會造成runtime error,這會幫助我們找到一些之前老代碼就存在的數據競爭與依賴。

高級FG用法:1)延遲創建資源,很早的聲明資源,直到使用時才創建,根據使用自動設置創建的desc;2)推導資源參數,可以根據input的格式/size來推導參數,比如在resolve pass需要一個downsample chain,簡單的可以自動直接推導出創建資源。3)移動子資源,可以幫助我們直接在多個pass復用資源,自動的創建子資源或者資源別名。

MS的舉例說明,我們有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掉。

當我們需要debug時,只需要加上move pass,這樣lighting pass和post pass會自動被cull掉,與他們相關的依賴項res也會被相應的cull掉。這會幫助我們更好的解耦每個模塊。

執行階段:執行每個rp的callback函數,裡面的代碼和沒有FG之前的一樣:包括使用RC、設置state等,dispatch和draw;唯一不同的是這兒需要根據handle去devirtualize真正的GPU資源。

對於Async compute有額外的幾點考慮:可以從FG中直接推導;但是還需要人為介入:它會帶來內存的增加,如果濫用會降低性能(因為FG會監視其非同步的生命周期、依賴等等);所以可以選擇性的讓rp進行async queue,從主timeline kick off,當main queue第一次使用async queue中的output資源時作為sync point,res的生命周期自動延長至sync point。

舉例說明:SSAO讀取depth輸出到raw AO上,filter把raw AO處理為AO,之後作為輸入給Lighting pass

我們想把AO變為async compute,這樣rawAO的聲明周期就延長至lighting pass(main queue中第一次使用其output依賴的地方)。

這裡是我們需要打開async compute的地方,rp會被在async queue中執行,其他的會自動處理。

重要的是在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邏輯上應該是延遲執行,捕捉使用值傳遞=。

Render modules:2種渲染模塊:1)Free-standing stateless function

類似這種函數(addMyPass):帶有參數的函數,參數和返回值是一些resource handles,frostbite中大多數函數都是這種。

第二種是類似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。對於一幀之內臨時的資源,我們希望最小化它們的生命周期。在真正使用時才分配資源,可以在渲染系統的葉子結點模塊分配資源,儘快的回收,更容易的寫獨立包含的代碼,不需要考慮其他模塊/全局的資源傳遞創建等。它對於FG是非常重要的。

對於不同平台底層的實現不一樣,對於xb1使用物理內存的aliasing,對於ps4以及dx12使用virtual memory的aliasing,對於dx11使用obj pools(PC好慘)。對於texture和buffer管理不同,buffer在frostbite中大部分用來傳遞數據給gpu,我們對它們不使用aliasing,所以它們簡單的使用linear allocator;textures可以看做臨時的內存,我們對它們使用pool機制來複用。

這是實際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的管理還是比不使用任何內存管理效果好得多。

在XB1上直接使用物理內存地址,這樣可以更底層的操作數據,比如可以任意分割數據的排布,圖示中的lightbuffer分割為了兩部分的內存復用,這樣可以更有效率的使用內存而且很少會帶來內存碎片。

如果從virtual memory上來看,每種資源還是擁有自己獨立的虛擬內存地址。每幀需要創建資源時,我們依舊會分配虛擬內存地址,然後從物理內存的pool中選擇需求page的大小分配給虛擬內存(注意lightingbuffer引用了兩個pages),等到資源使用完畢再歸還page到pool中。

需要仔細考慮:必須小心,首先需要確定資源的metadata state(fastclear/discard/diable),其次要確定資源的生命周期,比想像中的要難很多,要考慮render和compute,保證在使用前已經分配了真正的物理內存頁。

新分配的資源必須進行clear(copy)/discard,他初始化資源的metadata(類似fastclear,不會改表資源的surface只是更新其中的metadata),最好選擇discard。

需要在GPU端加入同步點,aliasing barrier類似resource transition barrier,普通的resource barrier不足夠,為了建立不同邏輯資源間的復用聯繫我們需要使用aliasing

barrier,它會通知資源在aliasing之前和之後的狀態。

舉例說明:兩個同時運行的ps和cs,它們同時讀取不同的資源,但是兩個資源是aliasing復用的,aliasing barrier會串列化運行ps和cs。

但是會造成性能的下降,如果在乎性能最好顯式的使用async compute,這會最遲的復用resource避免並行的內存衝突。

BF4中相同的場景/解析度/渲染特性的比較:沒用aliasing之前147mb

DX12的aliasing降到了80mb

PS4 77mb

XB1 76mb

4k沒有aliasiing 1GB

DX12 472mb,節省了一半多的內存!

總結:知道每幀全局的信息有很多好處(復用內存,半自動化的async compute,簡單的管線配置,可視化和debug工具);圖表pipeline的表示非常吊,有非常直觀的感受,類似cpu job graphs或者shader graphs,C++的新特性減少了重構的麻煩。

未來工作:全局的res barrier優化,async compute的全自動化,不需要顯式的標記;profile導向的優化。

Q&A: 1)resolve MSAA是一個rp嗎,是否有vulkan等的移植計劃?resolve是個rp,因為只做ps,xb,pc所以沒有vulkan的計劃 2)有沒有dx11 alasing的對比:dx11 res pool在fb中可復用的不多 3)如果復用rp,blackboard如何使用:第一種類型比如downsamp/resolve rp,直接顯式傳遞參數,第二種,blackboard並不是一個全局instance,可以拆分為多個棧上bb,那麼對於復用的rp來說只需要索引它們對應的就可以 4)如果每幀之間有並行會怎麼樣:我們在fg中沒有處理,這是未來我們優化方向,目前對於多幀的資源我們沒有使用aliasing 5)會自動轉換graphic到compute嗎:不會 6)有沒有使用多線程的fg處理:rp的execute階段是線性的,但是depth/gbuffer pass等使用shading system的是例外,shading system會產生大量的dp,這些dp之間是並行的,但是render pass之間是並行的,未來如果可以split commandlist,那麼dp間並行也是可以做的 7)ESRAM之間數據的傳遞:沒做 8)什麼時候build commandbuffer:在executioin階段 9)devirtualize怎麼做的:需要顯式的完成,input是resource id,output是真正的resource,resource在execution之前需要被分配完成,在execution時我們只需要查找LUT哪些資源需要使用,然後返回真正的resource。

可擴展的渲染管線最近大熱,不論是unity、unreal都在推進這些概念,Frostbite作為EA目前唯一的引擎需要提供給多個項目使用所以也有這種急切的需求。它的好處顯而易見,文中已經提及多次,內存的優化讓人印象深刻,隨著DX12這種偏底層的API需要user programmer自己掌控資源,FG的應用應該更加廣泛和深刻;另外還有一個好處Yuriy沒有提及就是它能很好地提供管線中的dependency,對於job system的細粒度多線程並行是一個非常好的引導。我個人認為有一點可能值得探究,從產品角度來看它確實是好事:服務於多個產品必然需要更加靈活的架構,但是這也使得整個引擎更加面向對象(因為解耦封裝了很多),而且code-driven更加面向過程(代碼流都是straight-forward的procedure),這影響了DOD的設計理念而且可能會造成整個引擎性能的小幅下降(考慮cache miss),對於sony旗下的「野」工作室:Naughty dog/santa monica等產品比較單一,幾年就應付幾款遊戲,所以可以做大量性能上的specific優化來創造超越3A的遊戲,EA的frostbite大有EA內部unreal的感覺,是否會造成產品的性能問題呢?


推薦閱讀:

從零開始手敲次世代遊戲引擎(五十四)
[DOD Series][GCAP09] Pitfalls of Object Oriented Programming
[CppCon16] Rainbow Six Siege - Quest for Performance
從零開始手敲次世代遊戲引擎(四十二)
《Exploring in UE4》物理模塊淺析[原理分析]

TAG:遊戲引擎 | 寒霜引擎Frostbite | 戰地遊戲Battlefield |