本系列文章是對 metalkit.org 上面MetalKit內容的全面翻譯和學習.
在深入記憶體管理時有很多話題需要探討.上次我們已經瞭解了建立MTLBuffer
物件有三種選項設定:用新資料分配一塊新記憶體,用已存在區域複製資料到一塊新記憶體,重用一塊已經存在的分配區不復制資料.因為我們以前並不關注記憶體,就讓我們驗證一下證明這確實是真的.首先我們複製資料到另一分配區:
let count = 2000
let length = count * MemoryLayout< Float >.stride
var myVector = [Float](repeating: 0, count: count)
let myBuffer = device.makeBuffer(bytes: myVector, length: length, options: [])
withUnsafePointer(to: &myVector) { print($0) }
print(myBuffer.contents())
複製程式碼
注意: withUnsafePointer() 函式提供給我們的實際資料的記憶體地址在堆上,而不是指向資料的指標(在棧上)的地址.
你的輸出看起來會像這樣:
0x000000010043e0e0
0x0000000102afd000
複製程式碼
注意到上面地址的最後三位數字了嗎?這是來自於頁對齊資料,因為地址是以0 mod pageSize
確定的,因為最後三位是0
,因為我們的頁尺寸是0x1000
.
現在我們接著看Storage Modes
,我們上次曾簡短提到過.至少需要記住四條主要規則,每種儲存模式一條:
Mode | Description |
---|---|
Shared | 在macOS 緩衝器,iOS/tvOS 資源上為預設;masOS 紋理上不可用. |
Private | 主要用於資料只被GPU 訪問的情況下 |
Memoryless | 僅用於iOS/tvOS 晶片的臨時渲染目標(紋理). |
Managed | macOS 紋理的預設模式;在iOS/tvOS 資源上不可用. |
對於一個更好的大圖片,下面是完整的作弊表,讓你無需記憶上面的規則,更容易使用:
最複雜的情況是,在當masOS
處理緩衝器時,資料需要同時被CPU
和GPU
訪問.我們選擇儲存模式時,是基於下面一個或多個條件為真來決定的:
- Private - 對於最多隻改變一次的大尺寸資料,那它就不是"髒"的.建立一個
Shared
模式的源緩衝器,然後位塊傳送它的資料到一個Private
模式的目標緩衝器中.在本例中資源一致不是必須的,因為資料只被GPU
訪問.該操作是花費最低的(一個一次性花費). - Managed - 對於很少改變(每幾幀)的中等尺寸資料,它就是部分"髒"的.一個資料副本儲存在系統記憶體中給
CPU
使用,另一份儲存在GPU
記憶體中.資源一致性通過同步兩份副本來嚴格控制. - Shared - 對於每幀都更新的小尺寸資料,那它就是完全"髒"的.資料存放在系統記憶體中,並同時對
CPU
和GPU
可見,可修改.資源一致性只在命令緩衝器界限內被保證.
如何保證資源的一致性?首先,確保所有的來自CPU
的修改已經在命令緩衝器被提交(檢查命令緩衝器狀態屬性是否是MTLCommandBufferStatusCommitted)之前完成了.在GPU
結束執行命令緩衝器後,CPU
只應該在GPU
發訊號給CPU
,告知CPU命令緩衝器結束執行(檢查命令緩衝器狀態屬性是否是MTLCommandBufferStatusCompleted)之後,再開始著手修改.
最後,讓我們看看masOS
的資源是如何同步完成的.對於緩衝器:在CPU
寫入後使用didModifyRange() 將修改項通知GPU
,這樣Metal
可以只更新這個資料區域;
在GPU
寫入後,在一個位塊傳送操作內,用synchronize(resource:) 來重新整理快取,這樣CPU
就可以訪問更新後的資料.
對於紋理:在CPU
寫入後,使用兩個replace() 區域函式中的一個,將修改項通知GPU
,這樣Metal
可以只更新這個資料區域;在GPU
寫入後,在一個位塊傳送操作內,使用兩個synchronize() 函式中的一個,來允許Metal
在GPU
結束脩改資料後再更新系統快取副本.
原始碼source code已釋出在Github上.
下次見!