Metal日記:使用步驟指南

Quinn? 士魁發表於2019-01-14

本文參考資料:

juejin.im/post/5b1e8f…

xiaozhuanlan.com/topic/04598…

developer.apple.com/videos/play…

github.com/quinn0809/G…

cloud.tencent.com/developer/a…

devstreaming-cdn.apple.com/videos/wwdc…

Metal處理邏輯

無論是CoreImage、GPUImage框架,還是Metal、OpenGL框架,處理邏輯類似:

輸入(資源+邏輯 )-> 黑盒 -> 輸出

CoreImage 可以選擇GPU處理->Metal->CoreImage,也可以選擇CPU處理

GPUImage 有OpenGL ES版,也有Metal版本(Metal 版本極為簡陋)

Metal使用大致分為:

  • build :shader
  • initialize :device and Queues Render Objects
  • Render:commandBuffer、ResourceUpdate、renderEncoder、Display

Metal日記:使用步驟指南
Metal 為控制GPU的程式語言 其實從程式碼來講,大部分時間都是在CPU完成元件的建立,包括shader,pipline,encoder。

build :shader

主要完成shader的編譯,涉及到vertex 、fragment

Metal中的shader是MSL語言,SIMD的存在支援MSL與原生程式碼共享資料結構。

一個簡單的vertexShader :

vertex ThreeInputVertexIO threeInputVertex(device packed_float2 *position [[buffer(0)]],
                                       device packed_float2 *texturecoord [[buffer(1)]],
                                       device packed_float2 *texturecoord2 [[buffer(2)]],
                                       uint vid [[vertex_id]])
{
    ThreeInputVertexIO outputVertices;
    
    outputVertices.position = float4(position[vid], 0, 1.0);
    outputVertices.textureCoordinate = texturecoord[vid];
    outputVertices.textureCoordinate2 = texturecoord2[vid];
    
    return outputVertices;
}
複製程式碼

outputVertices.position = float4(position[vid], 0, 1.0); position[vid] 是float2 SIMD 是 Apple 提供的一款方便原生程式與著色器程式共享資料結構的庫。

開發者可以基於SIMD框架在Objective-C標頭檔案中定義一系列資料結構,在原生程式碼和著色器程式中通過#include包含這個標頭檔案,兩者就都有了這個結構的定義。

Metal日記:使用步驟指南
ThreeInputVertexIO 宣告如下:

struct ThreeInputVertexIO
{
    float4 position [[position]];
    float2 textureCoordinate [[user(texturecoord)]];
    float2 textureCoordinate [[user(texturecoord2)]];

};
複製程式碼

device packed_float2 *position [[buffer(0)]]

device packed_float2 *texturecoord [[buffer(1)]]

packed_float2是型別 positiontexturecoord是變數名

device是記憶體修飾符,Metal種的記憶體訪問主要有兩種方式:Device模式和Constant模式,由程式碼中顯式指定。

Device模式是比較通用的訪問模式,使用限制比較少,而Constant模式是為了多次讀取而設計的快速訪問只讀模式,通過Constant記憶體模式訪問的引數的資料的位元組數量是固定的,特點總結為: Device支援讀寫,並且沒有size的限制; Constant是隻讀,並且限定大小; 如何選擇Device和Constant模式? 先看資料size是否會變化,再看訪問的頻率高低,只有那些固定size且經常訪問的部分適合使用constant模式,其他的均用Device。

[[buffer(0)]][[buffer(1)]]是控制程式碼,在MSL中不同的型別用不同的buffer表示,與renderCommandEncoder時相對應:

    //buffer 
    renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
    renderEncoder.setVertexBuffer(textureBuffer1, offset: 0, index: 1)
    renderEncoder.setVertexBuffer(textureBuffer2, offset: 0, index: 2)

    ······
    //samper
    [renderEncoder setFragmentSampler:sampler atIndex:0];
    [renderEncoder setFragmentSampler:sampler1 atIndex:0];
    ······
    //texture
    renderEncoder.setFragmentTexture(texture, index: 0)
    renderEncoder.setFragmentTexture(texture1, index: 1)
    ······
複製程式碼

index 與 [[buffer(0)]]相對應,如,此時上文MSL的vertexShader中

  • [[buffer(0)]] 為vertex資料
  • [[buffer(1)]]為第一個紋理座標資料
  • [[buffer(2)]]為第二個紋理座標資料

index與shader中宣告的[[buffer(x)]]嚴格對應,否則在Metal Validation Layer中極可能會報錯(通常是記憶體讀取越界),或者繪製出不符合預期的結果。 vertexShader的執行次數與頂點數量有關,即vid為索引數。

