Why is an iPhone XS getting worse CPU performance when using the camera live than an iPhone 6S Plus?

I'm using live camera output to update a CIImage on a MTKView. My main issue is that I have a large, negative performance difference where an older iPhone gets better CPU performance than a newer one, despite all their settings I've come across are the same.

This is a lengthy post, but I decided to include these details since they could be important to the cause of this problem. Please let me know what else I can include.


Below, I have my captureOutput function with two debug bools that I can turn on and off while running. I used this to try to determine the cause of my issue.


applyLiveFilter - bool whether or not to manipulate the CIImage with a CIFilter.

updateMetalView - bool whether or not to update the MTKView's CIImage.


// live output from camera
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection){


    /*              
     Create CIImage from camera.
     Here I save a few percent of CPU by using a function 
     to convert a sampleBuffer to a Metal texture, but
     whether I use this or the commented out code 
     (without captureOutputMTLOptions) does not have 
     significant impact. 
    */


    guard let texture:MTLTexture = convertToMTLTexture(sampleBuffer: sampleBuffer) else{
        return
    }


    var cameraImage:CIImage = CIImage(mtlTexture: texture, options: captureOutputMTLOptions)!


    var transform: CGAffineTransform = .identity


    transform = transform.scaledBy(x: 1, y: -1)


    transform = transform.translatedBy(x: 0, y: -cameraImage.extent.height)


    cameraImage = cameraImage.transformed(by: transform)


    /*
    // old non-Metal way of getting the ciimage from the cvPixelBuffer
    guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else
    {
        return
    }


    var cameraImage:CIImage = CIImage(cvPixelBuffer: pixelBuffer)
    */


    var orientation = UIImage.Orientation.right


    if(isFrontCamera){
        orientation = UIImage.Orientation.leftMirrored
    }


    // apply filter to camera image
    if debug_applyLiveFilter {
        cameraImage = self.applyFilterAndReturnImage(ciImage: cameraImage, orientation: orientation, currentCameraRes:currentCameraRes!)
    }


    DispatchQueue.main.async(){
        if debug_updateMetalView {
            self.MTLCaptureView!.image = cameraImage
        }
    }


}

Below is a chart of results between both phones toggling the different combinations of bools discussed above:

Link to Performance Chart


Even without the Metal view's CIIMage updating and no filters being applied, the iPhone XS's CPU is 2% greater than iPhone 6S Plus's, which isn't a significant overhead, but makes me suspect that somehow how the camera is capturing is different between the devices.

  • My AVCaptureSession's preset is set identically between both phones (AVCaptureSession.Preset.hd1280x720)
  • The CIImage created from captureOutput is the same size (extent) between both phones.


Are there any settings I need to set manually between these two phones AVCaptureDevice's settings, including activeFormat properties, to make them the same between devices?

The settings I have now are:


if let captureDevice = AVCaptureDevice.default(for:AVMediaType.video) {
    do {
        try captureDevice.lockForConfiguration()
            captureDevice.isSubjectAreaChangeMonitoringEnabled = true
            captureDevice.focusMode = AVCaptureDevice.FocusMode.continuousAutoFocus
            captureDevice.exposureMode = AVCaptureDevice.ExposureMode.continuousAutoExposure
        captureDevice.unlockForConfiguration()


    } catch {
        // Handle errors here
        print("There was an error focusing the device's camera")
    }
}


My MTKView is based off code written by Simon Gladman, with some edits for performance and to scale the render before it is scaled up to the width of the screen using Core Animation suggested by Apple.


class MetalImageView: MTKView
{
    let colorSpace = CGColorSpaceCreateDeviceRGB()


    var textureCache: CVMetalTextureCache?


    var sourceTexture: MTLTexture!


    lazy var commandQueue: MTLCommandQueue =
        {
            [unowned self] in


            return self.device!.makeCommandQueue()
            }()!


