Clipping fundamentally flawed in Core Graphics when using AffineTransform

Hey Forum, I think I found a highly problematic bug that’s hard to work around:

TL;DR
When using the Core Graphics renderer on macOS, clipping masks will not be subpixel accurate, leading to undefined/unrendered regions. This appears to be an issue which is unrelated to JUCE, but affects its GUI rendering, likely causing nasty artifacts in combination with opaque components, because JUCE relies heavily on clip masks.

Detailed description and steps to reproduce
Observations made on macOS 10.15, 2020 i7 iMac, 24" / high dpi (retina) display.
Reproduced with JUCE 6.0.5 as well as 6.1.3, macOS deployment target does not seem to play a role

Here we go: I recently added the option to resize a plugin UI in the simplest way: adding an affine transform with a user definable scale factor to my top level component. This actually works great and reliably, but I recently noticed that with particular scale factors ultra thin lines appear between several UI elements.
This only happens when rendering with Core Graphics, not with the JUCE software renderer, not on Windows, and not with OpenGL. I simplified it until I arrived at the mechanics that apply clip masks and the Core Graphics renderer.
Here is a simple repro that you can try out quickly (just put this in the paint code of a component):

This red line should not be there (view the image in original size if you can’t see it!).

You can also draw two adjacent rectangles, ending up with a tiny uncovered section in between, only using either excludeClipRegion or reduceClipRegion, the problem stays the same.
Repro also works when not scaling, but just translating by a subpixel amount, e.g. AffineTransform::translation(0.852f, 0.44f).

As you can see, not much going on there. JUCE does everything right, it doesn’t even transform the coordinates, because Core Graphics / Quartz is capable of transforming the coordinate space itself. JUCE hands over the correct integer coordinates, and Core Graphics API is a good conceptual match for the JUCE low level graphics context (in theory).

So, at this point I am very confident that this is coming from the antialiasing that Core Graphics applies in such situations, which apparently is imprecise, not subpixel accurate. You can disable antialiasing for Core Graphics, but believe me, you do NOT want that, and I haven’t verified whether it’s correct or if it produces less apparent rounding errors.

Rendering with the other low level graphics contexts that JUCE offers works perfectly fine!

Possible workarounds
Usually, having opaque components, leading to large portions of your GUI not being drawn at all, benefiting from clipping operations and “early outs” in the JUCE GUI tree traversal, is a desired optimization in complex GUI layouts. It is also likely to produce the above issue if you rely on AffineTransform anywhere in the hierarchy, like in my “scaling a plugin UI” situation.

One option is to use combinations of disable clipping and setOpaque(false) on affected components, moving background rendering to parent components, and in order to “fix” the subpixel accurateness issues, rendering a pixel beyond the clip bounds where it might be necessary. At least I found a way to change my UI in that fashion without completely destroying render performance, and on a strictly case by case basis. It felt very hacky and wrong.

I haven’t tried it yet, maybe it’s possible to disable anti aliasing in core graphics solely/temporarily for the compositing of clipped areas (I don’t think so).

It’s possible to switch to OpenGL (just attach an OpenGL context to your top level component), but OpenGL comes with its own can of worms and it’s practically deprecated.

Conclusion
If anyone here would be so kind to confirm this issue, it should probably escalated to Apple.

If it is indeed a flaw in Core Graphics, we are in even more dire need for a new rendering backend for macOS. The more so with the Core Graphics renderer being slow as it is (search this forum for Core Graphics), and OpenGL being “almost dead”…

Thanks for reading, if I overlooked something or made a dumb mistake, please enlighten me :slight_smile:

3 Likes

Update: The best workaround I found so far is temporarily disabling antialiasing when applying/modifying clipping areas - that actually works. I don’t really want to carry around modified JUCE code though, so I’ll temporarily switch to OpenGL on Mac.