【问题标题】:Holding onto a MTLTexture from a CVImageBuffer causes stuttering持有 CVImageBuffer 中的 MTLTexture 会导致卡顿
【发布时间】:2017-09-18 22:22:10
【问题描述】:

我正在从CVImageBuffers(来自相机和玩家)创建一个MTLTexture,使用CVMetalTextureCacheCreateTextureFromImage 获取CVMetalTexture,然后使用CVMetalTextureGetTexture 获取MTLTexture

我看到的问题是,当我后来使用 Metal 渲染纹理时,我偶尔会看到渲染的视频帧乱序(视觉上它在时间上来回断断续续),大概是因为 CoreVideo 正在修改底层 CVImageBuffer存储和MTLTexture 只是指向那里。

在我释放 MTLTexture 对象之前,有什么方法可以让 CoreVideo 不接触该缓冲区并使用其池中的另一个缓冲区?

我目前的解决方法是使用 MTLBlitCommandEncoder 对纹理进行 blitting,但因为我只需要保持纹理约 30 毫秒,这似乎是不必要的。

【问题讨论】:

  • 在您完成金属纹理之前,您是否一直强烈引用CVMetalTexture?还是您只持有对MTLTexture 对象的强引用?
  • 我强烈引用MTLTexture 只是因为一些实现细节。坚持CVMetalTextureCVImageBuffer 对象会解决我的问题吗?
  • 我不知道。它可能。这只是我的一个猜测。如果您可以轻松尝试,那么您应该尝试。 :)
  • 可能是 Apple TSI?

标签: ios iphone swift metal core-video


【解决方案1】:

我最近遇到了同样的问题。问题是 MTLTexture 是无效的,除非它拥有的 CVMetalTextureRef 仍然存在。在您使用 MTLTexture 的整个过程中(一直到当前渲染周期结束),您必须保持对 CVMetalTextureRef 的引用。

【讨论】:

  • 这是Metal下CMSampleBufferRefs成功纹理化的关键,谢谢!
  • C0C0AL0C0 我认为这也是Apple示例代码MetalVideoCapture中屏幕撕裂的解决方案? stackoverflow.com/questions/38879518/…(正如你已经回答了我相信的那个问题,但后来删除了它)
  • 保留 CVMetalTextureRef 数组解决了该问题,但会导致内存泄漏,从而导致内存不足崩溃。如何解决这个问题?
  • 谢谢 jasongregori!
【解决方案2】:

我遇到了同样的问题,但是对 CVMetalTexture 对象的额外引用并没有解决我的问题。

据我所知,只有在我的金属代码完成对前一帧的处理之前,我从相机接收到新帧时才会发生这种情况。

似乎 CVMetalTextureCacheCreateTextureFromImage 只是在像素缓冲区的顶部创建了一个纹理,相机正在将数据输入其中。因此,从 Metal 代码异步访问它会导致一些问题。

我决定创建一个 MTLTexture 的副本(它也是异步的,但足够快)。

这里是 CVMetalTextureCacheCreateTextureFromImage() 的描述

"该函数创建或返回缓存的CoreVideo Metal纹理缓冲区,根据指定映射到图像缓冲区,创建基于设备的图像缓冲区和MTLTexture对象之间的实时绑定。",

【讨论】:

  • 您能详细说明一下您是如何制作副本并反馈回来的吗?
  • 关于如何实现这一点的任何示例/伪代码?
【解决方案3】:

您的问题似乎取决于您如何管理会话以获取原始相机数据。

我认为你可以通过这个类(MetalCameraSession)深入实时地分析相机会话以了解你的会话的当前状态:

