日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

腾讯游戏学院专家分析:Unity在移动设备的GPU内存机制

發布時間:2024/8/26 编程问答 46 豆豆
生活随笔 收集整理的這篇文章主要介紹了 腾讯游戏学院专家分析:Unity在移动设备的GPU内存机制 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

導語CPU和GPU是共享一份內存的嗎?騰訊游戲學院專家Donald將在本文嘗試以一張貼圖紋理的虛擬內存占用為例,解答一些內存方面的問題。本篇主要分析iOS系統,后續會更新安卓篇。

開發手機游戲時,常聽到身邊的人傳授經驗:“CPU和GPU是共享一份內存的”,但這句經驗到底具體指的是什么,仿佛總得不到細節精確的回答。

因此,本文嘗試以一張貼圖紋理的虛擬內存占用為例,就以下問題進行分析和解答:

1)是否的確主存顯存共享一份貼圖虛擬內存?

2)如果問題1證實的確只有一份,紋理虛擬內存的完整流程是怎樣?Unity將該紋理文件在主存加載好紋理數據后,是直接調用圖形API傳遞該主存指針,從而GPU能直接訪問該主存中的紋理數據?還是需要調用圖形API將該主存中的紋理數據拷貝到另一份虛擬內存中,以供GPU訪問?拷貝完成后紋理主存部分如何處置?

01術語

為清晰表達避免概念混淆,本文采取以下術語:

物理內存(Physical Memory):具體的存儲硬件,各種SDRAM,比如LPDDR是移動設備常用的一種低功耗SDRAM。

虛擬內存(Virtual Memory):對物理內存的一種邏輯映射。

主存(Main Memory/Primary Memory):CPU能讀寫的虛擬內存。

顯存(Graphics Memory):GPU能讀寫的虛擬內存。

另外,外存(External storage):外部存儲,“硬盤”,在移動設備一般是Flash。

02 iOS篇

2.1硬件

如下4圖[1][2]所示,iPhone6只有A8里擁有一塊物理內存(1GB LPDDR3 RAM),且CPU/GPU晶片中并無物理內存(SDRAM),只有物理內存的接口(SDRAM Interface)。

且A8采取PoP封裝(Package on Package),即將CPU/GPU晶片和物理內存豎直排列于A8芯片中,將CPU/GPU晶片移除后,在下一層露出了它倆共用的一塊物理內存。

注:晶片中有高速Cache緩存,類型為SRAM。
?

[iPhone6的物理內存位于Apple A8里]

[Apple A8晶片里,只有SDRAM的接口,并無SDRAM]

[A8 GPU PowerVR 6450里只有System Memory Interface,并無SDRAM]

[A8 SoC CPU/GPU晶片和物理內存采取PoP封裝。將CPU/GPU晶片從SoC移除后,露出下一層的DRAM物理內存]


其他iOS設備,iPhone、iPad等,亦如此,硬件層面,它們的物理內存都為統一內存(Unified Memory)架構,即主存和顯存都位于同樣的物理內存硬件中。

而桌面電腦一般是分離物理內存(Discrete Memory)架構。

2.2圖形API

自2013年的AppleA7(iPhone 5s)起iOS設備便支持Metal[3],考慮當下(2018)的市場份額,故只討論支持Metal的情況,而不討論iOS上OpenGLES的情況。

系統層面,Metal支持主存顯存同時訪問同一塊虛擬內存,即MTLBuffer的options為MTLStorageModeShared[4,5,6],此情況已無主存顯存之分,Shared模式是Buffer(比如頂點緩存、索引緩存)的默認創建模式,在iOS中Shared也是紋理緩存的默認創建模式。
?

[Resource storage modes in iOS and tvOS]


此時對該虛擬內存的修改,會同時反饋到CPU和GPU上,除非CPU準備好Buffer的內容后不再修改,但一旦CPU對Buffer進行了二次修改,為避免和GPU的訪問沖突,需要有一定的同步機制,比如三重緩沖(Tripple Buffering)[7]。

Pirvate模式為GPU單獨訪問的虛擬內存,主要用于RenderTexture等情況[9],并非當前重點。

2.3分析Unity在iOS的實現

雖然圖形API機制如此,但引擎內部實現大相徑庭,保守起見,具體結論應以引擎具體邏輯為準。

