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