Skip to content
Andrei Golli

Forcing XDR brightness on the macOS desktop

A MacBook Pro panel reaches 1,600 nits, but macOS caps ordinary windows at 600. I gave the whole desktop the rest by scaling the display's gamma table.

Andrei Golli6 min read

Thoughts or corrections? Write to me.

An M-series MacBook Pro display is rated for 1,000 nits sustained and 1,600 at its peak. A plain white window shows about 600. So the panel can go nearly three times brighter than the brightest white you see all day, and you never get to use it.

macOS keeps that range for HDR (high dynamic range): a photo, a Dolby Vision clip, a game that asks for it. Put a white document next to an HDR video and the gap is obvious, because the video's highlights are visibly brighter than anything your editor can draw. Ordinary windows are capped at 600 nits, and the other 1,000 sit there, switched off for everything else.

I wanted the whole desktop to use them. It takes two moves: make macOS believe HDR is on screen, then rewrite the display's gamma table so a white window pushes past the 600-nit cap. The second move hinges on a quirk in how macOS resizes that table, and that is the part worth reading for.

Where the other 1,000 nits go

macOS rates brightness on a scale it calls EDR, for extended dynamic range. On it, 1.0 is the cap on a normal window, which Apple calls SDR white, up to 600 nits, and higher values are brighter. The 1,600-nit peak does not move when you change the brightness slider; the cap does. Apple's WWDC talk defines it: "1.0 represents SDR max … scaled by the display brightness slider."

The gap between the cap and the peak is the headroom: how far past the cap a pixel may go. Turn the slider down and the cap drops while the peak holds, so there is more headroom to take. At its dimmest my display reports 16× the cap; at full brightness, about 3×.

NSScreen reports the live value:

read-headroom.swiftswift
let s = NSScreen.main!
print(s.maximumExtendedDynamicRangeColorComponentValue,       // headroom now
      s.maximumPotentialExtendedDynamicRangeColorComponentValue) // headroom possible

With nothing HDR on screen it reads 1.2. I need to raise that, then spend the room it opens.

It only opens while HDR is on screen

That number will not rise on its own. Apple's docs say it climbs above 1.0 only "if any content onscreen has requested EDR," and a desktop of plain windows requests nothing.

So I give macOS the smallest scrap of HDR it will take: a window two pixels wide, drawn with Metal in a color above 1.0. That is enough. macOS counts the display as HDR and opens the headroom, which reaches the ceiling in under three seconds and holds for about fifteen after the window closes. The headroom is only permission, though. Nothing is brighter yet. The second move spends it.

The gamma table

Every pixel leaves the display through its gamma table, a lookup that maps each input level to an output level. One CoreGraphics call reads mine:

zsh
$ ./read-gamma
samples 1024   [0] 0.000000   [512] 0.500733   [1023] 1.000000
max 1.000000   deviation from a straight line: 0.000000

It is a straight line from 0 to 1 across 1,024 entries, topping out at 1.0. That 1.0 is the 600-nit cap: it is why a white window never goes brighter, and it is what I move next.

Scaling the line

Multiply every entry by 1.6 and full white maps to 1.6 instead of 1.0, past the cap and into the headroom. With the window holding the headroom open, the display actually draws it. One Accelerate call scales each color channel, and CGSetDisplayTransferByTable writes the table back:

scale.swiftswift
var factor: Float = 1.6                       // output 1.6 at full input
vDSP_vsmul(defaultRed,   1, &factor, &scaledRed,   1, vDSP_Length(count))
vDSP_vsmul(defaultGreen, 1, &factor, &scaledGreen, 1, vDSP_Length(count))
vDSP_vsmul(defaultBlue,  1, &factor, &scaledBlue,  1, vDSP_Length(count))
CGSetDisplayTransferByTable(displayID, count, scaledRed, scaledGreen, scaledBlue)
The default table stops at the 600-nit cap. Scaling it ×1.6 pushes white past it, into the headroom.

Now every entry above 1.0 asks for a brighter pixel than a normal window gets, and the headroom allows it. There is one catch, and it is the number of entries in your table.

Why the table size matters

The display has 1,024 entries. Write a table that size and nothing changes: read it back and every value is capped at 1.0. Write 256 and the brightness sticks. CGSetDisplayTransferByTable resamples your table to the hardware's length, and the resampler handles the two sizes differently. Here is its code from the macOS binary; watch for one thing only, whether a value is ever compared against 1.0.

the gamma resampler, disassembledasm
resample(channels, dstCount, dst, srcCount, r, g, b):
  cmp    w1, w3           ; dstCount vs srcCount
  b.ne   resample_path    ; sizes differ -> no clamp
 
copy_path:                ; sizes equal
  ldr    s2, [x6, x11]    ; v = src[i]
  fcmp   s2, s0           ; s0 = 1.0
  fcsel  s2, s0, s2, gt   ; v > 1.0  ->  1.0
  fcmp   s2, #0.0
  fcsel  s2, s1, s2, mi   ; v < 0.0  ->  0.0
  str    s2, [x9]         ; dst[i] = v
 
resample_path:            ; sizes differ
  ldr    s5, [x4, x13]    ; lo = src[idx]
  ldr    s6, [x4, x14]    ; hi = src[idx + 1]
  fmul   d5, d4, d5       ; lo * (1 - frac)
  fmadd  d5, d3, d6, d5   ; + hi * frac
  str    s3, [x2, x11]    ; dst[i] = result   (never compared to 1.0)
Step 1 of 3

It compares your entry count with the hardware's. Equal counts take the copy path; different counts take the resample path.

I confirmed it on plain arrays, with no display attached: a 1,024-entry ramp scaled by 1.6 came back clamped to 1.0, and a 256-entry one kept its 1.6.

macOS clamps your values to 1.0 only when the table matches the hardware's length. A shorter table skips the clamp, so 256 entries work and 1,024 do not.

So the table's length picks the path. Match the hardware and macOS treats your table as final and clamps it back to the 600-nit cap. Use a different length and it resamples, and the resampler never checks the ceiling. On macOS 27, 256 entries is the whole trick.

Keeping it

One write is not enough, because macOS keeps overwriting the table. Auto-brightness redraws it on the light sensor, Night Shift warms it on a schedule, and ColorSync reloads the display profile on any display change. So I read the table back every couple of seconds and reapply it when the top entry drifts. That check also caught a macOS 26 beta where CGSetDisplayTransferByTable returned success and did nothing.

In summary

The panel can go well past 600 nits; macOS just reserves that range for HDR. Taking it for the desktop is two moves: keep a sliver of HDR on screen to hold the headroom open, and write a scaled gamma table whose white sits above the cap. The scaling survives only because macOS resamples a wrong-sized table without clamping it. Match the hardware's length and it is capped straight back to 600 nits.

Further reading

The code here is from Fullbright, my open-source XDR brightness app.

Get new posts by email

About once a month, I'll share what I'm researching or learning.