先以紋理為例,Unity在iOS+Metal上從紋理文件存儲到最終紋理顯存,其二進制流的完整流程是怎樣的?

人肉閱讀分析Unity源碼是耗時且可能不準確的。結合Profiler等工具進行分析,會省時精確,事半功倍。這樣也可順帶對Profile工具的綜合應用進行介紹。所以下面,先假設我們不知道Metal的機制,試從現象推斷出原因。
?

[GFXMemory測試Demo]


先創建一個名為GFXMemory的測試demo,分別有3張分辨率足夠大的4096x4096的紋理貼圖,格式分別設為RGBA32、RGB24、ASTC5x5,通過運行時點擊對應的區域,才單獨加載對應貼圖,顯示在屏幕中。

準備做Profile測試先查證以下問題:

由于3張紋理分辨率非常大且開啟Mipmaps,其內存占用理應是期待紋理虛擬內存=85.33MB+64.00MB+13.65MB=162.98MB,如果最終內存穩定后,本進程的虛擬內存占用約為進程內存~=啟動內存+已加載紋理內存,即可證實紋理虛擬內存占用的確只有一份,否則如果進程虛擬內存約為進程內存~=啟動內存+2*已加載紋理內存,即可證實主存、顯存各持一份紋理貼圖。

Unity版本為2017.4.8f1、XCode版本為10.1、運行設備為iPhone6s。

先用Unity以Development Build進行XCode工程導出,Development Build僅僅是為了能用Unity Memory Profiler進行Profile。

XCode中對Unity-iPhone工程進行Edit Scheme,并如下圖開啟Malloc Stack,是為了在命令行對memorygraph使用malloc_history命令查看內存創建的堆棧。
?

[開啟Malloc Stack才能對memorygraph方能使用malloc_history命令查看內存創建的堆棧]


XCode中構建版本,USB連接iPhone6s并在其上運行,等待幾秒鐘待內存穩定后:

在XCode點擊“Debug Memory Graph”,截取得出XCode的內存統計,并且Export為xcode_empty.memorygraph文件

點擊UI加載上面3張紋理后,等待幾秒鐘待內存穩定后:

在Unity用Memory Profiler點擊Take Snapshot,截取得出Unity的內存統計,并另存為unity.memsnap3文件

在XCode點擊“Capture GPU Frame”,截取得到當前幀的GPU快照,并另存為xcode.gputrace文件

在XCode點擊“Debug Memory Graph”,截取得出XCode的內存統計,并且Export為xcode.memorygraph文件

注意上述操作都確保游戲是一次運行針對同一進程的4次抓取結果,從而確保內存地址穩定。

我們在命令行執行命令vmmap--summary./xcode_empty.memgraph,得到加載紋理前的虛擬內存占用約為111.3MB,如下圖:
?

[加載紋理前,Native虛擬內存占用約為111.3MB]


上圖我們應關心“DIRTY SIZE”和“SWAPPED SIZE”,前者代表已寫虛存大小、后者代表已寫待壓縮虛存大小。iOS和一般OS不一樣,不采取虛存切頁(Paging)的機制,而是采取壓縮內存的機制。而在iOS中所謂的內存占用(Memory Footprint)事實上是MemoryFootprint=DirtySize+CompressedSize,iOS以MemoryFootprint的大小作為Killapp的依據。注意Swapped Size是待壓縮的大小,壓縮后方為Compressed Size。[8]
?

[Memory Footprint=Dirty Size+Compressed Size]


我們再執行命令vmmap--summary./xcode.memgraph,得到加載紋理后的虛擬內存占用約為297.8MB,如下圖:
?

[加載紋理后,Native虛擬內存占用約為297.8MB]


從而,加載紋理額外虛擬內存占用=297.9MB-111.3MB=186.6MB~=期待紋理虛擬內存占用162.98MB,而186.6MB<<325.96MB,從而幾乎已經證實問題1,的確主存顯存共享一份貼圖虛擬內存。至于為何會多出186.6MB-162.98MB~=23.62MB,我們會在后面證實到。

但僅僅從內存增幅來認定內存共享一份,顯得還不夠精確。

這時有個貌似合理的猜想:“如果GPU里用到的紋理虛擬內存地址,剛好等于MemoryGraph中對應的紋理虛擬地址,就說明它們必然是共享一份內存了”。

