Big Sur Beta 9 - Serious Drawing Bug Involving getRectsBeingDrawn:count:

I have submitted this via Feedback Assistant but am posting this here to ensure this bug gets some attention.

On previous versions of macOS, when you invalidated an area using setNeedsDisplayInRect:, calling the getRectsBeingDrawn:count: method in the drawRect: method for a view would correctly return the invalidated area.

Under Big Sur Beta 9, calling getRectsBeingDrawn:count: to determine the invalid area always returns the entire frame of the view. This is bad because routines that rely on getRectsBeingDrawn:count: to optimize drawing are getting the wrong information, and will draw more than they need to draw.

But even more serious is this: even getRectsBeingDrawn:count: reports the entire view as being invalid, you cannot successfully to draw into the area that has not been invalidated.

A sample demonstrating the problem can be found at https://github.com/TroikaTronix/BigSurDrawingTest

The READ ME gives further details about the bug and how the sample code reproduces the problem.

Accepted Reply

Hi Mark, sorry, I should have mentioned that we set .layer.contentFormat each time in -viewWillDraw (and in -layerWillDraw under iOS). When investigating this back when iOS 12 was new we found that the format is often reset by the OS. I didn’t investigate this further under macOS, just copied the fix from iOS.

One difference under macOS is that .layer.contentFormat returns kCAContentsFormatRGBA8Uint, even before setting it. Nevertheless setting this same value does make a difference. Under iOS, it returns "automatic" before changing it.

Kai

Replies

We are seeing the same issue. We invested quite a lot in minimizing drawing to optimize performance, which is in vain under this bug.

Additionally, even under Catalina, -getRectsBeingDrawn:count: seems to return just one rectangle (which matches the direct rectangle passed to -drawRect:), no matter what was invalidated before. For example, after sending -setNeedsDisplayInRect: to the same view twice with two small non-overlapping rectangles, -getRectsBeingDrawn:count: returns a single rectangle which is the union of the two rectangles passed to -setNeedsDisplayInRect:. Actually drawing with different colors reveals that the clip is still set to just the two small rectangles, but the OS no longer reveals this information.
Thanks kai_2 for providing a second confirmation of this bug. Did you also submit your bug report using Feedback Assistant?

Given that my code example (see GitHub link above) demonstrates this totally reproducible problem, it certainly would be good to receive some feedback from Apple that indicates this will be addressed. Right now in Feedback Assistant, it still says "Resolution Open"

Best Wishes,
Mark
I noticed the same problem (reported FB8820682).

Sample view to reproduce: https://gist.github.com/krzyzanowskim/1ca0e13edec6fd43d235d0a344f831b8 if anyone care.
I see it macOS 11.0 (20A5395g), Xcode 12.2 beta 3 (12B5035g).

And indeed (as @kai_2 said), clip is still set! that is very strange.
Hi,
Before seeing your post I've just finished building a sample app that demonstrates exactly the same thing.
I was investigating why our app runs slowly on Big Sur.

I've built the sample app with Xcode 12.2 beta 3, and can confirm this is still well and truly broken.
The dirtyRect is always wrong. Always the full size of the view. -getRectsBeingDrawn:count: never returns the correct result.

Apple have frequently talked about the need to use -getRectsBeingDrawn:count: as an essential optimisation.
Just trying to come to terms with what to do about it.

I used random colours in drawRect, and a button to set a small dirty area. For me the rendering only *sometimes* clips the drawing to my set dirty area. It frequently still fills the whole view.



I noticed this under Catalina too.
Additionally, I empirically found that there's a limit on the number of dirty rects you can set before it breaks completely. I think it was about 8.

Because of this I implemented a fix to coalesce my own list of dirty rects.
With this new problem on Big Sur, that too no longer works.
We found a fix for this issue which we derived about two years ago when the same problem turned up under iOS 12: set view.layer.contentsFormat = kCAContentsFormatRGBA8Uint.

Reasons seems to be the automatic backing store format adaption introduced in iOS 12 (and now as it seems under macOS 11). With this enabled (the default, .contentsFormat == "Automatic" (no constant for this in the headers)), views always pass the full bounds rectangle to -drawRect:, although sometimes somehow clipping the redraw to the actual invalidated area (not using the Quartz clip, which also matches the full bounds).

