(SOLVED) Repaint() ignores opacity and repaints parent Component

Hey guys!

I’m running into some CPU issues with repainting and hope that anyone could help :slight_smile:

I have a small MIDI activity LED Component. It sets a “needs repaint” flag when it receives a MIDI event. The LED component checks the flag using a Timer, and calls repaint() if needed.

The LED has setPaintingIsUnclipped(true) and setOpaque(true). [EDIT] The issue occurs no matter if I call setPaintingIsUnclipped or not.

When the LED calls repaint(), it goes into internalRepaintUnchecked, and then, without checking for opacity, straight into parentComponent->internalRepaint().

I’ve set breakpoints in the paint() method of my background component. It’s getting called repeatedly, and the clip region seems to be the area below the LED. I might be missing something, but considering the LED is opaque, why is the parent being repainted?

Another thing I noticed is that if two small Components need to be repainted, and they are in two opposite corners of the UI, JUCE repaints almost the entire background, and not just the small areas below each of the two Components. Is this intended?

You probably don’t want both of these on, conceptually they’re kinda mutually exclusive and you may run into some issues having both of them on outside of the issue you’re experiencing.

I would only use setPaintingIsUnclipped(true) for things that may be transparent but will only draw inside their bounds, and setOpaque(true) for things that draw over their entire bounds

This may depend on the platform, for instance I know the CoreGraphics backend on OSX will coalesce the separate rectangles into one big region

Thanks @TonyAtHarrison! That’s very helpful insight, especially about Core Graphics (I’m on OSX). Important to know.

You probably don’t want both of these on, conceptually they’re kinda mutually exclusive

That’s interesting; could you explain why they’re exclusive, and what types of issues could happen? Because from the docs they sound unrelated, one is about transparency, the other about bounds.
I’ve removed the setPaintingIsUnclipped call, but the parent still repaints…

Only conceptually, so to speak, as you can have both on… but how can a Component be opaque and draw anywhere on screen? It would really mean only the lowest Component in the hierarchy could be seen if we really enforced the rule of “fill the entire clip region if you’re marking yourself opaque” :slight_smile:

You’ll run into the issue that you’re seeing, for one. Also seen here:

And on top of that there will actually be unnecessary clipping because JUCE isn’t checking the “unclipped painting” flag of siblings when drawing each child. So excludeClipRegion() is still used for all these component bounds, even though the components aren’t clipping anyway when they draw themselves:

1 Like

Ok, that makes sense in a way.

Thanks for the link! :+1: I’ve enabled the JUCE_COREGRAPHICS_RENDER_WITH_MULTIPLE_PAINT_CALLS flag mentioned there, and it’s much better now. I’ll keep an eye out if anything else has a disadvantage from it, but maybe it’s better to keep it on for my purposes.

From what I can tell now, some of my opaque components cause the background to be repainted because I have a scaling AffineTransform on my entire UI. So even though they are opaque, at the borders they have to be mixed with the background because their position is between pixels.

I should mention that this won’t work on the latest macOS SDK / Xcode 10… I spoke with @t0m about it at ADC and Apple broke an API JUCE needs for determining the dirty regions :slightly_frowning_face:

We use it too so we’re staying on Xcode 9.x for the time being

@TonyAtHarrison @t0m We noticed that this method doesn’t work on ios. Everything gets repainted even with this flag and cpu for a simple animation is through the roof.

If this document is still correct, the reason is that

Each UIView is treated as a single element. When you request a redraw, in part or whole, by calling -setNeedsDisplayInRect: or -setNeedsDisplay: , the entire view will be marked for updates.

So JUCE only has one top-level UIView that contains everything (assuming there are no other UIViewComponents). And it seems that nothing is keeping track of the actual regions that need to be redrawn (there’s no getRectsBeingDrawn:count: on iOS).

3 Likes

Yes, that’s right. It’s also the case that getRectsBeingDrawn doesn’t work as expected on the latest version of macOS, and Metal also requires backed views to operate correctly. We’ll need to rework how JUCE interoperates with the native views to improve things.

3 Likes

This, IMHO, is a huge issue. We’ve been racking our brains trying to figure out workarounds and tricks to lower graphical CPU, because right now moving any widget in ios, causes the cpu to shoot to 100% on any knob animation.

The only way is to have all widgets non transparent so the background never has to redraw.

1 Like

From what I’ve seen, I don’t think even that would help.

