Skip to main content

三年的 Metal 旅程

7 月

21, 2020

by coberst


技術

我們在三年前把成像器換成了 Metal。 轉換過程非常地順利,Metal 在 iOS 的效能也很棒。 我們想花一點時間分享我們做這個決定的過程,還有執行之後的成果。(先在這裡講明,成果很棒喔!) 當初的反思大部分都還成立,不過 Metal 現在的表現越來越好了,我們想在轉換成 Metal 滿三年的這個時間重新和大家分享一下。

那就讓我們搭上時光機回到 2016 年 12 月,我們剛剛在 iOS 上推出早期的 Metal 成像器版本的時候吧。

為什麼要用 Metal?

Apple 在 2014 年的全球開發者大會(WWDC)宣布 Metal 的時候,我第一個反應是完全不理它。 Metal 只能在尚未普及的最新硬體上使用,雖然 Apple 提到它可以解決 CPU 效能方面的問題,但是只幫到市場佔有率極小的使用者族群的話,效能最高和效能最低的裝置之間的差距將會拉得更大。 我們當時只有在 Apple 裝置上使用 OpenGL ES2,才剛開始在 Android 裝置上推出。

以下是兩年半過後 Metal 在我們使用者族群的市場佔有率:

這個比例讓 Metal 跟以前相比更加吸引人。 Metal 的確還是對最老舊的裝置沒有幫助,但是因為 GL 在 iOS 的市場不斷在縮小,和我們會為老舊與較新的裝置常常會給予不同內容,我們覺得花一點資源優化 Metal 是合理的。 因為您的 iOS Metal 程式碼可以在微幅調整之下在 Mac 上執行,您就算把重心放在行動裝置上也是可以在 Mac 上使用 Metal。(我們目前只在 iOS 上推出 Metal。)

我覺得市場佔有率還可以更深入分析一下。 在 iOS 上,我們為 iOS 8.3 以上版本的裝置支援 Metal。雖然有一些使用者因為作業系統版本的限制沒有辦法執行 Metal,還在使用 GL 的 25% 使用者之中大部分只是在使用較舊的 SGX 硬體裝置。 這些裝置也沒有 OpenGL ES 3 功能,我們可以為它們走一個較低端的成像路線。(儘管如此,我們還是希望所有裝置都能支援 Metal,幸好 GL 和 Metal 的分歧只會持續改善。) Metal API 對 Mac 而言是比較新的,有一大部分是因為作業系統;只有 OSX 10.11 以上版本的裝置才可以使用 Metal,而我們有一半的使用者在使用較舊的作業系統版本。問題不是出在硬體上,而是軟體上。(我們 95% 的 Mac 使用者在使用 OpenGL 3.2 以上版本。)

綜觀市場佔有率,我們除了換成 Metal 還有其他的途徑。 有一個方法是用 MoltenGL,這樣可以繼續使用我們現有的 OpenGL 程式碼,不過效能應該會更好。還有一個是換到 Vulkan(讓 PC 和未來 Android 的效能更好),然後使用 MoltenVK。 我稍微試了一夏 MoltenGL 對結果不太滿意,我花了一點功夫才能讓現有的程式碼成功執行,而效能與標準 OpenGL 之間的差距沒有我想像中地那麼大。 講到 MoltenVK,我覺得要把一個低階 API 放在另一個低階 API 的上層是不好的行為。這種行為會造成阻抗不匹配,讓您無法達成效能最佳化。這樣做或許還是比使用之前的高階 API 好,但是一開始選擇低階 API 的原因就是要讓效能最佳化,而這樣做會難以達成此目標。 另外很重要的一點是套用 Metal 比套用 Vulkan 簡單很多,這個我之後會解釋。不過因為這樣,與 Vulkan -> Metal 的包裝庫相比,我會更傾向 Metal -> Vulkan 的包裝庫。

還有值得注意的是新款 iPhone 的 iOS 10 似乎沒有 GL 驅動程式,因為 GL 會直接套用在 Metal 之上。 意思就是使用 OpenGL 只能為您省去些微的開發工作,畢竟 OpenGL「寫完一次程式,在哪裡都可以執行」的初衷沒有應用在行動裝置上面。

