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 }