懷著這個想法,我們用XCode打開xcode.gputrace文件,搜索得出4096_rgba32的虛擬內存地址為0x1083f5b80,如下圖:
?

[GPUTrace文件顯示4096_rgba32紋理的虛擬內存地址為0x1083f5b80]


Unity Memory Profiler Editor本不支持顯示對象的Native虛擬內存地址,簡單修改其源碼,讓其在面板上顯示Unity Native Object的虛擬內存地址,4096_rgba32紋理的虛擬內存地址為0x1083f53b0紋理,如下圖:
?

[Unity Memory Profiler顯示4096_rgba32紋理的虛擬內存地址為0x1083f53b0]


“CPU/GPU訪問的紋理地址不一樣,這證實這張紋理不是CPU/GPU共享的!”但可惜,不能因此得出這個結論。

我們控制臺針對GPUTrace的地址使用命令malloc_history./xcode.memgraph-fullStacks 0x1083f5b80,有下圖輸出:
?

[GPUTrace紋理對象AGXA9FamilyTexture地址的堆分配函數棧]


針對Unity Memory Profiler的地址使用命令malloc_history./xcode.memgraph-fullStacks 0x1083f53b0,有下圖輸出:
?

[Unity Memory Profiler紋理對象Texture2D地址的堆分配函數棧]


使用XCode再次打開xcode.memgraph,搜索地址0x1083f5b80,發現其類型是“AGXA9FamilyTexture”,而且對象大小僅僅只有528字節,見下圖:
?

[0x1083f5b80地址對應的,僅僅是紋理對象,而并非我們最關心的紋理內容]


上面3圖,證實了上面的地址僅僅是紋理對象,而并非我們最關心的紋理內容地址。比如AGXA9FamilyTexture是Metal的紋理對象,Texture2D是Unity的紋理對象,紋理對象內部有指針指向了紋理內容。

如果我們不修改Unity源碼,我們無法得知Texture2D中紋理內容的地址。如何得知紋理內容到底在哪呢?

留意上面vmmap--summary命令顯示加載紋理前后的內存占用,增幅最大的內存區域(Region)是“IOKit”,我們不妨看看里面到底是啥,通過vmmap--verbose./xcode.memgraph|grep"IOKit",有以下結果:
?

[IOKit內存區域里,有明顯的貼圖內容虛擬內存占用]


上面非常像我們3張紋理貼圖內容的內存占用大小(下面才解釋為什么64.0MB變為85.3MB),而左邊就是它們的虛擬內存地址。

我們嘗試用malloc_history./xcode.memgraph--fullStacks“上述3個地址”,發現都不能打印出分配它們的棧,說明它們并非使用傳統malloc在堆(Heap)上分配,如下圖。事實上IOKit是iOS的驅動框架,該區域內存是驅動相關的虛擬內存區域,手機靚號交易通過額外的實驗可以知道,Metal最重要的MTLBuffer分配,不管Dirty與否,都是在IOKit這個驅動區域進行內存分配。

[IOKit區域是驅動相關的虛擬內存地址,并不能通過malloc_history打印出來]


但是!當我們在XCode打開xcode.memgraph后,如下圖,搜索地址“0x11c3e0000”得出該85.3MB的IOKit內存,而引用它的,恰好就是我們上面發現的地址為0x1083f5b80的Metal的紋理對象!
?


至此,我們通過硬件分析、圖形API分析和虛擬內存Profile分析,比較折騰,終于得出以下結論:

iOS設備中只有一塊物理內存硬件。

主存地址和顯存地址在同一個地址空間(Address Space)中,即虛存地址空間(Virtual Address Space)。

虛擬內存中的確只有一份紋理內容,而且該紋理內容的確就是被GPU所用的紋理。

我們接著討論問題2。由于問題2需要回答的是貼圖內存走向,不能通過分析某一時刻的虛擬內存得出結論,而要使用帶有Timeline的Profiler,這里使用Instruments。

我們進行3種Profiler:Timer Profiler以觀察CPU耗時情況及捕捉函數調用棧,Allocations以觀察堆內存分配釋放情況,VM Tracker以觀察所有虛擬內存的分配釋放情況。

針對Time Profiler,我們可以打開其High Frequency選項,以采樣到更精細的函數調用棧。
?

