CAMetalLayer.nextDrawable() is very time-consuming, take 5ms-12ms

I think CAMetalLayer.nextDrawable() shouldn't be very time-consuming, it should be less than 1ms. But occasionally, it will take more than 5ms, just 7ms~13ms, which is a very long time. How can we optimize this? Since this will cause rendering junk.....

How did you measure that? nextDrawable() may block if previously requested drawable are still being used, which means that you're asking too much from the GPU and it's not able to give back the drawables quickly enough.

You should first profile with Metal System Trace in Instruments: https://developer.apple.com/documentation/metal/performance_tuning/using_metal_system_trace_in_instruments_to_profile_your_app

This will show when nextDrawable is blocked.

Hi, Here is the code I use to measure:


class TestVC: UIViewController {
    
    let metalLayer:CAMetalLayer = CAMetalLayer()
    
    var displayLink:CADisplayLink!
    
    var thread:Thread!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = .red
        let scale = UIScreen.main.scale
        metalLayer.frame = self.view.bounds
        metalLayer.drawableSize = CGSize(width: self.view.frame.width * scale, height: self.view.frame.height * scale)
        self.view.layer.addSublayer(metalLayer)
        
        self.addDisplayLinkInTask()
    }
    
    func addDisplayLinkInTask() {
        self.thread = Thread(block: {
            RunLoop.current.add(NSMachPort(), forMode: .common)
            RunLoop.current.run()
        })
        self.thread.name = "sub thread"
        self.thread.start()
        
        self.perform(#selector(addDisplayLink), on: thread, with: nil, waitUntilDone: false)
    }
    
    @objc func addDisplayLink() {
        self.displayLink = CADisplayLink(target: self, selector: #selector(onDisplayLink))
        if #available(iOS 15.0, *) {
            self.displayLink.preferredFrameRateRange = .init(minimum: 60, maximum: 120, preferred: 120)
        } else {
            self.displayLink.preferredFramesPerSecond = 120
        }
        self.displayLink.add(to: .current, forMode: .common)
    }
    
    @objc private func onDisplayLink() {        
        let startTime = CACurrentMediaTime()
        let frameDrawable = metalLayer.nextDrawable()!
        let timeUsed = CACurrentMediaTime() - startTime
        
        // If time used to get next drawble over 3ms,
        // we print it here to indicate this method take much time!
        if (timeUsed > 0.003) {
            print("CAMetalLayer.nextDrawable take much time!! -> \(String(format: "%.2f", timeUsed * 1000)) ms")
        }
        frameDrawable.present()
    }
}

I tried a similar way with MTKView as I'm more used to it.

class ViewController: UIViewController, MTKViewDelegate {
    var mtkView: MTKView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        mtkView = MTKView(frame: view.bounds, device: MTLCreateSystemDefaultDevice()!)
        mtkView.preferredFramesPerSecond = 120
        mtkView.autoResizeDrawable = true
        mtkView.delegate = self

        mtkView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(mtkView)
        
        NSLayoutConstraint.activate([
            mtkView.leftAnchor.constraint(equalTo: view.leftAnchor),
            mtkView.rightAnchor.constraint(equalTo: view.rightAnchor),
            mtkView.topAnchor.constraint(equalTo: view.topAnchor),
            mtkView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
        ])
    }
    
    func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {}
    
    func draw(in view: MTKView) {
        let startTime = CACurrentMediaTime()
        let drawable = view.currentDrawable
        let timeUsed = CACurrentMediaTime() - startTime
        if (timeUsed > 0.003) {
            print("CAMetalLayer.nextDrawable take much time!! -> \(String(format: "%.2f", timeUsed * 1000)) ms")
        }
        
        guard let drawable else {
            print("no drawable available")
            return
        }

        drawable.present()
    }
}

and it gives similar logs. However, looking at the profiling trace I don't think there's an issue at all:

You can see with "LCD" line that surface is regularly displayed every 16ms, and with "Time Profiler" line that CPU isn't busy. This shows that getting the current/next drawable is not slow but is just blocking because it's too early. My understanding is that the draw() method (or in your case the onDisplayLink() method) is called "too early". In fact it's not really too early: it's a bit ahead of time to let you perform some work in advance before you actually need the drawable. This is described in more details there: https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/MTLBestPracticesGuide/Drawables.html

CAMetalLayer.nextDrawable() is very time-consuming, take 5ms-12ms
 
 
Q