    lazy var ciContext: CIContext =
        {
            [unowned self] in


            return CIContext(mtlDevice: self.device!)
            }()




    override init(frame frameRect: CGRect, device: MTLDevice?)
    {
        super.init(frame: frameRect,
                   device: device ?? MTLCreateSystemDefaultDevice())






        if super.device == nil
        {
            fatalError("Device doesn't support Metal")
        }


        CVMetalTextureCacheCreate(kCFAllocatorDefault, nil, self.device!, nil, &textureCache)


        framebufferOnly = false


        enableSetNeedsDisplay = true


        isPaused = true


        preferredFramesPerSecond = 30
    }


    required init(coder: NSCoder)
    {
        fatalError("init(coder:) has not been implemented")
    }


    // The image to display
    var image: CIImage?
    {
        didSet
        {
            setNeedsDisplay()
        }
    }


    override func draw(_ rect: CGRect)
    {
        guard var
            image = image,
            let targetTexture:MTLTexture = currentDrawable?.texture else
        {
            return
        }


        let commandBuffer = commandQueue.makeCommandBuffer()


        let customDrawableSize:CGSize = drawableSize


        let bounds = CGRect(origin: CGPoint.zero, size: customDrawableSize)


        let originX = image.extent.origin.x
        let originY = image.extent.origin.y


        let scaleX = customDrawableSize.width / image.extent.width
        let scaleY = customDrawableSize.height / image.extent.height


        let scale = min(scaleX*IVScaleFactor, scaleY*IVScaleFactor)
        image = image
            .transformed(by: CGAffineTransform(translationX: -originX, y: -originY))
            .transformed(by: CGAffineTransform(scaleX: scale, y: scale))




        ciContext.render(image,
                         to: targetTexture,
                         commandBuffer: commandBuffer,
                         bounds: bounds,
                         colorSpace: colorSpace)




        commandBuffer?.present(currentDrawable!)


        commandBuffer?.commit()


    }


}



My AVCaptureSession (captureSession) and AVCaptureVideoDataOutput (videoOutput) are setup below:


func setupCameraAndMic(){
    let backCamera = AVCaptureDevice.default(for:AVMediaType.video)


    var error: NSError?
    var videoInput: AVCaptureDeviceInput!
    do {
        videoInput = try AVCaptureDeviceInput(device: backCamera!)
    } catch let error1 as NSError {
        error = error1
        videoInput = nil
        print(error!.localizedDescription)
    }


    if error == nil &&
        captureSession!.canAddInput(videoInput) {


        guard CVMetalTextureCacheCreate(kCFAllocatorDefault, nil, MetalDevice, nil, &textureCache) == kCVReturnSuccess else {
            print("Error: could not create a texture cache")
            return
        }


        captureSession!.addInput(videoInput)            


        setDeviceFrameRateForCurrentFilter(device:backCamera)


        stillImageOutput = AVCapturePhotoOutput()


        if captureSession!.canAddOutput(stillImageOutput!) {
            captureSession!.addOutput(stillImageOutput!)


            let q = DispatchQueue(label: "sample buffer delegate", qos: .default)
            videoOutput.setSampleBufferDelegate(self, queue: q)
            videoOutput.videoSettings = [
                kCVPixelBufferPixelFormatTypeKey as AnyHashable as! String: NSNumber(value: kCVPixelFormatType_32BGRA),
                kCVPixelBufferMetalCompatibilityKey as String: true
            ]
            videoOutput.alwaysDiscardsLateVideoFrames = true


            if captureSession!.canAddOutput(videoOutput){
                captureSession!.addOutput(videoOutput)
            }


            captureSession!.startRunning()


        }


    }


    setDefaultFocusAndExposure()
}

The video and mic are recorded on two separate streams. Details on the microphone and recording video have been left out since my focus is performance of live camera output.

Replies

2% might be the debug environment. Can we see your performance check data too?



Also, which cameras are you testing? Front, rear, both?

Any Apple Staff want to jump on this and care to explain?

