inrascable
inrascable

Reputation: 23

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?

Upvotes: 2

Views: 2234

Answers (2)

roberto
roberto

Reputation: 587

you can with python, try this: https://github.com/wtnb75/runmetal you can compile a metal kernel in a string and link it with numpy arrays

Upvotes: 0

warrenm
warrenm

Reputation: 31782

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.

Upvotes: 8

Related Questions