Search code examples
swiftmetal

Can I program Apple Metal graphics without Xcode?


Is it possible to code a Metal graphics program with only an editor an the terminal? If it is, What would be an example of a minimal application?

I'd like to try some generative coding but without Xcode. I was able to make small C or Python programs with OpenGL but its deprecation by Apple is making me consider Metal.

Can I use Swift and some header inclusions or something like that? or do I need a whole lot more?


Solution

  • You can compile and run an app that uses Metal from the command line with no Xcode project at all, but you still need the infrastructure (SDK and toolchain) provided by Xcode, so you'll need to have it installed.

    It's quite straightforward to write a simple app in Swift that creates the requisite Metal objects, encodes some work, and writes the result to a file. For the purposes of this question, I'll provide the source for a simple Cocoa app that goes a bit further by creating a window that hosts a MetalKit view and draws to it. With this scaffolding, you could write a very sophisticated app without ever launching Xcode.

    import Foundation
    import Cocoa
    import Metal
    import MetalKit
    
    class AppDelegate : NSObject, NSApplicationDelegate {
        let window = NSWindow()
        let windowDelegate = WindowDelegate()
        var rootViewController: NSViewController?
    
        func applicationDidFinishLaunching(_ notification: Notification) {
            window.setContentSize(NSSize(width: 800, height: 600))
            window.styleMask = [ .titled, .closable, .miniaturizable, .resizable ]
            window.title = "Window"
            window.level = .normal
            window.delegate = windowDelegate
            window.center()
    
            let view = window.contentView!
            rootViewController = ViewController(nibName: nil, bundle: nil)
            rootViewController!.view.frame = view.bounds
            view.addSubview(rootViewController!.view)
    
            window.makeKeyAndOrderFront(window)
    
            NSApp.activate(ignoringOtherApps: true)
        }
    }
    
    class WindowDelegate : NSObject, NSWindowDelegate {
        func windowWillClose(_ notification: Notification) {
            NSApp.terminate(self)
        }
    }
    
    class ViewController : NSViewController, MTKViewDelegate {
        var device: MTLDevice!
        var commandQueue: MTLCommandQueue!
    
        override init(nibName nibNameOrNil: NSNib.Name?, bundle nibBundleOrNil: Bundle?) {
            super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
        }
    
        required init?(coder: NSCoder) {
            super.init(coder: coder)
        }
    
        override func loadView() {
            device = MTLCreateSystemDefaultDevice()!
            commandQueue = device.makeCommandQueue()!
    
            let metalView = MTKView(frame: .zero, device: device)
            metalView.clearColor = MTLClearColorMake(0, 0, 1, 1)
            metalView.delegate = self
    
            self.view = metalView
        }
    
        func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
        }
    
        func draw(in view: MTKView) {
            guard let commandBuffer = commandQueue.makeCommandBuffer(),
                  let passDescriptor = view.currentRenderPassDescriptor else { return }
            if let commandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: passDescriptor) {
                // set state, issue draw calls, etc.
                commandEncoder.endEncoding()
            }
            commandBuffer.present(view.currentDrawable!)
            commandBuffer.commit()
        }
    }
    
    func makeMainMenu() -> NSMenu {
        let mainMenu = NSMenu()
        let mainAppMenuItem = NSMenuItem(title: "Application", action: nil, keyEquivalent: "")
        let mainFileMenuItem = NSMenuItem(title: "File", action: nil, keyEquivalent: "")
        mainMenu.addItem(mainAppMenuItem)
        mainMenu.addItem(mainFileMenuItem)
    
        let appMenu = NSMenu()
        mainAppMenuItem.submenu = appMenu
    
        let appServicesMenu = NSMenu()
        NSApp.servicesMenu = appServicesMenu
    
        appMenu.addItem(withTitle: "Hide", action: #selector(NSApplication.hide(_:)), keyEquivalent: "h")
        appMenu.addItem({ () -> NSMenuItem in
            let m = NSMenuItem(title: "Hide Others", action: #selector(NSApplication.hideOtherApplications(_:)), keyEquivalent: "h")
            m.keyEquivalentModifierMask = [.command, .option]
            return m
        }())
        appMenu.addItem(withTitle: "Show All", action: #selector(NSApplication.unhideAllApplications(_:)), keyEquivalent: "")
    
        appMenu.addItem(NSMenuItem.separator())
        appMenu.addItem(withTitle: "Services", action: nil, keyEquivalent: "").submenu = appServicesMenu
        appMenu.addItem(NSMenuItem.separator())
        appMenu.addItem(withTitle: "Quit", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
    
        let fileMenu = NSMenu(title: "Window")
        mainFileMenuItem.submenu = fileMenu
        fileMenu.addItem(withTitle: "Close", action: #selector(NSWindowController.close), keyEquivalent: "w")
    
        return mainMenu
    }
    
    let app = NSApplication.shared
    NSApp.setActivationPolicy(.regular)
    
    NSApp.mainMenu = makeMainMenu()
    
    let appDelegate = AppDelegate()
    NSApp.delegate = appDelegate
    
    NSApp.run()
    

    Although this looks like a lot of code, the bulk of it is Cocoa boilerplate for creating and interacting with a window and its main menu. The Metal code is only a few lines tucked away in the middle (see func draw).

    To build and run this app, save the code to a Swift file (I called mine MinimalMetal.swift) and use xcrun to locate the tools and SDKs necessary to build:

    xcrun -sdk macosx swiftc MinimalMetal.swift -o MinimalMetal
    

    This creates an executable named "MinimalMetal" in the same directory, which you can run with

    ./MinimalMetal 
    

    This is a full-fledged app, with a window, menu, runloop, and 60 FPS drawing. From here, you can use all the features of Metal you might want to.