[打開Time Profiler的High Frequency,以捕捉到更精細的函數調用棧]


Profile結果如下圖。其中3個紅框左到右分別表示加載RGBA32、RGB24、ASTC5x5時的情況。
?

[進行Time Profiler、Allocations、VM Tracker的Profiler,圖中3個紅框分別是加載RGBA_32、RGB24、ASTC5x5時的情況]


大致觀察上圖可以發現:

CPU消耗尖刺(Spike):RGB24>RGBA32>>ASTC5x5

堆內存消耗尖刺:RGB24>RGB32>>ASTC5x5

虛擬內存消耗則整體呈現持續增長

我們先看最左邊RGBA32的CPU消耗情況,如下兩圖,分別為加載RGB24紋理時CPU消耗Spike的前期和后期。
?

[加載RGB24紋理時CPU消耗Spike的前期]

[加載RGB24紋理時CPU消耗Spike的后期]


不需無頭緒地辛苦閱讀海量引擎代碼,有的放矢,立刻可精確看出Unity在加載紋理時主要工作分兩部分:文件加載(File::Read())和紋理上傳(UploadTexture2DData())。

而且發現將時間線在前后期中間不管如何細分,都只出現了上面2個主要消耗,說明了只有這兩個工作線程在工作,我們只需分析它們相信已足夠找出紋理加載的流程。我們也發現在整個紋理加載過程中,主線程只有非常少的Update空轉占用,證實紋理加載幾乎是脫離主線程工作的。

文件加載函數棧看起來比較通用,先從紋理上傳的函數棧看起應該會更快解決問題。

閱讀源碼,發現其關鍵流程如下:

AsyncUploadManager.cpp中,AsyncUploadManager.AsyncResourceUpload()從m_UploadQueue不斷Dequeue出FileAssetUploadInstruction類型的對象ftuInstr,其非常重要,描述了這次紋理上傳的所有關鍵數據。根據紋理類型,調用了2D紋理函數static Upload2DTexture()。

AsyncUploadManager.cpp中,static Upload2DTexture()將ftuInstr->buffer直接賦值給UInt8*uploadBuffer,至此,首次顯式出現了紋理內容的指針,可以看出,非常關鍵的問題是,到底FileAssetUploadInstruction::buffer是從哪來的?但先不急,先把這個棧看完。接著把uploadBuffer和ftuInstr里幾乎所有關于紋理的數據,傳遞給Texture.cpp的static UploadTexture2DData()。

Texture.cpp文件中,static UploadTexture2DData()調用gfxDevice.UploadTexture2D(),通知GfxDeviceMetal進行紋理上傳。

TextureMetal.mm文件中,static UploadTexture()通過[MTLDevice newTextureWithDescriptor]創建Metal的紋理對象,指定了紋理分辨率、格式、mipmap層數等,并且在IOKit區域里已為該對象分配了用于存放紋理內容的內存區域。

TextureMetal.mm文件中,static UploadMipPyramid()為各個mipmap層算出分辨率,最終調用了Metal API[MTLTexture replaceRegion],將對應的紋理數據最終拷貝到了MTLTexture對象中。注本接口名叫“替換replace”,事實上是進行了紋理內容數據進行了“拷貝copy”操作。

通過以上比較啰嗦的分析,可以看出就算是在Metal進行紋理上傳,也難免有紋理內容拷貝的過程。用[MTLDevice newTextureWithDescriptor]創建紋理對象及其指向的紋理內容空間,把FileAssetUploadInstruction的buffer數據,加以一定處理(Crunch、紋理格式轉換等),最終通過[MTLTexture replaceRegion]將紋理內容數據拷貝到了驅動虛擬內存IOKit區域里。

那到底這個buffer數據到底從哪來的?當然,從上文和類名包含“File”,已經可以猜出是從外存讀取得來,但不精確證實不服氣,我們將注意力回到上面的文件加載調用棧。堆棧協助代碼閱讀,發現很簡單:

在AsyncReadManagerThreaded.cpp里,

AsyncReadManagerThreaded::ThreadEntry()不斷從m_Requests里拿出AsyncReadCommand類型的實例command,

并且打開紋理文件對象指針:File*file=m_OpenFilesCache.OpenCached(command->fileName);