import AVFoundation
import Metal
public protocol MetalCameraSessionDelegate {
    func metalCameraSession(_ session: MetalCameraSession, didReceiveFrameAsTextures: [MTLTexture], withTimestamp: Double)
    func metalCameraSession(_ session: MetalCameraSession, didUpdateState: MetalCameraSessionState, error: MetalCameraSessionError?)
}
public final class MetalCameraSession: NSObject {
    public var frameOrientation: AVCaptureVideoOrientation? {
        didSet {
            guard
                let frameOrientation = frameOrientation,
                let outputData = outputData,
                outputData.connection(withMediaType: AVMediaTypeVideo).isVideoOrientationSupported
            else { return }

            outputData.connection(withMediaType: AVMediaTypeVideo).videoOrientation = frameOrientation
        }
    }
    public let captureDevicePosition: AVCaptureDevicePosition
    public var delegate: MetalCameraSessionDelegate?
    public let pixelFormat: MetalCameraPixelFormat
    public init(pixelFormat: MetalCameraPixelFormat = .rgb, captureDevicePosition: AVCaptureDevicePosition = .back, delegate: MetalCameraSessionDelegate? = nil) {
        self.pixelFormat = pixelFormat
        self.captureDevicePosition = captureDevicePosition
        self.delegate = delegate
        super.init();
        NotificationCenter.default.addObserver(self, selector: #selector(captureSessionRuntimeError), name: NSNotification.Name.AVCaptureSessionRuntimeError, object: nil)
    }
    public func start() {
        requestCameraAccess()
        captureSessionQueue.async(execute: {
            do {
                self.captureSession.beginConfiguration()
                try self.initializeInputDevice()
                try self.initializeOutputData()
                self.captureSession.commitConfiguration()
                try self.initializeTextureCache()
                self.captureSession.startRunning()
                self.state = .streaming
            }
            catch let error as MetalCameraSessionError {
                self.handleError(error)
            }
            catch {
                print(error.localizedDescription)
            }
        })
    }
    public func stop() {
        captureSessionQueue.async(execute: {
            self.captureSession.stopRunning()
            self.state = .stopped
        })
    }
    fileprivate var state: MetalCameraSessionState = .waiting {
        didSet {
            guard state != .error else { return }

            delegate?.metalCameraSession(self, didUpdateState: state, error: nil)
        }
    }
    fileprivate var captureSession = AVCaptureSession()
    internal var captureDevice = MetalCameraCaptureDevice()
    fileprivate var captureSessionQueue = DispatchQueue(label: "MetalCameraSessionQueue", attributes: [])
#if arch(i386) || arch(x86_64)
#else
    /// Texture cache we will use for converting frame images to textures
    internal var textureCache: CVMetalTextureCache?
#endif
    fileprivate var metalDevice = MTLCreateSystemDefaultDevice()
    internal var inputDevice: AVCaptureDeviceInput? {
        didSet {
            if let oldValue = oldValue {
                captureSession.removeInput(oldValue)
            }
            captureSession.addInput(inputDevice)
        }
    }
    internal var outputData: AVCaptureVideoDataOutput? {
        didSet {
            if let oldValue = oldValue {
                captureSession.removeOutput(oldValue)
            }
            captureSession.addOutput(outputData)
        }
    }
    fileprivate func requestCameraAccess() {
        captureDevice.requestAccessForMediaType(AVMediaTypeVideo) {
            (granted: Bool) -> Void in
            guard granted else {
                self.handleError(.noHardwareAccess)
                return
            }

            if self.state != .streaming && self.state != .error {
                self.state = .ready
            }
        }
    }
    fileprivate func handleError(_ error: MetalCameraSessionError) {
        if error.isStreamingError() {
            state = .error
        }

        delegate?.metalCameraSession(self, didUpdateState: state, error: error)
    }
    fileprivate func initializeTextureCache() throws {
#if arch(i386) || arch(x86_64)
        throw MetalCameraSessionError.failedToCreateTextureCache
#else
        guard
            let metalDevice = metalDevice,
            CVMetalTextureCacheCreate(kCFAllocatorDefault, nil, metalDevice, nil, &textureCache) == kCVReturnSuccess
        else {
            throw MetalCameraSessionError.failedToCreateTextureCache
        }
#endif
    }
    fileprivate func initializeInputDevice() throws {
        var captureInput: AVCaptureDeviceInput!
        guard let inputDevice = captureDevice.device(mediaType: AVMediaTypeVideo, position: captureDevicePosition) else {
            throw MetalCameraSessionError.requestedHardwareNotFound
        }
        do {
            captureInput = try AVCaptureDeviceInput(device: inputDevice)
        }
        catch {
            throw MetalCameraSessionError.inputDeviceNotAvailable
        }
        guard captureSession.canAddInput(captureInput) else {
            throw MetalCameraSessionError.failedToAddCaptureInputDevice
        }
        self.inputDevice = captureInput
    }
    fileprivate func initializeOutputData() throws {
        let outputData = AVCaptureVideoDataOutput()

        outputData.videoSettings = [
            kCVPixelBufferPixelFormatTypeKey as AnyHashable : Int(pixelFormat.coreVideoType)
        ]
        outputData.alwaysDiscardsLateVideoFrames = true
        outputData.setSampleBufferDelegate(self, queue: captureSessionQueue)

        guard captureSession.canAddOutput(outputData) else {
            throw MetalCameraSessionError.failedToAddCaptureOutput
        }

        self.outputData = outputData
    }
    @objc
    fileprivate func captureSessionRuntimeError() {
        if state == .streaming {
            handleError(.captureSessionRuntimeError)
        }
    }
    deinit {
        NotificationCenter.default.removeObserver(self)
    }
}
extension MetalCameraSession: AVCaptureVideoDataOutputSampleBufferDelegate {
#if arch(i386) || arch(x86_64)
#else
    private func texture(sampleBuffer: CMSampleBuffer?, textureCache: CVMetalTextureCache?, planeIndex: Int = 0, pixelFormat: MTLPixelFormat = .bgra8Unorm) throws -> MTLTexture {
        guard let sampleBuffer = sampleBuffer else {
            throw MetalCameraSessionError.missingSampleBuffer
        }
        guard let textureCache = textureCache else {
            throw MetalCameraSessionError.failedToCreateTextureCache
        }
        guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
            throw MetalCameraSessionError.failedToGetImageBuffer
        }
        let isPlanar = CVPixelBufferIsPlanar(imageBuffer)
        let width = isPlanar ? CVPixelBufferGetWidthOfPlane(imageBuffer, planeIndex) : CVPixelBufferGetWidth(imageBuffer)
        let height = isPlanar ? CVPixelBufferGetHeightOfPlane(imageBuffer, planeIndex) : CVPixelBufferGetHeight(imageBuffer)
        var imageTexture: CVMetalTexture?
        let result = CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, textureCache, imageBuffer, nil, pixelFormat, width, height, planeIndex, &imageTexture)
        guard
            let unwrappedImageTexture = imageTexture,
            let texture = CVMetalTextureGetTexture(unwrappedImageTexture),
            result == kCVReturnSuccess
        else {
            throw MetalCameraSessionError.failedToCreateTextureFromImage
        }
        return texture
    }
    private func timestamp(sampleBuffer: CMSampleBuffer?) throws -> Double {
        guard let sampleBuffer = sampleBuffer else {
            throw MetalCameraSessionError.missingSampleBuffer
        }

        let time = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)

        guard time != kCMTimeInvalid else {
            throw MetalCameraSessionError.failedToRetrieveTimestamp
        }

        return (Double)(time.value) / (Double)(time.timescale);
    }
    @objc public func captureOutput(_ captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, from connection: AVCaptureConnection!) {
        do {
            var textures: [MTLTexture]!

            switch pixelFormat {
            case .rgb:
                let textureRGB = try texture(sampleBuffer: sampleBuffer, textureCache: textureCache)
                textures = [textureRGB]
            case .yCbCr:
                let textureY = try texture(sampleBuffer: sampleBuffer, textureCache: textureCache, planeIndex: 0, pixelFormat: .r8Unorm)
                let textureCbCr = try texture(sampleBuffer: sampleBuffer, textureCache: textureCache, planeIndex: 1, pixelFormat: .rg8Unorm)
                textures = [textureY, textureCbCr]
            }

            let timestamp = try self.timestamp(sampleBuffer: sampleBuffer)

            delegate?.metalCameraSession(self, didReceiveFrameAsTextures: textures, withTimestamp: timestamp)
        }
        catch let error as MetalCameraSessionError {
            self.handleError(error)
        }
        catch {
            print(error.localizedDescription)
        }
    }
