Skip to content

Commit 5c86235

Browse files
committed
Better handling of the Mac Mini HDMI situation
Blacklisted known HDMI dummy General stability improvements for arm64 Lazy display update implemented if configuration change is requested (waits 2 seconds until it performs actual display update since MacOS usually sends 4-8 config change signals which made changes very costly)
1 parent 27bf565 commit 5c86235

5 files changed

Lines changed: 108 additions & 57 deletions

File tree

MonitorControl/AppDelegate.swift

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
2121
let coreAudio = SimplyCoreAudio()
2222
var accessibilityObserver: NSObjectProtocol!
2323

24+
var willReconfigureDisplay: Bool = false
25+
2426
lazy var preferencesWindowController: PreferencesWindowController = {
2527
let storyboard = NSStoryboard(name: "Main", bundle: Bundle.main)
2628
let mainPrefsVc = storyboard.instantiateController(withIdentifier: "MainPrefsVC") as? MainPrefsViewController
@@ -46,7 +48,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
4648
self.statusItem.button?.image = NSImage(named: "status")
4749
self.statusItem.menu = self.statusMenu
4850
self.checkPermissions()
49-
CGDisplayRegisterReconfigurationCallback({ _, _, _ in app.updateDisplays() }, nil)
51+
CGDisplayRegisterReconfigurationCallback({ _, _, _ in app.displayReconfigured() }, nil)
5052
self.updateDisplays()
5153
}
5254

