Session 604 : Metal for OpenGL Developers前端
關於做者:能夠在這裏找到一些關於個人信息。c++
Metal 是 Apple 開發的一款圖形引擎。本文將對比 OpenGL,詳細介紹 Metal 的對象模型以及開發思想,旨在幫助 OpenGL 開發者更容易地轉向 Metal 開發。數組
因爲 Metal 與 OpenGL 同爲底層圖形引擎,所以閱讀本文須要必定的圖形基礎。本文假定讀者已經具有必定圖形學知識並對 OpenGL 熟悉。數據結構
對於廣大圖形開發者來講,有着很是多的工具可供選擇。多線程
對於普通的 2D、3D 圖形開發者來講,有 Apple 原生的SpriteKit
、SceneKit
等框架,而對於遊戲開發者來講,有Unity
、虛幻
等強大的第三方遊戲引擎。閉包
在條件容許的狀況下,開發者們應該儘量使用上層框架進行開發,以專一於業務,屏蔽圖形學細節。以上上層框架應是開發者們的首選。併發
但在某些特定場景如要求跨平臺、包大小限制等場景下,開發者們可能不得不使用 OpenGL 來開發。由於 OpenGL 跨平臺,性能佳,且不佔用過大的包大小等優勢,使 OpenGL 至今仍然普遍被使用。app
但有着超過25年曆史的 OpenGL 技術自己,隨着現代圖形技術的發展,遇到了一些問題:框架
隨着圖形學的發展,OpenGL 自己設計上存在的問題已經影響了 GPU 真正性能的發揮,所以 Apple 設計了 Metal。異步
爲了解決這些問題,Metal 誕生了。
它爲現代 GPU 設計,並面向 OpenGL 開發者。它擁有:
Metal 簡化了 CPU 參與渲染的步驟,儘量地讓 GPU 去控制資源。與此同時,擁有更現代的設計,使操做處於可控,結果可預測的狀態。在優化設計的同時,它仍然是一個直接訪問硬件的框架。與 OpenGL 相比,它更加接近於 GPU,以得到更好的性能。
古老的 OpenGL 已經沒法適應現代圖形技術的發展,而 Metal 爲現代圖形技術而設計,是 OpenGL 的優良替代品。
Apple 早在 2014 年就推出了 Metal,通過四年的鋪墊,於今年 WWDC 祭出了大殺器:
雖然目前 API 還可以使用,可是被標記棄用的 API 極可能會在將來的某一刻被永遠抹去。
所以,是時候開始使用 Metal 了。
在 OpenGL 中,全部資源如 Buffer,Texture 等都依附於一個上下文(Context)。
而在 Metal 中,狀況則徹底不一樣。Metal 使用一系列更小,職責更加清晰的對象去分別管理各種資源,開發者們從對象名中一眼就能認出他的職責。
Metal Device 能夠看作是 GPU 的入口,今後爲入口能夠去生成,操做更加具體的資源和對象。
由 Metal Device 能夠繼續構造出 Texture、Buffer 和 Pipeline(Pipeline 中包含了着色器程序)等資源對象。
熟悉 OpenGL 的讀者會發如今 OpenGL 中也存在這些概念。它們的用途是大同小異的,但在構造和管理等的工程設計上有很大的區別。
紋理(Texture)、緩衝區(Buffer)等資源對象將直接從 Metal Device 對象建立,建立之後,對象是不可變的,但內部的圖像數據是可變的。
渲染管線(Render Pipeline)、深度模板(Depth Stencil)等對象經過狀態描述(Descriptor)建立,對象以及內部數據都是不可變的。
因爲不可變對象的存在,使得 Metal 在只須要在建立對象時檢查一次對象便可。而 OpenGL 在每次繪製之前都須要檢查對象是否發生變化,在這一點上 Metal 將會得到更好的性能。
除此以外,因爲不可變對象的存在,在多線程中,Metal 不須要使用線程鎖,所以也會獲得更好的性能。
Metal 將渲染進一步抽象成了命令,以命令和命令隊列的形式進行管理。
上文中提到的 Metal Device,能夠生成一個命令隊列(Command Queue)。這個命令隊列由 Metal 自行維護,而開發者只須要往這個隊列裏面丟命令就能夠了。
Metal 中的命令(Command Buffer)是 GPU 任務的抽象封裝,近似於OpenGL 中的一次繪製調用(draw call)。若是讀者閱讀過 cocos2d-x 的源碼的話就會發現,cocos2d-x 的渲染系統也進行了相似的封裝,以便於進行合併、批量回執等優化操做。
Metal 中的命令分爲四種類型:
這些命令被添加到命令隊列之後,Metal 會自行按順序執行命令。
命令如何被建立呢,能夠經過命令編碼器(Command Buffer Encoder)建立。上文提到命令有四種類型,因此命令編碼器也有四種類型,分別編碼四個類型的命令。命令編碼階段徹底由 CPU 負責。
在命令編碼器中,開發者能夠具體設置命令的各項參數,並最終生成命令對象交於命令隊列。
Metal 的對象模型在設計上有許多優於 OpenGL 的地方。例如不可變對象的存在能夠簡化多線程操做以及節約對象檢查的時間,命令系統的存在使渲染系統能更好地進行優化。
除了這些,Metal 還擁有優秀的面向對象封裝。相比於 API 晦澀,處處使用數字句柄的 OpenGL,在開發和維護的效率上都有質的飛躍。
在瞭解了 Metal 的對象模型之後,就能夠開始實戰了。
以 OpenGL 程序移植爲例,來具體體驗一下 Metal 的實戰。
本節將會分構建時、初始化時和渲染時這三個階段來說。
在程序編譯構建之時,Metal 的着色器程序將會被提早編譯。
Metal 所使用的着色器語言 Metal SL 是一套基於 C++ 擴展的語言。class、namespace、enum 等 C++ 中的特性均可以應用在 Metal 的着色器中,甚至還可使用 template。相比基於C語言擴展的 OpenGL SL,可謂是質的改變。
固然,毫無疑問的,向量矩陣運算,圖形相關的類必然也是內建好的,下面來具體看看 Metal 的着色器程序該怎麼寫。
渲染時所需的頂點着色器:
vertex VertexOutput myVertexShader(uint vid [[ vertex_id ]],
device Vertex * vertices [[ buffer(0) ]],
constant Uniforms & uniforms [[ buffer(1) ]])
{
VertexOutput out;
out.clipPos = vertices[vid].modelPos * uniforms.mvp;
out.texCoord = vertices[vid].texCoord;
return out;
}
複製代碼
vertex
前綴表明這是一個頂點着色器,VertexOutput
是函數的返回值,這是一個自定義的結構體,具體結構暫且先無論。myVertexShader
爲函數名,後面跟的是參數。
這個函數有兩個參數,一個是uint
類型的參數名爲vid
,然後面跟的[[ vertex_id ]]
是參數的句柄。
這是一個新的概念,Metal 給每一個參數擴展了一個句柄,這和 OpenGL 相似。每一個參數會有個句柄,在 CPU 往 GPU 傳遞參數時須要這個對應的句柄才能夠傳過來。那麼這裏vid
參數的句柄爲vertex_id
,這是一個內建的句柄,表示繪製時頂點的索引數。
第二個參數爲類型爲Vertex
指針,Vertex
也是個自定義結構體,具體內容暫且無論。它是一個結構體指針,句柄爲[[ buffer(0) ]]
。在 Metal 中,不一樣類型的參數的句柄是分開計算的。vertices
參數的句柄爲buffer
的0
。
同理,下面是個片元着色器函數:
fragment float4 myFragmentShader(VertexOutput in [[ stage_in ]],
constant Uniforms & uniforms [[ buffer(3) ]],
texture2d<float> colorTex [[ texture(0) ]],
sampler texSampler [[ sampler(1) ]])
{
return colorTex.sample(texSampler, in.texCoord * uniforms.coordScale);
}
複製代碼
它的輸入時頂點着色器的輸出,且擁有紋理、採樣器等參數。返回值是四維浮點數組,即該片元的顏色值。
下面來看看這兩個自定義的結構:
struct Vertex {
float4 modelPos;
float2 texCoord;
};
struct VertexOutput {
float4 clipPos [[position]];
float2 texCoord;
};
複製代碼
Vertex
由 CPU 輸入,得到模型的三維座標以及貼圖的映射座標,通過頂點着色器處理以後輸出VertexOutput
,這個結構做爲片元着色器的輸入,進入片元着色器計算片元顏色。這個流程與傳統的 OpenGL 相同。
結構中包含內簡句柄position
的參數,片元着色器的輸入結構中必須包含有此句柄的參數,不然會沒法經過編譯。
有了上文中的着色器程序,使用時如何傳遞參數給着色器程序呢?
在 OpenGL 中,開發者須要首先要根據參數名得到參數句柄,而後利用句柄進行參數傳遞(高版本的 OpenGL 也支持經過 layout 寫死句柄),而使用 Metal 時,是在編碼器編碼階段經過編碼器和句柄直接進行參數傳遞。
[renderEncoder setFragmentBuffer:myUniformBuffer offset:0 atIndex:0];
[renderEncoder setFragmentTexture:myColorTexture atIndex:0];
[renderEncoder setFragmentSampler:mySampler atIndex:1];
複製代碼
函數中使用的 index 便是該參數的句柄。經過編碼器的這一系列操做,已經可以將參數正確傳遞到着色器程序中了。
SIMD 是 Apple 提供的一款方便原生程序與着色器程序共享數據結構的庫。
開發者能夠在頭文件中定義一系列結構,在原生代碼和着色器程序中經過#include
包含這個頭文件,二者就都有了這個結構的定義。
使用 SIMD 能最大程度減小因爲結構 layout 上的不一樣引起的問題。
Metal 的着色器程序在編譯時會被編譯成類型爲metallib
的文件,在這個文件中,着色器程序並無真正被編譯成二進制,而是隻通過了編譯器前端的中間態。在運行時會被真正編譯成二進制,這一步僅須要徹底編譯的時間的一半。
固然,Metal 也支持在運行時編譯着色器源碼,但 Apple 並不支持這麼作。這樣許多問題沒法在編譯時定位,不方便開發與維護。
Metal 使用了比 OpenGL 更爲高級的着色器語言,並在編譯時編譯着色器代碼以快速暴露錯誤,以及 SIMD 等工具庫能夠幫助開發者們更快速地開發與維護圖形代碼。
在 Metal 初始化時,須要根據上文提到的對象模型,構造一系列對象。
Device 象徵着一個 GPU,全部紋理、緩衝區等都基於這個對象產生。
id<MTLDevice> device = MTLCreateSystemDefaultDevice();
複製代碼
在 iOS 中,只有一個 GPU,所以只會有一個MTLDevice
對象,在 macOS 中,多塊顯卡就會帶來多 GPU,即多個MTLDevice
對象。
id<MTLCommandQueue> commandQueue = [device newComandQueue];
複製代碼
建立 Texture 時,將使用一個 TextureDescriptor 對象。它包含了一系列 Texture 所須要的屬性,並使用這個 Descriptor,從 Device 建立一個 Texture Object 對象。Texture Object 會管理一塊內存,真正存放紋理的數據。
對於真正存放紋理數據的內存,開發者選擇存儲模式以控制 CPU 和 GPU 如何管理這片內存:
Apple 推薦在 iOS 中使用 shared mode,而在 macOS 中使用 managed mode。
在瞭解了內存管理方法後,建立一個 Texture 實現以下:
MTLTextureDescriptor *textureDescriptor = [MTLTextureDescriptor new];
textureDescriptor.pixelFormat = MTLPixelFormatBGRA8Unorm;
textureDescriptor.width = 512;
textureDescriptor.height = 512;
textureDescriptor.storageMode = MTLStorageModeShared;
id<MTLTexture> texture = [device newTextureWithDescriptor:textureDescriptor];
複製代碼
填充圖像數據:
NSUInteger bytesPerRow = 4 * image.width;
MTLRegion region =
{
{0,0,0}, //Origin
{ 512, 512, 1 } // Size
};
[texture replaceRegion:region
mipmapLevel:0
withBytes:imageData
bytesPerRow:bytesPerRow];
複製代碼
與 OpenGL 不一樣的是,Metal 中的 Texture:
Metal 還提供了 MetalKit 來快速建立 Texture。
Metal 中,全部無結構的數據都使用 Buffer 來管理。與 OpenGL 相似的,頂點、索引等數據都經過 Buffer 管理。
因爲數據是無結構的,所以如何管理由開發者本身制定。如如下方法能夠利用編譯器來計算數據偏移來管理數據:
id<MTLBuffer> buffer = [device newBufferWithLength:bufferDataByteSize
options:MTLResourceStorageModeShared];
struct MyUniforms *uniforms = (struct MyUniforms*) buffer.contents;
uniforms->modelViewProjection = modelViewProjection;
uniforms->sunPosition = sunPosition;
複製代碼
這種方式下,開發者須要考慮內存對其因素。float3
、int3
、uint3
等結構佔用的內存空間並不是12字節,而是16字節。若是確實須要這樣打包數據,須要使用packed_float3
等這類數據結構。
Pipeline 一樣須要經過一個 Descriptor 來建立。如下是建立一個渲染管線須要的參數:
在制定了着色器函數,各種渲染狀態之後,就可使用這個 Descriptor,經過 Device 建立一個 Pipeline 對象。如下是建立渲染管線的實現:
id<MTLLibrary> defaultLibrary = [device newDefaultLibrary];
id<MTLFunction> vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"];
id<MTLFunction> fragmentFunction = [defaultLibrary newFunctionWithName:@"fragmentShader"];
MTLRenderPipelineDescriptor *pipelineStateDescriptor = [MTLRenderPipelineDescriptor new];
pipelineStateDescriptor.vertexFunction = vertexFunction;
pipelineStateDescriptor.fragmentFunction = fragmentFunction;
pipelineStateDescriptor.colorAttachments[0].pixelFormat = MTLPixelFormatRGBA8Unorm;
id<MTLRenderPipelineState> pipelineState;
pipelineState = [device newRenderPipelineStateWithDescriptor:pipelineStateDescriptor
error:nil];
複製代碼
在 OpenGL 中,一個 Shader Program 對象只包含頂點着色器和片元着色器,而在 Metal 中,包含了以上描述提到的全部屬性。所以在構造一個渲染管線之前,必須肯定所有這些參數之後纔可以建立。
以上資源對象都須要付出昂貴的開銷。
Pipeline 建立須要後臺編譯,Texture 和 Buffer 須要分配內存。所以這些操做應儘量在初始化時一次性操做。
初始化結束之後,程序將會進入主題 —— 渲染循環。
在 Metal 中,命令系統對渲染循環進行了封裝。開發者們只要在一個渲染循環內將要作的事編碼成命令後丟入命令隊列便可。
上文中已經介紹了 Metal 中的四種命令,它們都派生自同一個父類,它們的使用方法是同樣的。一個完整的命令執行閉環是這樣的:
阻塞式的完整的閉環實現以下:
id<MTLCommandBuffer> commandBuffer = [commandQueue commandBuffer];
// 編碼命令...
[commandBuffer commit];
[commandBuffer waitUntilCompleted];
複製代碼
非阻塞式的閉環實現以下:
id<MTLCommandBuffer> commandBuffer = [commandQueue commandBuffer];
// 編碼命令...
commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> commandBuffer) {
// 回調 CPU...
}
[commandBuffer commit];
複製代碼
固然,非阻塞的使用方法更值得推薦。如下是一種推薦使用的資源三重緩衝的模式,利用回調來使 CPU 和 GPU 更高效地配合。
建立三幀的資源緩衝區來造成一個緩衝池。CPU 將每一幀的數據按順序寫入緩衝區供 GPU 使用。
當 GPU 觸發回調時,CPU 將釋放該幀的緩衝區,並於下一幀使用。
以此來減小 GPU 和 CPU 互相等待的環節,提升性能。三重緩衝的實現以下:
首先構造緩衝區以及信號量:
id <MTLBuffer> myUniformBuffers[3];
dispatch_semaphore_t frameBoundarySemaphore = dispatch_semaphore_create(3);
NSUInteger currentUniformIndex = 0;
複製代碼
在渲染循環中經過信號量來實現三重緩衝的循環。
dispatch_semaphore_wait(frameBoundarySemaphore, DISPATCH_TIME_FOREVER);
currentUniformIndex = (currentUniformIndex + 1) % 3;
[self updateUniformResource: myUniformBuffers[currentUniformIndex]];
[commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> commandBuffer) {
dispatch_semaphore_signal(frameBoundarySemaphore);
}];
[commandBuffer commit];
複製代碼
以上就是 Apple 推薦的資源更新方式。
在有了資源的更新方式之後,就要進行渲染了。
渲染命令和渲染管線的狀態息息相關,所以在建立渲染命令之前須要知道渲染管線的狀態。開發者們能夠經過一個狀態描述對象來描述渲染管線的狀態。
由渲染管線狀態得到渲染命令編碼器的實現以下:
MTLRenderPassDescriptor * desc = [MTLRenderPassDescriptor new];
desc.colorAttachment[0].texture = myColorTexture;
desc.depthAttachment.texture = myDepthTexture;
id <MTLRenderCommandEncoder> encoder = [commandBuffer renderCommandEncoderWithDescriptor: desc];
複製代碼
GPU 渲染圖像的步驟大體能夠分爲:加載、渲染、存儲。開發者能夠指定這三個步驟具體作什麼事。
通過這個步驟會獲得最終的圖像。注意途中的深度緩衝區在存儲步驟時候被標記爲了 Don't care,結果會被拋棄(discard),不會被存儲。
是否須要拋棄隨圖像渲染的用途而定,若是是用於顯示的圖像,那麼深度信息已經沒有用了,沒有必要被存儲。而若是是用於其餘表面貼圖或是用於後處理,深度信息可能仍然有用,須要存儲下來。
存儲的步驟是相對昂貴的,由於顯存帶寬是很是寶貴的資源,所以應該儘量拋棄沒必要要的數據。
如何指定這三個步驟的行爲呢?
MTLRenderPassDescriptor * desc = [MTLRenderPassDescriptor new];
desc.colorAttachment[0].texture = myColorTexture;
// 指定三個步驟的行爲
desc.colorAttachment[0].loadAction = MTLLoadActionClear;
desc.colorAttachment[0].clearColor = MTLClearColorMake(0.39f, 0.34f, 0.53f, 1.0f);
desc.colorAttachment[0].storeAction = MTLStoreActionStore;
id <MTLRenderCommandEncoder> encoder = [commandBuffer renderCommandEncoderWithDescriptor: desc];
複製代碼
在通過一系列繪製命令之後,圖像已經被離屏繪製到了一個 Texture 上。那麼如何把圖像最終顯示在屏幕上呢?
關於顯示的容器,Apple 爲開發者提供了MTKView
,這是一個來自MetalKit
的視圖。這個視圖包含了一個 drawable 對象。對於CoreAnimation
的開發者來講 drawable 這個概念應該不會陌生。
有了這個視圖,就能夠用於顯示 Texture 上的圖像了。
MTLRenderPassDescriptor* renderPassDescriptor = view.currentRenderPassDescriptor;
id <MTLRenderCommandEncoder> renderCommandEncoder =
[commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
// 編碼渲染命令...
[renderCommandEncoder endEncoding];
[commandBuffer presentDrawable:view.currentDrawable];
[commandBuffer commit];
複製代碼
最終使用 Command Buffer 的presentDrawable
方法便可。
當這個命令被 GPU 執行完成之後,就能看到圖像被顯示在屏幕上了。
本節從 Metal 渲染圖像的構建時、初始化時以及渲染時三個步驟詳細描述了 Metal 渲染圖像的流程。通過本節,相信大多數讀者已經清楚 Metal 渲染圖像的流程了。
爲了更清晰地與 OpenGL 的渲染流程做對比,如下是 OpenGL 和 Metal 渲染圖像的流程對比:
雖然 Metal 和 OpenGL 的職能是同樣的,但並不意味着程序中只能有 Metal 或 OpenGL 一方。
Metal 和 OpenGL 在程序中能夠被混合使用。IOSurface
和CVPixelBuffer
等數據結構能夠提供數據交換支撐。
這意味着開發者們若是想要移植 OpenGL 程序到 Metal,並不須要一口氣所有移植過去,由於它們支持混編。
關於混編,開發者們能夠參考這裏的代碼。
因爲 Metal 針對 CPU 多線程進行了設計,所以能夠儘量發揮多線程的做用,利用多線程進行命令編碼。
Apple 已經爲開發者作好了多線程編碼的準備工做,開發者可使用MTLParallelRenderCommandEncoder
編碼器來進行多線程並行編碼。
Metal 原生支持 GPU 計算。
這意味着許多能夠高併發的任務能夠經過 Metal 的計算命令交給 GPU 執行了。
在粒子系統,物理模擬等規模比較大的計算任務上,GPU 能夠本身計算,本身渲染,在必定程度上解放了 CPU。
關於調試工具,Apple 也已經爲開發者們準備好了。
GPU 調試器,可用於單步調試:
着色器調試器,可像調試普通函數同樣調試着色器函數:
着色器性能調試器:
渲染管線調試器:
Metal 追蹤調試器,可用於調試 Metal 完整行爲:
Metal 解決了不少 OpenGL 設計自己存在的問題。它是一款真正爲現代設備而設計的圖形引擎。它的對象模型,多線程支持通過了精心設計以知足現代開發的須要。通過四年的發展和沉澱,Metal 自己以及配套工具已經日趨成熟。
隨着今年 WWDC 蘋果宣佈 OpenGL 和 OpenCL 被棄用,宣佈着 Metal 的時代即將到來。那麼,是時候開始使用 Metal 了。
查看更多 WWDC 18 相關文章請前往 老司機x知識小集xSwiftGG WWDC 18 專題目錄