‹ projects

reverb-m1

Native wired VR on Apple silicon: Reverb G1 display bring-up and stereo demo
git clone https://umhau.org/code/reverb-m1.git
Log | Files | Refs

MetalOutput.swift (3781B)


      1 import AppKit
      2 import Metal
      3 import QuartzCore
      4 
      5 /// AppKit owns only the surface. Rendering never calls NSView.draw or nextDrawable on main.
      6 final class MetalSurface: NSView {
      7     let metalLayer = CAMetalLayer()
      8 
      9     init(device: MTLDevice) {
     10         super.init(frame: .zero)
     11         metalLayer.device = device
     12         metalLayer.pixelFormat = .bgra8Unorm
     13         metalLayer.framebufferOnly = true
     14         metalLayer.maximumDrawableCount = 3
     15         metalLayer.drawableSize = CGSize(width: 4320, height: 2160)
     16         metalLayer.contentsScale = 1 // native unscaled headset mode: one point per pixel
     17         layer = metalLayer
     18         wantsLayer = true
     19     }
     20 
     21     required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
     22 }
     23 
     24 /// One serial render worker for the lifetime of the session. The display link supplies
     25 /// a ready drawable and follows its layer's display, including reconnects and mode changes.
     26 final class MetalRenderLoop {
     27     private let layer: CAMetalLayer
     28     private let renderer: Renderer
     29     private let lock = NSLock()
     30     private var thread: Thread?
     31     private var runLoop: CFRunLoop?
     32     private var link: CAMetalDisplayLink? // touched only on the worker's run loop
     33     private var paused = true
     34     private var stopped = false
     35     private let finished = DispatchSemaphore(value: 0)
     36 
     37     init(layer: CAMetalLayer, renderer: Renderer) {
     38         self.layer = layer; self.renderer = renderer
     39     }
     40 
     41     func start() {
     42         let worker = Thread { [weak self] in self?.run() }
     43         worker.name = "Headset Metal rendering"
     44         worker.qualityOfService = .userInteractive
     45         thread = worker
     46         worker.start()
     47     }
     48 
     49     func setPaused(_ value: Bool) {
     50         lock.lock()
     51         paused = value
     52         let loop = runLoop
     53         lock.unlock()
     54         guard let loop else { return }
     55         CFRunLoopPerformBlock(loop, CFRunLoopMode.commonModes.rawValue) { [weak self] in self?.link?.isPaused = value }
     56         CFRunLoopWakeUp(loop)
     57     }
     58 
     59     func stop() {
     60         lock.lock()
     61         stopped = true
     62         let loop = runLoop
     63         lock.unlock()
     64         if let loop {
     65             CFRunLoopStop(loop)
     66             CFRunLoopWakeUp(loop)
     67         }
     68         if thread != nil && finished.wait(timeout: .now() + 0.5) == .timedOut {
     69             log("render worker did not stop within 0.5s")
     70         }
     71     }
     72 
     73     private func run() {
     74         defer { finished.signal() }
     75         autoreleasepool {
     76             let loop = CFRunLoopGetCurrent()!
     77             lock.lock()
     78             if stopped { lock.unlock(); return }
     79             runLoop = loop
     80             let initiallyPaused = paused
     81             lock.unlock()
     82 
     83             let displayLink = CAMetalDisplayLink(metalLayer: layer)
     84             displayLink.delegate = renderer
     85             displayLink.preferredFrameRateRange = CAFrameRateRange(minimum: 90, maximum: 90, preferred: 90)
     86             displayLink.preferredFrameLatency = 1
     87             displayLink.isPaused = initiallyPaused
     88             link = displayLink
     89             displayLink.add(to: .current, forMode: .common)
     90             let keepAlive = Port()
     91             RunLoop.current.add(keepAlive, forMode: .default)
     92             while true {
     93                 lock.lock()
     94                 let shouldStop = stopped
     95                 let interval = paused ? 0.25 : 0.01
     96                 lock.unlock()
     97                 if shouldStop { break }
     98                 // Drain drawable/update autoreleases on this dedicated thread each turn.
     99                 autoreleasepool { _ = CFRunLoopRunInMode(.defaultMode, interval, true) }
    100             }
    101             displayLink.invalidate() // exactly once, on its owning thread
    102             link = nil
    103             keepAlive.invalidate()
    104             lock.lock(); runLoop = nil; lock.unlock()
    105         }
    106     }
    107 }