Search code examples
swiftmacosstoryboardruntime-errormetal

CAMetalLayer nextDrawable returning nil because allocation failed


I'm new to Swift and Mac development and I'm trying to make a framework (only macOS) that can open a Metal window. I use Xcode Version 11.3.1 on macOS version 10.15.2.

I started from the examples that I found on the web (macOS/Command Line Tool project to start) and I do not see how to correct this error:

[CAMetalLayer nextDrawable] returning nil because allocation failed.

When the project is created with a storyboard (macOS/App project) the window opens with the expected content (filling in red) but when I try to replace the storyboard with code (macOS/Command Line Tool project) the execution loops on the error mentioned above .

Main.swift

import AppKit

class AppDelegate: NSObject, NSApplicationDelegate {
    var window: NSWindow!
    var viewController: ViewController!

    func applicationDidFinishLaunching(_ notification: Notification) {
        NSLog("Start app")

        viewController = ViewController()
        window = NSWindow(contentRect: NSMakeRect(0, 0, 1024, 768),
                          styleMask: .borderless,
                          backing: .buffered,
                          defer: false)

        window.contentViewController = viewController
        window.title = "Hey, new Window!"
        window.backgroundColor = NSColor.blue
        window.orderFrontRegardless()
    }

    func applicationWillTerminate(_ notification: Notification) {
        NSLog("Terminate app")
    }

    func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
        return true
    }
}

let app = NSApplication.shared
let appDelegate = AppDelegate()
app.delegate = appDelegate

app.run()

ViewController.swift

import Cocoa
import Metal
import MetalKit

class ViewController: NSViewController {

    var mtkView: MTKView!
    var renderer: Renderer!

    override func loadView() {
        mtkView = MTKView()
        self.view = mtkView
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        guard let defaultDevice = MTLCreateSystemDefaultDevice() else {
            print("Metal is not supported on this device")
            return
        }

        print("My GPU is: \(defaultDevice)")
        mtkView.device = defaultDevice

        guard let tempRenderer = Renderer(mtkView: mtkView) else {
            print("Renderer failed to initialize")
            return
        }
        renderer = tempRenderer

        mtkView.delegate = renderer
    }

    override var representedObject: Any? {
        didSet {
        // Update the view, if already loaded.
        }
    }
}

Renderer.swift

import Foundation
import Metal
import MetalKit

class Renderer : NSObject, MTKViewDelegate {

    let device: MTLDevice
    let commandQueue: MTLCommandQueue

    init?(mtkView: MTKView) {
        device = mtkView.device!
        commandQueue = device.makeCommandQueue()!
    }

    func draw(in view: MTKView) {
        guard let commandBuffer = commandQueue.makeCommandBuffer() else { return }

        guard let renderPassDescriptor = view.currentRenderPassDescriptor else { return }

        renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(1, 0, 0, 1)

        guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else { return }

        renderEncoder.endEncoding()

        commandBuffer.present(view.currentDrawable!)

        commandBuffer.commit()
    }

    func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {

    }
}

In my research I found only one post that could help me that was using NSViewControllerRepresentable but that did not solve my problem.


Solution

  • The most immediate issue here is that your CAMetalLayer, the layer that backs your MTKView, has size (0, 0). Therefore the layer can't allocate drawables for you to draw into.

    The reason your layer has zero size is because your MTKView has zero size, and the reason your view has zero size is because you create it without specifying a frame. Per the documentation, "Setting contentViewController causes the window to resize based on the current size of the contentViewController;" hence your window is also zero sized.

    Provide a size when creating your MTKView and things will "work":

    mtkView = MTKView(frame: NSMakeRect(0, 0, 100, 100))
    

    Echoing comments above, I recommend against this approach, since NSApplication's run() method doesn't do everything necessary to make the app a good macOS citizen. The app won't have a Dock tile, it won't have a main menu, and you won't be able to use common keyboard shortcuts like Cmd+Q without added effort. If you're designing a framework to make it easier for people to create windows with minimal effort, this is too minimal. Nor is it possible or supported to implement everything that NSApplicationMain does on your behalf with public API. Encourage users of your framework to follow platform design patterns rather than trying to wrest control away from them in the name of simplicity. If they're already running a full-fledged app with a proper run loop, you can create windows on their behalf without needing to touch the NSApplication stratum.