Reputation: 8988
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
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