Metal基礎實踐
簡介
Metal 提供了和 GPU 的底層互動,讓開發者可以使自己的iOS、macOS、tvOS應用表現出最佳的圖形和運算處理效能。該文章從蘋果官方提供的機組 demo 入手,介紹 MetalKit 和 Metal Shading Language 的使用,並使用它們進行圖形渲染和科學計算。
MetalKit使用的基本步驟
第一個最重要的類是 MTKView ,這是一個包裹了 UIView 或者 NSView 的物件,具備 Metal-spcific 的核心動畫功能,渲染的內容在 MTKView 上進行顯示。MTKView 比較重要的屬性是 preferredFramesPerSecond 、device 和 delegate 。
preferredFramesPerSecond 毋庸置疑是用來設定幀率的,這個幀率不是絕對的,會受限與裝置的最大幀率和最小幀率,當這個值大於最大幀率,則選擇最大幀率;小於最小幀率,則選擇最小幀率。其預設值為 60 。
device 用來獲取 Metal 與 GPU 互動的一系列物件,預設值是 nil ,需要使用
MTLCreateSystemDefaultDevice() 來主動獲取。
delegate 具備了 MTKViewDelegate ,其提供 mtkView:drawableSizeWillChange: 和 drawInMTKView: 回撥。每重新整理一幀 drawInMTKView: 回撥會被呼叫一次,在 drawInMTKView: 裡面可以進行繪製或者計算相關的工作;mtkView:drawableSizeWillChange: 回撥是在 MTKView 的 frame 發生改變的時候回撥,可以手機發生螢幕旋轉或者其他需要調整檢視的操作時,調整繪製區域。
第二類是 Metal 與 GPU 互動相關的物件
如圖是一個基本繪製的流程:

