• Edited

tl;dr disable temporal dithering on your M1/M2/M3 with Stillcolor.

I got 16” M3 Max MBP about a month ago and it’s been absolute hell. By far the worst screen I’ve ever used. It’s like staring into lasers. 1600 nits! Previously I’ve used a mid-2012 retina MBP for almost a decade, and briefly a 2019 16” Intel MBP (at reduced brightness). Those gave me no eyestrain, no dry eyes, no light sensitivity, no inability to focus.

So within the first 24 hours of getting the M3 Max I found LEDStrain and learned about PWM and temporal dithering, and so did my adventure begin of trying to make my new laptop usable for more than 1 hour a day.

At first I thought I could just connect it to an external monitor and adjust the color profile and brightness and everything will be fine. By the time I realized how futile those hacks were my 2-week return window was through.

I tried everything mentioned on this forum, including @NewDwarf boot-args, BetterDisplay with mirrored virtual displays, Iris, SwitchResX, etc. Disabled motion, transparency and True Tone, switched to sRGB, dimmed blue light, turned on Dark Mode, turned off Dark Mode. These measures helped a tiny bit, but my eyes still became severely fatigued after 1 hour of use (even on external monitors).

If you can’t measure it, you can’t fix it

I wanted to see how PWM and temporal dithering look like. I needed quantify these things to determine if 1) they were the cause 2) if any other display adjustments I make have an effect.

Detecting PWM

