Post

Replies

Boosts

Views

Activity

Crash when rendering CALayer using UIGraphicsImageRenderer on background thread
Hello! I’m experiencing a crash in my iOS/iPadOS app related to a CALayer rendering process. The crash occurs when attempting to render a UIImage on a background thread. The crashes are occurring in our production app, and while we can monitor them through Crashlytics, we are unable to reproduce the issue in our development environment. Relevant Code I have a custom view controller that handles rendering CALayers onto images. This method creates a CALayer on the main thread and then starts a detached task to render this CALayer into a UIImage. The whole idea is learnt from this StackOverflow post: https://stackoverflow.com/a/77834613/9202699 Here are key parts of my implementation: class MyViewController: UIViewController { @MainActor func renderToUIImage(size: CGSize, itemsToDraw: [MyDrawingItem], transform: CGAffineTransform) async -> UIImage? { // Create CALayer and add it to the view. CATransaction.begin() let customLayer = MyDrawingLayer() customLayer.setupContent(itemsToDraw: itemsToDraw) // Position the frame off-screen to it hidden. customLayer.frame = CGRect( origin: CGPoint(x: -100 - size.width, y: -100 - size.height), size: size) customLayer.masksToBounds = true customLayer.drawsAsynchronously = true view.layer.addSublayer(customLayer) CATransaction.commit() // Render CALayer to UIImage in background thread. let image = await Task.detached { customLayer.setNeedsDisplay() let renderer = UIGraphicsImageRenderer(size: size) let image = renderer.image { // CRASH happens on this line let cgContext = $0.cgContext cgContext.saveGState() cgContext.concatenate(transform) customLayer.render(in: cgContext) cgContext.restoreGState() } return image }.value // Remove the CALayer from the view. CATransaction.begin() customLayer.removeFromSuperlayer() CATransaction.commit() return image } } class MyDrawingLayer: CALayer { var itemsToDraw: [MyDrawingItem] = [] func setupContent(itemsToDraw: [MyDrawingItem]) { self.itemsToDraw = itemsToDraw } override func draw(in ctx: CGContext) { for item in itemsToDraw { // Render the item to the context (example pseudo-code). // All items are thread-safe to use. // Things to draw may include CGPath, CGImages, UIImages, NSAttributedString, etc. item.draw(in: ctx) } } } Crash Log The crash occurs at the following location: Crashed: com.apple.root.default-qos.cooperative 0 MyApp 0x5cb300 closure #1 in closure #1 in MyViewController.renderToUIImage(size: CGSize, itemsToDraw: [MyDrawingItem], transform: CGAffineTransform) + 4313002752 (<compiler-generated>:4313002752) 1 MyApp 0x5cb300 closure #1 in closure #1 in MyViewController.renderToUIImage(size: CGSize, itemsToDraw: [MyDrawingItem], transform: CGAffineTransform) + 4313002752 (<compiler-generated>:4313002752) 2 MyApp 0x1a4578 AnyModifier.modified(for:) + 4308649336 (<compiler-generated>:4308649336) 3 MyApp 0x7b4e64 thunk for @escaping @callee_guaranteed (@guaranteed UIGraphicsPDFRendererContext) -> () + 4315008612 (<compiler-generated>:4315008612) 4 UIKitCore 0x1489c0 -[UIGraphicsRenderer runDrawingActions:completionActions:format:error:] + 324 5 UIKitCore 0x14884c -[UIGraphicsRenderer runDrawingActions:completionActions:error:] + 92 6 UIKitCore 0x148778 -[UIGraphicsImageRenderer imageWithActions:] + 184 7 MyApp 0x5cb1c0 closure #1 in MyViewController.renderToUIImage(size: CGSize, itemsToDraw: [MyDrawingItem], transform: CGAffineTransform) + 100 (FileName.swift:100) 8 libswift_Concurrency.dylib 0x60f5c swift::runJobInEstablishedExecutorContext(swift::Job*) + 252 9 libswift_Concurrency.dylib 0x62514 swift_job_runImpl(swift::Job*, swift::SerialExecutorRef) + 144 10 libdispatch.dylib 0x15ec0 _dispatch_root_queue_drain + 392 11 libdispatch.dylib 0x166c4 _dispatch_worker_thread2 + 156 12 libsystem_pthread.dylib 0x3644 _pthread_wqthread + 228 13 libsystem_pthread.dylib 0x1474 start_wqthread + 8 Questions Is it safe to run UIGraphicsImageRenderer.image on the background thread? Given that I want to leverage GPU rendering, what are some best practices for rendering images off the main thread while ensuring stability? Are there alternatives to using UIGraphicsImageRenderer for background rendering that can still take advantage of GPU rendering? It is particularly interesting that the crash logs indicate the error may be related to UIGraphicsPDFRendererContext (crash log line number 3). It would be very helpful if someone could explain the connection between starting and drawing on a UIGraphicsImageRenderer and UIGraphicsPDFRendererContext. Any insights or guidance on this issue would be greatly appreciated. Thanks!!!
1
0
219
2w
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) }
0
1
278
Jan ’25
CATransaction commit [Crashed: com.apple.root.user-initiated-qos.cooperative]
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 cannot 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(). Crash traceback The method names in this traceback are mapped to those in the code sample below. The app name has been masked. Crashed: com.apple.root.user-initiated-qos.cooperative 0 MyApp 0x887408 specialized static CAUtils.commitCATransaction() + 4340151304 (<compiler-generated>:4340151304) 1 MyApp 0x887408 specialized static CAUtils.commitCATransaction() + 4340151304 (<compiler-generated>:4340151304) 2 MyApp 0x8874a4 specialized static CAUtils.addDrawingItemsToRenderer(***) + 250 (CAUtils.swift:250) 3 MyApp 0x887710 specialized static CAUtils.drawOnCGImageWithCARenderer(***) + 267 (CAUtils.swift:267) 4 MyApp 0x8878c0 specialized static CAUtils.drawOnCGImageWithCARendererWithRetry(***) + 315 (CAUtils.swift:315) 5 MyApp 0x736294 XXXManager.generateCGImages(***) + 570 (XXXManager.swift:570) 6 MyApp 0x73404c closure #1 in XXXManager.updateCachedCGImages(***) + 427 (XXXManager.swift:427) 7 libswift_Concurrency.dylib 0x61104 swift::runJobInEstablishedExecutorContext(swift::Job*) + 252 8 libswift_Concurrency.dylib 0x62514 swift_job_runImpl(swift::Job*, swift::SerialExecutorRef) + 144 9 libdispatch.dylib 0x15d8c _dispatch_root_queue_drain + 392 10 libdispatch.dylib 0x16590 _dispatch_worker_thread2 + 156 11 libsystem_pthread.dylib 0x4c40 _pthread_wqthread + 228 12 libsystem_pthread.dylib 0x1488 start_wqthread + 8 Code Sample Below is a sample of our code. While the complete snippet is too long, the issue occurs in addDrawingItemsToRenderer. Please refer to the other methods for completeness and reference purposes. private let transactionLock = NSLock() private let deviceLock = NSLock() private let device = MTLCreateSystemDefaultDevice()! /// This is the method we call from outside. @inline(never) static func drawOnCGImageWithCARenderer( layerRect: CGRect, drawingItems: [DrawingItem] ) -> CGImage? { guard let (texture, renderer) = addDrawingItemsToRenderer( layerRect: layerRect, drawingItems: drawingItems ) else { return nil } renderer.beginFrame(atTime: 0, timeStamp: nil) renderer.render() renderer.endFrame() guard let colorSpace = CGColorSpace(name: CGColorSpace.sRGB), let ciImage = CIImage(mtlTexture: texture, options: [.colorSpace: colorSpace]) else { return nil } let context = CIContext() return context.createCGImage(ciImage, from: ciImage.extent) } /// This is the method will the crash happens @inline(never) fileprivate static func addDrawingItemsToRenderer( layerRect: CGRect, drawingItems: [DrawingItem] ) -> (MTLTexture, CARenderer)? { // We have encapsulated everything related to CALayer and its // associated creations and manipulations within CATransaction // as suggested by engineers from Apple Feedback Portal. beginCATransaction() defer { commitCATransaction() // The crash happens here } let (layer, imageWidth, imageHeight) = addDrawingItemsToLayer(layerRect: layerRect, drawingItems: drawingItems) return createTextureAndRenderer( layer: layer, imageWidth: imageWidth, imageHeight: imageHeight ) } // Below are all internal methods. We have split the method into very // granular parts and marked them as @inline(never) to prevent the // compiler from inlining our code, which may otherwise obscure usage // trackback information in our crash reports. @inline(never) fileprivate static func beginCATransaction() { transactionLock.lock() CATransaction.begin() } @inline(never) fileprivate static func commitCATransaction() { // From our crash report, we believe the crash happens on this line. CATransaction.commit() // It is unlikely that the lock cause the crash as we added it only recently // to ensure that there is only one transaction on our background thread, // and after we added this lock, the crash rate indeed lowered, but still // not fully disappear transactionLock.unlock() } -------------------------------- // The methods below are provided for reference and completeness. While // they may have issues, they do not frequently appear in our crash // reports as the one caused by `CATransaction.commit()` @inline(never) fileprivate static func addDrawingItemsToLayer( layerRect: CGRect, drawingItems: [DrawingItem] ) -> (layer: CALayer, imageWidth: CGFloat, imageHeight: CGFloat) { let layer = CALayer() layer.isGeometryFlipped = SharedAppUtils.isIOS layer.anchorPoint = CGPoint.zero layer.bounds = layerRect layer.masksToBounds = true for drawingItem in drawingItems { // 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. let sublayerForDrawingItem = drawingItem.createCALayerOrCATextLayerOrCAShapeLayer() layer.addSublayer(sublayerForDrawingItem) } let imageWidth = max(1, layer.frame.size.width * UIScreen.main.scale) let imageHeight = max(1, layer.frame.size.height * UIScreen.main.scale) layer.transform = CATransform3DMakeScale(UIScreen.main.scale, UIScreen.main.scale, 1) layer.frame = .init(origin: .zero, size: .init(width: imageWidth, height: imageHeight)) return (layer, imageWidth, imageHeight) } @inline(never) fileprivate static func createTextureAndRenderer( layer: CALayer, imageWidth: CGFloat, imageHeight: CGFloat ) -> (MTLTexture, CARenderer)? { deviceLock.lock() defer { deviceLock.unlock() } let textureDescriptor = MTLTextureDescriptor.texture2DDescriptor( pixelFormat: .rgba8Unorm, width: Int(imageWidth), height: Int(imageHeight), mipmapped: false ) textureDescriptor.usage = [MTLTextureUsage.shaderRead, .shaderWrite, .renderTarget] guard let texture = device.makeTexture(descriptor: textureDescriptor) else { return nil } let renderer = CARenderer(mtlTexture: texture) renderer.bounds = layer.frame renderer.layer = layer.self return (texture, renderer) }
1
1
279
Jan ’25