轉換

轉換到 Metal 的過程整體而言還算滿輕鬆的。 我們對許多圖像 API 都累積了滿多經驗,包括 Direct 3D 9、11 等高階 API 與 PS4 GNM 等低階 API。 這讓我們在使用像 Metal 這樣的 API 這方面有獨特的優勢。Metal 擁有一些高階 API 的性質,同時還會留一些像 CPU-GPU 同步的工作給 App 開發人員處理。

唯一比較麻煩的地方是著色器的編譯。一旦這個階段結束,到了要編寫程式碼的時候,我們很快就發現這個 API 非常淺顯易懂,程式碼很自然地就生出來了。 我在一天花了大概十個小時弄出了一個可以簡略讓大部分東西成像的版本,之後再花了兩個禮拜進行程式碼整理、驗證問題處理、效能分析、效能最佳化等工作。 我們可以在那麼短的時間內完整套用這個 API,說明了這個 API 與操作工具的品質都非常高。 我認為最後的成功可以歸功於這幾個因素:

  • 您可以循序漸進地編寫程式碼,並在每一個階段都測試效果。 我們為了不讓最初的程式碼碰到問題而忽略了所有 CPU-GPU 同步、把某些狀態設置做得非常粗糙、使用了內建的引用跟蹤及避免同時運行 CPU 和 GPU。我們在最佳化階段的時候再把程式碼修到可以推出的地步,程式碼在整個過程中都保有了成像的功能。
  • 工具在您的手上,而且是非常有用的工具。 這對熟悉 Direct3D 11 的人可能沒什麼,不過這是我第一次在行動裝飾上使用乎相配合的 CPU 效能分析器、GPU 效能分析器、GPU 除錯器及 GPU API 驗證層。大部分的問題都在開發階段中都處理了,也讓程式碼達到最佳化的效果。
  • API 的階層某種程度上比 Direct3D 11 低,而且成像層的設定與同步等一些關鍵的低階決策還是留給了開發人員處理。但是它還是使用了傳統的資源模型(每一項資源都有附帶不需要流程屏障或配置轉換的使用標幟)與綁定模型(每一個成像器階段都有可以自由配置資源的欄位)。 這兩個模型都是大家非常熟悉與容易理解的,編寫小量的程式碼就可以迅速使其開始運作。

另外很好的一點是我們的 API 介面與類似 Metal 的 API 的相容性不錯。我們的 API 介面非常精實,不過也同時照顧到成像層等細節,我們可以很容易地寫出一個可運行的版本。 我在套用過程完全不需要儲存與還原狀態,也不用做資源生命週期與同步的困難決策。儲存與還原狀態的問題是許多 API 介面會遭遇的問題,主要是因為它們會把成像目標設置視為狀態變更,還有資源與狀態綁定不會受到狀態變更而影響。 成像方面唯一比較「麻煩」的一段程式碼是透過壓縮建立成像流程狀態的位元來建立成像流程狀態的程式碼。我們的 API 抽象層不包括流程狀態物件。 儘管如此,要搞定這個也不是什麼難事。 我會在另外一篇文章裡進一步介紹我們的 API 介面。

那麼,我們花了一個禮拜編譯著色器,兩個禮拜弄好改良過的版本1,最後的結果如何? 結果非常好,Metal 的效能完全名副其實。 首先,單執行緒的調度效能明顯地高於 OpenGL(根據承載量,成像畫格的繪圖與調度步驟的時間縮短了兩到三倍)。我們的 OpenGL 的特性是可以減少多於狀態的設置和透過快速的路徑減輕驅動程式的負荷,本身可以說是非常好了。 另外,在 Metal 運用多執行緒是輕而易舉的,只要您的成像程式碼可以支援就夠了。 我們還沒把繪畫與調度換到執行緒,不過已經在轉換透過成像執行緒準備資源的零件。這項工作與 OpenGL 相比完全不費吹灰之力。