一個簡單的fragmentShader :

fragment half4 lookupSplitFragment(TwoInputVertexIO fragmentInput [[stage_in]],
                              texture2d<half> inputTexture [[texture(0)]],
                              texture2d<half> inputTexture2 [[texture(1)]],
                              texture2d<half> inputTexture3 [[texture(2)]],
                              constant SplitUniform& uniform [[ buffer(1) ]])
{}
複製程式碼

同上文的renderCommandEncoder時,

  • inputTexture 為第一個紋理
  • inputTexture2 為第二個紋理
  • inputTexture3 為第三個紋理

如圖所示

SplitUniform 為自定義的引數,在此shader中的意義為split 的外界值。 SplitUniform的定義如下: 在metal檔案中:

typedef struct
{
    float intensity;
    float progress;

} SplitUniform;
複製程式碼

『intensity』filter的濃度

『progress』filtersplit 進度

Metal日記:使用步驟指南
shader 在xcode building 的 時候就會被 編譯到 metal library中 至此,本次目標渲染的shader 已經完成,下面開始初始化工作,將shader通過渲染管線聯絡起來。

初始化工作

  • devide
  • commandQueue
  • buffer
  • texture
  • pipline

Metal日記:使用步驟指南

初始化Device

devidemetal 控制的GPU 入口,是一個一次建立最好永久使用的物件,用來建立buffercommandtexture;在Metal最佳實踐之南中,指出開發者應該長期持有一個device物件(device 物件建立比較昂貴)

OC:

id<MTLDevice> device = MTLCreateSystemDefaultDevice();
複製程式碼

Swift:

guard let device = MTLCreateSystemDefaultDevice() else {
            fatalError("Could not create Metal Device")
}
複製程式碼

建立 CommandQueue 命令佇列

Metal 最佳實踐指南中,指出大部分情況下,開發者要重複使用一個命令佇列 通過Device -> commandQueue

/// device 建立命令佇列
   guard let commandQueue = self.device.makeCommandQueue() else {
       fatalError("Could not create command queue")
   }
複製程式碼

建立 Buffer 資料

Metal 中,所有無結構的資料都使用 Buffer 來管理。與 OpenGL 類似的,頂點、索引等資料都通過 Buffer 管理。 比如:vertexBuffer、textureCoordBuffer

/// 紋理座標buffer
let coordinateBuffer = device.makeBuffer(bytes: inputTextureCoordinates,
                    length: inputTextureCoordinates.count * MemoryLayout<Float>.size,
                    options: [])!
///頂點資料buffer
let vertexBuffer = device.makeBuffer(bytes: imageVertices,
                    length: imageVertices.count * MemoryLayout<Float>.size,
                    options: [])!
複製程式碼

這些Buffer在renderCommandEncoder中 進行編碼然後提交到GPU

Metal日記:使用步驟指南

建立 Texture

texture 可以理解為被加工的物件,設計者為它增加了一個描述物件MTLTextureDescriptor

在Metal中,有一個抽象物件,專門由於描述 teture 的詳情(fromat,width,height,storageMode)

storageMode為 控制CPU、GPU的記憶體管理方式。Apple 推薦在 iOS 中使用 shared mode,而在 macOS 中使用 managed mode。

Shared Storage:CPU 和 GPU 均可讀寫這塊記憶體。
Private Storage: 僅 GPU 可讀寫這塊記憶體,可以通過 Blit 命令等進行拷貝。
Managed Storage: 僅在 macOS 中允許。僅 GPU 可讀寫這塊記憶體,但 Metal 會建立一塊映象記憶體供 CPU 使用
複製程式碼

Metal日記:使用步驟指南

//紋理描述 器
let textureDescriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: pixelFormat,
                                       width: width,
                                       height: height,
                                       mipmapped: mipmapped)
//通過 devide建立簡單紋理(比如單色紋理)
guard let newTexture = device.makeTexture(descriptor: textureDescriptor) else {
            fatalError("Could not create texture of size: (\(width), \(height))")
 }
 // 通過 圖片建立 (MetalKit)
var textureLoader = MTKTextureLoader(device: self.device)
let imageTexture = try textureLoader.newTexture(cgImage: img, options: [MTKTextureLoader.Option.SRGB : false])


複製程式碼

MTKTextureLoader 也建議重複使用

建立 pipline 渲染管線

pipline:最為複雜的東西,也是最簡單的東西,說他複雜是因為,他的成員變數多;說簡單,是因為pipline只是一個所有資源的描述者

