Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to enable/disable GPU side temporal dithering to help with eye strain + CLI #2766

Closed
waydabber opened this issue Mar 16, 2024 · 26 comments
Assignees
Labels
done All tasks are completed enhancement New feature or request released Released
Milestone

Comments

@waydabber
Copy link
Owner

BD has some eye-strain related features: https://github.com/waydabber/BetterDisplay/wiki/Eye-care:-prevent-PWM-and-or-temporal-dithering

A new method discovered by @aiaf allows us to simply turn off temporal dithering on the GPU side with visible effects on the built-in displays of Apple Silicon Macs (note: since this works on IOMobileFramebuffer only, this is an Apple Silicon-only feature).

https://github.com/aiaf/Stillcolor/tree/main

Note: the toggle will be added to the Image Adjustments section for built-in displays. For external displays the option will reveal itself when holding OPTION (testing on various external displays I personally saw no real difference as most dithering happens at the display side).

Note: this will of course be a non-Pro since the method and Stillcolor is MIT licensed. All credits go to @aiaf for this discovery.

@waydabber waydabber self-assigned this Mar 16, 2024
@waydabber waydabber added this to the TBD milestone Mar 16, 2024
@waydabber waydabber added enhancement New feature or request in progress Implementing unreleased Not released yet in beta form labels Mar 16, 2024
@waydabber
Copy link
Owner Author

waydabber commented Mar 16, 2024

Screenshot 2024-03-28 at 16 57 03

@waydabber waydabber added done All tasks are completed and removed in progress Implementing labels Mar 16, 2024
@aiaf
Copy link

aiaf commented Mar 16, 2024

Great news! This will help a lot in generating more awareness about these dithering issues, hopefully enough for Apple to notice. Thanks!

I reckon though that disabling DCP-controlled dithering on external monitors is just if not more important than the built-in display. The reason being some users are still facing eyestrain issues with the built-in panels (PWM, Pixel Inversion, other types of flicker or light issues), and are using external monitors as their primary display with these machines.

@waydabber
Copy link
Owner Author

I agree.

By disabling DCP-controlled dithering you mean forcing a lower connection bit depth (and thus at least making it possible for the display not to use temporal dithering / FRC)? That would be great indeed. But for that you'd need to influence somehow how the connection is negotiated. Simply forcing the framebuffer to a lower bit depth might not in itself achieve the desired effect (?). Plus a display can do whatever it wants, it is very difficult to control how an external display behaves as much depends on the hardware, controller board, firmware etc. Right?

@aiaf
Copy link

aiaf commented Mar 16, 2024

No sorry my comment was regarding the visibility of the "GPU Dithering" option in the app. It should be just as visible for both internal and external displays IMHO. I use DCP/GPU interchangeably to refer to this type of dithering that Apple applies to the image before sending it off the TCON or external display.

Though it would be great if we can somehow hijack the process/handshake that determines available bandwidth and force a bit depth that way. I tried overriding windowserver.displays.plist CurrentInfo/UnmirrorInfo/LinkDescription for the internal display but it had no effect. It shows 7 for the Depth field (and 64bpp in AllRez) which doesn't make any sense (does it reflect DepthFormat rather than actual bit depth?) Connecting certain external monitors changes that number to 8 for the built-in panel (which then shows 32bpp in AllRez), with no apparent change in image quality. It's really weird.

I also tried overriding the ColorElement array property on IOMobileFramebuffer using IORegistryEntrySetCFProperty but I'm getting an "unsupported function" iokit error.

There's got to be a way.

How do you force the framebuffer to have a lower bit depth? I would like to experiment with that.

I agree with you regarding external display behavior.

@waydabber
Copy link
Owner Author

waydabber commented Mar 16, 2024

I think the "7" for the depth field shoul correspond to CGSDisplayModeDescription.depth, and the value can be 4, 7, 8 etc and it should be some kind of numeric reference to the framebuffer's pixel encoding format (but not entirely sure) and does not affect the connection. There is a bitsPerSample value which is more approperiate to determine the color depth (for the framebuffer, not the connection). The connection color depth is probably negotiated by the DCP during DPCD negotiation (displayport configuration).

