Runt
2025-06-27 e21b1c797955a231f2bcf71818e0259fbb6aeba1
LiveProject/tool/MetalRenderer.swift
@@ -4,75 +4,68 @@
//  渲染工具
//  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) {}
}