How to resolve CGDirectDisplayID changing issues on newer multi-GPU Apple laptops in Core Foundation/IO Kit?
In Mac OS X, every display gets a unique CGDirectDisplayID
number assigned to it. You can use CGGetActiveDisplayList(
) or [NSScreen screens]
to access them, among others. Per Apple's docs:
A display ID can persist across processes and system reboot, and typically remains constant as long as certain display parameters do not change.
On newer mid-2010 MacBook Pro's, Apple started using auto-switching Intel/nVidia graphics. Laptops have two GPU's, a low-powered Intel, and a high-powered nVidia. Previous dual-GPU laptops (2009 models) didn't have auto-GPU switching, and required the user to make a settings change, logoff, and then logon again to make a GPU switch occur. Even older systems only had one GPU.
There's an issue with the mid-2010 models where CGDirectDisplayID's don't remain the same when a display switches from one GPU to the next. For example:
- Laptop powers on.
- Built-In LCD Screen is driven by Intel chipset. Display ID: 30002
- External Display is plugged in.
- Built-In LCD Screen switches to nVidia chipset. It's display ID changes: 30004
- External Display is driven by nVidia chipset.
- ...at this point, the Intel chipset is dormant...
- User unplugs External Display.
- Built-In LCD Screen switches back to Intel chipset. It's display ID changes back to original: 30002
My question is, how can I match an old display ID to a new display ID when they alter due to a GPU change?
Thought about:
I've noticed that the display ID only changes by 2, but I don't have enough test Mac's available to determine if this is common to all new MacBook Pro's, or just mine. Kind of a kludge if "just check for display ID's which are +/-2 from one another" works, anyway.
Tried:
CGDisplayRegisterReconfigurationCallback(开发者_如何学C)
, which notifies before-and-after when displays are going to change, has no matching logic. Putting something like this inside a method registered with it doesn't work:
// Run before display settings change:
CGDirectDisplayID directDisplayID = ...;
io_service_t servicePort = CGDisplayIOServicePort(directDisplayID);
CFDictionaryRef oldInfoDict = IODisplayCreateInfoDictionary(servicePort, kIODisplayMatchingInfo);
// ...display settings change...
// Run after display settings change:
CGDirectDisplayID directDisplayID = ...;
io_service_t servicePort = CGDisplayIOServicePort(directDisplayID);
CFDictionaryRef newInfoDict = IODisplayCreateInfoDictionary(servicePort, kIODisplayMatchingInfo);
BOOL match = IODisplayMatchDictionaries(oldInfoDict, newInfoDict, 0);
if (match)
NSLog(@"Displays are a match");
else
NSLog(@"Displays are not a match");
What's happening above is:
- I'm caching oldInfoDict before display settings change.
- Waiting for display settings to change
- Then comparing oldInfoDict to newInfoDict by using
IODisplayMatchDictionaries()
IODisplayMatchDictionaries()
returns a BOOL, either YES they're the same, or NO they're different.
Unfortunately, IODisplayMatchDictionaries()
doesn't return YES if the same display changed GPU's. Here's an example of the dictionary's it's comparing (look at the IODisplayLocation
key):
// oldInfoDict (Display ID: 30002)
oldInfoDict: {
DisplayProductID = 40144;
DisplayVendorID = 1552;
IODisplayLocation = "IOService:/AppleACPIPlatformExpert/PCI0@0/AppleACPIPCI/IGPU@2/AppleIntelFramebuffer/display0/AppleBacklightDisplay";
}
// newInfoDict (Display ID: 30004)
newInfoDict: {
DisplayProductID = 40144;
DisplayVendorID = 1552;
IODisplayLocation = "IOService:/AppleACPIPlatformExpert/PCI0@0/AppleACPIPCI/P0P2@1/IOPCI2PCIBridge/GFX0@0/NVDA,Display-A@0/NVDA/display0/AppleBacklightDisplay";
}
As you can see, the IODisplayLocation
key changes when GPU's are switched, hence IODisplayMatchDictionaries()
doesn't work.
I can, theoretically, compared just the DisplayProductID
and DisplayVendorID
keys, but I'm writing end-user software, and am worried of a situation where users have two or more identical monitors plugged in (meaning they'll both have the same DisplayProductID/DisplayVendorID). In other words, it's a less-than-perfect solution open to potential glitches.
Any help is greatly appreciated! :)
Use CFUUIDRef which can be obtained using:
CGDisplayCreateUUIDFromDisplayID(CGDirectDisplayID displayID)
and you can get the display ID back using:
CGDisplayGetDisplayIDFromUUID(CFUUIDRef uuid)
This is what I'm using to uniquely identify displays even when their CGDirectDisplayID changes, for example was plugged into a different port. These functions aren't properly documented by Apple unfortunately, but my testing on multiple Macs with multiple displays shown that the CFUUIDRef obtained is unique and consistent -even after a reboot-, regardless of whether CGDirectDisplayID changed for whatever reason.
To check if a display is new/unique, take its CGDirectDisplayID and convert it to CFUUIDRef, and then compare the UUID, it is a many-to-one relationship, many CGDirectDisplayIDs will map to a single CFUUIDRef.
These API calls are available in ApplicationServices in 10.7 - 10.12, and ColorSync since 10.13.
I have found no conceputally better way than what you list as "Tried". But I found a solution for the ambiguity issue of comparing only vendor id and product id.
oldInfoDict
and newInfoDict
in your code contain an additional entry for key kIODisplayEDIDKey
(defined in IOGraphicsTypes.h) which contains the EDID of each connected display. My observations show that this data as a whole stays persistent between GPU switches. For example:
CGDirectDisplayID displayId = [[[screen deviceDescription] valueForKey:@"NSScreenNumber"] unsignedIntValue];
io_service_t displayPort = CGDisplayIOServicePort(displayId);
if (displayPort == MACH_PORT_NULL)
return nil; // No physical device to get a name from.
CFDictionaryRef infoDict = IODisplayCreateInfoDictionary(displayPort, kIODisplayOnlyPreferredName);
NSData *displayEdid = (NSData *)CFDictionaryGetValue(infoDict, CFSTR(kIODisplayEDIDKey));
NSLog(@"EDID: %@", displayEdid);
CFRelease(infoDict);
Looking at the data description of EDID in Wikipedia, this blob already contains manufacturer, product id and serial. So it's enough to compare displays by using the EDID data (or for example a hash of it if you only want to compare a shorter number).
While I'm no pro, I believe the answer is to allow Apple to notify you when the user changes displays. The info inf the callback contains flags for adding and removing CGDirectDisplayID
s.
The user shouldn't be adding or removing graphics cards during operation, so I would play with making list at startup, and whenever you get the "remove" flag set the next "add" operation to replace that ID in the list.
I'd try just printing the information you get back each time CGDisplayRegisterReconfigurationCallback
calls your function. See if you get one with a DeviceUID with a 'remove' flag, and then a subsequent call another with an 'add' flag. Checking those id's against CGGetActiveDisplayList
would also aid in understanding what's going on.
That's my best bet, hope it helps!
I have been using:
-(NSString *)uuidForDisplayID:(CGDirectDisplayID)displayID
{
CFUUIDRef uuidRef = CGDisplayCreateUUIDFromDisplayID (displayID);
CFStringRef string = CFUUIDCreateString (kCFAllocatorDefault, uuidRef);
CFRelease (uuidRef);
return ([(NSString *)string autorelease]);
}
However during a display removal event in the call back, even during the initial kCGDisplayBeginConfigurationFlag
, the call to CGDisplayCreateUUIDFromDisplayID
fails with an 'invalid displayID' error, thus I see no way to go from a ambiguous CGDirectDisplayID back to a stable UUID when a display is being removed.
Interestingly, one can still get the array of NSScreens and search for one matching the CGDirectDisplayID:
[[[screen deviceDescription] objectForKey:@"NSScreenNumber"] unsignedLongValue]
In Xcode the NSScreen
has a property called _UUIDString
which contains the same UUID but I see no way to get this UUID out of the NSScreen.
精彩评论