1 資源是如何上傳到GPU的
比起干說(shuō),還是結(jié)合Unity Profiler做實(shí)驗(yàn)來(lái)得直觀和有說(shuō)服力抱既。
首先準(zhǔn)備一個(gè)帶Mesh的Prefab职烧,保證從磁盤(pán)加載到CPU內(nèi)存之前游戲場(chǎng)景中沒(méi)有其他實(shí)例在使用該P(yáng)refab所指向的Mesh資源,然后在運(yùn)行過(guò)程的某個(gè)時(shí)刻開(kāi)始防泵,先后調(diào)用Load和Unload邏輯蚀之,通過(guò)Unity的Profiler觀察GPU內(nèi)存總量變化和上傳數(shù)據(jù)變化。
public class TestUpload : MonoBehaviour
{
private List<GameObject> tps = new List<GameObject>();
void Start()
{
StartCoroutine(CountDown());
}
IEnumerator CountDown()
{
yield return new WaitForSeconds(0.2f);
JustLoad("Assets/test.prefab");
yield return new WaitForSeconds(0.2f);
ReleaseAsset(0);
}
private void JustLoad(string path)
{
var go = AssetDatabase.LoadAssetAtPath<GameObject>(path); //只加載捷泞,甚至不激活和顯示
tps.Add(go);
}
private void ReleaseAsset(int idx)
{
tps[idx] = null;
EditorUtility.UnloadUnusedAssetsImmediate(); //使用此接口及時(shí)觸發(fā)GPU端資源釋放
}
private void OnDestroy()
{
tps.Clear();
}
}
下圖來(lái)自Profiler:
在Unity運(yùn)行期間足删,加載資源(Mesh或Texture)完成的同時(shí),無(wú)論當(dāng)前幀是否有渲染目標(biāo)Mesh或者使用目標(biāo)Texture的需要(即便只是加載了資源锁右,之后什么都不操作)都會(huì)觸發(fā)向GPU上傳數(shù)據(jù)的操作失受,并被保存在名叫GfxBuffer的內(nèi)部類(lèi)中。傳輸模式在Editor模式下是同步的咏瑟,會(huì)在觸發(fā)上傳的同一幀內(nèi)完成向GPU提交全部數(shù)據(jù)(Header和Binary)拂到。 同樣,當(dāng)Unity不再持有該Mesh資源的引用码泞,并且在尋求主動(dòng)釋放GPU資源時(shí)兄旬,顯存中的關(guān)聯(lián)資源(Vertex/Index Buffer等)才會(huì)被釋放,在Editor模式下浦夷,這個(gè)操作對(duì)應(yīng)了EditorUtility.UnloadUnusedAssetsImmediate()
方法辖试。
為了知道Unity在上傳GPU資源數(shù)據(jù)時(shí)到底干了什么,這邊比較了一下上傳關(guān)鍵幀和其他時(shí)間CPU部分負(fù)載的異同劈狐,利用Profiler自帶Hierarchy查找關(guān)鍵詞Mesh罐孝,比較異同后發(fā)現(xiàn):處于上傳負(fù)載的關(guān)鍵幀時(shí)期,Unity觸發(fā)了2個(gè)獨(dú)特的函數(shù)調(diào)用:
- Mesh.AwakeFromLoad
- Mesh.CreateMesh
這兩個(gè)方法在Prolier中的具體執(zhí)行層級(jí)如下圖所示:
可見(jiàn)當(dāng)讀取磁盤(pán)數(shù)據(jù)的回調(diào)一旦完成肥缔,就觸發(fā)了Mesh.AwakeFromLoad
莲兢,進(jìn)而觸發(fā)了Mesh.CreateMesh
,而這個(gè)方法內(nèi)部主要負(fù)責(zé)將Mesh中的"Vertice"和"Index"數(shù)據(jù)依序通過(guò)GeometryBuffer
上傳到GPU端续膳。
進(jìn)一步梳理下Mesh和Texture等資源的處理流程改艇,可以區(qū)分為兩種方式:
- 與場(chǎng)景同時(shí)加載
- 其本質(zhì)也是從磁盤(pán)加載,但是隨同場(chǎng)景出現(xiàn)而出現(xiàn)
- 在調(diào)用Profiler測(cè)試時(shí)坟岔,往往因?yàn)閳?chǎng)景早于Profiler工作而出現(xiàn)谒兄,所以一部分基于場(chǎng)景的Mesh和Texture早已在GPU了
- 運(yùn)行時(shí)由代碼觸發(fā)的從磁盤(pán)加載
- 這里如果是從構(gòu)建好的AssetBundle中獲取數(shù)據(jù),那么就有2中不同的上傳模式:
- Sync:
- 該模式在資源build時(shí)期(就是打AssetBundle時(shí)期)會(huì)將數(shù)據(jù)的Header和Binary全部打包到
.res
文件內(nèi), - 在游戲運(yùn)行時(shí),Unity從磁盤(pán)(Bundle)中讀取這個(gè)文件到內(nèi)存晾腔,之后會(huì)由主線程于一幀內(nèi)將資源(Header和Binary)Upload到GPU锐峭。
- 該模式在資源build時(shí)期(就是打AssetBundle時(shí)期)會(huì)將數(shù)據(jù)的Header和Binary全部打包到
- Async
- 還是在資源build時(shí)期,會(huì)將Header寫(xiě)入
.res
文件中妇多,Binary數(shù)據(jù)則寫(xiě)入.resS
文件中 - 在游戲運(yùn)行時(shí),Unity從磁盤(pán)(Bundle)中讀取
.res
文件到內(nèi)存,解析出Header數(shù)據(jù)啊研,之后Unity采用streams的方式從.resS
文件中加載Binary數(shù)據(jù)到GPU,這個(gè)過(guò)程使用了一個(gè)固定大小的Ring Buffer(環(huán)形緩沖)鸥拧,而且還會(huì)利用多線程党远,分多幀處理。
- 還是在資源build時(shí)期,會(huì)將Header寫(xiě)入
- Sync:
- 如果是從Resources目錄讀取資源住涉,或者在Editor模式下調(diào)用AssetDatabase獲取資源麸锉,那么統(tǒng)一走Sync模式
- 這里如果是從構(gòu)建好的AssetBundle中獲取數(shù)據(jù),那么就有2中不同的上傳模式:
備注1 Unity重復(fù)使用一段環(huán)形緩沖作為流式(Streaming)上傳數(shù)據(jù)到GPU的區(qū)域,這么做的主要目的是避免重復(fù)開(kāi)辟新的內(nèi)存舆声。在ProjectSettings->Quality->AsyncAssetUpload->BufferSize可以控制環(huán)形緩沖的大小花沉,默認(rèn)是4MB,最小可調(diào)到2MB媳握,最大則是2GB碱屁。當(dāng)單個(gè)Mesh或Texture的尺寸超過(guò)環(huán)形緩沖大小時(shí),Unity不得不重新開(kāi)辟RingBuffer以適應(yīng)上傳數(shù)據(jù)大小蛾找,出現(xiàn)這種情況會(huì)導(dǎo)致效率下降娩脾,因此最佳策略是手動(dòng)調(diào)整BufferSize,以滿足場(chǎng)景內(nèi)最大Mesh/Texture的尺寸打毛。
備注2 Unity也提供了控制每幀Upload時(shí)間(ms)的接口和設(shè)置柿赊,可以在ProjectSettings->Quality->AsyncAssetUpload->TimeSlice中調(diào)節(jié)每一幀最大可占用的CPU時(shí)長(zhǎng)俩功,這個(gè)數(shù)值越大,意味著GPU將越快獲得Mesh/Texture數(shù)據(jù)碰声,代價(jià)是CPU在提交數(shù)據(jù)的這段時(shí)間內(nèi)負(fù)荷增大诡蜓。
注意只有在出發(fā)C#加載Mesh資源的那一幀(上傳關(guān)鍵幀),系統(tǒng)才向GPU上傳了一定數(shù)量是頂點(diǎn)和索引緩沖數(shù)據(jù)胰挑。那一幀過(guò)后蔓罚,系統(tǒng)恢復(fù)“常態(tài)”。
部分關(guān)鍵參數(shù)名的含義參考如下官方文檔:
Vertex Buffer Upload In Frame Count/Bytes | The amount of geometry that the CPU uploaded to the GPU in the frame. This represents the vertex/normal/texcoord data. There might already be some geometry on the GPU. This statistic only includes geometry that Unity transfers in a frame. |
---|---|
Index Buffer Upload In Frame Count/Bytes | The amount of geometry that the CPU uploaded to the GPU in the frame. This represents the triangle indices data. There might already be some geometry on the GPU. This statistic only includes geometry that Unity transfers in a frame. |
2 allowSceneActivation
這個(gè)值的作用參考文檔即可:AsyncOperation.allowSceneActivation
簡(jiǎn)單來(lái)說(shuō)瞻颂,我們可以通過(guò)在加載場(chǎng)景前將該變量設(shè)置為false豺谈,從而控制Unity專(zhuān)心于該場(chǎng)景的異步加載,直到完成度達(dá)到90%后(也可以提前贡这,但是不能延后)再通過(guò)將allowSceneActivation設(shè)置為true從而重啟其他處于隊(duì)列中等待的AsyncOperation茬末,比如Unity官方提到的SceneManager.UnloadSceneAsync
。
我想說(shuō)的是藕坯,有時(shí)候如果沒(méi)有及時(shí)提前觸發(fā)非場(chǎng)景類(lèi)的AssetBundle異步加載团南,那么allowSceneActivation也會(huì)將這些Bundle的加載停住(stalled)炼彪,由于是異步提交的吐根,因此誤停其他Bundle加載也很可能是偶發(fā)的,不一定在測(cè)試的時(shí)候必現(xiàn)辐马,需要注意和提前規(guī)避拷橘。
3 ResetPreMappedBufferMemory
這個(gè)借口的作用參考這篇官方文檔:ParticleSystem.ResetPreMappedBufferMemory
之所以提及這個(gè)接口是因?yàn)橛许?xiàng)目遇到一個(gè)戰(zhàn)斗中內(nèi)存突然暴增的問(wèn)題,特別是在以高倍速播放戰(zhàn)斗畫(huà)面的情況下喜爷,Gfx Memory會(huì)有倍增的恐怖效果冗疮,而且戰(zhàn)斗結(jié)束一段時(shí)間后爆漲的內(nèi)存仍然沒(méi)有明顯回落。 導(dǎo)致這個(gè)問(wèn)題的原因是同屏粒子特效過(guò)多檩帐,使得Unity粒子系統(tǒng)底層申請(qǐng)了較大緩存用來(lái)存放Mesh等粒子渲染資源术幔。
很顯然Unity底層有專(zhuān)門(mén)算法控制額外內(nèi)存申請(qǐng)量,我們的游戲戰(zhàn)斗在高倍速播放過(guò)程中累積了大量同屏粒子特效的顯示請(qǐng)求湃密,這個(gè)請(qǐng)求量顯然嚇到了Unity诅挑。粗暴的解決方法是在內(nèi)存申請(qǐng)高峰之后,適時(shí)的調(diào)用ParticleSystem.ResetPreMappedBufferMemory()
方法重置這部分額外開(kāi)辟的內(nèi)存泛源。
一個(gè)疑問(wèn)是Unity的這項(xiàng)預(yù)申請(qǐng)大量?jī)?nèi)存的優(yōu)化是否僅針對(duì)大內(nèi)存設(shè)備啟用拔妥,或者會(huì)依據(jù)可用內(nèi)存大小自動(dòng)調(diào)整?因?yàn)槿绻彺嬷凳歉鶕?jù)可用內(nèi)存來(lái)的確定的上限的达箍,那么短時(shí)間內(nèi)存占用的爆發(fā)也算是一種可控范圍內(nèi)的技術(shù)處理没龙,我們無(wú)需額外修正,不過(guò)后來(lái)的測(cè)試表明并不是(至少2021版還不是)
分析上圖,沒(méi)有在4G手機(jī)上找到明顯的安可用比例申請(qǐng)內(nèi)存大小的證據(jù)硬纤。
當(dāng)然解滓,優(yōu)雅的解決方案是控制任何可能短時(shí)間內(nèi)大量生成粒子特效的情景,這其中包括了高倍速播放戰(zhàn)斗筝家,也包括其他諸如同屏多角色釋放大量粒子特效伐蒂,甚至粒子特效的制作本身。