VMDisplayMetalWindowController.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. //
  2. // Copyright © 2020 osy. All rights reserved.
  3. //
  4. // Licensed under the Apache License, Version 2.0 (the "License");
  5. // you may not use this file except in compliance with the License.
  6. // You may obtain a copy of the License at
  7. //
  8. // http://www.apache.org/licenses/LICENSE-2.0
  9. //
  10. // Unless required by applicable law or agreed to in writing, software
  11. // distributed under the License is distributed on an "AS IS" BASIS,
  12. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. // See the License for the specific language governing permissions and
  14. // limitations under the License.
  15. //
  16. class VMDisplayMetalWindowController: VMDisplayWindowController, UTMSpiceIODelegate {
  17. var metalView: VMMetalView!
  18. var renderer: UTMRenderer?
  19. @objc dynamic var vmDisplay: CSDisplayMetal?
  20. @objc dynamic var vmInput: CSInput?
  21. private var displaySizeObserver: NSKeyValueObservation?
  22. private var displaySize: CGSize = .zero
  23. private var isDisplaySizeDynamic: Bool = false
  24. private var isFullScreen: Bool = false
  25. private let minDynamicSize = CGSize(width: 800, height: 600)
  26. private var ctrlKeyDown: Bool = false
  27. // MARK: - User preferences
  28. @Setting("NoCursorCaptureAlert") private var isCursorCaptureAlertShown: Bool = false
  29. @Setting("AlwaysNativeResolution") private var isAlwaysNativeResolution: Bool = false
  30. @Setting("DisplayFixed") private var isDisplayFixed: Bool = false
  31. @Setting("CtrlRightClick") private var isCtrlRightClick: Bool = false
  32. private var settingObservations = [NSKeyValueObservation]()
  33. // MARK: - Init
  34. override func windowDidLoad() {
  35. super.windowDidLoad()
  36. metalView = VMMetalView(frame: displayView.bounds)
  37. metalView.autoresizingMask = [.width, .height]
  38. metalView.device = MTLCreateSystemDefaultDevice()
  39. guard let _ = metalView.device else {
  40. showErrorAlert(NSLocalizedString("Metal is not supported on this device. Cannot render display.", comment: "VMDisplayMetalWindowController"))
  41. logger.critical("Cannot find system default Metal device.")
  42. return
  43. }
  44. displayView.addSubview(metalView)
  45. renderer = UTMRenderer.init(metalKitView: metalView)
  46. guard let renderer = self.renderer else {
  47. showErrorAlert(NSLocalizedString("Internal error.", comment: "VMDisplayMetalWindowController"))
  48. logger.critical("Failed to create renderer.")
  49. return
  50. }
  51. renderer.mtkView(metalView, drawableSizeWillChange: metalView.drawableSize)
  52. renderer.changeUpscaler(vmConfiguration?.displayUpscalerValue ?? .linear, downscaler: vmConfiguration?.displayDownscalerValue ?? .linear)
  53. metalView.delegate = renderer
  54. metalView.inputDelegate = self
  55. settingObservations.append(UserDefaults.standard.observe(\.AlwaysNativeResolution, options: .new) { (defaults, change) in
  56. self.displaySizeDidChange(size: self.displaySize)
  57. })
  58. settingObservations.append(UserDefaults.standard.observe(\.DisplayFixed, options: .new) { (defaults, change) in
  59. self.displaySizeDidChange(size: self.displaySize)
  60. })
  61. if vm.state == .vmStopped || vm.state == .vmSuspended {
  62. enterSuspended(isBusy: false)
  63. DispatchQueue.global(qos: .userInitiated).async {
  64. if self.vm.startVM() {
  65. self.vm.ioDelegate = self
  66. }
  67. }
  68. } else {
  69. enterLive()
  70. vm.ioDelegate = self
  71. }
  72. }
  73. override func enterLive() {
  74. metalView.isHidden = false
  75. screenshotView.isHidden = true
  76. renderer!.source = vmDisplay
  77. displaySizeObserver = observe(\.vmDisplay!.displaySize, options: [.initial, .new]) { (_, change) in
  78. guard let size = change.newValue else { return }
  79. self.displaySizeDidChange(size: size)
  80. }
  81. if vmConfiguration!.shareClipboardEnabled {
  82. UTMPasteboard.general.requestPollingMode(forHashable: self) // start clipboard polling
  83. }
  84. super.enterLive()
  85. resizeConsoleToolbarItem.isEnabled = false // disable item
  86. }
  87. override func enterSuspended(isBusy busy: Bool) {
  88. if !busy {
  89. metalView.isHidden = true
  90. screenshotView.image = vm.screenshot?.image
  91. screenshotView.isHidden = false
  92. }
  93. if vmConfiguration!.shareClipboardEnabled {
  94. UTMPasteboard.general.releasePollingMode(forHashable: self) // stop clipboard polling
  95. }
  96. super.enterSuspended(isBusy: busy)
  97. }
  98. override func captureMouseButtonPressed(_ sender: Any) {
  99. captureMouse()
  100. }
  101. }
  102. // MARK: - Screen management
  103. extension VMDisplayMetalWindowController {
  104. fileprivate func displaySizeDidChange(size: CGSize) {
  105. guard size != .zero else {
  106. logger.debug("Ignoring zero size display")
  107. return
  108. }
  109. DispatchQueue.main.async {
  110. logger.debug("resizing to: (\(size.width), \(size.height))")
  111. guard let window = self.window else {
  112. logger.debug("Invalid window, ignoring size change")
  113. return
  114. }
  115. self.displaySize = size
  116. if self.isFullScreen {
  117. _ = self.updateHostScaling(for: window, frameSize: window.frame.size)
  118. } else {
  119. self.updateHostFrame(forGuestResolution: size)
  120. }
  121. }
  122. }
  123. func dynamicResolutionSupportDidChange(_ supported: Bool) {
  124. if isDisplaySizeDynamic != supported {
  125. displaySizeDidChange(size: displaySize)
  126. }
  127. isDisplaySizeDynamic = supported
  128. }
  129. func windowDidChangeScreen(_ notification: Notification) {
  130. logger.debug("screen changed")
  131. if let vmDisplay = self.vmDisplay {
  132. displaySizeDidChange(size: vmDisplay.displaySize)
  133. }
  134. }
  135. fileprivate func updateHostFrame(forGuestResolution size: CGSize) {
  136. guard let window = window else { return }
  137. guard let vmDisplay = vmDisplay else { return }
  138. let currentScreenScale = window.screen?.backingScaleFactor ?? 1.0
  139. let nativeScale = isAlwaysNativeResolution ? 1.0 : currentScreenScale
  140. // change optional scale if needed
  141. if isDisplaySizeDynamic || isDisplayFixed || (!isAlwaysNativeResolution && vmDisplay.viewportScale < currentScreenScale) {
  142. vmDisplay.viewportScale = nativeScale
  143. }
  144. let minScaledSize = CGSize(width: size.width * nativeScale / currentScreenScale, height: size.height * nativeScale / currentScreenScale)
  145. let fullContentWidth = size.width * vmDisplay.viewportScale / currentScreenScale
  146. let fullContentHeight = size.height * vmDisplay.viewportScale / currentScreenScale
  147. let contentRect = CGRect(x: window.frame.origin.x,
  148. y: 0,
  149. width: ceil(fullContentWidth),
  150. height: ceil(fullContentHeight))
  151. var windowRect = window.frameRect(forContentRect: contentRect)
  152. windowRect.origin.y = window.frame.origin.y + window.frame.height - windowRect.height
  153. if isDisplaySizeDynamic {
  154. window.contentMinSize = minDynamicSize
  155. window.contentResizeIncrements = NSSize(width: 1, height: 1)
  156. window.setFrame(windowRect, display: false, animate: false)
  157. } else {
  158. window.contentMinSize = minScaledSize
  159. window.contentAspectRatio = size
  160. window.setFrame(windowRect, display: false, animate: true)
  161. }
  162. }
  163. fileprivate func updateHostScaling(for window: NSWindow, frameSize: NSSize) -> NSSize {
  164. guard displaySize != .zero else { return frameSize }
  165. guard let vmDisplay = self.vmDisplay else { return frameSize }
  166. let currentScreenScale = window.screen?.backingScaleFactor ?? 1.0
  167. let targetContentSize = window.contentRect(forFrameRect: CGRect(origin: .zero, size: frameSize)).size
  168. let targetScaleX = targetContentSize.width * currentScreenScale / displaySize.width
  169. let targetScaleY = targetContentSize.height * currentScreenScale / displaySize.height
  170. let targetScale = min(targetScaleX, targetScaleY)
  171. let scaledSize = CGSize(width: displaySize.width * targetScale / currentScreenScale, height: displaySize.height * targetScale / currentScreenScale)
  172. let targetFrameSize = window.frameRect(forContentRect: CGRect(origin: .zero, size: scaledSize)).size
  173. vmDisplay.viewportScale = targetScale
  174. logger.debug("changed scale \(targetScale)")
  175. return targetFrameSize
  176. }
  177. fileprivate func updateGuestResolution(for window: NSWindow, frameSize: NSSize) -> NSSize {
  178. guard let vmDisplay = self.vmDisplay else { return frameSize }
  179. let currentScreenScale = window.screen?.backingScaleFactor ?? 1.0
  180. let nativeScale = isAlwaysNativeResolution ? currentScreenScale : 1.0
  181. let targetSize = window.contentRect(forFrameRect: CGRect(origin: .zero, size: frameSize)).size
  182. let targetSizeScaled = isAlwaysNativeResolution ? targetSize.applying(CGAffineTransform(scaleX: nativeScale, y: nativeScale)) : targetSize
  183. logger.debug("Requesting resolution: (\(targetSizeScaled.width), \(targetSizeScaled.height))")
  184. let bounds = CGRect(origin: .zero, size: targetSizeScaled)
  185. vmDisplay.requestResolution(bounds)
  186. return frameSize
  187. }
  188. func windowWillResize(_ sender: NSWindow, to frameSize: NSSize) -> NSSize {
  189. guard !self.isDisplaySizeDynamic else {
  190. return frameSize
  191. }
  192. guard !self.isDisplayFixed else {
  193. return frameSize
  194. }
  195. return updateHostScaling(for: sender, frameSize: frameSize)
  196. }
  197. func windowDidEndLiveResize(_ notification: Notification) {
  198. guard self.isDisplaySizeDynamic, let window = self.window else {
  199. return
  200. }
  201. _ = updateGuestResolution(for: window, frameSize: window.frame.size)
  202. }
  203. func windowDidEnterFullScreen(_ notification: Notification) {
  204. isFullScreen = true
  205. }
  206. func windowDidExitFullScreen(_ notification: Notification) {
  207. isFullScreen = false
  208. }
  209. func windowDidBecomeKey(_ notification: Notification) {
  210. if let window = self.window {
  211. _ = window.makeFirstResponder(metalView)
  212. }
  213. }
  214. func windowDidResignKey(_ notification: Notification) {
  215. if let window = self.window {
  216. _ = window.makeFirstResponder(nil)
  217. }
  218. }
  219. }
  220. // MARK: - Input events
  221. extension VMDisplayMetalWindowController: VMMetalViewInputDelegate {
  222. private func captureMouse() {
  223. let action = { () -> Void in
  224. self.vm.requestInputTablet(false)
  225. self.metalView?.captureMouse()
  226. }
  227. if isCursorCaptureAlertShown {
  228. let alert = NSAlert()
  229. alert.messageText = NSLocalizedString("Captured mouse", comment: "VMDisplayMetalWindowController")
  230. alert.informativeText = NSLocalizedString("To release the mouse cursor, press ⌃+⌥ (Ctrl+Opt or Ctrl+Alt) at the same time.", comment: "VMDisplayMetalWindowController")
  231. alert.showsSuppressionButton = true
  232. alert.beginSheetModal(for: window!) { _ in
  233. if alert.suppressionButton?.state ?? .off == .on {
  234. self.isCursorCaptureAlertShown = false
  235. }
  236. DispatchQueue.main.async(execute: action)
  237. }
  238. } else {
  239. action()
  240. }
  241. }
  242. private func releaseMouse() {
  243. vm.requestInputTablet(true)
  244. metalView?.releaseMouse()
  245. }
  246. func mouseMove(absolutePoint: CGPoint, button: CSInputButton) {
  247. guard let window = self.window else { return }
  248. let currentScreenScale = window.screen?.backingScaleFactor ?? 1.0
  249. let viewportScale = vmDisplay?.viewportScale ?? 1.0
  250. let frameSize = metalView.frame.size
  251. let newX = absolutePoint.x * currentScreenScale / viewportScale
  252. let newY = (frameSize.height - absolutePoint.y) * currentScreenScale / viewportScale
  253. let point = CGPoint(x: newX, y: newY)
  254. logger.debug("move cursor: cocoa (\(absolutePoint.x), \(absolutePoint.y)), native (\(newX), \(newY))")
  255. vmInput?.sendMouseMotion(button, point: point)
  256. vmDisplay?.forceCursorPosition(point) // required to show cursor on screen
  257. }
  258. func mouseMove(relativePoint: CGPoint, button: CSInputButton) {
  259. let translated = CGPoint(x: relativePoint.x, y: -relativePoint.y)
  260. vmInput?.sendMouseMotion(button, point: translated)
  261. }
  262. private func modifyMouseButton(_ button: CSInputButton) -> CSInputButton {
  263. let buttonMod: CSInputButton
  264. if button.contains(.left) && ctrlKeyDown && isCtrlRightClick {
  265. buttonMod = button.subtracting(.left).union(.right)
  266. } else {
  267. buttonMod = button
  268. }
  269. return buttonMod
  270. }
  271. func mouseDown(button: CSInputButton) {
  272. vmInput?.sendMouseButton(modifyMouseButton(button), pressed: true, point: .zero)
  273. }
  274. func mouseUp(button: CSInputButton) {
  275. vmInput?.sendMouseButton(modifyMouseButton(button), pressed: false, point: .zero)
  276. }
  277. func mouseScroll(dy: CGFloat, button: CSInputButton) {
  278. var scrollDy = dy
  279. if vmConfiguration?.inputScrollInvert ?? false {
  280. scrollDy = -scrollDy
  281. }
  282. vmInput?.sendMouseScroll(.smooth, button: button, dy: dy)
  283. }
  284. private func sendExtendedKey(_ button: CSInputKey, keyCode: Int) {
  285. if (keyCode & 0xFF00) == 0xE000 {
  286. vmInput?.send(button, code: Int32(0x100 | (keyCode & 0xFF)))
  287. } else if keyCode >= 0x100 {
  288. logger.warning("ignored invalid keycode \(keyCode)");
  289. } else {
  290. vmInput?.send(button, code: Int32(keyCode))
  291. }
  292. }
  293. func keyDown(keyCode: Int) {
  294. if (keyCode & 0xFF) == 0x1D { // Ctrl
  295. ctrlKeyDown = true
  296. }
  297. sendExtendedKey(.press, keyCode: keyCode)
  298. }
  299. func keyUp(keyCode: Int) {
  300. if (keyCode & 0xFF) == 0x1D { // Ctrl
  301. ctrlKeyDown = false
  302. }
  303. sendExtendedKey(.release, keyCode: keyCode)
  304. }
  305. func requestReleaseCapture() {
  306. releaseMouse()
  307. }
  308. }