I wrote a bit about "jank" in JUCE

Curious what other people’s experiences have been and if anyone has any dark art “repaint” secrets I don’t already know…

9 Likes

I always assumed the issue came from the fact the JUCE was originally built for waveform, which has a fairly basic UI and so never needed support for animations and whatnot.

Just simple things like not having a consistent way of styling widgets always irritates me - the other day I wanted to change the colour of the rectangle drawn around a juce::ToggleButton… guess what colour ID it uses? Probably outlineColourId right? Nope, tickDisabledColourId.

Paint debugging is a tricky one. JUCE does have the JUCE_ENABLE_REPAINT_DEBUGGING macro that will flash components when they’re repainted… however that’s misleading as the OS will sometimes draw more, or less than what JUCE flags. MacOS has a similar tool to highlight areas of the screen being redrawn (Quartz Debug), maybe there’s one for Windows too?

Screen recording janky UIs is a good tip, you can then step through frame-by-frame to see what’s really going on. I’ve also found that using the slow-motion mode on my phone to record the screen can also be useful because the way your monitor actually refreshes can completely change how your UI looks when it comes to animations - I used to use a crappy old lcd TV as a monitor and recording that in slow-mo showed how it would sweep the screen from top to bottom as it redrew, and a lot of the jank I was seeing was just because of how slow it did that!

Hopefully some of the changes in JUCE 7 around synching with screen refresh rates may improve things here?

1 Like

Ha that’s great!

Hopefully I didn’t come off as too critical of JUCE. I do feel like UI has evolved a fair amount in the last 10+ years and there’s a bit of catch up for JUCE (speaking of, love your stack blur PR, it’s a pre-req for efficient shadows!). But ultimately dealing with jank seems inevitable in a framework that has low level manual (er, semi-automatic?) paint timing. If anything, some more tooling and docs around it would be nice!

2 Likes

one thing that you maybe forgot about in your article was that you can always just setBufferedImage(true) on a component in order to make it bypass all its automatic repaint calls, except for the ones apearing at the end of resized(). because then you can really surgically define when to repaint stuff manually

1 Like

In most cases it’s actually slower to render a component from an image, so you shouldn’t just blindly go calling setBufferedToImage(true) on things without benchmarking them. Text is one place where it is almost always faster to render from an image, so that might improve the case of the caret in the text editor.

4 Likes

maybe it would be cool if juce::Component had a new method called reducePaintCalls() or so that only has this “less-paint calls”-ish property, but without the image-lookup table

Ahh, true, I totally ignored this. I was under the impression a component’s cachedImage invalidates on any repaint() call. So for example, my CaretComponent calls setVisible on itself, which calls repaint on itself, which calls repaint on the TextEditor, which calls repaint on the Form its in… so all of those components would have their cache invalidated. So, I don’t think it would rescue me from “jank” caused by repaint calls (which are everywhere!).

Actually, maybe like others, I’m generally unclear about the “good” setBufferedToImage use cases.

The docs are a bit murky, but reading the code it seems that cachedImage->paint is only ever called in paintWithinParentContext which is called from paintComponentAndChildren (which recursively goes through the hierarchy top down painting children)

So if I’m understanding correctly, setBufferedToImage would be better named cacheComponentAndChildrenUntilRepaint — the cache is only used when the component’s parent is painting and is calling paintComponentAndChildren (opposite direction of what’s happening with the caret example).

Oooh, this makes sense!

I thought setOpaque was supposed to work this way (the whole point of calling repaint() on the parent for transparent components is you need to repaint what’s “behind” the changing component), but it’s late and I’m not seeing it in the code…

That is the case, yes.

When you call repaint() on a component that’s using a cached image, if that component is visible any part of the image that overlaps the area being redrawn (because it’s not always the entire component, see below) will be invalidated. In turn this will mean the paint() method will be invoked on that component so as to update the invalidated portion of the image.

juce::Component::setVisible() will call repaint() on the component in question if it has just been made visible - if not it will call repaint() on the parent. See here.

When you call repaint() on a component, you’re not saying “please call paint() on this component at some point in the near future”. What you’re doing is first invalidating the cache, if the component on which repaint() was called is set to buffer to an image, then telling the OS, “this portion of the screen needs to be redrawn on the next update please”. The OS will then come back around at some point and tell the ComponentPeer which portions of the screen need to be redrawn. See juce::ComponentPeer::handlePaint().