Indeed, you should not really rely on setOpaque() to avoid too much repainting.
In my use case (which is full of transparency effects anyway), i spent a lot of time making sure my UI paints quickly. For me a big win was making drawFittedText() more efficient when redrawing the same text over and over again. You can achieve that by caching the GlyphArrangement object, which is the result of the glyphs positionning algorithm which is really expensive.

2 Likes

Yeah, I also noticed that a lot of CPU goes into creating the GlyphArrangement over and over again. May I ask, are you caching the GlyphArrangements in a kind of global cache, or do you have a custom Label class? A global cache would probably have to cache GlyphArrangements based on width, Font, String and Justification.

I have a custom Label class, as well a custom TextButton and DrawableButton. So all caches are local. Indeed it means you need to rebuild the cache when text, Font or justification changes. Then you also have to make sure nobody uses drawFittedText() in their custom components.

1 Like

Thanks for the info! :+1: That’s a good way to do it, but I would need something that covers DrawableText created by JUCE’s SVG parser.

@t0m : I was hitting this on iOS. CGContextGetClipBoundingBox() does NOT return the area requested by “[view setNeedsDisplayInRect …]” even though the context (or something else) is clipped in a way that makes the clipping look correct when JUCE_ENABLE_REPAINT_DEBUGGING is 1. The result is, that each components paint() is called when the Component hierarchy is traversed because the clip region returned by the CoreGraphicsContext is always the whole darn screen. So if you have some components that do some heavy lifting in their paint() and you hope being prudent with your repaint() calls will save your sorry bottom you’re out of luck.

Luckily this can be easily rectified by:

  1. adding a RectangleList to UIViewComponentPeer
  2. add each area passed to UIViewComponentPeer::repaint() to it
  3. clip the CoreGraphicsContext to it (in UIViewComponentPeer::drawRect)
  4. clear the rectangle list

If you do this, painting on iOS will be much faster!

Here’s a pull-request: https://github.com/WeAreROLI/JUCE/pull/571

1 Like

Does this same problem present with macOS do you know? Or is the clipping text correct on that OS?

So the danger here is that it’s a little more complicated than just monitoring direct rects ourselves. See the “Layer-Backed Views” section here:

https://developer.apple.com/documentation/macos_release_notes/macos_mojave_10_14_release_notes/appkit_release_notes_for_macos_10_14?language=objc

I know this is for macOS, but similar reasoning applies to what’s happening on iOS.

When layers are involved there may be multiple layers “in flight”. You render once to a layer, that layer is sent off to the GPU, and you get presented with a new layer to render into whilst the GPU is processing the previous one. When I’ve been playing around on macOS I’ve found this behaviour can kick in when you have multiple windows displayed and they are refreshing independently. I’ve not actually seen any drawing defects, but it has affected what you can do with do with a context (things like accessing the memory behind the context directly becomes impossible).

When explicitly using layers, which is mandatory if Metal is involved, you get a fresh layer to render into every time. You must keep a copy of the previous layer’s contents if you only want to redraw the area that has changed.

I’m willing to try tracking dirty rects manually on both iOS and macOS, but I’d like to update the master branch from develop and do some bugfixing before introducing what I feel is a relatively risky change.

1 Like

Yeah, this really isn’t as simple as it sounds. Even without the layer stuff that Tom mentioned, if you only draw the rectangles that you’ve marked as dirty, then when the OS invalidates a rectangle itself and triggers a repaint directly (e.g. when part of a window is exposed for the first time), then this won’t be in the list, so would end up being blank.

Keeping our own dirty list only works when we’re explicitly in control of a buffered copy of the entire window, which is exactly what we do for the openGL rendering engine.

Thanks tom. I read somewhere, that the CALayer that’s behind a UIView is caching the contents of the last draw. I could not find an official source for that claim. So I see your point.

It’s odd, that the context (or something else) clearly IS clipped to the rects requested via setNeedsDisplayInRect as can be seen when enabling repaint debugging or when clearing the whole context before painting the JUCE app. Why would iOS request repainting the whole thing just to throw away most of it?

@jules: Indeed: adding a view-sized rect to the list initially (and probably on each resize) is what one needs to to. No matter if CALayer is caching or if JUCE is doing the caching. But I get your point!

This leaves me somewhat puzzled - Everything looks correct and is refreshingly fast - yet I’m with one foot in undefined-behaviour land …

@t0m: when you’re going to track dirty rects manually after the merge etc. will you then use an image based context as a cache ?