Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 59 additions & 6 deletions Platform/macOS/Display/VMDisplayQemuMetalWindowController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ class VMDisplayQemuMetalWindowController: VMDisplayQemuWindowController {
private var globalEventMonitor: Any? = nil
private var ctrlKeyDown: Bool = false
private var screenChangedToken: Any?
/// `CAMetalDisplayLink` on macOS 14 and later
private var displayLink: AnyObject?

private var displayConfig: UTMQemuConfigurationDisplay? {
vmQemuConfig?.displays[id]
Expand Down Expand Up @@ -97,17 +99,27 @@ class VMDisplayQemuMetalWindowController: VMDisplayQemuWindowController {
logger.critical("Failed to create renderer.")
return
}
if rendererFpsLimit > 0 {
metalView.preferredFramesPerSecond = rendererFpsLimit
} else if let maxFps = self.window?.screen?.maximumFramesPerSecond {
metalView.preferredFramesPerSecond = maxFps
}
if #available(macOS 14, *), let layer = metalView.layer as? CAMetalLayer {
// A CAMetalDisplayLink paces the layer at the rate it requests; MTKView's own
// timer can stay at 60 Hz on a ProMotion display.
let link = CAMetalDisplayLink(metalLayer: layer)
link.delegate = self
link.add(to: .main, forMode: .common)
metalView.isPaused = true
metalView.enableSetNeedsDisplay = false
displayLink = link
}
updateFrameRate()
renderer.changeUpscaler(displayConfig?.upscalingFilter.metalSamplerMinMagFilter ?? .linear, downscaler: displayConfig?.downscalingFilter.metalSamplerMinMagFilter ?? .linear)
vmDisplay?.addRenderer(renderer) // can be nil if primary
metalView.delegate = renderer
metalView.inputDelegate = self

screenChangedToken = NotificationCenter.default.addObserver(forName: NSWindow.didChangeScreenNotification, object: nil, queue: .main) { [weak self] _ in
screenChangedToken = NotificationCenter.default.addObserver(forName: NSWindow.didChangeScreenNotification, object: nil, queue: .main) { [weak self] notification in
// the new screen can refresh at a different rate
if let self = self, notification.object as? NSWindow == window {
updateFrameRate()
}
// update minSize when we change screens
if let self = self,
let window = window,
Expand All @@ -124,7 +136,29 @@ class VMDisplayQemuMetalWindowController: VMDisplayQemuWindowController {
super.windowDidLoad()
}

/// Refresh at the rate of the screen the window is on, unless the user set a limit.
private func updateFrameRate() {
let fps: Int
if rendererFpsLimit > 0 {
fps = rendererFpsLimit
} else if let maxFps = window?.screen?.maximumFramesPerSecond {
fps = maxFps
} else {
return
}
if #available(macOS 14, *), let link = displayLink as? CAMetalDisplayLink {
let rate = Float(fps)
link.preferredFrameRateRange = CAFrameRateRange(minimum: rate, maximum: rate, preferred: rate)
} else {
metalView.preferredFramesPerSecond = fps
}
}

override func windowWillClose(_ notification: Notification) {
if #available(macOS 14, *), let link = displayLink as? CAMetalDisplayLink {
link.invalidate()
}
displayLink = nil
vmDisplay?.removeRenderer(renderer!)
stopAllCapture()
if let screenChangedToken = screenChangedToken {
Expand Down Expand Up @@ -160,13 +194,21 @@ class VMDisplayQemuMetalWindowController: VMDisplayQemuWindowController {
displaySizeDidChange(size: vmDisplay.displaySize)
}
super.enterLive()
// pick up an FPS limit changed while the window was open
updateFrameRate()
if #available(macOS 14, *), let link = displayLink as? CAMetalDisplayLink {
link.isPaused = false
}
setControl(.resize, isEnabled: false) // disable item
if isWindowFocusAutoCapture {
captureMouse()
}
}

override func enterSuspended(isBusy busy: Bool) {
if #available(macOS 14, *), let link = displayLink as? CAMetalDisplayLink {
link.isPaused = true
}
if !busy {
metalView.isHidden = true
screenshotView.image = vm.screenshot?.image
Expand Down Expand Up @@ -735,3 +777,14 @@ extension VMDisplayQemuMetalWindowController {
window.beginSheet(sheetWindow)
}
}

// MARK: - Display link

@available(macOS 14, *)
extension VMDisplayQemuMetalWindowController: CAMetalDisplayLinkDelegate {
func metalDisplayLink(_ link: CAMetalDisplayLink, needsUpdate update: CAMetalDisplayLink.Update) {
metalView.displayLinkDrawable = update.drawable
renderer.draw(in: metalView)
metalView.displayLinkDrawable = nil
}
}
20 changes: 20 additions & 0 deletions Platform/macOS/Display/VMMetalView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,26 @@ class VMMetalView: MTKView {
@Setting("IsISOKeySwapped") private var isISOKeySwapped = false
@Setting("IsCapsLockKey") private var isCapsLockKey: Bool = false

/// Drawable supplied by a display link for the frame being drawn, used instead of one
/// acquired from the layer.
var displayLinkDrawable: CAMetalDrawable?

override var currentDrawable: CAMetalDrawable? {
displayLinkDrawable ?? super.currentDrawable
}

override var currentRenderPassDescriptor: MTLRenderPassDescriptor? {
guard let drawable = displayLinkDrawable else {
return super.currentRenderPassDescriptor
}
let descriptor = MTLRenderPassDescriptor()
descriptor.colorAttachments[0].texture = drawable.texture
descriptor.colorAttachments[0].loadAction = .clear
descriptor.colorAttachments[0].storeAction = .store
descriptor.colorAttachments[0].clearColor = clearColor
return descriptor
}

private enum ModifierKeySwap {
case none
case ctrlCmd
Expand Down