Then, each component will draw itself and any of its children that overlap the portion of the screen marked for repainting. Any component that’s using a cached image will obviously just draw the cached image (which will call paint() if the cache was invalidated to update the image), all other components will then just call paint(). What this means is that in general, when you flag a component as needing to repaint, it, its parent, and all of its ancestors will be drawn, but its siblings won’t (unless they overlap).

In the case of your caret :carrot: - when you call repaint() on the caret you’re saying that small portion of the screen needs to redraw. If you haven’t called setBufferedToImage(true) on the text editor, then every single time the caret’s made invisible, your entire text editor is being rendered since the paint() method will be invoked (but even though you paint the entire component, only the small portion under the caret is actually being ‘used’ because the rest is outside the clip bounds of the context). Text rendering is very expensive, so if your text editor contains a lot of text this process is going to be very slow. If you call setBufferedToImage(true) on the editor, when the caret is made invisible only a tiny portion of the cached image will be drawn to the screen which will be extremely cheap.

In general, if a component takes longer to draw in its paint() method than it would to draw an image of the same size, call setBufferedToImage(true) on it. If in doubt, profile your app with it turned off, profile it with it turned on, and choose the one that gave the best performance!

Kind of, the key is that if a component is cached to an image then that image will be drawn instead of calling the component’s paint() method. The component’s paint() method will only ever be invoked if you invalidate the cache by explicitly calling repaint() on the component (or any other action that causes repaint() to be called for that component).

I had assumed that when a CachedImageComponent is drawn, it would only draw the portion of the image that overlaps with the clip region of the graphics context.

Looking at the implementation of StandardCachedImageComponent:

That doesn’t seem to be the case… meaning if you only needed to draw a single pixel of a cached image, it would draw the entire image to the context, most of which would be ignored.

Unless I’m missing something, there’s an optimisation to be made there because drawing images is expensive so only drawing as few pixels as possible could be a huge improvement in some cases.

E.g. you could do something like:

const auto clipBounds = g.getClipBounds();
const auto areaToDraw = imageBounds.getIntersection (clipBounds);
g.drawImage (image,
             clipBounds.getX(), clipBounds.getY(), clipBounds.getWidth(), clipBounds.getHeight(),
             areaToDraw.getX(), areaToDraw.getY(), areaToDraw.getWidth(), areaToDraw().getHeight(),
             true);

Maybe this is already handled within the graphics implementation of drawing an image? I don’t know.

You can actually supply your own juce::CachedComponentImage to a juce::Component where you can customise the behaviour of invalidating and updating the cache, if you really wanted.

3 Likes

I assumed this was the case here as well - worth a test for sure!

Right, so in the case of the caret :carrot:, every time it turns off via setVisible, repaint is called on the parent text editor, invalidating the cachedImage, telling the OS the entire text editor is dirty.

I believe this happens regardless, due to the explicit call to repaint that setVisible makes — this is part of the behavior I wanted to override in the CaretComponent class…

This is a good summary! I might quote you / ask for your review on my next article on “How JUCE Components work” :smiley:

When Component::setVisible (false) is called, it calls repaint() on the parent and when repaint() is called, it bubbles up the stack of components, calling repaint() on each one. This causes all the buffered images to get cleared, which seems to completely defeat the purpose of buffering to image. Seems like it’s only useful for the top level component. Seems like when you call repaint on a component, it should just map its bounds to the ComponentPeer and call repaint on that. Then the peer will call paint() bubbling up if needed or use the cached image.

I haven’t looked at the code a lot, maybe there is something I’m overlooking.

1 Like

Hmmm, this stuff gets complicated. I’ve been thinking better clarification in the documentation would be helpful. Maybe a table for which methods call repaint, etc.

The best mental model I’ve found is “The OS paints top-down” through the component hierarchy and like you said, “repaint bubbles bottom up.”

The cachedImages should to only invalidate the area of the child region as the repaint propagates upwards. So, I think unless repaint() is called explicitly on the parent without bounds, the cachedImage stays useful?..

setVisible is a bit nasty in that it unconditionally calls repaint on the whole parent.

I was wrong about this, repaintParent is friendly and only invalidates the component’s bounds in the parent:

void Component::repaintParent()
{
    if (parentComponent != nullptr)
        parentComponent->internalRepaint (ComponentHelpers::convertToParentSpace (*this, getLocalBounds()));
}

Combining with what Jimmi pointed to:

each component will draw itself and any of its children that overlap the portion of the screen marked for repainting .

My conclusion is that setBufferedToImage is useful for a component when :

  1. The component + children are fairly expensive to create
  2. Child components are at different discrete locations and might be painted at different times from each other
  3. repaint() isn’t being manually called a lot on the component

So, anything that renders text seems does seem like a good candidate… :carrot:

Turns out I’ve never actually read the documentation for setBufferedToImage closely.

Setting this flag to true will cause the component to allocate an internal buffer into which it paints itself and all its child components, so that when asked to redraw itself

I didn’t realize the children get added to the buffer as well. I assumed each Component got its own buffer and then they would just be blitted on top each other. This means setting setBufferedToImage(true) on a TextEditor is pretty much pointless, because the cursor blinking has to regenerate the image. Even if all the text isn’t being redraw, all the layout still needs to be redone and that is expensive. Maybe the TextEditor should cache its GlyphArrangement?

I think I might have confused myself a bit, I just updated my previous post. setVisible(false) (what the cursor does) actually calls repaintParent which supposedly only invalidates the cursor’s bounds in the cachedImage, I think?.. But my testing shows that the TextEditor’s paint method is called, so I’m a bit confused now.

Yes, if a portion of the cached image is invalid, then it needs to be regenerated. So then TextEditor::paint is called to fill in a portion of the image.

Hmm then I don’t really understand the point of partial invalidation. Is there a way the cachedImage can still be used for other (valid) bounds? If we are currently painting a child, that means the parent has already been painted and has no more need for the cachedImage.

Ahhhh maybe this is where isOpaque comes in. If the child is opaque, then the parent can remove those bounds, and it won’t intersect with what’s being considered “dirty” at paint time.

I would call this thread “mildly interesting” because as a Windows 10 user I’m somewhat immune to jank. e.g. On all major browsers (Edge, Firefox, Chrome) you can’t even resize a window without lagging behind the surface area :joy:

Never heard the term jank before. Sounds like a very broad term. I use these terms for visual unpleasant but non critical “problems”.

  • Jitter: While resizing, a fixed layout or position jumps back and forth due to rounding or int casting.

  • Tearing: Region updates split graphics in half due to disabled VSync or unsync repaints.

  • Frame Lag: An otherwise smooth animation suddenly slows down. Often due to complex path rendering.

  • Artifacts: Regions use memory, texture, image data that was not cleared and can possibly contain “anything” / noise.

  • Stutter: A linear moving object (eg. at the rate of 1px per frame) seems to skip frames. Most often because of inaccurate frame timings.

  • Micro Stutter: Using a variable timestep, objects moving on float coordinates seem to jump back and forth due to different delta time roundings.

  • Cracks: Specific to GPU / Triangle rendering. Vertices that render on float coordinates introduce small pixel cracks on resize with non-uniform scales. Again due to rounding. Better to use a fixed resolution and scale with integer factors and render into a framebuffer instead.

  • Glitches: Small repaint regions are not updated, resulting in pixel artifacts.

  • Blurry: Non DPI aware implementations result in linear interpolated image textures. Often resulting in non anti aliases rectangle borders due to the hard polygon edges.

About the repaint() issue. During development/prototyping I recommend not using it at all. Same for setBufferedImage(). Later during testing you’ll eventually see outdated regions. The advantage: You avoid unnecessary repaints() and will eventually find a pattern where and when an update should happen. Most of the time it’s really just one function.

For OpenGL debugging I strongly recommend https://renderdoc.org/
It’s really amazing. You can capture frames and look at individual commands and preview every step of the frame constructions.

4 Likes

When your components are being painted, you could check the clip bounds of the juce::Graphics object supplied and only draw elements that are within those bounds. For example if you had some text at the top of the component but only the bottom potion is drawn, yo could just skip drawing the text.

Although I find a better approach for this is to simply have child components to draw each element of a more complex component so you get this sort of behaviour already.

Yeah, if you call setOpaque (true) on a component then any components behind it won’t need to be repainted since you’ve said the opaque component will draw to the full area.

1 Like