除此之外,Metal 所附帶的工具容易操作又可靠,可以讓我們多處理一些效能方面的問題。 我們的成像程式碼很關鍵的環節是使用 CPU 運算世界空間的照明資料並上玩到 3D 紋理區域的系統,這項工作必須在 OpenGL ES 2 硬體上模擬。 這些更新是局部性的,我們沒有辦法將紋理完全複製,只能依賴驅動程式運用 glTexSubImage3D 的方式。 我們曾經嘗試利用 PBO 增強更新效能,但是最後在 Android 和 iOS 上都面臨了大幅的穩定性問題。 Metal 有兩個內建的區域上傳方式。第一個是 GPU 尚未讀取紋理時使用的 MTLTexture.replaceRegion,另一個是可以非同步上傳區域,但還是可以讓 GPU 準時開始使用紋理的 MTLBlitCommandEncoder(copyFromBufferToTexture 或 copyFromTextureToTexture)。

這兩個方式都沒有達到我期望的速度。第一個方式其實不太能使用,因為我們必須要支援有效的局部更新,還有它以一個看起來非常慢的位址轉換技術完全透過 CPU 運行。 第二個方式成功了,不過似乎是用一系列 2D 圖像轉換來填滿 3D 紋理,這樣不僅要花許多資源在 CPU 端設置指令,也非常吃 GPU 的承載量。 現在用 OpenGL 的話我們就不用玩了,這兩個方式的效能所需的資源跟直接為 OpenGL 做一次類似的更新是差不多的。 幸好 Metal 有容易使用的運算著色器,我們透過一個非常簡單的運算著色器可以完成緩衝器 ->> 3D 紋理的上傳。這個操作在 CPU 和 GPU 都可以快速地運行,基本上完全解決了這部分程式碼的效能問題2

最後大略做個總結,維護 Metal 程式碼可以說完全不費力。跟我們其他支援的 API 相比,目前所有需要加入的功能在 Metal 上都更好加入,而我認為這個趨勢將會繼續下去。 當時有一些對於新增一個 API 就必須要持續維護的疑慮,不過與 OpenGL 相比,Metal 的維護並沒有佔用很多資源。而且,因為我們不再需要在 iOS 上支援 OpenGL ES 3,我們甚至可以簡化部分的 OpenGL 程式碼。

穩定性

Metal 目前在 iOS 上非常穩定。 我不知道它在 2014 年推出時的表現,也不知道它目前在 Mac 上的表現,但是 iOS 上的驅動程式和工具都非常令人滿意。

我們在 iOS 10 上遇到了一個與載入使用 Xcode 7 編譯的著色器相關的驅動程式問題,最後透過切換到 Xcode 8 解決了。我們另外在 iOS 9 上有了一次驅動程式崩潰,後來發現是 nextDrawable API 的不當運用而導致的。 除了這兩個狀況以外,我們沒有遇到其他的行為異常或崩潰,Metal 作為一個非常新的 API,各方面的表現都非常出色。

除此之外,我們在 Metal 也可以使用一系列多樣化的工具,其中包括:

  • 非常晚整的驗證層,可以偵測到使用 API 時的常見問題。 這基本上就像 Direct3D 的除錯功能;這對 Direct3D 的使用者應該很熟悉,但是在 OpenGL 卻是未知的領域。(ARB_debug_callback 理論上可以解決這個問題,但是實際上在大部分情況沒辦法使用,可以使用的時候幫助也不大。)
  • 顯示所有調度的指令,連帶其狀態、成像目標內容、紋理內容等資訊的 GPU 除錯器。我沒有需要用過著色器除錯器,不知道到底有沒有這個東西,另外緩衝器偵測可以不用那麼複雜。但是整體而言,這個除錯器已經很不錯了。
  • 顯示個別工作的效能數據(時間、頻寬)與個別著色器執行時間的 GPU 效能分析器。 因為 GPU 是圖塊成像的處理器,您應該沒辦法獲得個別繪畫呼叫的時間點。 當我們考慮到 iOs 上的圖像 API 完全沒有 GPU 時間點的資訊的時候,能擁有這些資訊就特別顯得珍貴。
  • 顯示 CPU 與 GPU 成像負荷量排程的 CPU/GPU 時間軸追蹤器(Metal System Trace)。它與 GPUView 相似,雖然某些地方的 UI 有一點奇怪,不過操作上還是比 GPUView 簡單。
  • 可以驗證著色器程式碼和偶爾給您有用的提示的線下著色器編譯器。另外,此編譯器也可以將著色器轉換成能快速載入和效能優良的二進位大型物件。因為驅動程式編譯器可以更快運行,載入時間也會隨之縮短。

