| | |
| | | // 渲染工具 |
| | | // Created by 倪路朋 on 6/26/25. |
| | | // |
| | | import Metal |
| | | import MetalKit |
| | | |
| | | class Renderer: NSObject, MTKViewDelegate { |
| | | static var shared: Renderer? |
| | | |
| | | class MetalRenderer: NSObject, MTKViewDelegate { |
| | | private var device: MTLDevice! |
| | | private var commandQueue: MTLCommandQueue! |
| | | private var pipelineState: MTLRenderPipelineState! |
| | | private var texture: MTLTexture? |
| | | private var textureDescriptor: MTLTextureDescriptor! |
| | | private var currentData: [UInt8]? |
| | | private var textureWidth = 0 |
| | | private var textureHeight = 0 |
| | | private var textureCache: CVMetalTextureCache! |
| | | private var currentPixelBuffer: CVPixelBuffer? |
| | | |
| | | weak var mtkView: MTKView? { |
| | | didSet { |
| | | guard let view = mtkView else { return } |
| | | device = view.device |
| | | commandQueue = device.makeCommandQueue() |
| | | |
| | | let library = device.makeDefaultLibrary() |
| | | let pipelineDesc = MTLRenderPipelineDescriptor() |
| | | pipelineDesc.vertexFunction = library?.makeFunction(name: "vertexShader") |
| | | pipelineDesc.fragmentFunction = library?.makeFunction(name: "fragmentShader") |
| | | pipelineDesc.colorAttachments[0].pixelFormat = view.colorPixelFormat |
| | | |
| | | pipelineState = try? device.makeRenderPipelineState(descriptor: pipelineDesc) |
| | | |
| | | Renderer.shared = self |
| | | } |
| | | func setup(view: MTKView) { |
| | | self.device = view.device |
| | | self.commandQueue = device.makeCommandQueue() |
| | | CVMetalTextureCacheCreate(nil, nil, device, nil, &textureCache) |
| | | } |
| | | |
| | | func updateFrame(data: [UInt8], width: Int, height: Int) { |
| | | currentData = data |
| | | textureWidth = width |
| | | textureHeight = height |
| | | func updateFrame(pixelBuffer: CVPixelBuffer) { |
| | | self.currentPixelBuffer = pixelBuffer |
| | | } |
| | | |
| | | func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {} |
| | | |
| | | func draw(in view: MTKView) { |
| | | guard let drawable = view.currentDrawable, |
| | | let descriptor = view.currentRenderPassDescriptor, |
| | | let data = currentData else { return } |
| | | let pixelBuffer = currentPixelBuffer else { return } |
| | | |
| | | if texture == nil || texture?.width != textureWidth || texture?.height != textureHeight { |
| | | textureDescriptor = MTLTextureDescriptor.texture2DDescriptor( |
| | | pixelFormat: .rgba8Unorm, |
| | | width: textureWidth, |
| | | height: textureHeight, |
| | | mipmapped: false |
| | | ) |
| | | textureDescriptor.usage = [.shaderRead, .shaderWrite] |
| | | texture = device.makeTexture(descriptor: textureDescriptor) |
| | | var textureRef: CVMetalTexture? |
| | | let width = CVPixelBufferGetWidth(pixelBuffer) |
| | | let height = CVPixelBufferGetHeight(pixelBuffer) |
| | | |
| | | let status = CVMetalTextureCacheCreateTextureFromImage( |
| | | nil, textureCache, pixelBuffer, nil, |
| | | .bgra8Unorm, width, height, 0, &textureRef) |
| | | |
| | | guard status == kCVReturnSuccess, |
| | | let cvTexture = textureRef, |
| | | let texture = CVMetalTextureGetTexture(cvTexture) else { return } |
| | | |
| | | let commandBuffer = commandQueue.makeCommandBuffer()! |
| | | let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor)! |
| | | encoder.setFragmentTexture(texture, index: 0) |
| | | encoder.endEncoding() |
| | | |
| | | // 简单拷贝(不做 shader 处理) |
| | | let blitEncoder = commandBuffer.makeBlitCommandEncoder()! |
| | | let dstTexture = drawable.texture |
| | | if dstTexture.width != texture.width || dstTexture.height != texture.height { |
| | | print("❌ 尺寸不一致,无法 blit:src = \(texture.width)x\(texture.height), dst = \(dstTexture.width)x\(dstTexture.height)") |
| | | return |
| | | } |
| | | blitEncoder.copy(from: texture, |
| | | sourceSlice: 0, |
| | | sourceLevel: 0, |
| | | sourceOrigin: MTLOrigin(x: 0, y: 0, z: 0), |
| | | sourceSize: MTLSize(width: width, height: height, depth: 1), |
| | | to: drawable.texture, |
| | | destinationSlice: 0, |
| | | destinationLevel: 0, |
| | | destinationOrigin: MTLOrigin(x: 0, y: 0, z: 0)) |
| | | blitEncoder.endEncoding() |
| | | |
| | | let region = MTLRegionMake2D(0, 0, textureWidth, textureHeight) |
| | | data.withUnsafeBytes { ptr in |
| | | texture?.replace(region: region, mipmapLevel: 0, withBytes: ptr.baseAddress!, bytesPerRow: textureWidth * 4) |
| | | } |
| | | |
| | | let commandBuffer = commandQueue.makeCommandBuffer() |
| | | let encoder = commandBuffer?.makeRenderCommandEncoder(descriptor: descriptor) |
| | | encoder?.setRenderPipelineState(pipelineState) |
| | | encoder?.setFragmentTexture(texture, index: 0) |
| | | encoder?.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4) |
| | | encoder?.endEncoding() |
| | | commandBuffer?.present(drawable) |
| | | commandBuffer?.commit() |
| | | commandBuffer.present(drawable) |
| | | commandBuffer.commit() |
| | | print("绘制画面") |
| | | } |
| | | |
| | | func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {} |
| | | } |