將file的內容,全都讀取到command->buffer里:bool readOk=file->Read(command->offset,command->buffer,command->size)==command->size;,說明command->buffer指向的內存已經分配好了內存以供紋理文件讀入。

那么command->buffer的內存哪里分配而來呢?

由于內存分配的CPU消耗可能很小,就算是高精度的Sampler也可能在Time Profiler里找不到,這里我們明顯要求救于Allocation,如下圖,我們選擇“Call Trees”分類,框選在加載紋理時,內存飆升時的時段,發現132.03MB內存是在AsyncUploadManager::ManageTextureUploadRingBufferMemory()中分配給m_DataRingBuffer。
?

[文件讀取的緩存應該是在堆上分配]

[紋理上傳過程中,最大的堆內存分配是分配給了`AyncUploadManager.m_DataRingBuffer`]


通過以上種種分析,已經掌握了不少信息和關鍵字,找出答案已是臨門一腳了:



AsyncUploadManager::ScheduleAsyncRead()從m_DataRingBuffer申請紋理內容大小的內存空間,同時將指針賦值給asyncReadCommand->buffer和ftuInstr->buffer,從而文件讀取線程將紋理文件內容寫到asyncReadCommand->buffer指向的堆內存,渲染線程在通過ftuInstr->buffer將紋理內容從同一堆內存獲取到。

至此,回答了問題2。

最后的最后,上面提到的RGB24紋理的特殊情況,為什么其虛擬內存占用大小不是64MB,而是和RGBA32一樣,都是85.3MB?結合上面已知流程,分析可知,原因是Metal并不支持RGB24,在運行時都會轉為RGBA32,如下:
?

[Metal不支持RGB24,交給GPU使用前需要轉換為RGBA32,]


這能從以下Time Profiler以及Allocation棧輕易證實:
?

[Metal不支持RGB24,交給GPU使用前需要轉換為RGBA32,需要消耗CPU進行一次BlitImage]

[Metal不支持RGB24,交給GPU使用前需要轉換為RGBA32,需要在堆內存申請臨時內存進行一次BlitImage]


2.4結論

通過Profile結果和源碼,我們證實了:iOS設備中只有一塊物理內存硬件,主存地址和顯存地址在同一塊虛存地址空間中,虛存最終的確只有一份紋理內容位于IOKit區域中,而且該紋理內容的確就是被GPU所用的紋理。

在紋理上傳過程中,Unity先在堆內存申請緩存,然后將紋理文件內容讀進緩存里,然后調用圖形API將該該紋理內容數據拷貝到IOKit虛存中,供GPU訪問。拷貝完成后緩存視乎情況從堆內存釋放。

過程中,我們展示了在iOS中各種Profile工具的實際使用方法。

也介紹了一些基礎的內存知識和概念。

03 Android篇

打算未來才做Android的Profile實驗和分析報告,但通過上面的分析看來,可以大膽預測:

Android設備也是基于ARM架構,想必各種Vendor的設備也是只有一塊物理內存硬件;

上面的函數棧大多平臺無關,而且Vulkan和Metal是同一代的圖形框架,所以Unity在Vulkan上的實現內存流程應該和Metal非常類似;

由于GLES是較老的框架,所以其內存流程可能和Metal類似,但要留意GLES具體情況,和其在驅動內部gralloc的使用情況,有沒有額外的拷貝。

引用

[1]ifixit-iPhone 6 Teardown

[2]Chipworks Disassembles Apple’s A8 SoC

[3]Metal_(API)#Supported_GPUs#Supported_GPUs)

[4]Metal Best Practices Guide-Resource Options

[5]Metal-Resource Storage Mode

[6]MTLBuffer

[7]Triple Buffering

[8]iOS Memory Deep Dive

[9]Choosing a Resource Storage Mode in iOS and tvOS

[10]MTLBuffer makeTexture

關于騰訊游戲學院專家團

如果你的游戲也富有想法充滿創意,如果你的團隊現在也遇到了一些開發瓶頸,那么歡迎你來聯系我們。騰訊游戲學院聚集了騰訊及行業內策劃、美術、程序等領域的游戲專家,我們將為全世界的創意游戲團隊提供專業的技術指導和游戲調優建議,解決團隊在開發過程中遇到的一系列問題。

總結

以上是生活随笔為你收集整理的腾讯游戏学院专家分析:Unity在移动设备的GPU内存机制的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。