如過您熟悉 Direct3D 或遊戲機,您可能會覺得上面這些工具都是本來就應該有的。不過相信我,它們在 OpenGL 的世界裡都是非常稀有的,每一個都會令人感到興奮。在行動裝置上有一大堆問題都是司空見慣,像是偶爾出問題的驅動程式,沒有驗證功能、沒有 GPU 除錯器、沒有有用的 GPU 效能分析器、沒辦法收集 GPU 排程資料及被迫使用文字為主的著色器語言(而每一個供應商的語法分析器都有些微的差異)。

Metal 在編寫程式碼和推出程式兩個方面都是很好的 API。 它操作簡單、效能容易評估、有強大的驅動程式及有完善的工具。 Metal 在可移植性以外的項目都完勝 OpenGL。 就算我們要看可移植性,OpenGL 在現實上應該只會在三個平台上使用(iOS、Android、Mac),而其中兩個平台現在也已經支援 Metal 了。另外,在一個平台上編寫 OpenGL 的程式碼會因為各種原因常常沒辦法在另一個平台上使用,所以 OpenGL 所謂的可移植性也只是僅供參考而已。

如果您正在使用 Unity、UE4 等第三方引擎,它們現在已經支援 Metal 了。如果喜歡圖像程式編寫或注重效能的您尚未使用這些引擎,並對 iOS 或 Mac 有一番研究,我強烈推薦您嘗試 Metal 看看。 它一定不會讓您失望的。

Metal 的現狀

就我們的觀點而言,Metal 這三年來最大的改變是它受到了大規模的採用。

三年前,有四分之一的裝置必須使用 OpenGL。 我們現在只有 2% 的使用者在使用 OpenGL,也就是說我們的 OpenGL 後端已經不怎麼重要了。 我們目前還在繼續維護 OpenGL,不過這項工作將會在近期內終止。

現在的驅動程式也比以前更好了,我們基本上不會在 iOS 上看到驅動程式的問題。有問題的時候也通常會在早期原型上出現,這些問題在成品推出前都會修好。

我們也花了一點時間改良我們的 Metal 後端,改良工作注重以下三點:

重製著色器編譯工具鏈

還有一件這三年來的事情是 Vulkan 的推出和開發。 雖然兩個 API 看起來完全不一樣(的確是如此), Vulkan 的生態系統給了成像界人士一套非常棒的開源工具,合起來就可以很容易地進行成品品質的編譯工作。

我們利用函式庫製作了一個編譯工具鏈,它可以利用運算著色器等 DX11 功能接收 HLSL 源代碼,編譯成 SPIRV,將該 SPIRV 最佳化,最後再把最佳化後的 SPIRV 轉換成 MSL(Metal Shading Language)。 這個工具鏈取代了我們我們上一個工具鏈,那個工作鏈只能接收 DX9 HLSL 源代碼,遇到比較複雜的著色器也有一些準確度的問題。

