Metal:對 iOS 中 GPU 程式設計的高度優化的框架

發表於2015-03-08

Metal 框架支援 GPU 加速高階 3D 影像渲染,以及資料平行計算工作。Metal 提供了先進合理的 API,它不僅為圖形的組織、處理和呈現,也為計算命令以及為這些命令相關的資料和資源的管理,提供了細粒度和底層的控制。Metal 的主要目的是最小化 GPU 工作時 CPU 所要的消耗。– Metal Programming Guide

Metal 是針對 iPhone 和 iPad 中 GPU 程式設計的高度優化的框架。其名字來源是因為 Metal 是 iOS 平臺中最底層的圖形框架 (意指 “最接近硬體”)。

該框架被設計用來實現兩個目標: 3D 圖形渲染和平行計算。這兩者有很多共同點。它們都在數量龐大的資料上並行執行特殊的程式碼,並可以在 GPU. 上執行。

什麼人應該使用 Metal?

在談論 API 和語言本身之前,我們應該討論一下什麼樣的開發者能從 Metal 中受益。正如上面提過的,Metal 提供兩個功能: 圖形渲染和平行計算。

對於尋找遊戲引擎的開發者來說,Metal 不是最佳選擇。蘋果官方的的 Scene Kit (3D) 和 Sprite Kit (2D) 是更好的選擇。這些 API 提供了包括物理模擬在內的更高階別的遊戲引擎。另外還有功能更全面的 3D 引擎,例如 Epic 的 Unreal Engine 或 Unity,二者都是跨平臺的。使用這些引擎,你無需直接使用 Metal 的 API,就可以從 Metal 中獲益。

編寫基於底層圖形 API 的渲染引擎時,除了 Metal 以外的其他選擇還有 OpenGL 和 OpenGL ES。OpenGL 不僅支援包括 OSX,Windows,Linux 和 Android 在內的幾乎所有平臺,還有大量的教程,書籍和最佳實踐指南等資料。目前,Metal 的資源非常有限,並且僅限於搭載了 64 位處理器的 iPhone 和 iPad。但另外一方面,因為 OpenGL 的限制,其效能與 Metal 相比並不佔優勢,畢竟後者是專門用來解決這些問題的。

如果想要一個 iOS 上高效能的平行計算庫,答案非常簡單。Metal 是唯一的選擇。OpenCL 在 iOS 上是私有框架,而 Core Image (使用了 OpenCL) 對這樣的任務來說既不夠強大又不夠靈活。

使用 Metal 的好處

Metal 的最大好處就是與 OpenGL ES 相比顯著降低了消耗。在 OpenGL 中無論建立緩衝區還是紋理,OpenGL 都會複製一份以防止 GPU 在使用它們的時候被意外訪問。出於安全的原因複製類似紋理和緩衝區這樣的大的資源是非常耗時的操作。而 Metal 並不複製資源。開發者需要負責在 CPU 和 GPU 之間同步訪問。幸運的是,蘋果提供了另一個很棒的 API 使資源同步訪問更加容易,那就是Grand Central Dispatch。雖然使用 Metal 時仍然有些這方面的問題需要注意,但是一個在渲染時載入和解除安裝資源的先進的引擎,在避免額外的複製後能夠獲得更多的好處。

Metal 的另外一個好處是其預估 GPU 狀態來避免多餘的驗證和編譯。通常在 OpenGL 中,你需要依次設定 GPU 的狀態,在每個繪製指令 (draw call) 之前需要驗證新的狀態。最壞的情況是 OpenGL 需要再次重新編譯著色器 (shader) 以反映新的狀態。當然,這種評估是必要的,但 Metal 選擇了另一種方法。在渲染引擎初始化過程中,一組狀態被烘焙 (bake) 至預估渲染的 路徑 (pass) 中。多個不同資源可以共同使用該渲染路徑物件,但其它的狀態是恆定的。Metal 中一個渲染路徑無需更進一步的驗證,使 API 的消耗降到最低,從而大大增加每幀的繪製指令的數量。

Metal API

雖然這個平臺上許多 API 都暴露為具體的類,但 Metal 提供的大多是協議。因為 Metal 物件的具體型別取決於 Metal 執行在哪個裝置上。這更鼓勵了面向介面而不是面向實現程式設計。然而,這同時也意味著,如果不使用 Objective-C 執行時的廣泛而危險的操作,就不能子類化 Metal 的類或者為其增加擴充套件,

Metal 為了速度而在安全性上做了必要的妥協。對於錯誤,蘋果的其它框架顯得更加安全和健壯,而 Metal 則完全相反。在某些時候,你會收到指向內部緩衝區的裸指標,你必須小心的同步訪問它。OpenGL 中發生錯誤時,結果通常是黑屏;然而在 Metal 中,結果可能是完全隨機的效果,例如閃屏和偶爾的崩潰。之所以有這些陷阱,是因為 Metal 框架是對 GPU 的非常輕量級抽象。

