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…
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
build :shader
主要完成shader的編譯,涉及到vertex 、fragment
Metal中的shader是MSL語言,SIMD的存在支持MSL與原生代碼共享數(shù)據(jù)結(jié)構(gòu)。
一個(gè)簡單的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; } 復(fù)制代碼outputVertices.position = float4(position[vid], 0, 1.0); position[vid] 是float2 SIMD 是 Apple 提供的一款方便原生程序與著色器程序共享數(shù)據(jù)結(jié)構(gòu)的庫。
開發(fā)者可以基于SIMD框架在Objective-C頭文件中定義一系列數(shù)據(jù)結(jié)構(gòu),在原生代碼和著色器程序中通過#include包含這個(gè)頭文件,兩者就都有了這個(gè)結(jié)構(gòu)的定義。
ThreeInputVertexIO 聲明如下: struct ThreeInputVertexIO {float4 position [[position]];float2 textureCoordinate [[user(texturecoord)]];float2 textureCoordinate [[user(texturecoord2)]];}; 復(fù)制代碼device packed_float2 *position [[buffer(0)]]
device packed_float2 *texturecoord [[buffer(1)]]
packed_float2是類型 position、texturecoord是變量名
device是內(nèi)存修飾符,Metal種的內(nèi)存訪問主要有兩種方式:Device模式和Constant模式,由代碼中顯式指定。
Device模式是比較通用的訪問模式,使用限制比較少,而Constant模式是為了多次讀取而設(shè)計(jì)的快速訪問只讀模式,通過Constant內(nèi)存模式訪問的參數(shù)的數(shù)據(jù)的字節(jié)數(shù)量是固定的,特點(diǎn)總結(jié)為: Device支持讀寫,并且沒有size的限制; Constant是只讀,并且限定大小; 如何選擇Device和Constant模式? 先看數(shù)據(jù)size是否會(huì)變化,再看訪問的頻率高低,只有那些固定size且經(jīng)常訪問的部分適合使用constant模式,其他的均用Device。
[[buffer(0)]]、[[buffer(1)]]是句柄,在MSL中不同的類型用不同的buffer表示,與renderCommandEncoder時(shí)相對應(yīng):
//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];······//texturerenderEncoder.setFragmentTexture(texture, index: 0)renderEncoder.setFragmentTexture(texture1, index: 1)······ 復(fù)制代碼index 與 [[buffer(0)]]相對應(yīng),如,此時(shí)上文MSL的vertexShader中
- [[buffer(0)]] 為vertex數(shù)據(jù)
- [[buffer(1)]]為第一個(gè)紋理坐標(biāo)數(shù)據(jù)
- [[buffer(2)]]為第二個(gè)紋理坐標(biāo)數(shù)據(jù)
index與shader中聲明的[[buffer(x)]]嚴(yán)格對應(yīng),否則在Metal Validation Layer中極可能會(huì)報(bào)錯(cuò)(通常是內(nèi)存讀取越界),或者繪制出不符合預(yù)期的結(jié)果。 vertexShader的執(zhí)行次數(shù)與頂點(diǎn)數(shù)量有關(guān),即vid為索引數(shù)。
一個(gè)簡單的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) ]]) {} 復(fù)制代碼同上文的renderCommandEncoder時(shí),
- inputTexture 為第一個(gè)紋理
- inputTexture2 為第二個(gè)紋理
- inputTexture3 為第三個(gè)紋理
SplitUniform 為自定義的參數(shù),在此shader中的意義為split 的外界值。 SplitUniform的定義如下: 在metal文件中:
typedef struct {float intensity;float progress;} SplitUniform; 復(fù)制代碼『intensity』 為filter的濃度
『progress』 為filter的 split 進(jìn)度
shader 在xcode building 的 時(shí)候就會(huì)被 編譯到 metal library中 至此,本次目標(biāo)渲染的shader 已經(jīng)完成,下面開始初始化工作,將shader通過渲染管線聯(lián)系起來。初始化工作
- devide
- commandQueue
- buffer
- texture
- pipline
初始化Device
devide 是 metal 控制的GPU 入口,是一個(gè)一次創(chuàng)建最好永久使用的對象,用來創(chuàng)建buffer、command、texture;在Metal最佳實(shí)踐之南中,指出開發(fā)者應(yīng)該長期持有一個(gè)device對象(device 對象創(chuàng)建比較昂貴)
OC:
id<MTLDevice> device = MTLCreateSystemDefaultDevice(); 復(fù)制代碼Swift:
guard let device = MTLCreateSystemDefaultDevice() else {fatalError("Could not create Metal Device") } 復(fù)制代碼創(chuàng)建 CommandQueue 命令隊(duì)列
Metal 最佳實(shí)踐指南中,指出大部分情況下,開發(fā)者要重復(fù)使用一個(gè)命令隊(duì)列 通過Device -> commandQueue
/// device 創(chuàng)建命令隊(duì)列g(shù)uard let commandQueue = self.device.makeCommandQueue() else {fatalError("Could not create command queue")} 復(fù)制代碼創(chuàng)建 Buffer 數(shù)據(jù)
Metal 中,所有無結(jié)構(gòu)的數(shù)據(jù)都使用 Buffer 來管理。與 OpenGL 類似的,頂點(diǎn)、索引等數(shù)據(jù)都通過 Buffer 管理。 比如:vertexBuffer、textureCoordBuffer
/// 紋理坐標(biāo)buffer let coordinateBuffer = device.makeBuffer(bytes: inputTextureCoordinates,length: inputTextureCoordinates.count * MemoryLayout<Float>.size,options: [])! ///頂點(diǎn)數(shù)據(jù)buffer let vertexBuffer = device.makeBuffer(bytes: imageVertices,length: imageVertices.count * MemoryLayout<Float>.size,options: [])! 復(fù)制代碼這些Buffer在renderCommandEncoder中 進(jìn)行編碼然后提交到GPU
創(chuàng)建 Texture
texture 可以理解為被加工的對象,設(shè)計(jì)者為它增加了一個(gè)描述對象MTLTextureDescriptor
在Metal中,有一個(gè)抽象對象,專門由于描述 teture 的詳情(fromat,width,height,storageMode)
storageMode為 控制CPU、GPU的內(nèi)存管理方式。Apple 推薦在 iOS 中使用 shared mode,而在 macOS 中使用 managed mode。
Shared Storage:CPU 和 GPU 均可讀寫這塊內(nèi)存。 Private Storage: 僅 GPU 可讀寫這塊內(nèi)存,可以通過 Blit 命令等進(jìn)行拷貝。 Managed Storage: 僅在 macOS 中允許。僅 GPU 可讀寫這塊內(nèi)存,但 Metal 會(huì)創(chuàng)建一塊鏡像內(nèi)存供 CPU 使用 復(fù)制代碼 //紋理描述 器 let textureDescriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: pixelFormat,width: width,height: height,mipmapped: mipmapped) //通過 devide創(chuàng)建簡單紋理(比如單色紋理) guard let newTexture = device.makeTexture(descriptor: textureDescriptor) else {fatalError("Could not create texture of size: (\(width), \(height))")}// 通過 圖片創(chuàng)建 (MetalKit) var textureLoader = MTKTextureLoader(device: self.device) let imageTexture = try textureLoader.newTexture(cgImage: img, options: [MTKTextureLoader.Option.SRGB : false])復(fù)制代碼MTKTextureLoader 也建議重復(fù)使用
創(chuàng)建 pipline 渲染管線
pipline:最為復(fù)雜的東西,也是最簡單的東西,說他復(fù)雜是因?yàn)?#xff0c;他的成員變量多;說簡單,是因?yàn)閜ipline只是一個(gè)所有資源的描述者
在Metal中,有一個(gè)抽象對象,專門由于描述 pipline 的 詳情的對象Descriptor,包含了(頂點(diǎn)著色器,片段著色器,顏色格式,深度等) colorAttachments,用于寫入顏色數(shù)據(jù) depthAttachment,用于寫入深度信息 stencilAttachment,允許我們基于一些條件丟棄指定片段MTLRenderPassDescriptor 里面的 colorAttachments,支持多達(dá) 4 個(gè) 用來存儲(chǔ)顏色像素?cái)?shù)據(jù)的 attachment,在 2D 圖像處理時(shí),我們一般只會(huì)關(guān)聯(lián)一個(gè)。 即 colorAttachments[0]。 復(fù)制代碼 let descriptor = MTLRenderPipelineDescriptor()descriptor.colorAttachments[0].pixelFormat = MTLPixelFormat.bgra8Unormdescriptor.vertexFunction = vertexFunctiondescriptor.fragmentFunction = fragmentFunction 復(fù)制代碼關(guān)于shader 函數(shù) 的創(chuàng)建:
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)") } 復(fù)制代碼defaultLibrary 為通過device 創(chuàng)建 的 函數(shù)庫,上文我們在編譯的時(shí)候已經(jīng)編譯好了頂點(diǎn)著色器以及片段著色器,這是通過
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")}復(fù)制代碼可以獲取到 defaultLibrary,這是有Metal 提供的方法
到目前為止,我們已經(jīng)完成了渲染所需的子控件的構(gòu)造,初始化,下面將介紹 命令編碼,提交,渲染
Render:commandBuffer、ResourceUpdate、renderEncoder、Display
renderEncoder
上文我們創(chuàng)建了渲染管線狀態(tài),這里我們需要根據(jù)RenderPassDescriptor生成一個(gè) RenderCommandEncoder,在encoder中鏈接shader GPU 渲染圖像的步驟大致可以分為:加載、渲染、存儲(chǔ)。開發(fā)者可以指定這三個(gè)步驟具體做什么事。
MTLRenderPassDescriptor * desc = [MTLRenderPassDescriptor new]; desc.colorAttachment[0].texture = myColorTexture;// 指定三個(gè)步驟的行為 desc.colorAttachment[0].loadAction = MTLLoadActionClear; desc.colorAttachment[0].clearColor = MTLClearColorMake(0.39f, 0.34f, 0.53f, 1.0f); desc.colorAttachment[0].storeAction = MTLStoreActionStore; 復(fù)制代碼myColorTexture 可以理解為容器,用于安置渲染的結(jié)果。
上文有提到編碼:
//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];······//texturerenderEncoder.setFragmentTexture(texture, index: 0)renderEncoder.setFragmentTexture(texture1, index: 1)······ 復(fù)制代碼編碼所需代碼大致如下:
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() 復(fù)制代碼提交渲染
commandBuffer.present(drawable)commandBuffer.commit() 復(fù)制代碼渲染時(shí)的三幀緩存: 創(chuàng)建三幀的資源緩沖區(qū)來形成一個(gè)緩沖池。CPU 將每一幀的數(shù)據(jù)按順序?qū)懭刖彌_區(qū)供 GPU 使用。
提交時(shí),分為同步提交(阻塞),異步提交(非阻塞) 阻塞:
id<MTLCommandBuffer> commandBuffer = [commandQueue commandBuffer];// 編碼命令...[commandBuffer commit];[commandBuffer waitUntilCompleted]; 復(fù)制代碼非阻塞:
id<MTLCommandBuffer> commandBuffer = [commandQueue commandBuffer];// 編碼命令...commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> commandBuffer) {// 回調(diào) CPU... }[commandBuffer commit]; 復(fù)制代碼重申:本文參考資料:
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…
總結(jié)
以上是生活随笔為你收集整理的Metal日记:使用步骤指南的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: checkbox居中 editor_如何
- 下一篇: datagrid vue_类似 easy