There are a bunch of DP, link and DPCD related stuff in IOKit which I am yet to explore. Maybe @joevt knows more about these (but afaik he is not working on Apple Silicon stuff):

                       _IODPControllerCreateWithService, _IODPControllerGetAVController, 
                       _IODPControllerGetMaxLaneCount, _IODPControllerGetMaxLinkRate, 
                       _IODPControllerGetMinLaneCount, _IODPControllerGetMinLinkRate, 
                       _IODPControllerGetTypeID, _IODPControllerSetDriveSettings, 
                       _IODPControllerSetLaneCount, _IODPControllerSetLinkRate, _IODPControllerSetMaxLaneCount, 
                       _IODPControllerSetMaxLinkRate, _IODPControllerSetMinLaneCount, 
                       _IODPControllerSetMinLinkRate, _IODPControllerSetQualityPattern, 
                       _IODPControllerSetScramblingInhibited, _IODPControllerSetSupportsDownspread, 
                       _IODPControllerSetSupportsEnhancedMode, _IODPCreateStringWithLinkTrainingData, 
                       _IODPDeviceCreate, _IODPDeviceCreateWithLocation, _IODPDeviceCreateWithService, 
                       _IODPDeviceGetAVDevice, _IODPDeviceGetController, _IODPDeviceGetLinkTrainingData, 
                       _IODPDeviceGetMaxLaneCount, _IODPDeviceGetMaxLinkRate, _IODPDeviceGetRevisionMajor, 
                       _IODPDeviceGetRevisionMinor, _IODPDeviceGetSupportsDownspread, 
                       _IODPDeviceGetSupportsEnhancedMode, _IODPDeviceGetSymbolErrorCount, 
                       _IODPDeviceGetTypeID, _IODPDeviceReadDPCD, _IODPDeviceWriteDPCD, 
                       _IODPServiceCreate, _IODPServiceCreateWithLocation, _IODPServiceCreateWithService, 
                       _IODPServiceGetAVService, _IODPServiceGetDevice, _IODPServiceGetSinkCount, 
                       _IODPServiceGetSymbolErrorCount, _IODPServiceGetTypeID, _IODPServiceRetrainLink

So what I want to say is that there seems to be various stuff to set connection parameters and also to read DPCD data (similar to how it is possible on Intel I guess, AllRez has the means to do that on that platform) to at least figure out the current connection link configuration?

@waydabber
Copy link
Owner Author

