UE高級性能剖析技術(1)-- RHI線程(渲染提交)

在最前面

基於UE的手遊客戶端的性能主要由這七大部分構成:CPU邏輯,CPU渲染,圖形API(提交),GPU渲染,內存,帶寬,加載時間。這幾個基本元素又會合力衍生出一些新的性能指標,例如功耗(往往同gpu負載和帶寬緊密相關)。同時這七部分又構成一個閉合的木桶,最長的一塊是主要瓶頸,並且瓶頸可以在這幾塊轉移流動。作爲開發者我們解決性能問題的步驟一般都是按照做性能剖析,解讀結果,定位問題,增加剖析代碼,優化問題,重複剖析的迭代過程來執行。而高效準確詳細的對性能進行剖析得到結果是第一步,在任何引擎上,只要我們能做到在任意時刻準確的獲取想要的性能剖析結果,那麼纔會胸有成竹不會慌,該系列文章將歸納總結在ue下對每一性能指標的剖析方法,做深入分析,我們需要工具的幫助,也需要程序員理解引擎並知道如何去編寫合適的剖析代碼。

 

最近剛好做過一輪RHI線程的剖析,第一篇就從RHI開始,我會堅持把後面幾篇寫下去。

 

渲染API瓶頸

渲染API瓶頸是3D手遊的常見瓶頸,我們常說的drawcall 過多了,卡渲染就是指的卡在這裏,其實這個卡渲染卡的是cpu。爲什麼drawcall會卡,因爲cpu需要通過對渲染api的調用來驅動gpu做事情,1個drawcall的背後是一堆渲染api的調用,下面是一個常見的drawcall過程,

 

可以看到爲了一次繪製(1個drawcall),要設置shader,創建buffer等等,這些相比最後的draw那一步來說都是相對更費時的。

當測試反饋給我們卡drawcall的時候,作爲程序我們需要一種手段來衡量出確切的當前做哪些drawcall,或者說繪製哪些東西更耗,最好是精確到耗在繪製哪個模型的哪個api調用上,我們才能真正的給美術予以優化指導。

UE中精確定位RHI瓶頸

在UE中,pc和android平臺通常渲染api的調用會放在一個單獨的線程,叫做RHI線程,這個線程專門負責渲染指令的提交,即調用顯卡的API。我們分析渲染提交的卡頓就是要分析這個RHI線程。

多線程渲染工作模型

但是RHI線程不是單獨存在的,它需要同game,render線程協作,rhi的卡頓可能不只是rhi的卡頓,首先需要清楚UE裏面RHI線程和其他線程的工作模式:

 

 

這裏面game render rhi gpu分別在4個並行的工作線上,有這樣幾個特點:

  1. game thread最多可以等渲染一幀,也就是說渲染如果第N幀的渲染在第N+1幀的game tick結束時還沒有完成,那麼渲染就會把game卡住,render 和rhi不會有幀延遲。
  2. game是render和rhi的源驅動者,game的卡頓可能會卡住渲染
  3. render 負責產生drawcall,rhi負責提交drawcall,因此render的卡頓也可能卡住rhi提交。
  4. 渲染的最後一步要swapbuffer,即等待gpu完成,所以gpu的卡頓也可能會卡住rhi。
  5. 除了gamethread本身,render  rhi 和gpu的工作都是存在間隙的,即game邏輯餵給渲染任務的時機會影響渲染工作的密度,也會影響到渲染的時間,小量多次會浪費渲染效率。

 

UE中rhi的瓶頸的來源

現在我們知道rhi的卡頓可能來自於以下幾種情況:

a RHI指令自身的卡頓,即通常所說的卡drawcall,過多的dc,過多的渲染狀態切換,過多的渲染資源創建,等等;

b game或者render thread的卡頓

c gpu的卡頓。

 

對於情況b,我們可以通過UE的status 看當前的game和render的線程執行時間來容易的判斷出來,來排除是rhi上出了問題。

對於情況c, UE的status中在rhi線程上會統計一個叫做swapbuffer的時間,如果這個時間過長,那麼就是gpu瓶頸了。

 

真正比較麻煩的是定位情況a,即對於rhi指令本身的卡頓瓶頸。對於這種情況UE自帶的stat工具通常不能給出比較有力的分析結果,自帶的方法只能統計一幀在rhi上做幾種給定操作的時間,但是在複雜的線程條件下,有時很難確定這些卡頓的幕後原因,有時rhi問題只是一個表象,爲了得到rhi線程瓶頸的確切原因,我們至少要能夠明確以下幾個事情:

 

1 Rhi線程的執行是由一堆有序的rhi command組成的,我們要能捕捉到具體的那一個rhi command的執行時間比較長,比如是創建場景中哪個房子的vb?

2 是在render thread的哪一個步驟塞入的渲染數據導致了這個rhi command執行的時間比較長,是在渲染陰影的時候,還是渲染basepass,還是做遮擋剔除?

3 是在game thread的哪一個步驟塞入的渲染數據導致了這個rhi command執行的時間比較長?是在加載場景?還是在繪製UI的時候?

 

筆者在項目中遇到過一個問題,在一些低端機,rhi會有時突然卡頓幾秒以上,看stat文件如下:

我只能看到在rhi 線程的thcikbegin階段發生了巨大的卡頓,然後就沒有細節了,不知道是具體哪個rhicommand,然後看gamethread在wait,也不知道是game thread的哪一步觸發了這個rhi瓶頸。我們需要一些辦法。

 