#endif
}

通过这个类了解不同的会话类型和发生的错误(MetalCameraSessionTypes):

import AVFoundation
public enum MetalCameraSessionState {
    case ready
    case streaming
    case stopped
    case waiting
    case error
}
public enum MetalCameraPixelFormat {
    case rgb
    case yCbCr
    var coreVideoType: OSType {
        switch self {
        case .rgb:
            return kCVPixelFormatType_32BGRA
        case .yCbCr:
            return kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
        }
    }
}
public enum MetalCameraSessionError: Error {
    case noHardwareAccess
    case failedToAddCaptureInputDevice
    case failedToAddCaptureOutput
    case requestedHardwareNotFound
    case inputDeviceNotAvailable
    case captureSessionRuntimeError
    case failedToCreateTextureCache
    case missingSampleBuffer
    case failedToGetImageBuffer
    case failedToCreateTextureFromImage
    case failedToRetrieveTimestamp
    public func isStreamingError() -> Bool {
        switch self {
        case .noHardwareAccess, .failedToAddCaptureInputDevice, .failedToAddCaptureOutput, .requestedHardwareNotFound, .inputDeviceNotAvailable, .captureSessionRuntimeError:
            return true
        default:
            return false
        }
    }
    public var localizedDescription: String {
        switch self {
        case .noHardwareAccess:
            return "Failed to get access to the hardware for a given media type."
        case .failedToAddCaptureInputDevice:
            return "Failed to add a capture input device to the capture session."
        case .failedToAddCaptureOutput:
            return "Failed to add a capture output data channel to the capture session."
        case .requestedHardwareNotFound:
            return "Specified hardware is not available on this device."
        case .inputDeviceNotAvailable:
            return "Capture input device cannot be opened, probably because it is no longer available or because it is in use."
        case .captureSessionRuntimeError:
            return "AVCaptureSession runtime error."
        case .failedToCreateTextureCache:
            return "Failed to initialize texture cache."
        case .missingSampleBuffer:
            return "No sample buffer to convert the image from."
        case .failedToGetImageBuffer:
            return "Failed to retrieve an image buffer from camera's output sample buffer."
        case .failedToCreateTextureFromImage:
            return "Failed to convert the frame to a Metal texture."
        case .failedToRetrieveTimestamp:
            return "Failed to retrieve timestamp from the sample buffer."
        }
    }
}

