騰訊遊戲學院專家:UE高階效能剖析技術之RHI

遊資網發表於2019-09-11
導語如何高效準確詳細的對效能進行剖析?騰訊遊戲學院專家Leonn將從RHI(渲染提交)開始,歸納總結在UE下對每一效能指標的剖析方法。

基於UE的手遊客戶端的效能主要由這七大部分構成:CPU邏輯、CPU渲染、圖形API(提交)、GPU渲染、記憶體、頻寬、載入時間。這幾個基本元素又會合力衍生出一些新的效能指標,例如功耗(往往同GPU負載和頻寬緊密相關)。同時這七部分又構成一個閉合的木桶,最長的一塊是主要瓶頸,並且瓶頸可以在這幾塊轉移流動。

作為開發者,我們解決效能問題的步驟一般都是按照做效能剖析、解讀結果、定位問題、增加剖析程式碼、優化問題、重複剖析的迭代過程來執行。而高效準確詳細的對效能進行剖析得到結果是第一步,在任何引擎上,只要我們能做到在任意時刻準確的獲取想要的效能剖析結果,那麼才會胸有成竹不會慌,該系列文章將歸納總結在UE下對每一效能指標的剖析方法,做深入分析,我們需要工具的幫助,也需要程式設計師理解引擎並知道如何去編寫合適的剖析程式碼。

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

渲染API瓶頸

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

騰訊遊戲學院專家:UE高階效能剖析技術之RHI

騰訊遊戲學院專家:UE高階效能剖析技術之RHI

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

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

UE中精確定位RHI瓶頸

在UE中,PC和android平臺通常渲染API的呼叫會放在一個單獨的執行緒,叫做RHI執行緒,這個執行緒專門負責渲染指令的提交,即呼叫顯示卡的API。我們分析渲染提交的卡頓就是要分析這個RHI執行緒。

1.多執行緒渲染工作模型

但是RHI執行緒不是單獨存在的,它需要同Game,Render執行緒協作,RHI的卡頓可能不只是RHI的卡頓,首先需要清楚UE裡面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邏輯餵給渲染任務的時機會影響渲染工作的密度,也會影響到渲染的時間,小量多次會浪費渲染效率。

2.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檔案如下:

騰訊遊戲學院專家:UE高階效能剖析技術之RHI

騰訊遊戲學院專家:UE高階效能剖析技術之RHI

騰訊遊戲學院專家:UE高階效能剖析技術之RHI

我只能看到在RHI執行緒的Thcik begin階段發生了巨大的卡頓,然後就沒有細節了,不知道是具體哪個RHI command,然後看Game thread在wait,也不知道是Game thread的哪一步觸發了這個RHI瓶頸。我們需要一些辦法。

定位UE中RHI執行緒的瓶頸

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

首先定義一個巨集,只有我們需要捕捉這些詳細的RHI瓶頸時開啟,因為這些操作會存在較大的overhead。

1.定位具體RHI Command的時間

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

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

騰訊遊戲學院專家:UE高階效能剖析技術之RHI

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

2.定位在Render哪個階段發生了RHI瓶頸

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

騰訊遊戲學院專家:UE高階效能剖析技術之RHI

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

3.定位在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的區段,在前後加上程式碼如下:

  1. #if STAT_RHI_ADVANCED
  2. FlushRenderingCommands(true)
  3.    DECLARE_SCOPE_CYCLE_COUNTER(TEXT("XXX"), STAT_XXX, STATGROUP_RHI_GAME_SYNC);
  4. #endif
  5.   //game 程式碼段
  6.     …
  7.     …
  8. //
  9. #if STAT_RHI_ADVANCED
  10. FlushRenderingCommands(true);
  11. #endif
複製程式碼

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

通過在Game thread的主要邏輯處,插入這些同步RHI執行緒的程式碼,當RHI執行緒發生瓶頸的時候,我們只要檢視當前Gamethread在哪裡停住(wait event),就可以判斷是什麼Game邏輯導致了RHI執行緒的瓶頸。有了這個機制,我們接著截stat檔案,會看到當RHI處於巨大瓶頸時,Game thread停在了這裡:

騰訊遊戲學院專家:UE高階效能剖析技術之RHI

凶手被抓住了,是一個資源正在被快取池預載入!

這個奇怪的RHI上的卡頓的真正原因其實是Game執行緒上在載入一個模型資源!如果只依靠UE本來的stat分析,是無論如何都不可能猜到這個幕後的凶手的。

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

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


作者: Leonn  
來源:騰訊GWB遊戲無界
原地址:https://mp.weixin.qq.com/s/oBISXGsHplTNeqAcRzHtEA

相關文章