App.swift (15753B)
1 // The app wakes the headset over HID and covers its display with a non-activating surface, 2 // renders the stereo scene fullscreen at 4320x2160 @ 90 Hz, and re-takes control the moment the display returns. 3 // 4 // Health is watched PASSIVELY only (macOS display callbacks + the headset's own 0x05 status reports). 5 // Avoid intrusive USB-C/DisplayPort polling. Drops have also occurred without polling; 6 // use callbacks to observe the link and distinguish recovery from sustained stability. 7 import AppKit 8 import Metal 9 import QuartzCore 10 11 /// A display surface, never the keyboard focus or an exclusive display owner. 12 final class HeadsetWindow: NSPanel { 13 override var canBecomeKey: Bool { false } 14 override var canBecomeMain: Bool { false } 15 } 16 17 /// The headset's display: vendor 0x220E (8718). 18 func isHeadsetDisplay(_ id: CGDirectDisplayID) -> Bool { CGDisplayVendorNumber(id) == 8718 } 19 20 func onlineHeadsetDisplay() -> CGDirectDisplayID? { 21 var ids = [CGDirectDisplayID](repeating: 0, count: 16) 22 var n: UInt32 = 0 23 CGGetOnlineDisplayList(16, &ids, &n) 24 return ids.prefix(Int(n)).first(where: isHeadsetDisplay) 25 } 26 27 func log(_ s: String) { print(String(format: "%.3f ", Date().timeIntervalSince1970) + s) } 28 29 private func isNativeHeadsetMode(_ mode: CGDisplayMode) -> Bool { 30 mode.pixelWidth == 4320 && mode.pixelHeight == 2160 && mode.width == 4320 && mode.height == 2160 && mode.refreshRate == 90 31 } 32 33 private let displayChanged: CGDisplayReconfigurationCallBack = { id, flags, ctx in 34 guard !flags.contains(.beginConfigurationFlag), let ctx else { return } 35 guard flags.contains(.removeFlag) || flags.contains(.addFlag) || flags.contains(.setModeFlag) || flags.contains(.movedFlag) else { return } 36 let enteredAt = Date() 37 log("[display callback] id=\(id) flags=\(flags.rawValue) main=\(Thread.isMainThread)") 38 let session = Unmanaged<HeadsetSession>.fromOpaque(ctx).takeUnretainedValue() 39 if flags.contains(.removeFlag) { 40 session.recoveryDisplayRemoved(id) 41 } 42 if flags.contains(.addFlag), isHeadsetDisplay(id) { session.recoveryDisplayAdded(id) } 43 onMainRunLoop { 44 log(String(format: "[display callback] handled after %.1f ms", Date().timeIntervalSince(enteredAt) * 1000)) 45 // Vendor metadata may already be gone when REMOVE is delivered. 46 if flags.contains(.removeFlag) { session.displayLost(id) } 47 if flags.contains(.addFlag), isHeadsetDisplay(id) { session.displayArrived(id) } 48 if flags.contains(.setModeFlag) { session.displayModeChanged(id) } 49 if flags.contains(.movedFlag) { session.displayMoved(id) } 50 } 51 } 52 53 final class HeadsetSession: NSObject, NSApplicationDelegate { 54 let hmd: HMD 55 let renderer: Renderer 56 let view: MetalSurface 57 private let renderLoop: MetalRenderLoop 58 private var window: HeadsetWindow? 59 private var opticsControls: OpticsControls? 60 private(set) var display: CGDirectDisplayID? 61 private var lostAt: Date? 62 private var hidRetry: Timer? 63 private var healthTimer: Timer? 64 private var callbackRegistered = false 65 private var stopping = false 66 private var recoveryTimer: Timer? 67 private let recovery: LinkRecovery 68 private var lastHealthFrames = 0 69 private var lastHealthTime: Double = CACurrentMediaTime() 70 private var terminationTimer: Timer? 71 private var outputConfigured = false 72 73 init(device: MTLDevice?, recoveryHelper: URL) throws { 74 let headset = HMD() 75 hmd = headset 76 recovery = LinkRecovery(helper: recoveryHelper, wake: { try headset.screenOn() }) 77 renderer = try Renderer(device: device) 78 view = MetalSurface(device: renderer.device) 79 renderLoop = MetalRenderLoop(layer: view.metalLayer, renderer: renderer) 80 super.init() 81 renderLoop.start() 82 } 83 84 func start() { 85 hmd.onStatus = { log("[hmd] \($0)") } 86 hmd.onPanel = { [weak self] on in if !on { self?.panelOff() } } 87 hmd.onDisconnected = { [weak self] in 88 guard let self, !self.stopping else { return } 89 log("[hmd] USB device removed; reopening when available") 90 self.hmd.stop() 91 self.scheduleHIDRetry() 92 } 93 94 let me = Unmanaged.passUnretained(self).toOpaque() 95 let result = CGDisplayRegisterReconfigurationCallback(displayChanged, me) 96 callbackRegistered = result == .success 97 if !callbackRegistered { log("display callback registration failed: \(result.rawValue)") } 98 startHID() 99 healthTimer = Timer.scheduledTimer(withTimeInterval: 10, repeats: true) { [weak self] _ in 100 guard let self else { return } 101 if let id = self.display { self.displayModeChanged(id) } 102 let age = self.hmd.lastIMUAt.map { String(format: "%.1fs", Date().timeIntervalSince($0)) } ?? "never" 103 let now = CACurrentMediaTime() 104 let metrics = self.renderer.frameMetrics() 105 let fps = Double(metrics.presented - self.lastHealthFrames) / (now - self.lastHealthTime) 106 self.lastHealthTime = now; self.lastHealthFrames = metrics.presented 107 log(String(format: "health display=%@ submitted=%d presented=%d skipped=%d fps=%.2f presentAge=%.2fs GPUms=%.2f GPUerrors=%d IMU=%d lastIMU=%@ status=%d", 108 self.display.map(String.init) ?? "absent", metrics.submitted, 109 metrics.presented, metrics.skipped, fps, now - metrics.lastPresentedTime, 110 metrics.gpuSeconds * 1000 / Double(max(1, metrics.completed)), metrics.errors, 111 self.hmd.imuSamples, age, self.hmd.statusReports)) 112 } 113 if let id = onlineHeadsetDisplay() { displayArrived(id) } else { 114 log("waiting for the headset display...") 115 recover() 116 } 117 } 118 119 func showOpticsControls() { opticsControls = OpticsControls(renderer: renderer) } 120 121 private func startHID() { 122 guard !stopping else { return } 123 do { 124 try hmd.start(mode: .activate) 125 hidRetry?.invalidate(); hidRetry = nil 126 log("[hmd] wake commands accepted; waiting for IMU and video evidence") 127 if display == nil { recover() } 128 } catch { log("[hmd] \(error.localizedDescription)"); scheduleHIDRetry() } 129 } 130 131 private func scheduleHIDRetry() { 132 guard hidRetry == nil, !stopping else { return } 133 hidRetry = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { [weak self] _ in self?.startHID() } 134 } 135 136 func applicationWillTerminate(_ notification: Notification) { stop() } 137 138 func stopAfter(_ seconds: Double) { 139 terminationTimer = Timer.scheduledTimer(withTimeInterval: seconds, repeats: false) { _ in NSApp.terminate(nil) } 140 } 141 142 func stop() { 143 guard !stopping else { return } 144 stopping = true 145 terminationTimer?.invalidate(); terminationTimer = nil 146 hidRetry?.invalidate(); hidRetry = nil 147 healthTimer?.invalidate(); healthTimer = nil 148 recoveryTimer?.invalidate(); recoveryTimer = nil 149 recovery.stop() 150 renderLoop.stop() 151 if callbackRegistered { 152 CGDisplayRemoveReconfigurationCallback(displayChanged, Unmanaged.passUnretained(self).toOpaque()) 153 callbackRegistered = false 154 } 155 hmd.stop() 156 window?.orderOut(nil) 157 opticsControls?.close(); opticsControls = nil 158 display = nil 159 } 160 161 /// Put the render surface on the headset without exclusive display capture. 162 func displayArrived(_ id: CGDirectDisplayID) { 163 guard !stopping else { return } 164 if display == id { displayModeChanged(id); return } 165 if let previous = display { displayLost(previous) } 166 if let t = lostAt { log(String(format: "headset display back after %.0f ms", Date().timeIntervalSince(t) * 1000)) } 167 lostAt = nil 168 log("attaching renderer to headset display \(id)") 169 display = id 170 placeHeadsetToRight(id) 171 recovery.trackDisplay(id) 172 recoveryTimer?.invalidate(); recoveryTimer = nil 173 guard setNativeMode(id) else { 174 display = nil 175 log("cannot render at required native mode") 176 recover() 177 return 178 } 179 configureOutput(id) 180 } 181 182 /// Keep the Mac's left desktop edge available for its left-positioned Dock. 183 private func placeHeadsetToRight(_ id: CGDirectDisplayID) { 184 var ids = [CGDirectDisplayID](repeating: 0, count: 16), count: UInt32 = 0 185 guard CGGetOnlineDisplayList(16, &ids, &count) == .success else { return } 186 let other = ids.prefix(Int(count)).filter { $0 != id } 187 guard let right = other.map({ CGDisplayBounds($0).maxX }).max() else { return } 188 let y = CGDisplayBounds(CGMainDisplayID()).minY 189 let current = CGDisplayBounds(id) 190 guard current.minX != right || current.minY != y else { return } 191 var configuration: CGDisplayConfigRef? 192 guard CGBeginDisplayConfiguration(&configuration) == .success, let configuration else { return } 193 let moved = CGConfigureDisplayOrigin(configuration, id, Int32(right), Int32(y)) 194 guard moved == .success else { CGCancelDisplayConfiguration(configuration); return } 195 let result = CGCompleteDisplayConfiguration(configuration, .forSession) 196 log("headset desktop placement to right: \(result.rawValue)") 197 } 198 199 func displayMoved(_ id: CGDirectDisplayID) { 200 guard display == id, !stopping else { return } 201 configureOutput(id) 202 } 203 204 func displayModeChanged(_ id: CGDirectDisplayID) { 205 guard !stopping, display == id, let current = CGDisplayCopyDisplayMode(id) else { return } 206 let changed = !isNativeHeadsetMode(current) 207 if changed { 208 log("headset mode changed to \(current.pixelWidth)x\(current.pixelHeight)@\(current.refreshRate); restoring native 90 Hz") 209 renderLoop.setPaused(true) 210 outputConfigured = false 211 guard setNativeMode(id) else { 212 log("rendering paused: cannot restore native 90 Hz") 213 return 214 } 215 } 216 if changed || !outputConfigured { configureOutput(id) } 217 } 218 219 private func configureOutput(_ id: CGDirectDisplayID) { 220 guard !stopping, display == id else { return } 221 // Keep this surface above the headset desktop without taking over macOS display capture. 222 let bounds = CGDisplayBounds(id) 223 let mainH = CGDisplayBounds(CGMainDisplayID()).height 224 let frame = NSRect(x: bounds.minX, y: mainH - bounds.maxY, width: bounds.width, height: bounds.height) 225 let win = window ?? HeadsetWindow(contentRect: frame, styleMask: [.borderless, .nonactivatingPanel], backing: .buffered, defer: false) 226 win.level = .statusBar 227 win.hidesOnDeactivate = false 228 win.ignoresMouseEvents = true 229 win.collectionBehavior = [.canJoinAllSpaces, .stationary, .ignoresCycle, .fullScreenAuxiliary] 230 win.contentView = view 231 win.setFrame(frame, display: true) 232 win.orderFrontRegardless() 233 window = win 234 view.metalLayer.drawableSize = CGSize(width: 4320, height: 2160) 235 outputConfigured = true 236 renderLoop.setPaused(false) 237 log("rendering on headset \(frame.size), drawable \(view.metalLayer.drawableSize); dedicated Metal display loop at 90 Hz") 238 } 239 240 func displayLost(_ id: CGDirectDisplayID) { 241 guard id == display else { return } 242 if lostAt == nil { lostAt = Date() } 243 display = nil 244 outputConfigured = false 245 renderLoop.setPaused(true) 246 window?.orderOut(nil) 247 log("headset display LOST") 248 recover() 249 } 250 251 func panelOff() { log("headset reports display not ready") } 252 253 // Callback entry must notify recovery before SkyLight can block the main thread. 254 func recoveryDisplayRemoved(_ id: CGDirectDisplayID) { 255 recovery.displayRemoved(id) 256 if Thread.isMainThread, display == id { 257 lostAt = Date() 258 renderLoop.setPaused(true) 259 } 260 } 261 func recoveryDisplayAdded(_ id: CGDirectDisplayID) { recovery.displayAdded(id) } 262 263 func recover() { 264 guard !stopping, display == nil, recoveryTimer == nil else { return } 265 if onlineHeadsetDisplay() != nil { 266 recoveryTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { [weak self] _ in 267 guard let self else { return } 268 self.recoveryTimer = nil 269 guard !self.stopping, self.display == nil else { return } 270 if let id = onlineHeadsetDisplay() { 271 log("headset display online; retrying headset attachment without a DP reset") 272 self.displayArrived(id) 273 } else { self.recovery.requestRecovery() } 274 } 275 } else { recovery.requestRecovery() } 276 } 277 278 /// 4320x2160 pixels at 90 Hz, unscaled. 279 private func setNativeMode(_ id: CGDirectDisplayID) -> Bool { 280 let opts = [kCGDisplayShowDuplicateLowResolutionModes: true] as CFDictionary 281 let modes = (CGDisplayCopyAllDisplayModes(id, opts) as? [CGDisplayMode]) ?? [] 282 guard let m = modes.first(where: isNativeHeadsetMode) else { 283 log("no unscaled 4320x2160@90 mode"); return false 284 } 285 if CGDisplayCopyDisplayMode(id)?.ioDisplayModeID == m.ioDisplayModeID { return true } 286 let r = CGDisplaySetDisplayMode(id, m, nil) 287 log("mode 4320x2160@90 (ui \(m.width)x\(m.height)): \(r == .success ? "ok" : "error \(r.rawValue)")") 288 return r == .success 289 } 290 } 291 292 func runApp() { 293 do { try prepareFactoryOptics() } catch { 294 print("cannot load headset optical calibration:", error); exit(1) 295 } 296 let app = NSApplication.shared 297 app.setActivationPolicy(.regular) 298 let session: HeadsetSession 299 let helper = option("--recovery-helper").map { URL(fileURLWithPath: $0) } 300 ?? Bundle.main.executableURL!.deletingLastPathComponent().appendingPathComponent("hpmctl") 301 do { session = try HeadsetSession(device: MTLCreateSystemDefaultDevice(), recoveryHelper: helper) } catch { print("renderer failed:", error); exit(1) } 302 app.delegate = session 303 session.start() 304 if let value = option("--run-seconds") { 305 guard let seconds = Double(value), seconds > 0, seconds.isFinite else { 306 session.stop(); print("--run-seconds must be a positive number"); exit(2) 307 } 308 session.stopAfter(seconds) 309 } 310 session.showOpticsControls() 311 app.activate(ignoringOtherApps: true) 312 signal(SIGINT, SIG_IGN); signal(SIGTERM, SIG_IGN) 313 let signals = [SIGINT, SIGTERM].map { value -> DispatchSourceSignal in 314 let source = DispatchSource.makeSignalSource(signal: value, queue: .main) 315 source.setEventHandler { app.terminate(nil) } 316 source.resume() 317 return source 318 } 319 withExtendedLifetime((session, signals)) { app.run() } 320 } 321 322 /// Read once before opening the rendering session; later starts use the validated local cache. 323 private func prepareFactoryOptics() throws { 324 if option("--calibration") != nil || FileManager.default.fileExists(atPath: HeadsetOptics.cacheURL.path) { 325 _ = try HeadsetOptics.loadCached() 326 return 327 } 328 let reader = HMD() 329 try reader.start(mode: .observe) 330 defer { reader.stop() } 331 let block = try WMRConfigBlock(raw: [UInt8](reader.readFactoryConfiguration())) 332 _ = try HeadsetOptics(json: block.json) 333 let cache = HeadsetOptics.cacheURL 334 try FileManager.default.createDirectory(at: cache.deletingLastPathComponent(), withIntermediateDirectories: true) 335 try block.json.write(to: cache, options: .atomic) 336 log("cached headset factory optical calibration") 337 }