Duncan Groenewald
Duncan Groenewald

Reputation: 8988

How does one render a CIImage to CAMetalLayer from background thread

Should it be possible to render to a CAMetalLayer from a background thread as shown below. Note that I have commented out the DispatchQueue.global.async{} since it generates a SwiftUI error because updates must be from the main thread.

If not then what is the correct/best way to do this to avoid blocking the UI thread - if that is even possible. I would like to render as the user drags the adjustment slider but there seems to be a performance hit with the UI becoming jerky if the image size is too big.

Somehow Pixelmator Pro appear to have implemented a method that allows adjustments to be applied to images with no noticeable UI lag or stutter. Any suggestions will be appreciated.

func display(_ layer: CALayer) {
        
        // DispatchQueue.global(qos: .userInitiated).async {
        
        if let drawable = self.metalLayer.nextDrawable(),
           let commandBuffer = self.commandQueue.makeCommandBuffer() {
            
            
            let colorAttachment = self.passDescriptor.colorAttachments[0]!
            colorAttachment.texture = drawable.texture
            colorAttachment.loadAction = .clear
            colorAttachment.storeAction = .dontCare
            colorAttachment.clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 0)
            
            if let rawFilter = self.rawFilter {
                
                // Required in order to clear the screen if no selection
                let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: self.passDescriptor)!
                renderEncoder.endEncoding()
                
                
                if let processed = self.process(rawFilter) {
                    
                    let x = self.size.width/2 - processed.extent.width/2
                    let y = self.size.height/2 - processed.extent.height/2
                    
                    
                    self.context.render(processed,
                                        to: drawable.texture,
                                        commandBuffer: commandBuffer,
                                        bounds: CGRect(x:-x, y:-y, width: self.size.width, height:  self.size.height),
                                        colorSpace: self.colorSpace)
                    
                    
                    
                }
            }
            else {
                // Required in order to clear the screen if no selection
                let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: self.passDescriptor)!
                renderEncoder.endEncoding()
                
            }
            
            commandBuffer.commit()
            commandBuffer.waitUntilScheduled()
            
            drawable.present()
            
            // }
        }
    }

Upvotes: 0

Views: 565

Answers (1)

Duncan Groenewald
Duncan Groenewald

Reputation: 8988

OK I just figured out why my solution was not working - I was creating an image histogram in the process() function and setting a UI control image - this just needed to be wrapped in a DispatchQueue.main.async{} call.

To prevent too many calls in response to a slider move don't call this unless the render cycle has completed

var isBusy = false

func display(_ layer: CALayer) {
    
   guard !self.isBusy else {
       return
   }
   self.isBusy = true

    DispatchQueue.global(qos: .userInitiated).async {
    
    if let drawable = self.metalLayer.nextDrawable(),
       let commandBuffer = self.commandQueue.makeCommandBuffer() {
        
        
        let colorAttachment = self.passDescriptor.colorAttachments[0]!
        colorAttachment.texture = drawable.texture
        colorAttachment.loadAction = .clear
        colorAttachment.storeAction = .dontCare
        colorAttachment.clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 0)
        
        if let rawFilter = self.rawFilter {
            
            // Required in order to clear the screen if no selection
            let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: self.passDescriptor)!
            renderEncoder.endEncoding()
            
            
            if let processed = self.process(rawFilter) {
                
                let x = self.size.width/2 - processed.extent.width/2
                let y = self.size.height/2 - processed.extent.height/2
                
                
                self.context.render(processed,
                                    to: drawable.texture,
                                    commandBuffer: commandBuffer,
                                    bounds: CGRect(x:-x, y:-y, width: self.size.width, height:  self.size.height),
                                    colorSpace: self.colorSpace)
                
                
                
            }
        }
        else {
            // Required in order to clear the screen if no selection
            let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: self.passDescriptor)!
            renderEncoder.endEncoding()
            
        }
        
        commandBuffer.commit()
        commandBuffer.waitUntilScheduled()
        
        // Present on the main thread - not sure if this is necessary but it seems like it to get the UI to update
        DispatchQueue.main.async {
           drawable.present()
        }
        self.isBusy = false
        }
    }
}

Upvotes: 1

Related Questions