You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
56 lines
2.7 KiB
56 lines
2.7 KiB
import CoreGraphics
|
|
import MusubiCore
|
|
import OSLog
|
|
|
|
/// Switches a display to a target refresh rate for the lifetime of the app and restores it on request.
|
|
public enum DisplayModeSwitcher {
|
|
private static let log = Logger(subsystem: "studio.zielke.musubi", category: "display")
|
|
|
|
/// Returns the refresh rate in effect afterwards, and the original mode to restore later (nil if unchanged).
|
|
@discardableResult
|
|
public static func setRefreshRate(_ hz: Double, on display: CGDirectDisplayID) -> (refreshRate: Double, original: CGDisplayMode?) {
|
|
guard let current = CGDisplayCopyDisplayMode(display) else { return (0, nil) }
|
|
let options = [kCGDisplayShowDuplicateLowResolutionModes: true] as CFDictionary
|
|
let modes = (CGDisplayCopyAllDisplayModes(display, options) as? [CGDisplayMode]) ?? []
|
|
|
|
guard let target = DisplayModeSelection.mode(matching: hz, current: info(current), available: modes.map(info)),
|
|
let mode = modes.first(where: { info($0) == target })
|
|
else {
|
|
if abs(current.refreshRate - hz) > 0.1 {
|
|
log.notice("Display \(display): no \(hz) Hz mode, staying at \(current.refreshRate) Hz")
|
|
}
|
|
return (current.refreshRate, nil)
|
|
}
|
|
|
|
guard apply(mode, to: display) else { return (current.refreshRate, nil) }
|
|
log.info("Display \(display): \(current.refreshRate) Hz → \(mode.refreshRate) Hz")
|
|
return (mode.refreshRate, current)
|
|
}
|
|
|
|
public static func restore(_ mode: CGDisplayMode, on display: CGDirectDisplayID) {
|
|
if apply(mode, to: display) {
|
|
log.info("Display \(display): restored \(mode.refreshRate) Hz")
|
|
}
|
|
}
|
|
|
|
public static func currentRefreshRate(of display: CGDirectDisplayID) -> Double {
|
|
CGDisplayCopyDisplayMode(display)?.refreshRate ?? 0
|
|
}
|
|
|
|
private static func apply(_ mode: CGDisplayMode, to display: CGDirectDisplayID) -> Bool {
|
|
var config: CGDisplayConfigRef?
|
|
guard CGBeginDisplayConfiguration(&config) == .success else { return false }
|
|
CGConfigureDisplayWithDisplayMode(config, display, mode, nil)
|
|
// .forAppOnly: macOS reverts the mode when Musubi quits or crashes.
|
|
let result = CGCompleteDisplayConfiguration(config, .forAppOnly)
|
|
if result != .success { log.error("Display \(display): mode change failed (\(result.rawValue))") }
|
|
return result == .success
|
|
}
|
|
|
|
private static func info(_ mode: CGDisplayMode) -> DisplayModeInfo {
|
|
DisplayModeInfo(width: mode.width, height: mode.height,
|
|
pixelWidth: mode.pixelWidth, pixelHeight: mode.pixelHeight,
|
|
refreshRate: mode.refreshRate)
|
|
}
|
|
}
|