CVPixelBufferCreateWithBytes EXC_BAD_ACCESS woes

Hi, apologies if this is not the correct forum for this type of question - I'm not sure where else active to post.

I have a requirement to save and load a (non-planar) CVPixelBuffer to a file (in raw uncompressed binary format, not as png, jpg, etc), but cannot get CVPixelBufferCreateWithBytes to restore the data correctly.

The code fails with an exception "EXC_BAD_ACCESS (code=2, address=0x16bb0c1ff)" which occurs repeatedly within "libsystem_platform.dylib`_platform_memmove:".

import Foundation
import CoreImage
import UIKit

struct TestPixelBuffer
{

    private static func debuginfo( _ heading : String, _ pixelBuffer : CVPixelBuffer )
    {
        let width      = CVPixelBufferGetWidth( pixelBuffer )
          , height     = CVPixelBufferGetHeight( pixelBuffer )
          , pixelbytes = height * width * 4

        print( heading )
        print( "  width        = \( width )" )
        print( "  height       = \( height )" )
        print( "  pixelbytes   = \( pixelbytes )" )
        print( "  dataSize     = \( CVPixelBufferGetDataSize( pixelBuffer ) )" )
        print( "  bytesPerRow  = \( CVPixelBufferGetBytesPerRow( pixelBuffer ) )" )
        print( "  formatType   = \( CVPixelBufferGetPixelFormatType( pixelBuffer ) )" )
    }

    static func save( _ url : URL, _ pixelBuffer : CVPixelBuffer )
    {
        assert( !CVPixelBufferIsPlanar( pixelBuffer ) )
        CVPixelBufferLockBaseAddress( pixelBuffer, CVPixelBufferLockFlags.readOnly )

        if let baseAddress = CVPixelBufferGetBaseAddress( pixelBuffer )
        {
            let pointer  = baseAddress.assumingMemoryBound( to: UInt8.self )
              , rowbytes = CVPixelBufferGetBytesPerRow( pixelBuffer )
              , width    = CVPixelBufferGetWidth( pixelBuffer )
              , height   = CVPixelBufferGetHeight( pixelBuffer )
              , pixbytes = rowbytes * height
              , data     = Data( bytes: pointer, count: pixbytes )

            try! data.write( to: url )
            Self.debuginfo( "saved", pixelBuffer )

            // For testing purposes, load back the same data immediately
            let _ = Self.load( url, width, height )
        }
    }

    static func load( _ url : URL, _ width : Int, _ height : Int ) -> CVPixelBuffer?
    {
        if var data = try? Data( contentsOf: url, options: .uncached )
        {
            let bytesPerRow  = width * 4

            var output : CVPixelBuffer?

            let result = CVPixelBufferCreateWithBytes(
                kCFAllocatorDefault, width, height, kCVPixelFormatType_32BGRA,
                &data, bytesPerRow, nil, nil, nil, &output )

            guard result == kCVReturnSuccess, let pixelBuffer = output else
            {
                return nil
            }

            Self.debuginfo( "loaded", pixelBuffer )

            // For testing purposes, check the data loaded correctly by using it
            let ciimage = CIImage( cvPixelBuffer : pixelBuffer )  // ok
              , uiimage = UIImage( ciImage: ciimage )             // ok
              , _       = uiimage.pngData()                       // EXC_BAD_ACCESS here

            return pixelBuffer
        }
        return nil
    }

}

The output of a run shows that the structure is (at least partially correctly) restored from the file data:

saved
  width        = 1280
  height       = 720
  pixelbytes   = 3686400
  dataSize     = 3686464
  bytesPerRow  = 5120
  formatType   = 1111970369
loaded
  width        = 1280
  height       = 720
  pixelbytes   = 3686400
  dataSize     = 3686400
  bytesPerRow  = 5120
  formatType   = 1111970369

I've assumed the additional bytes reported by CVPixelBufferGetDataSize (above: dataSize vs pixelbytes) are not essential to consider since the loaded structure has the correct metrics - ref: avfoundation - Why CVPixelBufferGetDataSize always return 32-byte more data? - Stack Overflow

Any help much appreciated - thanks.

There are two problems that I notice here. The first is that when you pass data to CVPixelBufferCreateWithBytes, via &data, you're actually passing a reference to the Data struct, not to its contents. Swift has an array-to-pointer conversion that means this would (at least partially) work if you tried passing e.g. a [UInt8], but with Data you need to use, in this case, withUnsafeMutableBytes.

The second issue you're going to run into is that CVPixelBufferCreateWithBytes doesn't copy the passed-in memory, so the Data would keep ownership. There's nothing keeping the Data alive while the CVPixelBuffer is alive, so the returned CVPixelBuffer will likely end up pointing to invalid memory.

You have a couple of options here. What I'd suggest is you use CVPixelBufferCreate, get its storage with CVPixelBufferGetBaseAddress, and then do:

data.copyBytes(to: CVPixelBufferGetBaseAddress(pixelBuffer).assumingMemoryBound(to: UInt8.self), count: data.count)

Alternatively, if you want to potentially avoid an allocation, you could do:

let nsData = data as NSMutableData
let pixelBuffer = CVPixelBufferCreateWithBytes(allocator, width, height, pixelFormat, nsData.mutableBytes, bytesPerRow, { dataRef, _ in Unmanaged<NSMutableData>.fromOpaque(dataRef).release() }, Unmanaged.passRetained(nsData).toOpaque(), nil)

(or strongly capture nsData within the release callback) to ensure the data stays live while the pixel buffer exists.

4 Likes

Thank you so much! Your guidance was dead on point and resolved everything, including my headache - I'm very grateful.

1 Like