

Trying to get past the 500 nits limit of the MacBook Pro (and failing)
source link: https://alinpanaitiu.com/blog/over-500nits-failed/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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
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.
As soon as these devices were in the hands of users, requests started coming in for Lunar to provide an option to get past the 500 nits limit for everyday usage
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.
# Testing the system
# Playing a HDR video
I first started by playing this HDR test video (open it in latest Chrome or Safari for best results): hdr-test-pattern.webm
Which resulted in a blinding white at 1600 nits:
This generated the following logs in Console.app:
WindowServer Display 1 setting nits to 888.889
corebrightnessd SDR - perceptual ramp clocked: 227.095169 -> 252.268112 - 49.169426% (239.142059 Nits)
WindowServer Display 1 commitBrightness sdr: 211.603, headroom: 4.20075, ambient: 4.3396, filtered ambient: 13.6333, limit: 1600
# SDR cap in normal lighting
After setting the display brightness to max, I could see in the logs that SDR (Standard Dynamic Range) was being capped at 400 nits:
WindowServer Display 1 setting nits to 1600
WindowServer Display 1 setting display headroom hint to 7.56866
WindowServer Display 1 commitBrightness sdr: 216.548, headroom: 7.38865, ambient: 4.24854, filtered ambient: 13.3472, limit: 1600
corebrightnessd PCC: Set PCC: Factor:=1.0496 CabalFactor:=0.0033 time=2.000000 Lux:=13.6080 Nits:=229.1757 result=1 error=(null)
WindowServer Display 1 commitBrightness sdr: 301.188, headroom: 5.3123, ambient: 4.24854, filtered ambient: 13.3472, limit: 1600
WindowServer Display 1 setting nits to 1602.03
corebrightnessd levelPercentage 0.334298, level = 4.967383 (nits/pwm), lux = 15.000000
WindowServer Display 1 commitBrightness sdr: 301.571, headroom: -1, ambient: 4.79275, filtered ambient: 15.0569, limit: -1
WindowServer Display 1 setting display headroom hint to 5.27556
WindowServer Display 1 commitBrightness sdr: 321.478, headroom: 4.97701, ambient: 4.79275, filtered ambient: 15.0569, limit: 1600
WindowServer Display 1 commitBrightness sdr: 340.675, headroom: 4.69655, ambient: 4.79275, filtered ambient: 15.0569, limit: 1600
WindowServer Display 1 commitBrightness sdr: 377.322, headroom: 4.24041, ambient: 4.79275, filtered ambient: 15.0569, limit: 1600
corebrightnessd PCC: Set PCC: Factor:=1.0340 CabalFactor:=0.0023 time=2.000000 Lux:=15.0569 Nits:=377.3223 result=1 error=(null)
WindowServer Display 1 setting nits to 1600
WindowServer Display 1 setting display headroom hint to 4
WindowServer Display 1 commitBrightness sdr: 400, headroom: -1, ambient: 4.96577, filtered ambient: 15.6004, limit: -1
# SDR cap in direct sunlight
Shining a flashlight directly into the Ambient Light Sensor allowed SDR to jump up to 500 nits:
WindowServer Display 1 commitBrightness sdr: 400, headroom: -1, ambient: 322.204, filtered ambient: 1012.24, limit: -1
WindowServer Display 1 commitBrightness sdr: 400.484, headroom: 1, ambient: 322.204, filtered ambient: 1012.24, limit: 400.484
WindowServer Display 1 setting nits to 400.484
WindowServer Display 1 commitBrightness sdr: 401.15, headroom: 1, ambient: 322.204, filtered ambient: 1012.24, limit: 401.15
WindowServer Display 1 setting nits to 401.15
WindowServer Display 1 commitBrightness sdr: 401.223, headroom: 1, ambient: 322.204, filtered ambient: 1012.24, limit: 401.224
WindowServer Display 1 setting nits to 401.223
WindowServer Display 1 commitBrightness sdr: 401.223, headroom: -1, ambient: 370.814, filtered ambient: 1164.95, limit: -1
WindowServer Display 1 commitBrightness sdr: 401.552, headroom: 1, ambient: 370.814, filtered ambient: 1164.95, limit: 401.552
corebrightnessd PCC: Set PCC: Factor:=1.7464 CabalFactor:=0.0498 time=2.000000 Lux:=1164.9467 Nits:=401.5517 result=1 error=(null)
WindowServer Display 1 setting nits to 401.552
WindowServer Display 1 commitBrightness sdr: 402.219, headroom: 1, ambient: 370.814, filtered ambient: 1164.95, limit: 402.219
WindowServer Display 1 setting nits to 402.219
WindowServer Display 1 commitBrightness sdr: 402.885, headroom: 1, ambient: 370.814, filtered ambient: 1164.95, limit: 402.885
WindowServer Display 1 setting nits to 402.885
... lots of similar logs ...
WindowServer Display 1 setting nits to 495.458
WindowServer Display 1 commitBrightness sdr: 496.125, headroom: 1, ambient: 810.176, filtered ambient: 2545.24, limit: 496.125
WindowServer Display 1 setting nits to 496.125
WindowServer Display 1 commitBrightness sdr: 496.791, headroom: 1, ambient: 810.176, filtered ambient: 2545.24, limit: 496.792
WindowServer Display 1 setting nits to 496.791
WindowServer Display 1 commitBrightness sdr: 497.458, headroom: 1, ambient: 810.176, filtered ambient: 2545.24, limit: 497.458
WindowServer Display 1 setting nits to 497.458
WindowServer Display 1 commitBrightness sdr: 498.125, headroom: 1, ambient: 810.176, filtered ambient: 2545.24, limit: 498.125
WindowServer Display 1 setting nits to 498.125
WindowServer Display 1 commitBrightness sdr: 498.791, headroom: 1, ambient: 810.176, filtered ambient: 2545.24, limit: 498.792
WindowServer Display 1 setting nits to 498.791
WindowServer Display 1 commitBrightness sdr: 499.458, headroom: 1, ambient: 810.176, filtered ambient: 2545.24, limit: 499.458
WindowServer Display 1 setting nits to 499.458
WindowServer Display 1 commitBrightness sdr: 500, headroom: 1, ambient: 810.176, filtered ambient: 2545.24, limit: 500
WindowServer Display 1 setting nits to 500
WindowServer Display 1 commitBrightness sdr: 500, headroom: -1, ambient: 987.858, filtered ambient: 3103.45, limit: -1
# Dissecting the system
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.
I used dyld-shared-cache-extractor to drop the separate binaries on disk, then did another search there.
This surfaced up QuartzCore
as the single place where that string could be found.
# Trying to abuse QuartzCore
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.
code from main.swift
func setToMax(_ d: CAWindowServerDisplay) {
d.setBrightnessLimit(1600)
d.setHeadroom(1)
d.maximumBrightness = 1000.0
d.setSDRBrightness(600)
d.maximumHDRLuminance = 1600
d.maximumReferenceLuminance = 1600
d.maximumSDRLuminance = 1000
d.contrast = 1.1
d.commitBrightness(1)
// d.update() // segfault
}
let ws: CAWindowServer? = (CAWindowServer.server(withOptions: nil) as? CAWindowServer) // disconnects external displays
if let ws = ws,
let displays = ws.displays as? [CAWindowServerDisplay],
let d = displays.first(where: { $0.deviceName == "primary" })
{
setToMax(d)
}
commitBrightness
log line
monitorpanel Display 1 commitBrightness sdr: 600, headroom: 1, ambient: -1, filtered ambient: -1, limit: 1600
# CoreBrightness
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.
// Uninteresting Exported Symbols
_OBJC_CLASS_$_BrightnessSystem
_OBJC_CLASS_$_BrightnessSystemClient
_OBJC_CLASS_$_BrightnessSystemClientInternal
_OBJC_CLASS_$_CBAdaptationClient
_OBJC_CLASS_$_CBBlueLightClient
_OBJC_CLASS_$_CBClient
_OBJC_CLASS_$_CBKeyboardPreferencesManager
_OBJC_CLASS_$_CBTrueToneClient
_OBJC_CLASS_$_DisplayServicesClient
_OBJC_CLASS_$_KeyboardBrightnessClient
// Interesting Not Exported Symbols
-[CBBrightnessProxySKL brightnessNotificationRequestEDR]
-[CBBrightnessProxySKL brightnessRequestEDRHeadroom]
-[CBBrightnessProxySKL brightnessRequestRampDuration]
-[CBBrightnessProxySKL commitBrightness:]
-[CBBrightnessProxySKL initWithSLSBrightnessControl:]
-[CBBrightnessProxySKL setAmbient:]
-[CBBrightnessProxySKL setBrightnessLimit:]
-[CBBrightnessProxySKL setHeadroom:]
-[CBBrightnessProxySKL setNotificationQueue:]
-[CBBrightnessProxySKL setPotentialHeadroom:]
-[CBBrightnessProxySKL setSDRBrightness:]
-[CBBrightnessProxySKL setWhitePoint:rampDuration:error:]
-[CBBrightnessProxySKL unregisterNotificationBlocks]
-[CBDisplayModuleSKL configureEDRSecPerStop]
-[CBDisplayModuleSKL configurePCCDefaults]
-[CBDisplayModuleSKL getBrightnessLimit]
-[CBDisplayModuleSKL getDynamicSliderAdjustedNits:]
-[CBDisplayModuleSKL getDynamicSliderAdjustedSDRNits]
-[CBDisplayModuleSKL getLinearBrightnessForNits:]
-[CBDisplayModuleSKL getLinearBrightness]
-[CBDisplayModuleSKL getMaxNitsAdjusted]
-[CBDisplayModuleSKL getMaxNitsEDR]
-[CBDisplayModuleSKL getMaxPanelNits]
-[CBDisplayModuleSKL getNitsForLinearBrightness:]
-[CBDisplayModuleSKL getNitsForUserBrightness:]
-[CBDisplayModuleSKL getPerceptualBrightness]
-[CBDisplayModuleSKL getSDRBrightnessCurrent]
-[CBDisplayModuleSKL getSDRBrightnessTarget:]
-[CBDisplayModuleSKL getSDRNitsCapped]
-[CBDisplayModuleSKL getUserBrightnessForNits:]
-[CBDisplayModuleSKL getUserBrightnessSloperExtended]
-[CBDisplayModuleSKL getUserBrightness]
-[CBDisplayModuleSKL handleBrightnessCapOverride:]
-[CBDisplayModuleSKL initialiseEDR]
-[CBDisplayModuleSKL initialiseSDR]
-[CBDisplayModuleSKL luminanceToPerceptual:]
-[CBDisplayModuleSKL panelMaxNitsOverride:]
-[CBDisplayModuleSKL perceptualToLuminance:]
-[CBDisplayModuleSKL rampDynamicSlider:withLength:]
-[CBDisplayModuleSKL rampEDRHedroom:withLength:]
-[CBDisplayModuleSKL rampFactor:withLength:]
-[CBDisplayModuleSKL rampManagerUpdateHandling]
-[CBDisplayModuleSKL rampNitsCap:]
-[CBDisplayModuleSKL rampSDRBrightness:withLength:properties:]
-[CBDisplayModuleSKL requestEDRHeadroomImmediate:]
-[CBDisplayModuleSKL requestEDRHeadroomTransition:withLength:]
-[CBDisplayModuleSKL requestEDRHeadroomTransitionStop]
-[CBDisplayModuleSKL requestFactorImmediate:]
-[CBDisplayModuleSKL requestFactorTransition:withLength:]
-[CBDisplayModuleSKL requestFactorTransitionStop]
-[CBDisplayModuleSKL requestSDRBrightnessTransition:]
-[CBDisplayModuleSKL requestSDRBrightnessTransition:withLength:properties:]
-[CBDisplayModuleSKL requestSDRBrightnessTransitionStop]
-[CBDisplayModuleSKL supportsDynamicSlider]
-[CBDisplayModuleSKL supportsEDR]
-[CBDisplayModuleSKL supportsSDRBrightness]
-[CBDisplayModuleSKL updateAmbient]
-[CBDisplayModuleSKL updateAutoBrightnessState:]
-[CBDisplayModuleSKL updateBrightnessState]
-[CBDisplayModuleSKL updateContrastEnhancerState:]
-[CBDisplayModuleSKL updateDynamicSliderAmbient]
-[CBDisplayModuleSKL updateDynamicSliderAutoBrightness]
-[CBDisplayModuleSKL updateDynamicSliderChargerState]
-[CBDisplayModuleSKL updateDynamicSliderScaler:]
-[CBDisplayModuleSKL updateEDRAmbient]
-[CBDisplayModuleSKL updateSDRBrightness:]
-[CBDisplayModuleSKL updateSDRNits:]
-[CBEDR appliedCompensation]
-[CBEDR availableHeadroom]
-[CBEDR brightnessCap]
-[CBEDR cappedHeadroomFromUncapped:]
-[CBEDR copyStatusInfo]
-[CBEDR description]
-[CBEDR initWithRampPolicy:potentialHeadroom:andReferenceHeadroom:]
-[CBEDR maxHeadroom]
-[CBEDR panelMax]
-[CBEDR referenceHeadroom]
-[CBEDR sanityCheck]
-[CBEDR sdrBrightness]
-[CBEDR secondsPerStop]
-[CBEDR setAppliedCompensation:]
-[CBEDR setBrightnessCap:]
-[CBEDR setPanelMax:]
-[CBEDR setSdrBrightness:]
-[CBEDR setSecondsPerStop:]
-[CBEDR shouldUpdateEDRForRequestedHeadroom:targetHeadroom:rampTime:]
-[CBEDR stopsFromHeadroomRatio:]
-[CBNVRAM backlightNitsDefault]
-[CBNVRAM backlightNitsMax]
-[CBNVRAM backlightNitsMin]
-[CBNVRAM dealloc]
-[CBNVRAM init]
-[CBNVRAM readBacklightNits]
-[CBNVRAM setBacklightNitsMax:]
-[CBNVRAM writeBacklightNits:]
# SkyLight
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.
@import Darwin;
@import Foundation;
// clang -fmodules -F/System/Library/PrivateFrameworks -framework SkyLight -o headroom headroom.m && ./headroom
extern int SLSMainConnectionID(void);
extern CFTypeRef SLSDisplayGetCurrentHeadroom(int did);
extern void kSLSBrightnessRequestEDRHeadroom(int did, CFTypeRef headroom);
const int MAIN_DISPLAY_ID = 1;
int main(int argc, char** argv)
{
int cid = SLSMainConnectionID();
NSLog(@"SLSMainConnectionID: %d", cid);
CFTypeRef headroom = SLSDisplayGetCurrentHeadroom(MAIN_DISPLAY_ID);
kSLSBrightnessRequestEDRHeadroom(MAIN_DISPLAY_ID, headroom);
return 0;
}
# Other ideas
# Streaming to a dummy
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.
# Using private symbols
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.
Posted on: Length: Categories: Tags: See Also:
Recommend
-
11
Day 26: Trying to describe the TCP state machine in a readable way. Failing. • hackerschool • Today I made a bunch of progress (I can now be a TCP server,...
-
17
2617 members Technology Technology on Digg: the best articles, videos, tweets, and original content that the web is talking about right now.
-
9
Donald Trump is trying (and failing) to get around Twitter's ban Trump's attempts to evade his Twitter ban have been deleted. ...
-
5
New issue Spellcheck and Markdown nits #6899
-
13
Changelog Features XDR Brightness: go past the 500 nits limit of the new 2021 MacBook Pro and Pro Display XDR Warning: this is experimental. The syst...
-
7
Retina (Searing) Display — App lets you crank the new MacBook Pro’s brightness to over 1,000 nits It has a few caveats, but it works as advertised.
-
7
AMD Ryzen 7000 (Zen 4) allegedly failing to boost and clock past DDR5-5200...
-
6
Samsung Display’s new QD-OLED TV panels can hit 2,000 nits of peak brightness / 2,000 nits is around double what was offered by last year’s brightest OLED TVs, but only slightly higher than what LG’s competing sets ma...
-
7
Samsung QD-OLED TVs to reach up to 2,000 nits of peak brightness in 2023
-
3
Apple's AR/VR Headset Display Specs: 5000+ Nits Brightness for HDR, 1.41-Inch Diagonal Display and More
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK