Last active
June 6, 2023 12:20
-
-
Save iby/7f4168df16b8bc170ef587344b6c1444 to your computer and use it in GitHub Desktop.
Rendering animated CALayer off-screen using CARenderer with MTLTexture, https://stackoverflow.com/q/56150363/458356
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import AppKit | |
import Metal | |
import QuartzCore | |
let view = NSView(frame: CGRect(x: 0, y: 0, width: 600, height: 400)) | |
let circle = NSView(frame: CGRect(x: 0, y: 0, width: 50, height: 50)) | |
circle.wantsLayer = true | |
circle.layer?.backgroundColor = NSColor.red.cgColor | |
circle.layer?.cornerRadius = 25 | |
view.wantsLayer = true | |
view.addSubview(circle) | |
let textureDescriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .rgba8Unorm, width: 600, height: 400, mipmapped: false) | |
textureDescriptor.usage = [MTLTextureUsage.shaderRead, .shaderWrite, .renderTarget] | |
let device = MTLCreateSystemDefaultDevice()! | |
let texture: MTLTexture = device.makeTexture(descriptor: textureDescriptor)! | |
let queue: MTLCommandQueue = device.makeCommandQueue()! | |
let context = CIContext(mtlDevice: device) | |
let renderer = CARenderer(mtlTexture: texture) | |
var passDescriptor: MTLRenderPassDescriptor = MTLRenderPassDescriptor() | |
passDescriptor.colorAttachments[0].texture = texture | |
passDescriptor.colorAttachments[0].storeAction = .store | |
passDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 0.25) | |
passDescriptor.colorAttachments[0].loadAction = .clear | |
renderer.layer = view.layer | |
renderer.bounds = view.frame | |
let outputURL: URL = try! FileManager.default.url(for: .downloadsDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent("Off-screen Render") | |
try? FileManager.default.removeItem(at: outputURL) | |
try! FileManager.default.createDirectory(at: outputURL, withIntermediateDirectories: true, attributes: nil) | |
var frameNumber: Int = 0 | |
func render() { | |
Swift.print("Rendering frame #\(frameNumber)…") | |
let renderCommandBuffer: MTLCommandBuffer = queue.makeCommandBuffer()! | |
let renderCommandEncoder: MTLRenderCommandEncoder = renderCommandBuffer.makeRenderCommandEncoder(descriptor: passDescriptor)! | |
renderCommandEncoder.endEncoding() | |
renderCommandBuffer.commit() | |
renderCommandBuffer.waitUntilCompleted() | |
renderer.beginFrame(atTime: CACurrentMediaTime(), timeStamp: nil) | |
renderer.addUpdate(renderer.bounds) | |
renderer.render() | |
renderer.endFrame() | |
let blitCommandBuffer: MTLCommandBuffer = queue.makeCommandBuffer()! | |
let blitCommandEncoder: MTLBlitCommandEncoder = blitCommandBuffer.makeBlitCommandEncoder()! | |
blitCommandEncoder.synchronize(resource: texture) | |
blitCommandEncoder.endEncoding() | |
blitCommandBuffer.commit() | |
blitCommandBuffer.waitUntilCompleted() | |
let ciImage: CIImage = CIImage(mtlTexture: texture)! | |
let cgImage: CGImage = context.createCGImage(ciImage, from: ciImage.extent)! | |
let url: URL = outputURL.appendingPathComponent("frame-\(frameNumber).png") | |
let destination: CGImageDestination = CGImageDestinationCreateWithURL(url as CFURL, kUTTypePNG, 1, nil)! | |
CGImageDestinationAddImage(destination, cgImage, nil) | |
guard CGImageDestinationFinalize(destination) else { fatalError() } | |
frameNumber += 1 | |
} | |
var timer: Timer? | |
NSAnimationContext.runAnimationGroup({ context in | |
context.duration = 0.25 | |
view.animator().frame.origin = CGPoint(x: 550, y: 350) | |
}, completionHandler: { | |
timer?.invalidate() | |
render() | |
Swift.print("Finished off-screen rendering of \(frameNumber) frames in \(outputURL.path)…") | |
}) | |
// Make the first render immediately after the animation start and after it completes. For the purpose | |
// of this demo timer is used instead of display link. | |
render() // Fails to render first frame. | |
Timer.scheduledTimer(withTimeInterval: 0, repeats: false, block: { _ in render() }) // Renders the second frame. | |
timer = Timer.scheduledTimer(withTimeInterval: 1 / 30, repeats: true, block: { _ in render() }) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment