CATransaction commit() crashed on background thread [EXC_BREAKPOINT: com.apple.root.****-qos.cooperative]

Problem Description

We are developing a app for iOS and iPadOS that involves extensive custom drawing of paths, shapes, texts, etc. To improve drawing and rendering speed, we use CARenderer to generate cached images (CGImage) on a background thread. We adopted this approach based on this StackOverflow post: https://stackoverflow.com/a/75497329/9202699.

However, we are experiencing frequent crashes in our production environment that we can hardly reproduce in our development environment. Despite months of debugging and seeking support from DTS and the Apple Feedback platform, we have not been able to fully resolve this issue. Our recent crash reports indicate that the crashes occur when calling CATransaction.commit().

We suspect that CATransaction may not be functioning properly outside the main thread. However, based on feedback from the Apple Feedback platform, we were advised to use CATransaction.begin() and CATransaction.commit() on a background thread.

If anyone has any insights, we would greatly appreciate it.

Code Sample

The line CATransaction.commit() is causing the crash: [EXC_BREAKPOINT: com.apple.root.****-qos.cooperative]

private let transactionLock = NSLock() // to ensure one transaction at a time
private let device = MTLCreateSystemDefaultDevice()!

@inline(never)
static func drawOnCGImageWithCARenderer(
  layerRect: CGRect,
  itemsToDraw: [ItemsToDraw]
)
  -> CGImage? {
  // We have encapsulated everything related to CALayer and its
  // associated creations and manipulations within CATransaction
  // as suggested by engineers from Apple Feedback Portal.
  transactionLock.lock()
  CATransaction.begin()

  // Create the root layer.
  let layer = CALayer()
  layer.bounds = layerRect
  layer.masksToBounds = true

  // Add one sublayer for each item to draw
  itemsToDraw.forEach { item in
    // We have thousands or hundred thousands of drawing items to add.
    // Each drawing item may produce a CALayer, CAShapeLayer or CATextLayer.
    // This is also why we want to utilise CARenderer to leverage GPU rendering.
    layer.addSublayer(
      item.createCALayerOrCATextLayerOrCAShapeLayer()
    )
  }

  // Create MTLTexture and CARenderer.
  let textureDescriptor = MTLTextureDescriptor.texture2DDescriptor(
    pixelFormat: .rgba8Unorm,
    width: Int(layer.frame.size.width),
    height: Int(layer.frame.size.height),
    mipmapped: false
  )
  textureDescriptor.usage = [MTLTextureUsage.shaderRead, .shaderWrite, .renderTarget]
  let texture = device.makeTexture(descriptor: textureDescriptor)!
  let renderer = CARenderer(mtlTexture: texture)
  renderer.bounds = layer.frame
  renderer.layer = layer.self

  /* ********************************************************* */
  // From our crash report, this is where the crash happens.
  CATransaction.commit()
  /* ********************************************************* */
  transactionLock.unlock()

  // Rendering layers onto MTLTexture using CARenderer.
  renderer.beginFrame(atTime: 0, timeStamp: nil)
  renderer.render()
  renderer.endFrame()

  // Draw MTLTexture onto image.
  guard
    let colorSpace = CGColorSpace(name: CGColorSpace.sRGB),
    let ciImage = CIImage(mtlTexture: texture, options: [.colorSpace: colorSpace]) else {
    return nil
  }

  // Convert CIImage to CGImage.
  let context = CIContext()
  return context.createCGImage(ciImage, from: ciImage.extent)
}
CATransaction commit() crashed on background thread [EXC_BREAKPOINT: com.apple.root.****-qos.cooperative]
 
 
Q