然后您可以为AVFoundationAVCaptureDevice 使用包装器,它具有实例方法而不是类方法(MetalCameraCaptureDevice):

import AVFoundation
internal class MetalCameraCaptureDevice {
    internal func device(mediaType: String, position: AVCaptureDevicePosition) -> AVCaptureDevice? {
        guard let devices = AVCaptureDevice.devices(withMediaType: mediaType) as? [AVCaptureDevice] else { return nil }

        if let index = devices.index(where: { $0.position == position }) {
            return devices[index]
        }
        return nil
    }
    internal func requestAccessForMediaType(_ mediaType: String!, completionHandler handler: ((Bool) -> Void)!) {
        AVCaptureDevice.requestAccess(forMediaType: mediaType, completionHandler: handler)
    }
}

然后你可以有一个自定义的 viewController 类来控制像这样的相机(CameraViewController):

import UIKit
import Metal
internal final class CameraViewController: MTKViewController {
    var session: MetalCameraSession?
    override func viewDidLoad() {
        super.viewDidLoad()
        session = MetalCameraSession(delegate: self)
    }
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        session?.start()
    }
    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        session?.stop()
    }
}
// MARK: - MetalCameraSessionDelegate
extension CameraViewController: MetalCameraSessionDelegate {
    func metalCameraSession(_ session: MetalCameraSession, didReceiveFrameAsTextures textures: [MTLTexture], withTimestamp timestamp: Double) {
        self.texture = textures[0]
    }
    func metalCameraSession(_ cameraSession: MetalCameraSession, didUpdateState state: MetalCameraSessionState, error: MetalCameraSessionError?) {
        if error == .captureSessionRuntimeError {
            print(error?.localizedDescription ?? "None")
            cameraSession.start()
        }
        DispatchQueue.main.async { 
            self.title = "Metal camera: \(state)"
        }
        print("Session changed state to \(state) with error: \(error?.localizedDescription ?? "None").")
    }
}

最后你的类可能是这样的(MTKViewController),你有

public func draw(in: MTKView)

这正是您期望从缓冲摄像头获得的MTLTexture

import UIKit
import Metal
#if arch(i386) || arch(x86_64)
#else
    import MetalKit