Here is a MTKView draw function that anyone can test between devices. The filter is applied inside draw. Compare the performance between newer devices (iPhone X, XS) to older ones (iPhone 6S). You will see that the older phones perform better, even with battery impact. Add more CIFilters and and eventually the newer phones will be impacted far worse than older models.


override func draw(_ rect: CGRect)
{
  autoreleasepool {

  _ = semaphore.wait(timeout: DispatchTime.distantFuture)

  guard
  var image = image,
  let commandBuffer:MTLCommandBuffer = commandQueue.makeCommandBuffer(),
  let targetTexture:MTLTexture = currentDrawable?.texture else
  {
  semaphore.signal()
  return
  }

  let customDrawableSize:CGSize = drawableSize

  let bounds = CGRect(origin: CGPoint.zero, size: customDrawableSize)

  // apply filter
  image = image
  .applyingFilter("CIHueAdjust", parameters: [kCIInputImageKey: image,
  kCIInputAngleKey: 2.3])



  let originX = image.extent.origin.x
  let originY = image.extent.origin.y

  let scaleX = customDrawableSize.width / image.extent.width
  let scaleY = customDrawableSize.height / image.extent.height


  let scale = min(scaleX, scaleY)

  image = image
  .transformed(by: CGAffineTransform(translationX: -originX, y: -originY))
  .transformed(by: CGAffineTransform(scaleX: scale, y: scale))

  ciContext.render(image,
  to: targetTexture,
  commandBuffer: commandBuffer,
  bounds: bounds,
  colorSpace: colorSpace)

  commandBuffer.addScheduledHandler { [weak self] (buffer) in
  guard let unwrappedSelf = self else { return }
  readyForNewImage = true
  unwrappedSelf.semaphore.signal()
  }

  commandBuffer.present(currentDrawable!)
  commandBuffer.commit()


  }

}


This has held back our release for two months now. If anyone has any insight, please share. Thank you.

I am farily new to using Metal, and I'm working on the macOS only, so I don't know if what I suggest will help or not, and it can only be because of differences between your code and mine (language aside).


1. Your "draw" code is a lot more complicated than what I am using, this might be because I'm using a MetalLayer atatched to view (I didn't choose this method for any other reason than I've worked with CALayers before).


The sequence for updating that I use is as follows.

a. [CIMetalLayer nextDrawable]

b. [CIContext render:toMTLTexture...] using [CAMetalDrawable texture]

c. [CAMetalDrawable present]

d. [CAMetalLayer removeAllAnimations]


2. For a long time, Apple have advised against resizing images in Core Image when performance is critical, I see that you're using an affine transform to scale the image. Instead it is suggested that images be resized via Core Graphics before being converted to a CIImage.

My drawable size and image size are already configured to match.


It might not be of any use, but I thought I'd give it a try.

rowlands - Thanks for your response.


I was advised to use currentDrawable instead of nextDrawable. In either case, I did not get a performance difference. I don't have animations to remove or a CAMetalLayer. I've also experimented not scaling in draw or anywhere else, and it had very minimal or no impact. Even when setting the bounds in render to 10x10 pixels, it still uses way too much CPU on an iPhone XS.


I'll be posting a minimized version of this project on github soon, and will reply with the link. I hope the staff at Apple or other experts can take a crack at it and find out where the problem is.

To Apple Staff, and everyone else: Here is an example project that you can test for yourself: https://github.com/PunchyBass/Live-Filter-test-project


What to look for: Better performance difference on older Metal-capable iPhones than newer ones. This was personally tested with an iPhone 6S Plus and iPhone XS.

iPhone XS does not seem to ever change the drawableSize property in MTKViews when they change size. This goes against what is said in the documentation about drawableSize, but works correctly on iPhone 7 Plus and iPhone 6S Plus. I think this is a bug and a big part of the performance problem.

>I think this is a bug


File one via the link below, being sure to add your report # to your thread for reference - good luck.