Why M1 Macs Can't Handle Two Screens
Rough Notes
M1 Multiple Screens
Every time I plug my screens, their configurations are randomized.
It turns out, screens have UUIDs, in addition to their unit numbers, so you can use displayplacer to save a good config and later restore it.
Except you can’t! These days, the problem seems to arise because the UUIDs macOS assigns to screens don’t actually get assigned to the same screen when multiple screens of the same type are connected, such as when you dock your laptop.
I am not sure how macOS recognizes a screen on reconnection, but their own Quartz API provides a hint: the only stable identifiers that are exposed through this API are:
CGDisplaySerialNumber(…);
CGDisplayVendorNumber(…);
CGDisplayModelNumber(…);
All three are uint32_t. Apple’s documentation seems to believe that, taken together, these three numbers are unique enough, so if their tech writer team believes so, most likely so did the programmer.
The problem is, and anyone can verify this experimentally (code in appendix), that many modern screens don’t actually return a unique serial number when queried through this API. For example, as far as I can tell, all BenQ screens have the same serial 21573!
So what’s probably happening here, is that macOS persists UUIDs when the screen is disconnected, and then reassigns it when it reconnects based on the serial, but if you connect three screens of the same type, the UUIDs will be assigned to them at random.
Now, hardware vendors are mostly gung-ho with standards, but even they are not crazy enough to assign every monitor the same serial number. What’s actually going on is that the serial numbers don’t fit in the 4 bytes available for the uint32_t - for example, the serial number of my BenQ is 11 bytes long. (BenQ says 13, but the first two characters are just for show, and don’t show up in the EDID data.)
So easy-peezy, right. I would patch displayplacer to use the full serial as the persistent identifier, instead of the UUID, and all would be fine. The full serial is part of something called EDID, and a cursory online search found that IOKit should be the way to get it.
The first hurdle was that converting a Quartz display ID (typed CGDirectDisplayID) into an IOKit ID (in this case probably io_service_t) requires calling IOServicePortFromCGDisplayID, which Apple deprecated with a helpful message saying “there is no replacement, fuck off”.
A sensible alternative is walking to device kit in IORegistry until we find our screen. The usual way of doing this would be by searching the registry for a service of type IODisplayConnect, but sadly that doesn’t seem to exist on M1 macs anymore. (https://developer.apple.com/forums/thread/666383). And, by the way, neither does anything called IODPDevice exist, at least not as of Monterey.
Bit of an impasse. A former career in digital forensics taught me the amazing power of such sophisticated tools as strings and grep. I was sure that some subsystem of this ridiculous operating system must know the full serial number of my monitor. How could it not? You can’t implement a graphics system without being able to read the EDID.
The most verbose command I know on macOS of dumping everything about everything connected to a machine is the IORegistryExplorer, but its search function doesn’t let you match on the values of attributes. But it does have the ability export everything to a file, which when examined with file, turns out to be a regular plist.
A strings XXX | grep -i SERIALNUMBER later, I found that the OS does indeed have the information I was looking for. After running ioreg -lw0 and sifting through the output, I found that the serial number occurs as part of the “DisplayAttributes” property in an IOKit class called AppleCLCD2. Progress! Sadly, the UUID that Quartz assigns the to the display doesn’t appear to exist at the IOKit level, so we have to figure out some other way to pair these up. (Remember, the goal is to know which of the Quartz display IDs corresponds to which serial, because the latter is stable.)
Sadly, all the information Quartz makes available about the screen seems to be attributes that are not useful in identifying the same screen in IOKit state. In fact, with truly identical screen models, the only things that can be used to tell them apart are the serial number, which is only available in IOKit and any pseudorandom state lying around. So the task is to find pseudorandom state with enough randomness to make it unique, which has some property shared between the data in Quartz and the data in IOKit.
CoreDisplay is the same thing. You can get an info dictionary that contains the same information: the only extra is the name of the screen, which is again the same for all of them.
https://github.com/keith/dyld-shared-cache-extractor
./dyld-shared-cache-extractor /System/Library/dyld/dyld_shared_cache_arm64e ~/tmp/libraries nm -gU System/Library/Frameworks/CoreDisplay.framework/Versions/A/CoreDisplay
Something interesting occurred to me as part of this little adventure: Apple’s control over its hardware has made it possible for their kernel and driver programmers to get away with a lot of assumptions that fall apart when dealing with the outside world. By contrast, Linux and Windows have to run on a vast array of hardware, so driver coders for those platforms have developed a healthy sense of paranoia about hardware vendors, and have learned to verify every assumption.
Ultimately, external screens are one of the few accessories Apple doesn’t make in-house, and it has to deal with what’s out there. They’re doing a shit job, for the most part.