1.移動端GPU架構
Immediate Mode Rendering Architecture(IMR)
早先PC端的GPU大多數采用的是IMR的架構。IMR架構的大體流程如圖所示,值得注意的是對于每個像素,經過數次的color/depth的讀寫,最終繪制到framebuffer上。這當中的每一次讀寫,都是直接與內存交互。所以,頻繁讀寫內存,會消耗大量的帶寬,這個過程會大量發熱。對PC來說,為了更高的畫質和幀率,可以用更好的風扇和散熱從外部解決發熱問題,但是對于空間十分有限的手機來說,肯定不可能去加裝散熱了。
Tile-Based Rendering Architecture(TBR)
所以為了解決帶寬引起的發熱問題,現在的手機GPU普遍使用的是tiled-based架構,Adreno/Mali/PowerVR的GPU在這個基礎上有自家獨有的優化,比如PowerVR的TBDR架構中的HSR,可以完全消滅不透明物體渲染時的overdraw,這里就不展開討論了。
不過它們的核心思想是一致的,都是為GPU開辟了一塊on-chip memory,這塊on-chip memory的特點就是相比于與內存的交互,GPU讀寫它的開銷非常小。
Tile-based rendering將一個完整的framebuffer分為若干個tile,每個tile的內容完全繪制完畢之前,GPU只讀寫on-chip memory,一塊tile完全渲染完畢之后才會將on-chip memory中的內容一次性寫入內存,下圖描述了整體的繪制流程。
這樣,原本繪制每個像素都需要直接讀寫內存,現在改為全部讀寫開銷極低的on-chip memory,從而節約了帶寬,降低了功耗。
2.為什么用Vulkan?
這也是在移動端渲染開發必然要考慮的問題,要求引擎技術團隊對自己的項需求和Vulkan API,以及相關手機硬件特性都有非常清晰的認知。Vulkan API主要有以下幾點特殊的優勢:
- 它的驅動更薄。Vulkan更貼近底層,驅動不會像GLES一樣,進行大量的猜測和判斷,所以用得好可以顯著降低CPU的負載;
- Vulkan提供顯式的同步和帶寬控制指令,有效而正確地使用這些指令,可以提升GPU運行效率和降低GPU功耗。
- Vulkan 刻錄并提交commandbuffer的架構,更好地支持了多線程渲染。例如Unity PC端實現了用secondary commandbuffer多線程同時渲染同一場景中的物體,UE也利用Vulkan的特性,從渲染線程中抽離出了RHI線程,平均了多核負載,提升了多核效率。
- 因為Vulkan標準較新,且受到重視,所以在Vulkan標準定制時,直接包含了很多最新的硬件特性的使用方法。
所以在合理調度的情況下,Vulkan可以享受驅動薄和多線程帶來的CPU收益。而GPU端,在充分了解硬件架構和各個驅動的特點的情況下,手動控制同步和帶寬可以有效提升游戲性能。但是,往往一些項目花了非常多的時間和精力去優化渲染算法,改善渲染流程,或者添加自定義渲染管線,可僅僅是因為對某個硬件特性的不了解,或者用錯了某個API,導致性能下降非常嚴重或者導致優化了的算法反而是負提升,甚至包括早期的Unity、Unreal默認移動端的渲染管線,在這塊都或多或少有些不足。
3.游戲中的帶寬與功耗
先看一個非常簡單的例子。
上圖是用Unity的rendering commandbuffer實現的一個自定義后處理的渲染流程。當切換rendertarget的時候,如果我們只是使用了默認的接口去設置RT,傳遞到底層,可以看到Vulkan renderpass的load action是load,也就是保留前面的渲染結果。
而當我們使用下面這個接口顯示地設定了rendertarget的loadaction,使Vulkan的load action變為dontcare,可以看到GPU bound的情況下,幀率提升了1幀多。
這就是因為,當我們load這個rendertarget的時候,在Tile-base架構上,rendertarget從內存中被加載到on-chip memory上,所以產生了額外的開銷。而如果我們顯式地設置了dontcare,就減少了這次加載的時間和帶寬,在GPU bound的情況下,性能的提升直接體現在了幀率上。
Tile-based Rendering在延遲渲染中的應用
傳統延遲渲染的方式,Gbuffer渲染完畢后,寫入內存,然后lighting階段再sample gbuffers。而因為在lighting階段每個像素需要的是自己像素位置上的gbuffer信息,所以我們完全可以利用subpass,將gbuffer存放在on-chip memory中,后續lighting階段直接從on-chip memory里去讀取。省去了先存內存,再sample的兩次帶寬消耗。
下面是一個延遲渲染的demo,左邊是sample的方式,右邊是則是subpass的方式:
兩種情況下,lighting的算法,分辨率,燈的數量,fps包括GPU頻率都是固定不變的。唯一的區別就是是否與內存交互,左邊是采樣內存中存下來的Gbuffer;右邊則完全不與內存交互,直接寫入和讀取on-chip memory。
測試結果可以看到,shader的計算量是一樣的,但是內存頻率和帶寬都是subpass方案有很大提升,這里讀寫帶寬一共減少了4.9Gb/s,內存頻率也有所降低。
在幀率、GPU使用率和頻率均沒有差別的情況下,純因為帶寬的減少,產生了567mW的功率差,GPU平均溫度也降低了5度。
這里有一個大致的參考,內存帶寬所產生的功耗幾乎與GPU的功耗持平,,每消耗1GB的內存帶寬,大約會產生120mW的功耗,這也和之前5GB,567mW的測試結果吻合。
不僅僅是延遲渲染,只要渲染流程中想讀取當前像素位置的歷史顏色、深度,都可以用subpass。比如decal,某些粒子效果的半透明渲染、MSAA抗鋸齒等等,合理利用subpass,在帶寬上能得到可觀的收益。
當然,tile-based架構還是有一些缺點的:
比如tiling階段需要將vs全部計算完畢,并且所有的varying要回存內存,所以相比于IMR,除了共有的fetch geometry data的開銷,Tiling階段也有額外的延遲和帶寬消耗。
所以vertex data的組織也是非常重要的。比如下圖這個自定義的vertex data,aColor將最終傳給fragment shader用作自定義的渲染,
注意到每個數據都是有效數字只有1位的整數,卻用了32位的格式去存儲。在模型復雜,頂點較多的情況下,這也是一筆不小的開銷。實際上16位,甚至8位的格式就夠了。所以在知曉自己項目每個頂點數據的用途的情況下,我們應當使用盡量小的格式。
Index-Driven Vertex Shading(IDVS)
下圖是ARM上為了減少fetch geometry data的帶寬所做的優化,嚴格意義上它不能算是專門為TBR架構而設計的優化,但它的收益確實很大。
IDVS的思路就是vs中只先做position相關的計算,等做完剔除之后,才會去fetch其他attribute的數據,做varying相關的計算。但是這個優化要求開發者將position和其他的attribute分buffer存放,這樣GPU才能單獨fetch。
現在市面上絕大多數的手游都是左邊的這種情況,所有vertex data存在了同一個vkbuffer中;右邊則是推薦的做法,一個vkbuffer僅存position data,剩下的varying存在另一個vkbuffer中,在某些vs較重的情況下,兩者的功耗差距超過了30%。所以如果頂點數據復雜,我們還是值得去拆分一下vertex data的。
4.Vulkan中的同步
Vulkan同步中一個非常重要的元素就是pipeline barriers,只要有項目修改了引擎的原生管線,就一定會涉及到pipeline barrier的修改,而實際看下來幾乎所有的項目都有一些使用不恰當的地方。并且,如果項目開發周期比較長,使用的是較早版本Unity、Unreal引擎,那么原生代碼也或多或少有一些使用不當的地方。
以下三點都是Vulkan spec上面提到的pipeline barrier的作用。
第一,它可以控制執行順序,GPU真正執行指令時,并不一定是按照我們提交指令的順序去執行的,所以在指令之間添加一個pipeline barrier,可以保證barrier之前的指令先于barrier之后的指令執行。
只保證指令開始的順序,在并行的情況下,并不能控制指令結束的順序,牽涉到內存修改的情況下就會出現問題。
第二,pipeline barrier還保證了指令間內存的依賴關系,這個后面會詳細解讀。
第三點image的layout轉換其實同樣重要,有興趣的讀者可以查閱相關資料。
Hazards
因為實際情況要復雜得多,這里我們把讀寫模型簡化成GPU core - cache–內存三個部分:
寫數據時,GPU core完成對cache的修改后要flush cache,將其中的數據拷貝到內存中,這個過程我們稱之為make memory available。
而GPU core讀取數據時,要先invalidate cache,將內存中的數據加載到cache中,這個過程,我們稱之為make memory visible。
了解了這個簡化模型以后,來看內存讀寫的三個hazards:寫后讀、寫后寫、讀后寫。
如果不做同步,比如說寫后讀,很可能讀取指令執行時,前面的寫命令還沒來得及修改內存,從而導致渲染錯誤,這類情況就是需要通過同步指令去避免的。
Pipline Barrier 處理WAR、RAW、WAW的具體流程
三個問題當中最好解決的是讀后寫,對于它來說,只要通過pipeline barrier限制了指令執行順序,就能保證數據讀取的正確性。因為讀取指令先執行,那么數據就已經被加載到了cache中,即使最壞的情況,后面的寫入操作立刻完成,它修改的也僅僅是內存中的內容,無法影響到我們正在讀取的已經被加載到cache中的數據。
接下來我們來看其他兩個稍微復雜一點的同步。
Pipeline stage mask和access mask決定了這個pipeline barrier是對哪段內存生效。那么如果對于同一段內存進行先寫后讀,讀取操作一定要在所有數據都寫入內存之后才能進行。
所以,在pipeline barrier的保護下,整個同步流程如右上所示:
首先,讀取指令會被block住,等待寫指令執行完畢;srcAccessMask意味著將這段內存make available,保證data寫入內存完畢。dstAccessMask意味著將這段內存make visible,也就是保證剛才被寫入內存的data成功加載到cache中。
最后,執行讀取指令。這樣,整個內存同步就完成了。
寫后寫也是差不多的原理,先阻塞第二次寫操作,等待第一次寫操作完成,且data已經flush到了內存中,再執行后一次的寫操作。
上面是一個典型的寫后讀的同步例子,這是一個用采樣方式讀取Gbuffer的延遲渲染demo,模擬是游戲中最常見的一種流程,就是先渲染一張RT,然后后續的某個fragment shader采樣這個RT的結果,除了延遲渲染,常見的還有shadowmap,sss,一些實時的風場,腳印的前置流程等等。
右邊是完全正確的寫法,先block住lighting階段的fs,然后把之前Gbuffer階段color output stage的color attachment寫入的這段內存make available,然后再make visible同一段要被shader讀取的內存,再unblock fs,這樣lighting階段的fs就正確讀取到了Gbuffer。
左邊唯一的不同,是把本應等待的fragment階段改成了vertex,這樣就導致GPU會在vertex shading階段提前block住流水線。
這個問題非常隱蔽,首先Vulkan的validation layer不會報錯,因為它只是效率低,而不是一個錯誤。如果粗略地去測試alu,讀寫帶寬,渲染時間,可以看到這些數據在圖上的測試結果幾乎沒有區別。所以這個問題很容易就被忽略過去了。
測試分析工具的使用
各個Soc廠商都提供了自家GPU的分析測試工具,例如 Mali芯片的Mali streamline,PowerVR的芯片的PVRTune,高通的snapdragon profiler。在這個例子中,我們用的手機是Mali的GPU,所以用Mali的測試工具進行分析:
如上圖,計算量,讀寫帶寬,都沒有差別,唯一有變化的是這個External Bus Read Latency和External Bus Stall。
具體來看,內存總線讀取數據的阻塞周期有6倍的差距,這就是由于我們的同步指令讓該等待的stage提早到了vertex shading階段。但是由于GPU頻率足夠高,場景也不夠復雜,沒有達到GPU bound,這些延遲并沒有高到足以影響幀率的地步。
現在,讓我們人為地限制GPU頻率,模擬GPU bound的情形,這時內存總線讀寫延遲無法被覆蓋,問題就暴露出來了。
可以看到,Bus Stall對幀率的影響還是很明顯的,frametime相差了3毫秒,也就是8幀如果沒有進行足夠深入的測試,就很容會忽略掉這個問題,隨著項目向后期推進,渲染壓力逐漸變大,當問題暴露出來的時候,可能已經無法找到最初的原因了。
綜上,同步問題非常重要,我們要完全理解core-cache-memory這個模型,清楚地知道管線遇到的是哪種hazard,正確地使用pipeline barrier,從而充分發揮手動控制同步帶來的性能收益。
其他同步指令
當然Vulkan中還有很多其他的同步指令,例如subpass dependency,和barrier的用法幾乎一樣,但是只能同步renderpass內attachment相關的內存。Semaphore用于隊列之間的同步,fence用于同步GPU和CPU,Event現在手游上用的比較少,目前除了一兩款游戲自己對引擎進行了改動,只有unreal最新的版本在mobile shading renderer的occlusion query部分使用了它。
5.總結
本文首先介紹了移動端渲染架構及其特點,接著闡述了Vulkan API的優勢,再結合實際測試結果分析了Tile-based rendering的優缺點,最后重點介紹了Vulkan的顯式同步控制,結合具體場景和實測數據,給出了優化方案并分析了根因。
希望讀者可以通過本文更加深入地了解Vulkan API以及移動端渲染架構,結合具體的開發場景,合理利用測試分析工具,改善移動端渲染中的功耗、帶寬和同步問題。
-
RAW
+關注
關注
0文章
21瀏覽量
3834 -
cache技術
+關注
關注
0文章
41瀏覽量
1077 -
Vulkan
+關注
關注
0文章
28瀏覽量
5734 -
GPU芯片
+關注
關注
1文章
303瀏覽量
5886
發布評論請先 登錄
相關推薦
評論