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?
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.