繪製流程
我們需要一個具備 MTLCommandQueue 協議的物件,其負責在每一幀裡生產一系列具備 MTLCommandBuffer 協議的物件,Metal 與 GPU 的互動都會被寫入到這些具備 MTLCommandBuffer 協議的物件裡面,而這個寫入的過程需要通過一個具備 MTLXXXCommandEncoder(MTLRenderCommandEncoder、MTLComputeCommandEncoder 或者其他的Encoder)協議的物件。
以下是一個繪製的基本結構:
- (void)prepare { _device = MTLCreateSystemDefaultDevice(); _commandQueue = [_device newCommandQueue]; } - (void)drawInMTKView:(nonnull MTKView *)view { // 設定背景顏色 Color color = [self makeFancyColor]; view.clearColor = MTLClearColorMake(color.red, color.green, color.blue, color.alpha); id<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer]; commandBuffer.label = @"MyCommand"; MTLRenderPassDescriptor *renderPassDescriptor = view.currentRenderPassDescriptor; if(renderPassDescriptor != nil) { id<MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor]; renderEncoder.label = @"MyRenderEncoder"; /* 這裡寫入繪製相關 */ [renderEncoder endEncoding]; [commandBuffer presentDrawable:view.currentDrawable]; } // Finalize rendering here and submit the command buffer to the GPU [commandBuffer commit]; }
平行計算的 MTLComputeCommandEncoder 物件不需要藉助 MTLRenderPassDescriptor 來建立,其結構為:
- (void)prepare { _device = MTLCreateSystemDefaultDevice(); _commandQueue = [_device newCommandQueue]; } - (void)drawInMTKView:(nonnull MTKView *)view { id<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer]; commandBuffer.label = @"MyCommand"; id<MTLComputeCommandEncoder> computeEncoder = [commandBuffer computeCommandEncoder]; /* 平行計算相關 */ [computeEncoder endEncoding]; /* 有其他的 Encoder 可以繼續疊加 */ // Finalize rendering here and submit the command buffer to the GPU [commandBuffer commit]; }
繪製一個三角形
繪製管線是 GPU 處理影象渲染的一步步流程,如圖所示:

繪製管線
Metal 的繪製管線包含了 Vertex function 、Rasterization、Fragment function 三個階段,Vertex function 階段接受頂點資料(這裡的頂點資料包括了頂點的位置和顏色資訊),負責將頂點資料繪製到一個 2D 的可視區域;Rasterization 接受從 Vertex function 傳過來的頂點資料,決定哪些資料是要繪製在什麼地方的;Fragment function 將顏色值賦值到畫素(後期如果有紋理,也在這裡操作),最後輸出繪製後的影象。建立一個繪製管線的步驟如下:
MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init]; pipelineStateDescriptor.label = @"Simple Pipeline"; pipelineStateDescriptor.vertexFunction = vertexFunction; // 設定 Vertex function pipelineStateDescriptor.fragmentFunction = fragmentFunction; // 設定 Fragment function pipelineStateDescriptor.colorAttachments[0].pixelFormat = mtkView.colorPixelFormat; // 畫素顏色格式 // 使用的時候將 _pipelineState 賦值給 Encoder _pipelineState = [_device newRenderPipelineStateWithDescriptor:pipelineStateDescriptor error:&error];
其中 Vertex function 和 Fragment function 分別在頂點著色器和片段著色器中實現,需要使用 Metal Shading Language 來編寫,Rasterization 階段不提供程式設計化的介面。
Metal Shading Language 的語法和 C++ 14 很像,區別在於 C++ 14 是 CPU 上執行的語言,Metal Shading Language 執行在 GPU 上,GPU 提供更大並行處理能力,對有大量資料需要處理的 Vertex function 和 Fragment function 會大大的提高效率。編寫的著色器函式儲存為 .metal 檔案,其編譯分為兩個階段:
1. Front-end 階段發生在 XCode build 時,.metal 檔案會被編譯為 IR 檔案。 2. Back-end 階段發生在 runtime ,IR 檔案會被編譯為機器碼。
載入 IR 檔案的過程通過 device 的 newDefaultLibrary 方法,得到一個具備 MTLLibrary 協議的物件。取出裡面的 Vertex function 和 Fragment function 通過 MTLLibrary 物件的 newFunctionWithName 方法。
id<MTLLibrary> defaultLibrary = [_device newDefaultLibrary]; id<MTLFunction> vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"]; // 得到 Vertex function id<MTLFunction> fragmentFunction = [defaultLibrary newFunctionWithName:@"fragmentShader"]; // 得到 Fragment function
繪製使用的資料型別涉及 SIMD 和一個具備 MTLBuffer 協議的物件,SIMD 是一個獨立於 Metal 的庫,能簡化演算法和 GPU 處理流程,效率高使用十分方便,常在 Metal 應用中使用。MTLBuffer 是 Metal 提供的用來儲存大量頂點資料的 buffer ,其記憶體由 GPU 可訪問的記憶體分配,效率高,可以節省記憶體的佔用(當頂點資料量龐大的時候)。使用的時候一般自定義一個頂點資料結構,包含座標點和顏色資訊,這個資料結構的定義會用到 SIMD 裡面的型別:
typedef struct { vector_float2 position;// 一個二維的位置向量 vector_float4 color;// 一個四維的顏色向量 } AAPLVertex;
將定義好的 APPLVertex 陣列傳入 MTLBuffer 物件可以通過:
// Set up a simple MTLBuffer with our vertices which include texture coordinates static const AAPLVertex quadVertices[] = { // Pixel positions, Texture coordinates { {250,-250 },{ 1.f, 0.f } }, { { -250,-250 },{ 0.f, 0.f } }, { { -250,250 },{ 0.f, 1.f } }, { {250,-250 },{ 1.f, 0.f } }, { { -250,250 },{ 0.f, 1.f } }, { {250,250 },{ 1.f, 1.f } }, }; // Create our vertex buffer, and initialize it with our quadVertices array _vertices = [_device newBufferWithBytes:quadVertices length:sizeof(quadVertices) options:MTLResourceStorageModeShared];
之後使用 Encoder 的 setVertexBuffer: 方法可將 MTLBuffer 物件傳給頂點著色器(Vertex function),執行繪製管線。
最終的繪製的三角形如圖:

三角形