Trying to get past the 500 nits limit of the MacBook Pro (and failing)
Investigating why the new MacBook Pro XDR display is capped at 500 nits, despite being advertised as '1000 nits sustained'
February 4, 2022
Update: I finally found a way to go over the limit in Lunar v5.5.1
Exactly 3 months and a day after placing an order through a Romanian Apple reseller, I finally got my 14-inch M1 Max.
Well, actually.. I first got the wrong configuration (base model instead of CTO), had to return it to them after wasting a day on migrating my data to it, they sent my money back by mistake, had to pay them again, and after many calls and emails later the correct laptop arrived.
Over the last week I tried my best to figure out how to do this, but it’s either impossible to raise the nits limit from userspace, or I just don’t have the necessary expertise.
I’ll share some details that I found while reverse engineering my way through the macOS part that handles brightness.
Since Big Sur, macOS transitioned from having the frameworks on the disk as separate binaries, to having a single file containing all the system libraries, called a dyld_shared_cache.
New in macOS Big Sur 11.0.1, the system ships with a built-in dynamic linker cache of all system-provided libraries. As part of this change, copies of dynamic libraries are no longer present on the filesystem. Code that attempts to check for dynamic library presence by looking for a file at a path or enumerating a directory will fail. Instead, check for library presence by attempting to dlopen() the path, which will correctly check for the library in the cache. (62986286)
Searching for keywords from the above logs surfaced only the dyld cache as expected.
After looking through the QuartzCore binary with Ghidra and finding some iOS headers for it on limneos.net, I created a sample Swift project to try to use some of the exported functions from it: monitorpanel - main.swift
Based on some open-sourced iOS jailbreak tweaks, I noticed that developers used the CAWindowServer class to interface with the display and HID components directly. The class was available here so I tried to do the same on macOS.
Unfortunately, CAWindowServer.serverIfRunning always returns nil and while CAWindowServer.server(withOptions: nil) returns a seemingly valid server, all external displays are forcefully disconnected when that server is created.
Using the below code, I succeeded in producing the commitBrightness log line in Console, but nothing really changed.
While looking through Ghidra, I noticed that QuartzCore finally calls into CoreBrightness functions to increase the nits limit, so I took a look at the exported symbols on that binary.
Unfortunately, all the possibly useful symbols are not exported and trying to link against them would result in the undefined symbols error.
Adding the private symbols in the CoreBrightness.tbd file doesn’t help in this case.
I knew from previous work on window management that the SkyLight framework is closely related to the WindowServer so I took a look at that too.
SkyLight exports a lot of symbols, and fortunately I had a good example on how to use them inside yabai, a macOS window manager similar to i3 and bspwm.
But again, nothing useful is exported.
The function kSLSBrightnessRequestEDRHeadroom seemed promising but I always got a SIGBUS when trying to call it. I can’t find its implementation so I don’t know what parameters I should pass. I just guessed the first one could be a display ID.
As one Hacker News user pointed out, kSLSBrightnessRequestEDRHeadroom is actually a constant. And of course it is! It has the usual k prefix.. how did I miss that?
While discussing this matter with István Tóth, the developer of BetterDummy, he came up with an interesting idea.
Create a CGVirtualDisplay with the same size as the built-in display
Tone map the SDR contents of the built-in display to 1000nits HDR video
CGDisplayStream that video to the virtual display
Move the virtual display to the built-in display coordinates and use that as the main display
The streaming part already works in the latest Beta of BetterDummy and seems pretty fast as well. But adding tone mapping might cause this to be too resource intensive to be used.
I think linking can be done against private symbols using memory offsets, I remember doing something like that 8 years ago at BitDefender, while trying to use the unexported _decrypt and _generate_domain methods of some DGA malware.
But the dyld_shared_cache model of macOS is something new to me and I don’t have enough knowledge to be able to do that right now.
If someone has any idea how this can be achieved, I’d be glad if you could send me a hint through the Contact page.