#endif
open class MTKViewController: UIViewController {
    open var texture: MTLTexture?
    open func willRenderTexture(_ texture: inout MTLTexture, withCommandBuffer commandBuffer: MTLCommandBuffer, device: MTLDevice) {
    }
    open func didRenderTexture(_ texture: MTLTexture, withCommandBuffer commandBuffer: MTLCommandBuffer, device: MTLDevice) {
    }
    override open func loadView() {
        super.loadView()
#if arch(i386) || arch(x86_64)
        NSLog("Failed creating a default system Metal device, since Metal is not available on iOS Simulator.")
#else
        assert(device != nil, "Failed creating a default system Metal device. Please, make sure Metal is available on your hardware.")
#endif
        initializeMetalView()
        initializeRenderPipelineState()
    }
    fileprivate func initializeMetalView() {
#if arch(i386) || arch(x86_64)
#else
        metalView = MTKView(frame: view.bounds, device: device)
        metalView.delegate = self
        metalView.framebufferOnly = true
        metalView.colorPixelFormat = .bgra8Unorm
        metalView.contentScaleFactor = UIScreen.main.scale
        metalView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        view.insertSubview(metalView, at: 0)
#endif
    }
#if arch(i386) || arch(x86_64)
#else
    internal var metalView: MTKView!
#endif
    internal var device = MTLCreateSystemDefaultDevice()
    internal var renderPipelineState: MTLRenderPipelineState?
    fileprivate let semaphore = DispatchSemaphore(value: 1)
    fileprivate func initializeRenderPipelineState() {
        guard
            let device = device,
            let library = device.newDefaultLibrary()
        else { return }

        let pipelineDescriptor = MTLRenderPipelineDescriptor()
        pipelineDescriptor.sampleCount = 1
        pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
        pipelineDescriptor.depthAttachmentPixelFormat = .invalid
        pipelineDescriptor.vertexFunction = library.makeFunction(name: "mapTexture")
        pipelineDescriptor.fragmentFunction = library.makeFunction(name: "displayTexture")
        do {
            try renderPipelineState = device.makeRenderPipelineState(descriptor: pipelineDescriptor)
        }
        catch {
            assertionFailure("Failed creating a render state pipeline. Can't render the texture without one.")
            return
        }
    }
}
#if arch(i386) || arch(x86_64)
#else
extension MTKViewController: MTKViewDelegate {
    public func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
        NSLog("MTKView drawable size will change to \(size)")
    }
    public func draw(in: MTKView) {
        _ = semaphore.wait(timeout: DispatchTime.distantFuture)
        autoreleasepool {
            guard
                var texture = texture,
                let device = device
            else {
                _ = semaphore.signal()
                return
            }
            let commandBuffer = device.makeCommandQueue().makeCommandBuffer()
            willRenderTexture(&texture, withCommandBuffer: commandBuffer, device: device)
            render(texture: texture, withCommandBuffer: commandBuffer, device: device)
        }
    }
    private func render(texture: MTLTexture, withCommandBuffer commandBuffer: MTLCommandBuffer, device: MTLDevice) {
        guard
            let currentRenderPassDescriptor = metalView.currentRenderPassDescriptor,
            let currentDrawable = metalView.currentDrawable,
            let renderPipelineState = renderPipelineState
        else {
            semaphore.signal()
            return
        }
        let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: currentRenderPassDescriptor)
        encoder.pushDebugGroup("RenderFrame")
        encoder.setRenderPipelineState(renderPipelineState)
        encoder.setFragmentTexture(texture, at: 0)
        encoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4, instanceCount: 1)
        encoder.popDebugGroup()
        encoder.endEncoding()
        commandBuffer.addScheduledHandler { [weak self] (buffer) in
            guard let unwrappedSelf = self else { return }

            unwrappedSelf.didRenderTexture(texture, withCommandBuffer: buffer, device: device)
            unwrappedSelf.semaphore.signal()
        }
        commandBuffer.present(currentDrawable)
        commandBuffer.commit()
    }
}
#endif

现在您拥有所有资源,但您还可以找到所有 navoshta(作者)GitHUB 项目的 here 完整的所有 cmets 和有关代码的描述以及有关此项目的精彩教程 @987654322 @ 尤其是可以获取纹理的第二部分(您可以在下面的 MetalCameraSession 类中找到此代码):

guard
    let unwrappedImageTexture = imageTexture,
    let texture = CVMetalTextureGetTexture(unwrappedImageTexture),
    result == kCVReturnSuccess
else {
    throw MetalCameraSessionError.failedToCreateTextureFromImage
}

【讨论】:

  • 这是来自 repo 的大量源代码,但并没有真正告诉我从哪里开始查找代码中的错误,即使此源代码可以解决问题。乍一看,我似乎在做这个源代码的工作,你有什么线索可以避免闪烁问题的重要部分吗?
  • 我认为您应该将注意力集中在捕获会话上,以确保您的缓冲区不会受到错误线程时间的影响,例如捕获开始和设备的真实状态:存在控制缓冲区流量的信号量(MTKViewController)非常棒,确保正确构建管道..
  • 你试过这个库吗?
【解决方案4】:

问题可能是由于您的相机输入而出现的。如果您的镜头与预期输出的帧速率不完全相同,帧速率不匹配会导致奇怪的重影。尝试禁用自动调整帧速率。

此问题的其他原因可能是以下原因:

临界速度:某些速度会与帧速率同步,从而导致卡顿。帧率越低问题越明显。

子像素插值:还有其他情况,帧之间的子像素插值导致细节区域在帧之间闪烁。

成功渲染的解决方案是为您的帧速率使用正确的速度(每秒像素数),添加足够的运动模糊来隐藏问题,或减少图像中的细节量。

【讨论】:

  • 输入并不是真正的问题,因为如果我在回调中复制缓冲区,一切都很好。只有当我从缓冲区中获得MTLTexture 并尝试稍后渲染它(在回调之外)时,问题才会显现出来。我在提供给我的视频数据中看不到任何伪影。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-01-11
  • 1970-01-01
  • 2015-07-31
相关资源
最近更新 更多