4

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?

  • The main point of the sample code is to use CVDisplayLink to trigger rendering in the background whenever the display needs to be refreshed. Did you download and try the sample code, and what issues did you run into? – jtbandes Sep 02 '20 at 19:07
  • @jtbandes I downloaded Apple's sample's code, and it fixes my problem for the most part, except I have no idea how to implement the fix in Apple's code to mine. Also is there a way to do that without Core Video? –  Sep 02 '20 at 22:50
  • @jtbandes I got your comment, thanks for replying, but my main concern is when I try to just change the type of MetalView to CAMetalLayer, there is a lot of functionality that isn't available that I can't figure out how to render without, such as currentDrawable properties and currentRenderPassDescriptor or something like that –  Oct 12 '20 at 04:26
  • Yes, I think keeping the MetalView paused (as discussed in the first link in my post) might be a good idea. – jtbandes Oct 14 '20 at 16:17
  • 1
    You can avoid the warning by saving a reference to the mtkView and accessing that directly. However, the risk of thread-safety issues is real, in particular I don't know whether `draw()` is intended to be called from a background thread. Generally views should not be modified from background threads, but perhaps MTKView is an exception; I would recommend you do more research. – jtbandes Oct 23 '20 at 18:12
  • 1
    Sample code can be a good resource, and you can often get more definitive, although less timely, answers (from Apple employees) on their dev forums. – jtbandes Oct 23 '20 at 18:19

1 Answers1

2

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)
    }
  }
}
jtbandes
  • 115,675
  • 35
  • 233
  • 266