(parallel to this there is a discussion here - aiaf/Stillcolor#2 (reply in thread) - maybe these should be consolidated :))

@waydabber
Copy link
Owner Author

"No sorry my comment was regarding the visibility of the "GPU Dithering" option in the app. It should be just as visible for both internal and external displays IMHO. I use DCP/GPU interchangeably to refer to this type of dithering that Apple applies to the image before sending it off the TCON or external display."

-> right. I added the option to all displays, but by default on external displays you need to hold the OPTION key to appear (as with other framebuffer related stuff). I am a bit unsure about the nature of GPU dithering (spatial vs temporal - the former should not cause any eye-strain). On external displays I don't fully understand how it could be a viable temporal dithering, especially at low refresh rates, see my comment here: aiaf/Stillcolor#2 (reply in thread)). But I did not do any scientific tests and slow motion videos to confirm what is happening, so if you've done that (did you?), that should settle it of course.

@joevt
Copy link

joevt commented Mar 17, 2024

@waydabber , being able to get the DPCD info on Apple Silicon would be interesting, but it's probably a lower level than is needed for pixel format / depth. I haven't looked into ColorElement and TimingElement for a year.
https://forums.macrumors.com/threads/diy-5k-monitor-success.2253100/post-32093817

@jakedel
Copy link

jakedel commented Mar 17, 2024

@waydabber Disabling DCP dithering actually is very important on external displays, especially older ones:

I have two external monitor (one Samsung from late 2000s and another BenQ from 2015) that each seem to use their own 6bit + FRC algorithm to display """8bit""" color.

When connected to Intel Macs, the monitors display relatively "clean" images (except for the monitors' own FRC "moving static" which I still end up noticing and find pretty annoying anyway)...

But when connected to an Apple Silicon Mac, there's super obvious "dotted patterns" showing up on certain colors, and it is definitely temporal because there are some colors that straight up visibly, obviously flicker on these monitors — again, this only happens when connected to Apple Silicon, but not on Intel Macs. This is super noticeable on Dark Mode windows or when changing the shade of a gray object in apps like Figma.

(It doesn't matter whether the output is forced to RGB or YPbPr.)

I mostly suspect this is coming from the FRC pattern of the monitor "clashing against" the one generated by Apple's video output.

From the tests I've read that @aiaf has done with capture cards, the GPU is in fact generating moving dithering patterns when the desktop is supposed to be still, even on 60hz external video out.

There is a BenQ article about "monitor flickering when connected to M1 Macs" that doesn't really give any real solution, that I'm pretty sure was written due to someone else noticing the same issue:

https://www.benq.com/en-us/knowledge-center/knowledge/how-to-fix-mac-m1-m2-external-monitor-flicker.html

But now, for the first time in years, using the new disable dithering method actually fixes this issue with both external monitors, and probably any other 6bit + FRC monitor! My M1 Mac now generates the same "generally clean" output that I'm used to getting from my Intel Macs.

This is why I believe leaving this option visible by default is still very important for external monitors, as it's fixed a very obvious and noticeable monitor output issue that seemed unsolvable to me for years :)

@waydabber
Copy link
Owner Author

All right @jakedel @aiaf, I'll make the dithering option appear by default, no problem. :)

@aiaf
Copy link

aiaf commented Mar 17, 2024

There are a bunch of DP, link and DPCD related stuff in IOKit which I am yet to explore.

Thank you for these tips! This looks like a pain to test. I'll probably try my hand at it if I can get 100% confirmation the MBP built-in panels are 8-bit+FRC.

@aiaf
Copy link

aiaf commented Mar 17, 2024

@joevt that's a very useful post, thank you! The values you showed match the ones reverse engineered by the Asahi Linux project https://github.com/AsahiLinux/linux/blob/bd0a1a7d465fcb60685a2360565ed424bafff354/drivers/gpu/drm/apple/parser.h

By any chance do you know which process writes ColorElements on the IOMobileFramebuffer registry entry?

@joevt
Copy link

joevt commented Mar 17, 2024

@aiaf, I don't know which process creates those. Might have to grep the OS to find out.
On Intel Macs, here's a list of some interesting files (taken from WhateverGreen kext source code, excluding AMD, Intel, and Nvidia specific files):

/Library/Displays/Contents/Resources/Overrides/DisplayVendorID-%x/DisplayProductID-%x.mtdd\0\0\0\0\0\0\0
/Library/Displays/Contents/Resources/Overrides/DisplayVendorID-%x/DisplayProductID-%x\0\0\0\0\0\0\0

/System/Library/Displays/Contents/Resources/Overrides/DisplayVendorID-${VenIDhex}/DisplayProductID-${ProdIDhex}.plist
/System/Library/Displays/Contents/Resources/Overrides/DisplayVendorID-%x/DisplayProductID-%x
/System/Library/Displays/Contents/Resources/Overrides/DisplayVendorID-%x/DisplayProductID-%x.mtdd
/System/Library/Displays/Contents/Resources/Overrides/Dongles/%x%x%x%x%x%x%x%x%x
/System/Library/Displays/Contents/Resources/Overrides/Dongles/%x%x%x%x%x%x%x%x%x\0\0\0\0\0\0\0

/System/Library/Extensions/AppleBacklight.kext/Contents/MacOS/AppleBacklight
/System/Library/Extensions/AppleGraphicsControl.kext/Contents/PlugIns/AppleGraphicsDeviceControl.kext/Contents/MacOS/AppleGraphicsDeviceControl
/System/Library/Extensions/AppleGraphicsControl.kext/Contents/PlugIns/AppleGraphicsDevicePolicy.kext/Contents/MacOS/AppleGraphicsDevicePolicy
/System/Library/Extensions/AppleMCCSControl.kext/Contents/MacOS/AppleMCCSControl
/System/Library/Extensions/IOGraphicsFamily.kext/IOGraphicsFamily

/System/Library/Frameworks/CoreDisplay.framework/Versions/A/CoreDisplay
/System/Library/Frameworks/IOKit.framework/Versions/A/IOKit
/System/Library/Frameworks/Metal.framework/Metal

/System/Library/PrivateFrameworks/CoreLSKD.framework/Versions/A/CoreLSKD
/System/Library/PrivateFrameworks/CoreLSKDMSE.framework/Versions/A/CoreLSKDMSE
/System/Library/PrivateFrameworks/GPUSupport.framework/Versions/A/Libraries/libGPUSupportMercury.dylib
/System/Library/PrivateFrameworks/IOAccelerator.framework/Versions/A/IOAccelerator

/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/CoreGraphics.framework/Versions/A/Resources/WindowServer
/System/Library/Frameworks/CoreGraphics.framework/Versions/A/Resources/WindowServer
/System/Library/PrivateFrameworks/SkyLight.framework/Versions/A/Resources/WindowServer

Some kexts have a IOUserClient that a user app can communicate with. An IOFramebuffer (on Intel Macs) may have a user client with some methods (called through the IOKit framework or other user application accessible library) that only allow the WindowServer process to access. If you look at the output of ioreg -fliw0, you can see each user client has a pid which shows what process is using the user client. You'll see on an Intel Mac that all the IOFramebuffer have a IOFramebufferUserClient with a pid pointing to WindowServer. Only the WindowServer (or any framework used by WindowServer) can use the IOFramebufferUserClient. The IOFramebuffer can have one or more IOFramebufferSharedUserClient that any process can use.

On Apple Silicon, IOMobileFramebuffer replaces IOFramebuffer and IOMobileFramebufferUserClient replaces IOFramebufferUserClient (and/or IOFramebufferSharedUserClient?). But Apple Silicon also has IOFramebuffer (using IOServiceCompatibility) for compatibility with older software. AppleSilicon has a WindowServer process so I would check that and the frameworks it uses.

@waydabber
Copy link
Owner Author

waydabber commented Mar 18, 2024

It should be WindowServer. There is also the IOMFB_bics_daemon (in usr/libexec) which is started earlier than WindowServer during boot and seemingly has the calls inside that indicates it might configure framebuffer related things and can write stuff to the registry but it only deals with built-in displays based on what seems to be inside it plus it is never listed as client for any dispext framebuffers (+ on Macs with no built-in screen it is not attached to any even though the process is running). A process can become a client by using IOMobileFramebuffer framework's IOMobileFramebufferOpen() or some of the derivative calls that end up using this. The files listed above don't deal with IOMobileFramebuffer directly (SkyLight might as it has one or two references inside to deal with IOMobileFramebuffer but does not seem to do anything central) which is logical as most code should work both on Intel and Apple Silicon and should not be heavily Apple Silicon specific.

I have no idea how to figure out either which ColorElements is active for the current TimingElement (can only narrow it via circumstancial evidence - it would be great to know exactly, I wanted to add these details to BetterDisplay's Display Information... block) and how to change the color mode - but one can somewhat influence the selection by overriding the windowserver config file specifying some criteria (associated to a display's uuid) which is telling.

@waydabber
Copy link
Owner Author

waydabber commented Mar 18, 2024

to the original issue: added gpuDithering as an option for the CLI as well.

Examples (set all, set specific display, get, toggle):

betterdisplaycli set --gpuDithering=on
betterdisplaycli set --name=MyDisplay --gpuDithering=on
betterdisplaycli get --name=MyDisplay --gpuDithering
betterdisplaycli toggle --name=MyDisplay --gpuDithering

@waydabber waydabber changed the title Add option to enable/disable GPU side temporal dithering to help with eye strain Add option to enable/disable GPU side temporal dithering to help with eye strain + CLI Mar 18, 2024
@waydabber
Copy link
Owner Author

@waydabber waydabber modified the milestones: TBD, v2.3.0 Mar 19, 2024
@aiaf
Copy link

aiaf commented Mar 19, 2024

@joevt thank you for these tips. I've been poking around for a couple of days now. Lots of progress. Also thank you for AllRez- it's a tremendous piece of work.

Do you know if IO(Mobile)FrameBuffer clients only allow WindowServer privileged access because of specific entitlements, like the ones in

codesign -d --entitlements - /System/Library/PrivateFrameworks/SkyLight.framework/Versions/A/Resources/WindowServer

and

/System/Volumes/Update/mnt1/System/Library/Sandbox/Profiles/com.apple.WindowServer.sb

or do you think the restrictions are done in some other way? Perhaps these shouldn't matter if not running in Sandbox mode I reckon.

@waydabber thank you for these tips! Going to try Frida with WindowServer IOMFB_bics_daemon and hopefully get something out of it. Re. ColorElements, my best guess is that the highest scoring element (barring any other disqualifying criteria like DSC support) is the one in use. I have no proof of this.

@waydabber
Copy link
Owner Author

According to my experience, only a small amount of IOMobileFramebuffer... calls are allowed (but some are allowed, BetterDisplay does use some). I think the reason is that there were several exploits back in the iOS 7/8/9 days that used vulnerabilities in the IOMobileFramebuffer framework so Apple locked things down learning from that (this is why many of the exploits/code examples from that time are less useful now) - but these are available to privileged system processes (observed using Frida).

@joevt
Copy link

joevt commented Mar 21, 2024

@aiaf - I haven't researched IOMobileFrameBuffer. The source code for IOFramebuffer says the first app gets access to the IOFramebufferUserClient - which is usually always WindowServer. A kext like WhateverGreen can make patches to allow other apps access. AllRez can use my WhateverGreen fork to do some things.

@waydabber
Copy link
Owner Author

Hmm. But I can get BetterDisplay to be a IOFramebufferUserClient and access the (non-locked down) IOFramebuffer... calls. That's what I am using to invert the screen or toggle grayscale for example.

#2745

Screenshot 2024-03-21 at 09 55 57

@aiaf
Copy link

aiaf commented Mar 21, 2024

Thanks @joevt.

@waydabber I think the idea is that those locked down calls are restricted to certain processes like WindowServer only.

@waydabber
Copy link
Owner Author

@aiaf - I did not experiment with this on Intel at all so I thought @joevt is explaining that on Intel the first process that connects can be an UserClient for the IOFramebuffer (Intel) - which might not necessarily be case with IOMobileFramebuffer (thus the screenshot with multiple clients). But now that I reread it, I probably just misunderstood. 😀 Things being locked down happens somewhat universally in private frameworks and I had the impression that lockdowns like that is managed by features like AMFI (which afaik can be turned off) and maybe other specific measures (such a specific measure could be what @joevt is mentioning of course).

Note: generally, even though it is always important for insight to understand what can be done with various things patched, standard protections turned off etc, from an app developer's perspective whenever something can't be accessed from user space on a vanilla installation, the usefulness of the discovery becomes academic as the feature can't be added to an app that is aimed at a more general audience. 😞

@joevt
Copy link

joevt commented Mar 22, 2024

An IOService can have many user clients of one or more types. A process indicates what kind of User Client they want to use. The IOService decides if it will allow the connection to be made.

An IOFramebuffer can have many IOFramebufferSharedUserClient but only one IOFramebufferUserClient (used by the Window Server). Any process can request a IOFramebufferSharedUserClient. But only the Window Server (or the first process) will be allowed to request a IOFramebufferUserClient.

AllRez uses kIOFBSharedConnectType. Only Window Server can use kIOFBServerConnectType. There's not much you can do with a IOFramebufferSharedUserClient. The interesting stuff (i2c for EDID, DDC/CI, DisplayPort) happens through other APIs which use other user clients. You can probably set a breakpoint after IOI2CInterfaceOpen to find what User Client is getting used.

@waydabber waydabber added released Released and removed unreleased Not released yet in beta form labels Apr 8, 2024
@Tech6767
Copy link

Tech6767 commented May 31, 2024

I just discovered this somewhat by accident today. I got an EIZO CS240 that is supposed to have true 10bit display afaik. I was wondering about vertical lines in certain grey tones. When turning off "GPU dithering" these disappear.
In betterdisplays display modes it still shows as 10 bit. I am doing color accurate work and I am not sensitive to PWM afaik.

I did not find information on what exactly is happening here. Is this really forcing 8 bit?? Does this mean, the 10 bit before were only achieved on software level, even though the panel should support 10 bit?
I want to enjoy the best the monitor is capable of. It is connected via USB-C to Displayport, so it should support true 10 bit, right? It looks better for me without the vertical lines, but I am confused, what this means for color critical work...

EDIT:
Just played around with an 8 bit (8+2 capable) monitor (which is in YUV mode bcs of HDMI...) and on a 16 bit Photoshop file with a gradient, with GPU dithering I see no banding. Without, I see banding. On the 10 bit EIZO I do not see banding no matter the settings. The vertical lines only appeared on the EIZO though, so my guess is, that even though it is a true 10 bit signal, the GPU dithering applies completely unjustified dithering, which causes artifacts.
Seems like 10 bit is still working.
Actually, the dithering is really good, with it enabled, the 8 bit monitor shows no banding, but it is horrible that Apple just force it, without options to turn it off in the native settings! If I want to get an accurate 8 bit preview, why do I need third-party software for that...

@waydabber
Copy link
Owner Author

10bit refers to the 10bit framebuffer depth (so the video memory has colors represented using 10bits). The display connection however may be 8 bits only. You might need to check the display's OSD if it can provide you info on the connection bit depth. If not, then you can check the display connection logs (this might be a bit Mac specific and low-level) to see what color mode was negotiated exactly.

@AliciaBurrito
Copy link

Oh my goodness, disabling GPU Dithering fixed the weird banding issues I was having exclusively on Apple Silicon Macs with my Asus PG35VQ in SDR mode!

Before I used BetterDisplay to force higher brightness in HDR as that would fix it but at the cost of color accuracy, but SDR mode always had horrible banding. Weirdly never showed up when plugging in my iPhone or iPad, and wasn't on my old Intel Mac.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
done All tasks are completed enhancement New feature or request released Released
Projects
None yet
Development

No branches or pull requests

6 participants