user14154190
user14154190

Reputation:

How, exactly, do I render Metal on a background thread?

This problem is caused by user interface interactions such as showing the titlebar while in fullsreen. That question's answer provides a solution, but not how to implement that solution.

The solution is to render on a background thread. The issue is, the code provided in Apple's is made to cover a lot of content so most of it will extraneous code, so even if I could understand it, it isn't feasible to use Apple's code. And I can't understand it so it just plain isn't an option. How would I make a simple Swift Metal game use a background thread being as concise as possible?

Take this, for example:

class ViewController: NSViewController {
    var MetalView: MTKView {
        return view as! MTKView
    }
    
    var Device: MTLDevice = MTLCreateSystemDefaultDevice()!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        MetalView.delegate = self
        MetalView.device = Device
        MetalView.colorPixelFormat = .bgra8Unorm_srgb
        Device = MetalView.device
        //setup code
    }
}

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

    }
    
    func draw(in view: MTKView) {
        //drawing code
    }
}

That is the start of a basic Metal game. What would that code look like, if it were rendering on a background thread?

To fix that bug when showing the titlebar in Metal, I need to render it on a background thread. Well, how do I render it on a background thread?

I've noticed this answer suggests to manually redraw it 60 times a second. Presumably using a loop that is on a background thread? But that seems... not a clean way to fix it. Is there a cleaner way?

Upvotes: 4

Views: 2756

Answers (1)

jtbandes
jtbandes

Reputation: 118671

The main trick in getting this to work seems to be setting up the CVDisplayLink. This is awkward in Swift, but doable. After some work I was able to modify the "Game" template in Xcode to use a custom view backed by CAMetalLayer instead of MTKView, and a CVDisplayLink to render in the background, as suggested in the sample code you linked — see below.


Edit Oct 22:
The approach mentioned in this thread seems to work just fine: still using an MTKView, but drawing it manually from the display link callback. Specifically I was able to follow these steps:

  1. Create a new macOS Game project in Xcode.
  2. Modify GameViewController to add a CVDisplayLink, similar to below (see this question for more on using CVDisplayLink from Swift). Start the display link in viewWillAppear and stop it in viewWillDisappear.
  3. Set mtkView.isPaused = true in viewDidLoad to disable automatic rendering, and instead explicitly call mtkView.draw() from the display link callback.

The full content of my modified GameViewController.swift is available here.

I didn't review the Renderer class for thread safety, so I can't be sure no more changes are required, but this should get you up and running.


Older implementation with CAMetalLayer instead of MTKView:

This is just a proof of concept and I can't guarantee it's the best way to do everything. You might find these articles helpful too:

class MyMetalView: NSView {
  var displayLink: CVDisplayLink?
  var metalLayer: CAMetalLayer!

  override init(frame frameRect: NSRect) {
    super.init(frame: frameRect)
    setupMetalLayer()
  }
  required init?(coder: NSCoder) {
    super.init(coder: coder)
    setupMetalLayer()
  }
  override func makeBackingLayer() -> CALayer {
    return CAMetalLayer()
  }
  func setupMetalLayer() {
    wantsLayer = true
    metalLayer = layer as! CAMetalLayer?
    metalLayer.device = MTLCreateSystemDefaultDevice()!
    // ...other configuration of the metalLayer...
  }

  // handle display link callback at 60fps
  static let _outputCallback: CVDisplayLinkOutputCallback = { (displayLink, inNow, inOutputTime, flagsIn, flagsOut, context) -> CVReturn in
    // convert opaque context pointer back into a reference to our view
    let view = Unmanaged<MyMetalView>.fromOpaque(context!).takeUnretainedValue()

    /*** render something into view.metalLayer here! ***/

    return kCVReturnSuccess
  }

  override func viewDidMoveToWindow() {
    super.viewDidMoveToWindow()

    guard CVDisplayLinkCreateWithActiveCGDisplays(&displayLink) == kCVReturnSuccess,
      let displayLink = displayLink
      else {
        fatalError("unable to create display link")
    }

    // pass a reference to this view as an opaque pointer
    guard CVDisplayLinkSetOutputCallback(displayLink, MyMetalView._outputCallback, Unmanaged<MyMetalView>.passUnretained(self).toOpaque()) == kCVReturnSuccess else {
      fatalError("unable to configure output callback")
    }

    guard CVDisplayLinkStart(displayLink) == kCVReturnSuccess else {
      fatalError("unable to start display link")
    }
  }

  deinit {
    if let displayLink = displayLink {
      CVDisplayLinkStop(displayLink)
    }
  }
}

Upvotes: 2

Related Questions