我們能走到這個地步很諷刺地與 Apple 完全無關。 我們想特別感謝 glslang(https://github.com/KhronosGroup/glslang)、spirv-opt(https://github.com/KhronosGroup/SPIRV-Tools)及 SPIRV-Cross (https://github.com/KhronosGroup/SPIRV-Cross)的貢獻人員與維護人員。 我們為函式庫貢獻了一套更新,讓我們可以推出新的工具鏈,並利用它將我們的著色器重新導向到 Vulkan、Metal 及 OpenGL 的 API。

支援 MacOS

移植到 macOS 一直以來都有可能發生,不過直到我們遺漏一些功能以前,我們都沒有太重視這一塊。 我們於是決定了要投入一些資源讓 Metal 可以在 macOS 上運作,讓我們可以獲得更快的成像速度和解鎖未來的一些可能性。

從推出的角度來看,這沒有什麼困難。 大部分 API 是完全一樣的,除了視窗管理以外,唯一需要大幅調整的地方是記憶體配給。 行動裝置上有緩衝器和紋理的共用記憶體空間,而 API 在電腦上則佔據一個擁有自己的影像記憶空間的專屬 GPU。

如果我們使用經過管理的資源,Metal 的執行環境將會幫您複製資料,可以快速解決這個問題。 這就是我們推出第一個版本的方法,不過我們後來為了減輕系統記憶體的負荷量而讓系統更明確地使用 Scratch 緩衝器複製資源資料。

macOS 與 iOS 最大的差別就在於穩定性。 我們在 iOS 上只需要在一個架構上支援一個驅動程式供應商,但在 MacOS 上卻是三個供應商(Intel、AMD、NVidia)都需要支援。 另外,我們在 iOS 上(幸運地!) 跳過了 iOS 8,也就是 Metal 「第一個」支援的 iOS 版本。在 macOS 上這樣做並不實際,因為我們當時會用到 Metal 的使用者太少了。 因為這些因素,我們在 macOS 的 API 裡相對無害和冷門的地方遇到了許多驅動程式問題。

我們仍然支援 macOS Metal 10.11 以上的版本,不過對於一些擁有難以處理的著色器編譯器問題的版本,我們開始停止了支援並切換到舊版 OpenGL 後端。以 10.11 版本為例,我們仍然支援 macOS Metal 10.11 以上的版本,不過對於一些擁有難以處理的著色器編譯器問題的版本,我們開始停止了支援並切換到舊版 OpenGL 後端。以 10.11 版本為例,Metal 最低的版本需求是 10.11.6。

效能方面的效益與我們的期望差不多。以市場占有率而言,我們的 macOS 使用者之中有大概 25% 在使用 OpenGL,大概 75% 在使用 Metal,這些數字看起來還不錯。 這也代表我們在未來可能會完全停止支援電腦版的 OpenGL,畢竟我們其他支援的平台都沒有在使用它。如果這樣做的話,我們就可以投入更多資源在那些更容易維護和達到良好效能的 API。

改良效能和記憶體消耗量

我們對於採用的圖像 API 功能一直以來都很保守,而 Metal 也不例外。 Metal 這幾年來為一些功能做了重大的更新,像是擁有明確堆而更好分配資源的 API、Metal 2 的圖塊著色器、引數緩衝器、GPU 端指令生成等等。

我們比較新的功能大部分都不會用到。 效能以目前來看還不錯,而我們想把重心放在那些所有裝置都適用的優化工作。圖塊著色器就是一個我們會忽略的項目,因為我們必須在成像器各個方面為它做非常特殊的支援,以及它只能在較新的硬體上使用。

儘管如此,我們花了一些時間微調後端某些部分,直接讓它運作得更快。我們進行的工作包括順暢地使用完全非同步的紋理上傳來減少遊戲載入時的圖像延遲,、在 macOS 上進行上述的記憶體最佳化、透過減少未命中快取等方式在後台各處將 CPU 調度最佳化等等。另外還有在允許的情況下使用不佔系統記憶體的紋理儲存,大幅降低我們新的陰影系統所需的記憶體空間;這也是我們少部分支援的新功能之一。

未來

我們沒有花太多時間在 Metal 優化上面,這整體來說是一件好事。我們在三年前編寫的程式碼大部分都可以快速而穩定的運行,這是一個成熟 API 的一大象徵。 就所花的時間還有我們與使用者所獲得的效益而言,移植到 Metal 是一項非常好的投資。

我們常常會針對不同 API 之間所需要的工作量重新做出平衡。我們為了未來的成像工作很有可能需要深入 Metal API 更新的地方。如果我們決定這樣做,我們一定會再寫一篇文章的。


  1. 嗯,還有大概一個禮拜修復測試過程中發現的問題
  2. 這些數據從 A10 上每個畫格更新 128 KB 的資料而得(2 個 32x16x32 RGBA8 區域)

Roblox Corporation 與此部落格不為任何公司或服務宣傳或背書。 另外,我們不保證此部落格裡的資訊的準確度、可靠度及完整度。

此部落格文章原本在 Roblox 科技部落格發布。