定位UE中rhi線程的瓶頸

我們需要分別將上面三種原因捕捉到,就能解開這個問題。

首先定義一個宏,只有我們需要捕捉這些詳細的rhi瓶頸時開啓,因爲這些操作會存在較大的overhead。

 

定位具體rhicommand的時間

對於rhi command的具體執行時間,我在FRHICommand的最終執行階段ExecuteAndDestruct中創建一個FScopeCycleCounter,counter的名字就直接rtti當前command的typename。

有時候我們需要更細節的知道這個command除了類型外的信息,例如如果這是一個createvb的command,那麼vb的原始模型名字是什麼,vb大小等,我在一些command處額外傳了一些debug用的string,然後在這些command的執行前補上一個FScopeCycleCounter。這樣我們就能拿到精確到具體rhicommand的提交耗時了。經過這個補充,我能拿到這樣的rhi 線程執行時間統計:

這樣謎底就清晰了很多,原來這時候存在大量的vb創建,數了一下,有幾百個,在同一幀內幾百個vb的創建,在低端android上會產生5秒鐘的超級卡頓,那麼問題來了,爲何在這一幀會同時產生這麼多的vb卡頓,是game或者render 上發生了什麼事情,如果我們查看當前的game thread ,它顯示的是wait,是不知道原因的,因爲game ,render ,rhi是分開工作的,我們現在rhi處於瓶頸已經不是事故的”第一現場”了,我們需要進一步讓你發生在第一現場。

 

定位在render的哪個階段發生了rhi的瓶頸

UE的rhicommandlist自帶了一個函數FRHICommandListImmediate::SetCurrentStat,可以用來讓render給rhi加一個標記,這個標記就可以認爲是render的某個階段的名字,UE自帶了在render 的很多階段下了這個標記,我們還可以自己補充,這個函數的原理如下:

 

這個status本身也是以command的形式插入隊列,所以每一條rhi執行的cmd會被統計到它之前最近的那個status tag下面,通過不斷的細分插入這些tag,我們可以跟蹤到rhi的cmd從是在render的哪個階段被產生。需要注意的是這個tag只能在render thread裏插入。我爲render thread補充了一些細化的tag後,如前面的圖,我發現這個大量的vb創建發生在渲染線程的一幀渲染結束到下一幀渲染開始之前,在這個階段有game 邏輯往render 裏面堆入了大量創建vb的指令,所以問題還要繼續往game thread 上找”第一現場”。

 

定位在Game的哪個階段產生RHI瓶頸

其實我們仍然可以模仿renderthread一樣在game thread上給rhi的command list裏面插入tag,但是有個問題,renderthread是一種相對簡單的render command的隊列的順序執行, tag量有限相對容易操作,但是game thread裏面邏輯極其複雜,我們希望可以複用game thread上面已經埋好的一些scope counter,不過game和rhi是兩條並行的thread,需要在我們關心的scope處讓二者能夠強行同步住,才能容易的使用game 自己的scope counter抓住rhi的執行。我們這樣去實現,假設下面是我們關心的一個game thread的區段,在前後加上代碼如下

#if STAT_RHI_ADVANCED

FlushRenderingCommands(true)

   DECLARE_SCOPE_CYCLE_COUNTER(TEXT("XXX "), STAT_XXX, STATGROUP_RHI_GAME_SYNC);

#endif

  //game 代碼段

    …

    …

//

#if STAT_RHI_ ADVANCED

     FlushRenderingCommands(true);

#endif

 

FlushRenderingCommands(true)的意思是在這個位置強制將所有當前的rhicommand執行完畢,阻塞住當前線程,所以上面這個代碼段的原有的XXX統計的時間將包括這段時間內因爲game thread上發生的渲染事件的渲染而花費的時間。

 

通過在game thread的主要邏輯處,插入這些同步rhi線程的代碼,當rhi線程發生瓶頸的時候,我們只要查看當前gamethread在哪裏停住(wait event),就可以判斷是什麼game 邏輯導致了rhi線程的瓶頸。有了這個機制,我們接着截stat文件,會看到當rhi處於巨大瓶頸時,game thread停在了這裏:

兇手被抓住了,是一個資源正在被緩存池預加載!

這個奇怪的rhi上的卡頓的真正原因其實是game 線程上在加載一個模型資源!如果只依靠ue本來的stat分析,是無論如何都不可能猜到這個幕後的兇手的。

 

那麼問題來了,一個資源的加載爲何會導致海量的vb同時創建?通過進一步的分析代碼,會發現因爲這裏用的是同步加載,而UE的同步加載的機制,是創建一個加載任務堆到同異步加載一樣的加載隊列裏,因爲不能保證依賴關係,所以要等待當前所有隊列中的任務完成才能繼續下去,也就是說當前的同步加載的時間絕不僅僅是加載完你要的這個模型而已,他需要將當前異步加載任務在隊列中的所有資源加載完!而這個時候恰恰處於場景在level streaming的階段,最後發現此事加載隊列中的資源上百個,這個同步加載遇上level streaming的結果就是,在這一幀要完成上百個模型的創建,模型的postload會初始化rhi資源,導致一幀內大量vb的創建,卡死rhi,所以罪魁禍首是同步加載,同步加載將level streaming的過程也強行同步了,找到了問題,我們就可以通過相關的優化手段來排除這個瓶頸。

 

RHI上的問題可能往往不只是rhi上的問題那麼簡單,通過上面說的一些方法我們可以清楚的看到各種rhi上瓶頸的真正原因。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章