@@ -113,22 +115,42 @@ class AppDelegate: NSObject, NSApplicationDelegate {
113115
// Update AVServices for External Displays
114116
func updateAVServices() {
115117
if Arm64DDCUtils.isArm64 {
118+
os_log("arm64 AVService update requested", type: .info)
116119
var displayIDs: [CGDirectDisplayID] = []
117120
for externalDisplay in DisplayManager.shared.getExternalDisplays() {
118121
displayIDs.append(externalDisplay.identifier)
119122
}
120123
for serviceMatch in Arm64DDCUtils.getServiceMatches(displayIDs: displayIDs) {
121-
for externalDisplay in DisplayManager.shared.getExternalDisplays() where externalDisplay.identifier == serviceMatch.displayID {
124+
for externalDisplay in DisplayManager.shared.getExternalDisplays() where externalDisplay.identifier == serviceMatch.displayID && serviceMatch.service != nil {
122125
externalDisplay.arm64avService = serviceMatch.service
123-
if Arm64DDCUtils.read(service: externalDisplay.arm64avService, command: UInt8(0xF1)) != nil {
126+
os_log("Display service match successful for display %{public}@", type: .info, String(serviceMatch.displayID))
127+
// This upsets devices with no read support
128+
// if Arm64DDCUtils.read(service: externalDisplay.arm64avService, command: UInt8(0xF1)) != nil {
129+
// externalDisplay.arm64ddc = true
130+
// }
131+
if !serviceMatch.isDiscouraged {
124132
externalDisplay.arm64ddc = true
125133
}
126134
}
127135
}
136+
os_log("AVService update done", type: .info)
137+
}
138+
}
139+
140+
// Handle display reconfiguration in a lazy way
141+
func displayReconfigured() {
142+
if !self.willReconfigureDisplay {
143+
self.willReconfigureDisplay = true
144+
os_log("Display to be reconfigured via updateDisplay in 2 seconds", type: .info)
145+
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
146+
self.updateDisplays()
147+
}
128148
}
129149
}
130150

131151
func updateDisplays() {
152+
os_log("Request for updateDisplay", type: .info)
153+
self.willReconfigureDisplay = false
132154
self.clearDisplays()
133155

134156
var onlineDisplayIDs = [CGDirectDisplayID](repeating: 0, count: 10)

MonitorControl/Info.plist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
<key>CFBundleShortVersionString</key>
2020
<string>$(MARKETING_VERSION)</string>
2121
<key>CFBundleVersion</key>
22-
<string>1448</string>
22+
<string>1513</string>
2323
<key>LSApplicationCategoryType</key>
2424
<string>public.app-category.utilities</string>
2525
<key>LSMinimumSystemVersion</key>

MonitorControl/Support/Arm64DDCUtils.swift

Lines changed: 78 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ class Arm64DDCUtils: NSObject {
1313
public struct DisplayService {
1414
var displayID: CGDirectDisplayID = 0
1515
var service: IOAVService?
16-
var serviceIoregPosition: Int64 = 0
16+
var serviceLocation: Int = 0
17+
var isDiscouraged: Bool = false
1718
}
1819

1920
#if arch(arm64)
@@ -30,21 +31,22 @@ class Arm64DDCUtils: NSObject {
3031
for displayID in displayIDs {
3132
for ioregServiceForMatching in ioregServicesForMatching {
3233
let score = self.ioregMatchScore(displayID: displayID, ioregEdidUUID: ioregServiceForMatching.edidUUID, ioregProductName: ioregServiceForMatching.productName, ioregSerialNumber: ioregServiceForMatching.serialNumber)
33-
let displayService = DisplayService(displayID: displayID, service: ioregServiceForMatching.service, serviceIoregPosition: ioregServiceForMatching.serviceIoregPosition)
34+
let isDiscouraged = self.checkIfDiscouraged(ioregService: ioregServiceForMatching)
35+
let displayService = DisplayService(displayID: displayID, service: ioregServiceForMatching.service, serviceLocation: ioregServiceForMatching.serviceLocation, isDiscouraged: isDiscouraged)
3436
if scoredCandidateDisplayServices[score] == nil {
3537
scoredCandidateDisplayServices[score] = []
3638
}
3739
scoredCandidateDisplayServices[score]?.append(displayService)
3840
}
3941
}
40-
var takenServiceIoregPositions: [Int64] = []
42+
var takenServiceLocations: [Int] = []
4143
var takenDisplayIDs: [CGDirectDisplayID] = []
4244
for score in stride(from: self.MAX_MATCH_SCORE, to: 0, by: -1) {
4345
if let scoredCandidateDisplayService = scoredCandidateDisplayServices[score] {
4446
for candidateDisplayService in scoredCandidateDisplayService {
45-
if !(takenDisplayIDs.contains(candidateDisplayService.displayID) || takenServiceIoregPositions.contains(candidateDisplayService.serviceIoregPosition)) {
47+
if !(takenDisplayIDs.contains(candidateDisplayService.displayID) || takenServiceLocations.contains(candidateDisplayService.serviceLocation)) {
4648
takenDisplayIDs.append(candidateDisplayService.displayID)
47-
takenServiceIoregPositions.append(candidateDisplayService.serviceIoregPosition)
49+
takenServiceLocations.append(candidateDisplayService.serviceLocation)
4850
matchedDisplayServices.append(candidateDisplayService)
4951
}
5052
}
@@ -112,11 +114,14 @@ class Arm64DDCUtils: NSObject {
112114

113115
private struct IOregService {
114116
var edidUUID: String = ""
117+
var manufacturerID: String = ""
115118
var productName: String = ""
116119
var serialNumber: Int64 = 0
120+
var location: String = ""
121+
var transportUpstream: String = ""
122+
var transportDownstream: String = ""
117123
var service: IOAVService?
118-
var displayAttributesIoregPosition: Int64 = 0
119-
var serviceIoregPosition: Int64 = 0
124+
var serviceLocation: Int = 0
120125
}
121126

122127
private static let MAX_MATCH_SCORE: Int = 6
@@ -132,16 +137,16 @@ class Arm64DDCUtils: NSObject {
132137
}
133138
let edidUUIDSearchKeys: [KeyLoc] = [
134139
// Vendor ID
135-
KeyLoc(key: String(format: "%04x", UInt16(kDisplayVendorID)).uppercased(), loc: 0),
140+
KeyLoc(key: String(format: "%04x", UInt16(max(0, min(kDisplayVendorID, 256 ^ 2 - 1)))).uppercased(), loc: 0),
136141
// Product ID
137-
KeyLoc(key: String(format: "%02x", UInt8((UInt16(kDisplayProductID) >> (0 * 8)) & 0xFF)).uppercased()
138-
+ String(format: "%02x", UInt8((UInt16(kDisplayProductID) >> (1 * 8)) & 0xFF)).uppercased(), loc: 4),
142+
KeyLoc(key: String(format: "%02x", UInt8((UInt16(max(0, min(kDisplayProductID, 256 ^ 2 - 1))) >> (0 * 8)) & 0xFF)).uppercased()
143+
+ String(format: "%02x", UInt8((UInt16(max(0, min(kDisplayProductID, 256 ^ 2 - 1))) >> (1 * 8)) & 0xFF)).uppercased(), loc: 4),
139144
// Manufacture date
140-
KeyLoc(key: String(format: "%02x", UInt8(kDisplayWeekOfManufacture)).uppercased()
141-
+ String(format: "%02x", UInt8(kDisplayYearOfManufacture - 1990)).uppercased(), loc: 19),
145+
KeyLoc(key: String(format: "%02x", UInt8(max(0, min(kDisplayWeekOfManufacture, 256 - 1)))).uppercased()
146+
+ String(format: "%02x", UInt8(max(0, min(kDisplayYearOfManufacture - 1990, 256 - 1)))).uppercased(), loc: 19),
142147
// Image size
143-
KeyLoc(key: String(format: "%02x", UInt8(kDisplayHorizontalImageSize / 10)).uppercased()
144-
+ String(format: "%02x", UInt8(kDisplayVerticalImageSize / 10)).uppercased(), loc: 30),
148+
KeyLoc(key: String(format: "%02x", UInt8(max(0, min(kDisplayHorizontalImageSize / 10, 256 - 1)))).uppercased()
149+
+ String(format: "%02x", UInt8(max(0, min(kDisplayVerticalImageSize / 10, 256 - 1)))).uppercased(), loc: 30),
145150
]
146151
for searchKey in edidUUIDSearchKeys where searchKey.key != "0000" && searchKey.key == ioregEdidUUID.prefix(searchKey.loc + 4).suffix(4) {
147152
matchScore += 1
@@ -157,16 +162,16 @@ class Arm64DDCUtils: NSObject {
157162
return matchScore
158163
}
159164

160-
// Iterate to the next requested item in the ioreg tree
161-
private static func ioregIterateToNext(ioregObjectName: String, iterator: inout io_iterator_t, position: inout Int64) -> io_service_t {
165+
// Iterate to the next AppleCLCD2 or DCPAVServiceProxy item in the ioreg tree and return the name and corresponding service
166+
private static func ioregIterateToNextObjectOfInterest(interests _: [String], iterator: inout io_iterator_t) -> (name: String, service: io_service_t)? {
167+
var objectName: String = ""
162168
var service: io_service_t = IO_OBJECT_NULL
163169
let name = UnsafeMutablePointer<CChar>.allocate(capacity: MemoryLayout<io_name_t>.size)
164170
defer {
165171
name.deallocate()
166172
}
167173
while true {
168174
service = IOIteratorNext(iterator)
169-
position += 1
170175
guard service != MACH_PORT_NULL else {
171176
service = IO_OBJECT_NULL
172177
break
@@ -175,71 +180,95 @@ class Arm64DDCUtils: NSObject {
175180
service = IO_OBJECT_NULL
176181
break
177182
}
178-
if String(cString: name) == ioregObjectName {
179-
break
183+
if String(cString: name) == "AppleCLCD2" || String(cString: name) == "DCPAVServiceProxy" {
184+
objectName = String(cString: name)
185+
return (objectName, service)
180186
}
181187
}
182-
return service
188+
return nil
183189
}
184190

185191
// Returns EDID UUDI, Product Name and Serial Number in an IOregService if it is found using the provided io_service_t pointing to a AppleCDC2 item in the ioreg tree
186-
private static func getIORegServiceAppleCDC2Properties(service: io_service_t, position: Int64 = 0) -> IOregService? {
192+
private static func getIORegServiceAppleCDC2Properties(service: io_service_t) -> IOregService {
193+
var ioregService = IOregService()
187194
if let unmanagedEdidUUID = IORegistryEntryCreateCFProperty(service, CFStringCreateWithCString(kCFAllocatorDefault, "EDID UUID", kCFStringEncodingASCII), kCFAllocatorDefault, IOOptionBits(kIORegistryIterateRecursively)), let edidUUID = unmanagedEdidUUID.takeRetainedValue() as? String {
188-
var ioregService = IOregService()
189-
ioregService.displayAttributesIoregPosition = position
190195
ioregService.edidUUID = edidUUID
191-
if let unmanagedDisplayAttrs = IORegistryEntryCreateCFProperty(service, CFStringCreateWithCString(kCFAllocatorDefault, "DisplayAttributes", kCFStringEncodingASCII), kCFAllocatorDefault, IOOptionBits(kIORegistryIterateRecursively)), let displayAttrs = unmanagedDisplayAttrs.takeRetainedValue() as? NSDictionary, let productAttrs = displayAttrs.value(forKey: "ProductAttributes") as? NSDictionary {
192-
if let productName = productAttrs.value(forKey: "ProductName") as? String {
193-
ioregService.productName = productName
194-
}
195-
if let serialNumber = productAttrs.value(forKey: "SerialNumber") as? Int64 {
196-
ioregService.serialNumber = serialNumber
197-
}
196+
}
197+
if let unmanagedDisplayAttrs = IORegistryEntryCreateCFProperty(service, CFStringCreateWithCString(kCFAllocatorDefault, "DisplayAttributes", kCFStringEncodingASCII), kCFAllocatorDefault, IOOptionBits(kIORegistryIterateRecursively)), let displayAttrs = unmanagedDisplayAttrs.takeRetainedValue() as? NSDictionary, let productAttrs = displayAttrs.value(forKey: "ProductAttributes") as? NSDictionary {
198+
if let manufacturerID = productAttrs.value(forKey: "ManufacturerID") as? String {
199+
ioregService.manufacturerID = manufacturerID
200+
}
201+
if let productName = productAttrs.value(forKey: "ProductName") as? String {
202+
ioregService.productName = productName
203+
}
204+
if let serialNumber = productAttrs.value(forKey: "SerialNumber") as? Int64 {
205+
ioregService.serialNumber = serialNumber
198206
}
199-
return ioregService
200207
}
201-
return nil
208+
if let unmanagedTransport = IORegistryEntryCreateCFProperty(service, CFStringCreateWithCString(kCFAllocatorDefault, "Transport", kCFStringEncodingASCII), kCFAllocatorDefault, IOOptionBits(kIORegistryIterateRecursively)), let transport = unmanagedTransport.takeRetainedValue() as? NSDictionary {
209+
if let upstream = transport.value(forKey: "Upstream") as? String {
210+
ioregService.transportUpstream = upstream
211+
}
212+
if let downstream = transport.value(forKey: "Downstream") as? String {
213+
ioregService.transportDownstream = downstream
214+
}
215+
}
216+
return ioregService
202217
}
203218

204219
// Sets up the service in an IOregService if it is found using the provided io_service_t pointing to a DCPAVServiceProxy item in the ioreg tree
205-
private static func setIORegServiceDCPAVServiceProxy(service: io_service_t, ioregService: inout IOregService, position: Int64 = 0) -> Bool {
220+
private static func setIORegServiceDCPAVServiceProxy(service: io_service_t, ioregService: inout IOregService) {
206221
if let unmanagedLocation = IORegistryEntryCreateCFProperty(service, CFStringCreateWithCString(kCFAllocatorDefault, "Location", kCFStringEncodingASCII), kCFAllocatorDefault, IOOptionBits(kIORegistryIterateRecursively)), let location = unmanagedLocation.takeRetainedValue() as? String {
222+
ioregService.location = location
207223
if location == "External" {
208-
ioregService.serviceIoregPosition = position
209224
ioregService.service = IOAVServiceCreateWithService(kCFAllocatorDefault, service)?.takeRetainedValue() as IOAVService
210-
return true
211225
}
212226
}
213-
return false
214227
}
215228

216229
// Returns IOAVSerivces with associated display properties for matching logic
217230
private static func getIoregServicesForMatching() -> [IOregService] {
218-
var position: Int64 = 0
231+
var serviceLocation: Int = 0
219232
var ioregServicesForMatching: [IOregService] = []
220233
let ioregRoot: io_registry_entry_t = IORegistryGetRootEntry(kIOMasterPortDefault)
221234
var iterator = io_iterator_t()
235+
var ioregService = IOregService()
222236
guard IORegistryEntryCreateIterator(ioregRoot, "IOService", IOOptionBits(kIORegistryIterateRecursively), &iterator) == KERN_SUCCESS else {
223237
return ioregServicesForMatching
224238
}
225239
while true {
226-
let serviceAppleCLCD2 = self.ioregIterateToNext(ioregObjectName: "AppleCLCD2", iterator: &iterator, position: &position)
227-
guard serviceAppleCLCD2 != IO_OBJECT_NULL else {
228-
break
229-
}
230-
// We will check if it has an EDID UUID. If so, then we take it as an external display
231-
if var ioregService = getIORegServiceAppleCDC2Properties(service: serviceAppleCLCD2, position: position) {
232-
// We will now iterate further, looking for the belonging "DCPAVServiceProxy" service (which should follow "AppleCLCD2" somewhat closely)
233-
let serviceDCPAVServiceProxy = self.ioregIterateToNext(ioregObjectName: "DCPAVServiceProxy", iterator: &iterator, position: &position)
234-
guard serviceDCPAVServiceProxy != IO_OBJECT_NULL else {
235-
break
240+
if let objectOfInterest = ioregIterateToNextObjectOfInterest(interests: ["AppleCLCD2", "DCPAVServiceProxy"], iterator: &iterator) {
241+
if objectOfInterest.name == "AppleCLCD2", objectOfInterest.service != IO_OBJECT_NULL {
242+
ioregService = self.getIORegServiceAppleCDC2Properties(service: objectOfInterest.service)
243+
serviceLocation += 1
244+
ioregService.serviceLocation = serviceLocation
236245
}
237-
// Let's now create an instance of IOAVService with this service and add it to the service store with the "AppleCLCD2" strings
238-
if self.setIORegServiceDCPAVServiceProxy(service: serviceDCPAVServiceProxy, ioregService: &ioregService, position: position) {
246+
if objectOfInterest.name == "DCPAVServiceProxy", objectOfInterest.service != IO_OBJECT_NULL {
247+
self.setIORegServiceDCPAVServiceProxy(service: objectOfInterest.service, ioregService: &ioregService)
239248
ioregServicesForMatching.append(ioregService)
240249
}
250+
} else {
251+
break
241252
}
242253
}
243254
return ioregServicesForMatching
244255
}
256+
257+
// Check if it is problematic to enable DDC on the display
258+
private static func checkIfDiscouraged(ioregService: IOregService) -> Bool {
259+
var modelIdentifier: String = ""
260+
let platformExpertDevice = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IOPlatformExpertDevice"))
261+
if let modelData = IORegistryEntryCreateCFProperty(platformExpertDevice, "model" as CFString, kCFAllocatorDefault, 0).takeRetainedValue() as? Data, let modelIdentifierCString = String(data: modelData, encoding: .utf8)?.cString(using: .utf8) {
262+
modelIdentifier = String(cString: modelIdentifierCString)
263+
}
264+
// This is a well known dummy plug (not a real display) but it breaks DDC communication on M1
265+
if ioregService.manufacturerID == "AOC", ioregService.productName == "28E850" {
266+
return true
267+
}
268+
// First service location of Mac Mini HDMI is broken for DDC communication
269+
if ioregService.transportDownstream == "HDMI", ioregService.serviceLocation == 1, modelIdentifier == "Macmini9,1" {
270+
return true
271+
}
272+
return false
273+
}
245274
}

MonitorControl/Support/PollingMode.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ enum PollingMode {
1212
case .none:
1313
return 0
1414
case .minimal:
15-
return 5
15+
return 3
1616
case .normal:
17-
return 10
17+
return 6
1818
case .heavy:
19-
return 100
19+
return 30
2020
case let .custom(val):
2121
return val
2222
}

MonitorControlHelper/Info.plist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
<key>CFBundleShortVersionString</key>
2020
<string>$(MARKETING_VERSION)</string>
2121
<key>CFBundleVersion</key>
22-
<string>1448</string>
22+
<string>1513</string>
2323
<key>LSApplicationCategoryType</key>
2424
<string>public.app-category.utilities</string>
2525
<key>LSBackgroundOnly</key>

0 commit comments

Comments
 (0)