Metal日記:使用步驟指南
在Metal中,有一個抽象物件,專門由於描述 pipline 的 詳情的物件Descriptor,包含了(頂點著色器,片段著色器,顏色格式,深度等)

colorAttachments,用於寫入顏色資料
depthAttachment,用於寫入深度資訊
stencilAttachment,允許我們基於一些條件丟棄指定片段

MTLRenderPassDescriptor 裡面的 colorAttachments,支援多達 4 個 用來儲存顏色畫素資料的 attachment,在 2D 影象處理時,我們一般只會關聯一個。
即 colorAttachments[0]。
複製程式碼
let descriptor = MTLRenderPipelineDescriptor()
    descriptor.colorAttachments[0].pixelFormat = MTLPixelFormat.bgra8Unorm
    descriptor.vertexFunction = vertexFunction
    descriptor.fragmentFunction = fragmentFunction
複製程式碼

關於shader 函式 的建立:

guard let vertexFunction = defaultLibrary.makeFunction(name: vertexFunctionName) else {
    fatalError("Could not compile vertex function \(vertexFunctionName)")
}
    
guard let fragmentFunction = defaultLibrary.makeFunction(name: fragmentFunctionName) else {
    fatalError("Could not compile fragment function \(fragmentFunctionName)")
}
複製程式碼

defaultLibrary 為通過device 建立 的 函式庫,上文我們在編譯的時候已經編譯好了頂點著色器以及片段著色器,這是通過

do {
            let frameworkBundle = Bundle(for: Context.self)
            let metalLibraryPath = frameworkBundle.path(forResource: "default", ofType: "metallib")!
            
            self.defaultLibrary = try device.makeLibrary(filepath:metalLibraryPath)
        } catch {
            fatalError("Could not load library")
        }
        
複製程式碼

可以獲取到 defaultLibrary,這是有Metal 提供的方法

到目前為止,我們已經完成了渲染所需的子控制元件的構造,初始化,下面將介紹 命令編碼,提交,渲染

Render:commandBuffer、ResourceUpdate、renderEncoder、Display

Metal日記:使用步驟指南

renderEncoder

上文我們建立了渲染管線狀態,這裡我們需要根據RenderPassDescriptor生成一個 RenderCommandEncoder,在encoder中連結shader GPU 渲染影象的步驟大致可以分為:載入、渲染、儲存。開發者可以指定這三個步驟具體做什麼事。

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;
複製程式碼

myColorTexture 可以理解為容器,用於安置渲染的結果。

Metal日記:使用步驟指南

上文有提到編碼:

    //buffer 
    renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
    renderEncoder.setVertexBuffer(textureBuffer1, offset: 0, index: 1)
    renderEncoder.setVertexBuffer(textureBuffer2, offset: 0, index: 2)

    ······
    //samper
    [renderEncoder setFragmentSampler:sampler atIndex:0];
    [renderEncoder setFragmentSampler:sampler1 atIndex:0];
    ······
    //texture
    renderEncoder.setFragmentTexture(texture, index: 0)
    renderEncoder.setFragmentTexture(texture1, index: 1)
    ······
複製程式碼

編碼所需程式碼大致如下:

        let commandBuffer = commonQueue.makeCommandBuffer()!
        let commandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescripor)!
        
        commandEncoder.setRenderPipelineState(pipelineState)
        commandEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
        commandEncoder.setFragmentTexture(texture, index: 0)
        commandEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4)
        commandEncoder.endEncoding()
複製程式碼

提交渲染

        commandBuffer.present(drawable)
        commandBuffer.commit()
複製程式碼

渲染時的三幀快取: 建立三幀的資源緩衝區來形成一個緩衝池。CPU 將每一幀的資料按順序寫入緩衝區供 GPU 使用。

提交時,分為同步提交(阻塞),非同步提交(非阻塞) 阻塞:

id<MTLCommandBuffer> commandBuffer = [commandQueue commandBuffer];

// 編碼命令...

[commandBuffer commit];

[commandBuffer waitUntilCompleted];
複製程式碼

非阻塞:

id<MTLCommandBuffer> commandBuffer = [commandQueue commandBuffer];

// 編碼命令...

commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> commandBuffer) {
	// 回撥 CPU...
}

[commandBuffer commit];
複製程式碼

Metal日記:使用步驟指南

重申:本文參考資料:

juejin.im/post/5b1e8f…

xiaozhuanlan.com/topic/04598…

developer.apple.com/videos/play…

github.com/quinn0809/G…

cloud.tencent.com/developer/a…

devstreaming-cdn.apple.com/videos/wwdc…

相關文章