一個有趣的方面是蘋果並沒有為 Metal 實現可以在 iOS 模擬器上使用的軟體渲染。使用 Metal 框架的時候應用必須執行在真實裝置上。

基礎 Metal 程式

在這部分中,我們會介紹寫出第一個 Metal 程式所必要的部分。這個簡單的程式繪製了一個正方形的旋轉。你可以在 GitHub 中下載這篇文章的示例程式碼

雖然不能涵蓋每一個細節,但我們儘量涉及至少所有的移動部分。你可以閱讀原始碼和參閱線上資源來深入理解。

使用 UIKit 建立裝置和介面

在 Metal 中,裝置是 GPU 的抽象。它被用來建立很多其它型別的物件,例如緩衝區,紋理和函式庫。使用MTLCreateSystemDefaultDevice 函式來獲取預設裝置:

注意 device 並不是一個詳細具體的類,正如前面提到的,它是遵循 MTLDevice 協議的類。

下面的程式碼展示瞭如何建立一個 Metal layer 並將它作為 sublayer 新增到一個 UIView 的 layer:

CAMetalLayer 是 CALayer 的子類,它可以展示 Metal 幀緩衝區的內容。我們必須告訴 layer 該使用哪個 Metal 裝置 (我們剛建立的那個),並通知它所預期的畫素格式。我們選擇 8-bit-per-channel BGRA 格式,即每個畫素由藍,綠,紅和透明組成,值從 0-255。

庫和函式

你的 Metal 程式的很多功能會被用頂點和片段函式的方式書寫,也就是我們所說的著色器。Metal 著色器用 Metal 著色器語言編寫,我們將在下面詳細討論。Metal 的優點之一就是著色器函式在你的應用構建到中間語言時進行編譯,這可以節省很多應用啟動時所需的時間。

一個 Metal 庫是一組函式的集合。你的所有寫在工程內的著色器函式都將被編譯到預設庫中,這個庫可以通過裝置獲得:

接下來構建渲染管道狀態的時候將使用這個庫。

命令佇列

命令通過與 Metal 裝置相關聯的命令佇列提交給 Metal 裝置。命令佇列以執行緒安全的方式接收命令並順序執行。建立一個命令佇列:

構建管道

當我們在 Metal 程式設計中提到管道,指的是頂點資料在渲染時經歷的變化。頂點著色器和片段著色器是管道中兩個可程式設計的節點,但還有其它一定會發生的事件 (剪下,柵格化和檢視變化) 不在我們的直接控制之下。管道特性中的後者的類組成了固定功能管道。

在 Metal 中建立一個管道,我們需要指定對於每個頂點和每個畫素分別想要執行哪個頂點和片段函式 (譯者注: 片段著色器又被稱為畫素著色器)。我們還需要將幀緩衝區的畫素格式告訴管道。在本例中,該格式必須與 Metal layer 的格式匹配,因為我們想在螢幕上繪製。

從庫中通過名字來獲取函式:

接下來建立一個設定了函式和畫素格式的管道描述器:

最後,我們從描述器中建立管道狀態。這會根據程式執行的硬體環境,從中間程式碼中編譯著色器函式為優化後的程式碼。

讀取資料到緩衝區

現在已經有了一個構建好的管道,我們需要用資料填充它。在示例工程中,我們繪製了一個簡單的幾何圖形: 一個旋轉的正方形。正方形由兩個共享一條邊的直角三角形組成:

每一行的前四個數字代表了每一個頂點的 x,y,z 和 w 元素。後四個數字代表每個頂點的紅色,綠色,藍色和透明值元素。

你可能會奇怪為什麼需要四個數字來描述 3D 空間中的一個位置。第四個頂點位置元素,w,是一個數學上的便利,使我們能以一種統一的方式描述 3D 轉換 (旋轉,平移,縮放)。這個細節在本文的示例程式碼並沒有體現。

為了使用 Metal 繪製頂點資料,我們需要將它放入緩衝區。緩衝區是被 CPU 和 GPU 共享的簡單的無結構的記憶體塊:

我們將使用另一個緩衝區來儲存用來旋轉正方形的旋轉矩陣。與預先提供資料不同,這裡只是通過建立規定長度的緩衝區來建立一個空間。

動畫

為了在螢幕上旋轉正方形,我們需要把轉換頂點作為頂點著色器的一部分。這需要更新每一幀的統一緩衝區。我們運用三角學知識,從當前旋轉角度生成一個旋轉矩陣,將它複製到統一緩衝區。

Uniforms 結構體只有一個成員,該成員是一個儲存了旋轉矩陣的 4×4 的矩陣。矩陣型別 matrix_float4x4 來自於蘋果的 SIMD 庫,該庫是一個型別的集合,它們可以從 資料並行操作 中獲益:

為了將旋轉矩陣複製到統一緩衝區中,我們取得它的內容的指標並將矩陣 memcpy 進去:

準備繪製

為了在 Metal layer 上繪製,首先我們需要從 layer 中獲得一個 ‘drawable’ 物件。這個可繪製物件管理著一組適合渲染的紋理:

接下來我們建立一個渲染路徑描述器,它描述了在渲染之前和完成之後 Metal 應該執行的不同動作。下面我們展示了一個渲染路徑,它將首先把幀緩衝區清除為純白色,然後執行繪製指令,最後將結果儲存到幀緩衝區來展示:

釋出繪製指令

要放入裝置的命令佇列的命令必須被編碼到命令緩衝區裡。命令緩衝區是一個或多個命令的集合,可以以一種 GPU 瞭解的緊湊的方式執行和編碼。

為了真正編碼渲染命令,我們還需要另一個知道如何將我們的繪製指令轉換為 GPU 懂得的語言的物件。這個物件叫做命令編碼器。我們將上面建立的渲染路徑描述器作為引數傳入,就可以向命令緩衝區請求一個編碼器:

在繪製指令之前,我們使用預編譯的管道狀態設定渲染命令編碼器並建立緩衝區,該緩衝區將作為頂點著色器的引數:

為了真正的繪製幾何圖形,我們告訴 Metal 要繪製的形狀 (三角形) 和緩衝區中頂點的數量 (本例中 6 個):

最後,執行 endEncoding 通知編碼器釋出繪製指令完成。

展示幀緩衝區

現在我們的繪製指令已經被編碼並準備就緒,我們需要通知命令緩衝區應該將結果在螢幕上顯示出來。呼叫 presentDrawable,使用當前從 Metal layer 中獲得的 drawable 物件作為引數:

執行 commit 告訴緩衝區已經準備好安排並執行:

就這麼多!

Metal 著色語言

雖然 Metal 和 Swift 是在 WWDC keynote 上被一同發表的,但著色語言是基於 C++11 的,有一些有限制的特性和增加的關鍵字。

Metal 著色語言實踐

為了在著色器裡使用頂點資料,我們定義了一個對應 Objective-C 中頂點資料的結構體:

我們還需要一個類似的結構體來描述從頂點著色器傳入片段著色器的頂點型別。然而,在本例中,我們必須區分 (通過使用[[position]] 屬性) 哪一個結構體成員應該被看做是頂點位置:

頂點函式在頂點資料中每個頂點被執行一次。它接收頂點列表的一個指標,和一個包含旋轉矩陣的統一資料的引用。第三個引數是一個索引,用來告訴函式當前操作的是哪個頂點。

注意頂點函式的引數後面緊跟著標明它們用途的屬性。在緩衝區引數中,引數中的索引對應著我們在渲染命令編碼器中設定緩衝區時指定的索引。Metal 就是這樣來區分哪個引數對應哪個緩衝區。

在頂點函式中,我們用頂點的位置乘以旋轉矩陣。我們構建矩陣的方式決定了效果是圍繞中心旋轉正方形。接著我們將這個轉換過的位置傳入輸出頂點。頂點顏色則從輸入引數中直接複製。

片段函式每個畫素就會被執行一次。Metal 在 rasterization 過程中會通過在每個頂點中指定的位置和顏色引數中新增來生成引數。在這個簡單的片段函式中,我們只是簡單的返回了 Metal 新增的顏色。這會成為螢幕畫素的顏色:

為什麼不乾脆擴充套件 OPENGL?

蘋果是 OpenGL 架構審查委員會的成員,並且歷史上也在 iOS 上提供過它們自己的 GL 擴充套件。但從內部改變 OpenGL 看起來是個困難的任務,因為它有著不同的設計目標。實際上,它必須有廣泛的硬體相容性以執行在很多不同的裝置上。雖然 OpenGL 還在持續發展,但速度緩慢。

而 Metal 則本來就是隻為了蘋果的平臺而建立的。即使基於協議的 API 最初看起來不太常見,但和其它框架配合的很好。Metal 是用 Objective-C 編寫的,基於 Foundation,使用 GCD 在 CPU 和 GPU 之間保持同步。它是更先進的 GPU 管道的抽象,而 OpenGL 想達到這些的話只能完全重寫。

Mac 上的 Metal?

OS X 上支援 Metal 也是遲早的事。API 本身並不侷限於 iPhone 和 iPad 使用的 ARM 架構的處理器。Metal 的大多數優點都可以移植到先進的 GPU 上。另外,iPhone 和 iPad 的 CPU 和 GPU 是共享記憶體的,無需複製就可以交換資料。這一代的 Mac 電腦並未提供共享記憶體,但這只是時間問題。或許,API 會被調整為支援使用了專門記憶體的架構,或者 Metal 只會執行在下一代的 Mac 電腦上。

總結

本文中我們嘗試給出公正並有所幫助的關於 Metal 框架的介紹。

當然,大多數的遊戲開發者並不會直接使用 Metal。然而,頂層的遊戲引擎已經從中獲益,並且開發者無需直接使用 API 就可以從最新的技術中得到好處。另外,對於那些想要發揮硬體全部效能的開發者來說,Metal 或許可以讓他們在遊戲中建立出與眾不同而華麗的效果,或者進行更快的平行計算,從而得到競爭優勢。

資源

相關文章