Used my phone camera in manual video mode with a really fast shutter speed (1/12000). Detected PWM on

  • 16” M3 Max MBP (edit: thin bars, vertical/horizontal depending on the phone's shutter direction)
  • 2020 iPad Pro (thin bars)
  • iPhone 15 Pro (diagonal waves)

PWM-free:

  • mid-2012 15” MBP with Retina display
  • M2 MacBook Air (need to re-test)
  • LG 32” 4K UltraFine (IPS)
  • Samsung 32” G7 (IPS)
  • BenQ 24” GL2450-b (TN)

Interestingly, some of these PWM-free displays which I’ve used for years were suddenly giving me severe eyestrain when connected to the M3 Max.

Detecting Temporal Dithering (aka FRC)

To visualize temporal dithering you can run a true video capture of your screen though ffmpeg, which can create a diff of each successive frame pair and output a new video of those diff frames.

  1. Install ffmpeg using MacPorts

sudo port install ffmpeg

  1. Transform your video input.mov in Terminal with this command:

ffmpeg -i input.mov -sws_flags full_chroma_int+bitexact+accurate_rnd -vf "format=gbrp,tblend=all_mode=grainextract,eq=contrast=-60" -c:v v210 -pix_fmt yuv422p10le diff.mov

This uses a filter called time blend (watch this crazy demo) which layers every frame on the frame preceding it using grainextract blend mode (previously called difference128). It gets the absolute difference of each RGB value in a pixel then adds 128. So if there’s no change from frame to frame, the output pixel should be RGB(128 128 128). We then adjust the contrast to make the difference more visible, and finally output a lossless video. I’m not an ffmpeg expert but the above command does the job. To output a compressed mp4 use -c:v libx264 diff.mp4. This does the job of demonstrating dithering at much lower file size. Not pixel perfect but passable.

Using QuickTime screen recording

This doesn’t work. I analyzed a lot of screen recordings from the 2012 MBP and the M3 Max and came to the conclusion that the recordings are at least a step before the application of temporal dithering. Whatever looks like dithering here is likely an artifact of compression.

Using a video capture card

Encouraged by @Seagull capture card thread, I got a Blackmagicdesign UltraStudio Recorder 3G. It accepts HDMI input and connects to your Mac using Thunderbolt 3. It can capture QuickTime Uncompressed 10-bit RGB at 1080p60. This is more than enough for our needs. But you gotta be careful with uncompressed videos, 3-4 seconds clock in at ~2GB.

Here are the results from the devices I had access to:

  • 15” 2012 MBP: no dithering
  • M2 Mac mini: dithers
  • M2 MacBook Air 13”: dithers
  • 16” M3 Max: dithers

Sample video demonstrating dithering (lossy compression)

Dithering ON, M3 Max, Sonoma desktop, 1s capture @ 1080p60

Just viewing the time blend video is pure torture! Makes you acutely aware of the muscles behind your eyes. If anyone wants the uncompressed recordings I can upload those but they’re heavy and look similar the compressed ones.

Time blend videos with dithering disabled simply show a plain gray screen.

Some findings about dithering:

  • Dithering happens at the refresh rate, I tested up to 60Hz. If you set your display to 24Hz, pixels will flicker 24 times a second instead of 60 (obviously).
  • If you play a 60fps video on a 60Hz display while dithering is enabled, you actually get no dithering in the video for the most part. There’s no time to dither.

Now that I knew that my laptop applies temporal dithering, I knew it was the cause for my eyestrain. Because the eyestrain was there even while using PWM-free displays, and it’s the only perceivable difference in output from my 2012 MBP and my M3 Max.

The hunt for a technical solution

I read all over this forum that it was impossible to disable dithering because the new Apple silicon GPUs are only capable of handling 10-bit color, and are always dithering no matter what. Seemed unbelievable and I was determined to find a solution with code.

I read this progress report on the Asahi Linux blog by marcan (they’re doing some incredibly hard and important work over there) about reverse engineering the M1 DCP (Display Coprocessor). They proved the CPU and DCP communicate back and forth. Messages like

IOMobileFramebufferAP::setDisplayRefreshProperties() at the DCP interface were encouraging and hinted at an ability to configure the display.

And over at their DCP tracer a IOMobileFramebufferAP::enable_disable_dithering(unsigned int) message was traced. All evidence that are there mechanisms in place to control dithering. The question now became how to send those messages.

In my rabbit hole dive I also came across 2 important tools:

  • ioreg which shows your I/O Registry
  • AllRez which dumps all display info in macOS.

So in the logs I saw a property called enableDither = Yes under a service called IOMobileFramebufferShim. All of this now seemed inter-related, and the puzzle pieces were falling into place.

Based on all of that I figured a good starting point would be IOMobileFramebuffer which iPhone Development Wiki describes as a “a kernel extension for managing the screen framebuffer. It is controlled by the user-land framework IOMobileFramework.” On macOS it’s called IOMobileFramebuffer. It’s a private framework with not much literature on it. One way to examine it is to run it through a disassembler.

So I loaded /System/Volumes/Preboot/Cryptexes/OS/System/Library/dyld/dyld_shared_cache_arm64e into Hopper, selected IOMobileFramebuffer and started looking at the symbols and found 2 interesting routines:

  • _IOMobileFramebufferEnableDisableDithering
  • _kern_EnableDisableDithering

My best guess was that _IOMobileFramebufferEnableDisableDithering is a wrapper around _kern_EnableDisableDithering which does the real work. Or is it the other way around?

_kern_EnableDisableDithering is a very simple function, the crux of it is this:

IOConnectCallScalarMethod(r0, 0x1e, &var_10, 0x1, 0x0, 0x0)

Calls selector 30 on the IOMobileFramebufferShim object with a boolean value. So I quickly made a command line project in Xcode that does just that and this is what I got:

(iokit/common) unsupported function

Uh oh! Disappointing. But I was not about to give up-- IOKit can return kIOReturnUnsupported for a variety of reasons, one of course being a complete lack of implementation, another being invalid or out of bounds arguments, or possibly a lack of privilege.

So I then loaded the IOMobileFramebuffer framework dynamically with the help of this gist and invoked IOMobileFramebufferEnableDisableDithering directly and as expected got the same result.

Maybe calling it wasn’t allowed from user space? But I wanted to dig deeper before messing around in kernel space.

So then I stumbled upon IOConnectSetCFProperty and I remembered enableDither = Yes from the registry. I thought I’ll just set it directly on the IOMFB object and maybe it will interpret it and affect the display downstream. So I did that and got:

(iokit/common) unsupported function

Again. I was starting to lose hope at this point but in a last ditch attempt I thought I will just modify the I/O Registry directly, even though my understanding was that the registry was a lens into device state and modifying it from the top won’t affect the devices per se.

So I did just that.

kern_return_t ret = IORegistryEntrySetCFProperty(service, CFSTR("enableDither"), kCFBooleanFalse);

And got:

(os/kern) successful

Awesome! I ran ioreg -lw0 | grep -i enableDither to see if the registry was touched.

    | |   |   |   "enableDither" = No
    | |   |   |   "enableDither" = No
    | |   |   |   "enableDither" = No
    | |   |   |   "enableDither" = No
    | |   |   |   "enableDither" = No

I thought I was dreaming! I wanted to verify so I plugged my capture card, recorded 3 seconds and ran it through ffmpeg and I couldn’t believe it! Dithering was gone! It was that simple.

Seeing is believing

The below video is a capture of YouTube playing a 1080p60 video.

At first dithering is enabled, then at 00:03:59 dithering is disabled and watch for yourself.

Dithering ON then OFF, M3 Max, Sonoma, YouTube @ 60fps, 1080p60

It turns out modifying the I/O Registry is a common way to tweak driver and device settings and there’s even a command line tool for that. However, the device driver/kernel class must allow and implement the inherited IOService::setProperties method for it to work.

Over the next couple of days I verified it with a few more recordings, including ones where I disable dithering mid-recording and the effect was immediate. Best of all I didn’t need all this proof-- I could simply use my computer again and suffer minimal eyestrain.

The downside of this method is that enableDither is reset back to Yes on computer restart. There’s possibly a way to avoid this by modifying the driver’s plist which might contain those properties, but that’s an exercise for another time. A simpler solution is Stillcolor which I developed to disable dithering on login and whenever a new display is connected.

Introducing Stillcolor for macOs

Stillcolor is a lightweight menu bar app which simply disables dithering on login and whenever a new device connects. It’s pretty much in beta at the moment and needs M1/M2/M3 and macOS >= 13 to run. Tested on macOS 14 only, so will appreciate feedback from everyone here.

Please bear in mind that there could be unintended consequences from disabling dithering, so use the app at your own risk. The app is released under the MIT license.

Download Stillcolor v1.0

(For some reason Chrome gives a suspicious download blocked warning-- I don’t know know why it does that, you can safely ignore it)

Make sure to enable “Launch at login”

To check wether it did the job, run the following in Terminal:

ioreg -lw0 | grep -i enableDither

Should see 1 or more ”enableDither” = No.

To re-enable dithering simply uncheck “Disable Dithering.”

A visual test that works for me is the Lagom LCD Gradient (banding) test

Set your built-in display’s color profile to sRGB at full brightness and look carefully at the gray parts, you should be able to see subtle banding when you disable dithering which happens in realtime.

Disabling dithering alongside other measures such reducing brightness and blue light will make using Macs enjoyable again. I think the built-in displays are still awful, and I recommend using an external monitor which you’ve previously been comfortable with.

Credits

Special thanks to my brother Ibraheem for letting me test on his Mac and display!

    Woah. Exceptional work aiaf! I only got to unpacking this dyld cache the last time I tried. Excited to dig into this some more. I'm sure there are more things that are accessible with this method.

    Other things and ideas that could be included in a Mac tool if anyone wants to spend a lot of time, or organize some funding aiaf or other developers.

    1. Reverse engineering Quartz Debug to give easy access to live framerate and possibly other monitor options. It's currently the only tool to show fps on mac, and it is outdated and doesn't go past 90. It even has the option to disable VRR and several other private apis.
    2. Automatic diff / compare from AllRez to spot changes when an external display is treated differently on the different usb-c ports.
    3. Forced refresh rate / disabling ProMotion / VRR by having a transparent overlay. (got an electron app demo that I've used quite a lot for this, can be confirmed with Quartz Debug for Xcode tools)
    4. Noise pattern overlay that makes it easier for the eyes to keep track of what depth the screen is at (same as above)
    5. Disabling of window shadows to avoid "artificial depth" that messes with focus (code in the Yabai source). Combinable with JankyBorders.
    6. Pulling, modifying and pushing of EDID for external monitor to force 8bit. Currently only possible on external monitors. Possible now with BetterDisplay and AW EDID Editor.
    7. Easy toggling of the terminal antialiasing. This seems to make a big difference here at least.
    8. ColorTable / Gamma modifications. Currently possible in BetterDisplay and Gamma Control.

    Againt. Great work! Excited to keep following this!

    • aiaf replied to this.

      aiaf To visualize temporal dithering you can run a true video capture of your screen though ffmpeg, which can create a diff of each successive frame pair and output a new video of those diff frames.

      Out of interest, could you send me such a capture?

      An original project by this forum is VideoDiff to compare frames in sequence and either display the result, or output to a file. But I suppose the right permutation of ffmpeg commands works too.

      • aiaf replied to this.

        Did some testing. This is exceptional. Deeply grateful!

        async Excited to dig into this some more. I'm sure there are more things that are accessible with this method.

        I've tried several functions from the IOMobileFramebuffer framework which actually worked, such as IOMobileFramebufferIsMainDisplay. However it returned true only on the built-in display. Here's a messy list I made of kern_* methods I found and the corresponding IOConnectCall*. I reckon some of these methods are generally wrappers around IOService::setProperties.

        JTL I've seen VideoDiff but I don't like Python so I figured I'll give ffmpeg a try first 🙂

        Here's 2 uncompressed seconds of macOS Sonoma desktop (no transparency or motion) https://we.tl/t-P39UguEqCI

        Tell me if you need any other particular screen or interface.

        • JTL replied to this.

          aiaf I've seen VideoDiff but I don't like Python so I figured I'll give ffmpeg a try first 🙂

          You don't have to like it. The rationale for developing it in Python was the choice of OpenCV with bindings for it and also NumPy made development easier, but it's already done.

          What I'd be curious about is if you could get the JAX accelerated version working under macOS with GPU acceleration. But that's a different topic.

            • Edited

            aiaf I’ve installed it on my MacBook Pro 16 M2 and it seems there is a difference between dithering on and off. A big difference. But still, I have some eye strain although the dithering is off, even on a connected external display. Maybe there is another effects besides temporal dithering?

            In addition, an app for iOS will be a bless! I can’t use the latest versions of iOS 17

              JTL what is this program? How can I run it on windows/linux?

              It seems I have another issue besides temporal dithering, some effects which cause to me also eye strain but not like dithering. Can I found which effect cause it to me with this tool?

              JTL I'll give it a shot!

              twomee could be the PWM on the built-in display of your MBP. Set the refresh rate to 60Hz instead of ProMotion and the color profile to sRGB. Also try BetterDisplay's brightness and color controls to reduce blue light. I find most comfort using HDMI with an external display like the Samsung G7 or the LG 4K UltraFine 32", but that's just me.

                • Edited

                aiaf i tried it now.
                it didn't help unfortunately.
                i am using betterDisaply to set the brightness of the laptop screen to 160% and it make the eye strain to be lower. if i set the brightness to be 100% or lower, i am getting strong eye strain. with the sRGB in the laptop screen i got a lower brightness which give me a the stronger eye strain.
                it seems not related to PWM because it occur also in the external display with same settings.
                i have a pop OS Linux distro which doesn't give an eye strain at all so its not the screen for sure. i am sure its a software problem. you sounds like a professional guy, do you have any ideas what it can be?

                  Wow this is incredible!! I don't have a M series Mac but this all sounds very promising!! Do you think you'll come up with a windows workaround that's better than the current methods ? Like a Nvidia driver modification?

                    @aiaf Impressive! That's a great feeling when you keep plugging away at something and then it suddenly says successful. 🙂 Well done.

                    There is something additional that can be tried too, perhaps related to what twomee was asking about. On my Windows PC I actually run it in YCbCr 4:4:4 w/ the HDMI IT Content flag turned off instead of the default of RGB w/ IT Content on. On the gradient test on my external monitor I do see subtle bands in this configuration which go away with RGB on. It might be related to my displayport to hdmi adapter (unknown) but there is a difference. I find it more comfortable in this mode..

                    So my suggestion is, on MacOS what about shifting the colourspace to YCbCr 4:4:4 and seeing how it is? I don't know if the IT Content flag exists there but it does on Intel. When the flag is on, it tells the display to do additional processing of the image based on the content, video, text or whatever.

                      twomee Could you detail exactly what pop OS Linux you have? I have Lenoo x280 with Windows 11 that gives me zero eye straing (except teams video) and Lenovo L13 that is totally unsuable, though it does not have PWM and it is also totally unsuable with the same external display that I user x280.

                      So I would like to test the Linux with the L13 to see if it's only a software issue with that laptop

                        twomee

                        I second this even though I'm not on iOS 17

                        • aiaf replied to this.

                          I'm not technical enough to understand all of this but this is one of the more promising posts (I think) I've seen on here in a while.

                          So do you notice any strain at all even after this or is your Mac completely comfortable now?

                          • aiaf replied to this.

                            This is the most epic post. Thank you for your hard work! You should set up a way for people to buy you a virtual pint if it helped them

                            WOW thankyou so much… I don't have immediate access to an M1/M2/M3 but Ill grab one asap to try this!!!! deeply hopefully and grateful!!

                            dev