Kai_2 : Regarding this: "We found a fix for this issue which we derived about two years ago when the same problem turned up under iOS 12: set view.layer.contentsFormat = kCAContentsFormatRGBA8Uint."

I tried this in my test application (https://github.com/TroikaTronix/BigSurDrawingTest) under Big Sur, and no difference regarding the update area reported by getRectsBeingDrawn:count:

In my app, I simply set contentsFormat for every Custom view I created. Is there something else I need to do?

Best Wishes,
Mark
Hi Mark, sorry, I should have mentioned that we set .layer.contentFormat each time in -viewWillDraw (and in -layerWillDraw under iOS). When investigating this back when iOS 12 was new we found that the format is often reset by the OS. I didn’t investigate this further under macOS, just copied the fix from iOS.

One difference under macOS is that .layer.contentFormat returns kCAContentsFormatRGBA8Uint, even before setting it. Nevertheless setting this same value does make a difference. Under iOS, it returns "automatic" before changing it.

Kai
To Kai and All,

I opened technical support incident on this bug. The word from Apple is that the new behavior described in my original report should be considered the correct behavior, and that -getRectsBeingDrawn:count: can no longer be relied upon. The expectation of developers who need drawing optimization that they "roll their own" system.

The workaround offered by Kai does in fact work and Apple verified that it is a valid workaround, with the caveat that doing so will circumvent the operating system from optimizing the backing store used for your view.

To implement the workaround, you need to add an override like this to your custom views:

Code Block
- (void) viewWillDraw
{
if (gIsRunningOnBigSur) {
CALayer* layer = self.layer;
layer.contentsFormat = kCAContentsFormatRGBA8Uint;
}
}

Note that I'm doing this only for Big Sur -- gIsRunningOnBigSur is set depending on the operating system elsewhere in my code

I don't do iOS, but Kai indicates that you need a similar override on iOS for -layerWillDraw.

I hope that saves somone else some hair pulling.

Best Wishes,
Mark
Hi Mark,

great summary!

One little thing, though: overwrites of -viewWillDraw should probably forward to super. If I recall correctly, this was crucial on older OS versions to dispatch the message to sub views, although that has been changed at some point.

Best regards
Kai
Hi there, we also run into this issue, and this thread was a great starting point to begin the investigation and look for workarounds!

Since no one brought this up, I'd like to share another workaround that reverts the behavior for the entire app, according to my findings. It's the NSViewUsesAutomaticLayerBackingStores user defaults flag. When set to false, everything gets back to normal.

I prefer a solution that would be constrained to particular views where I do custom drawing. The problem is that the workaround presented here only works for a flat backing layer. I haven't managed to make it work with the NSScrollView architecture.

For more details on my research, see this gist extracted from our internal documentation: https://gist.github.com/lukaskubanek/9a61ac71dc0db8bb04db2028f2635779

Do you have any idea or hints on how to make the workaround work with NSScrollView?

Lukas
Hi Lukas,

thanks a lot for making this gist accessible!

Unfortunately I can’t help concerning NSScrollView: we rolled our own scrolling quite a while ago for other reasons. Seems to be of benefit once again.
We are seeing same issue even with an additional problematic: NSViews that are nearby the initial invalidated view are getting false drawRect events that causes unneeded additinal paintings. We posted a call out on LinkedIn under #drawrectgate - hope that it will be picked up by Apple soon!
Even without using getRectsBeingDrawn:count the problem is obvious when trying to use self.needsDisplayInRect to redraw a portion of the view on Big Sur.
The entire view is erased whatever rect is passed to self.needsDisplayInRect thus making this call totally useless.
If the view is set top non opaque, then its the windows that erases everything.

This is a VERY SERIOUS BUG and makes Appkit almost UNUSABLE for efficient graphics/ redraws.

Already starting 10.14 the OS was erasing all View behind our back. But at least one could make a cleaver use of needsDisplayInRect to still be able to do partial redraws.

Now on Big Sur even that doesn’t work, forcing us to either redraw everything all the time or to use an extra buffer which is obviously a huge waste of CPU and memory. Plus my experience with that solution is performances gets catastrophic on huge screens…

If anybody knows a work around...

Eric Wenger,
Bryce,Metasynth,ArtMatic father
We just discovered that this will also help a lot to prevent unneccesary drawings outside the needsdisplay rect:

[view setWantsLayer: YES];
[[view layer] setDrawsAsynchronously: YES];