Render CAShapeLayers on CAMetalLayer

76 Views Asked by At

I am trying to draw a few hundred static paths to an image as fast as possible. So far the fastest solution I've come up with is using CAShapeLayers:

let layer = CALayer()

let f = CGRect(x: 0.0, y: 0.0, width: 1024.0, height: 1024.0)
let cgColor = UIColor.orange.cgColor

var lines: [CGPath] // populated with several hundred paths, some with hundreds of points

let start = CFAbsoluteTimeGetCurrent()
    
for path in lines {
    let pathLayer = CAShapeLayer()
    pathLayer.path = path
    pathLayer.strokeColor = cgColor
    pathLayer.fillColor = nil
    pathLayer.lineWidth = 1.0
    pathLayer.drawsAsynchronously = drawsAsynchronously
    layer.addSublayer(pathLayer)
}

UIGraphicsBeginImageContext(f.size)
let ctx = UIGraphicsGetCurrentContext()
layer.render(in: ctx!)
let newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext()

//time: 0.05170309543609619
print("time: \(CFAbsoluteTimeGetCurrent() - start)")

Drawing the lines takes about 0.001 a second, producing an image is what is taking most the time. Also, the above method is about the same speed or perhaps a bit faster than using CARenderer. So I've tried using a CAMetalLayer and generating an image with Metal instead of CoreGraphics.

I can verify that the lines are drawn to the layer, either by adding the layer to a view or rendering it with the above method, but I get a blank image out using CAMetalLayer's mtlTexture.

let layer = CAMetalLayer()
let f = CGRect(x: 0.0, y: 0.0, width: 1024.0, height: 1024.0)
shapeLayer.device = MTLCreateSystemDefaultDevice()!
shapeLayer.bounds = f
shapeLayer.frame = f
shapeLayer.pixelFormat = .bgra8Unorm
shapeLayer.framebufferOnly = false

for path in lines {
    let pathLayer = CAShapeLayer()
    pathLayer.path = path
    pathLayer.strokeColor = cgColor
    pathLayer.fillColor = nil
    pathLayer.lineWidth = 1.0
    pathLayer.drawsAsynchronously = drawsAsynchronously
    layer.addSublayer(pathLayer)
}

let outputImage = CIImage(mtlTexture: layer.nextDrawable()!.texture)

The resulting mage is blank: enter image description here

I tried using a MTLRenderPassDescriptor but the image is blank as well:

public func render(caLayer: CAMetalLayer) -> UIImage? {
    
    let passDescriptor = MTLRenderPassDescriptor()
    let tex = caLayer.nextDrawable()?.texture
    
    passDescriptor.colorAttachments[0].texture = tex
    passDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(0, 0.5, 1, 0.05);
    passDescriptor.colorAttachments[0].loadAction = .clear;
    passDescriptor.colorAttachments[0].storeAction = .store;

    guard let device = MTLCreateSystemDefaultDevice(),
          let commandQueue = device.makeCommandQueue(),
          let commandBuffer = commandQueue.makeCommandBuffer() else { return nil }
            
    let renderCommandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: passDescriptor)
    renderCommandEncoder?.endEncoding()
    commandBuffer.commit()
    
    commandBuffer.waitUntilCompleted()

    let ciImage: CIImage = CIImage(mtlTexture: tex!)!
    let img = UIImage(ciImage: ciImage)
    
    return img
    
}
0

There are 0 best solutions below