I’m starting to work on painting parts of the UI and I’m noticing that the CPU usage is rather high. This isn’t so surprising since I’m currently repainting the UI at 20hz. Is there a better way to paint static parts of my UI, like waveforms, while still moving around text and other data on top of the waveform? Currently it’s just a position bar over the waveform but I plan to add a movable ADSR envelope.
EDIT: Correct me if I’m wrong but my conclusion is that the way JUCE’s invalidation system works means that I will have to repaint the waveform each cycle no matter what. This is because I have multiple indicators rendering over the static waveform.
It will help you narrow down where the time is being spent. I managed to get a 15% cpu usage on a gui down to 3% using this. In my case it was an image being resized in the wrong place. But whatever it is, it’s best to just test to find the bottleneck, rather than doing optimisation that may not actually improve performance.
It could also be of great help to simplify the path as much as possible before painting it. If you are creating it from audio it probably contains many more points than could be drawn given the pixels on your display. That could be the bottleneck.
Ideally, you should be able to repaint the UI at 60hz.
For your case, rendering the waveform once and buffering the result should already do the trick. If you want to draw a vertical position indicator on top of it, consider making this a component and moving it around, trying to reduce the size of invalidated regions of the waveform behind it. If there are no affine transforms in the hierarchy, repainting the invalidated portion of the static background should be fast.
I’m assuming “Windows” and “No OpenGL” here, since your application is using only the JUCE software renderer in this case.
Generally, it comes down to avoiding unnecessary repaints. Some strategies and things to look out for:
Create components as siblings + z-order instead of nesting them as children
Use opaque components
Invalidate only areas that actually need to be repainted
using your own buffering-to-image-implementation (I recently wrote one to split a components rendering into a “static background” part that ends up combined into a backing image with other components doing the same, and a “realtime” part that uses regular paint)
Be mindful of component overlaps: if a single pixel of a parent gets invalidated, paint of the parent will be executed (with pixels discarded only via clipping)
resampling images is expensive
rendering text is expensive
prefer custom paths over multiple calls to primitive drawing functions, drawLine and the like (these construct a path under the hood)
performance is much more problematic on windows, since there is no hw acceleration there. memory access is the bottleneck: that means, buffering to image is not always more efficient
use benchmarks. I haven’t tried perfetto, but it’s been recommended on this forum many times, so it might be worth checking out.
if this is about a plugin: mind that multiple editor windows can be open at the same time, sharing the render thread with other plugins and the DAW
easy to try stress test: open 4 plugin editor windows in reaper, use 200% display scale (high dpi)
Advanced method, if you are really serious about animation:
Implement your own edge table rasterizer, operating on raw pixel data of an image buffer
Implement rasterizers optimized for special cases like single-colour-graphics, curve/envelope displays, and whatever else you need
a core idea here is to touch each memory location only once and avoid all the call and branching overhead in JUCEs edgetable rasterizer, which is obviously written for the generic case
the techniques are basically the ones used in software graphics engines before GPUs took over in the late 90s. there are lots of resources on how to do that, sadly it has become a “forgotten art form”. normally, why would anyone want to do this
The desirable solution:
JUCE gets a hardware accelerated renderer on windows (font rendering, paths, clipping, alpha blending, affine transforms). Basically the featureset that you get in coregraphics or any “canvas” style renderer.
I am certain the JUCE team is working on it, and I somehow suspect that what makes this so challenging is that JUCE covers so many special cases that need to be handled correctly and consistently across platforms. The entireness of the JUCE GUI has lots of coupling and dependencies, which is understandable considering how JUCE has grown into a rather large framework…
Wow, thanks for all the information! That’s very helpful, I just have a few clarifying questions.
Instead of making a separate position indicator component (because I’m dreading the thought of making components for every indicator I add), could I draw them all in the paintOverChildren method of my parent component with the same performance effect?
What makes opaque components better, isn’t it be the same area that needs to be redrawn?
What does it mean to invalidate an area? Is this something I’m doing myself?
I’m inferring that invalidated areas are how JUCE knows where to repaint. If only a small area is invalidated (like from a position indicator), how does JUCE know how to redraw that one specific part when I’m using larger paths to render? Is that what setBufferedToImage accomplishes?
I’m not looking to hardcore optimize my plugin, at least not until there’s more of a prototype and I have a better understanding of the mechanics involved. But this will still be very useful in designing the rendering correctly.
Each component has a rectangular bounding box. If a particular area needs to be redrawn, only components overlapping the invalidated regions need to be redrawn. JUCE knows the bounding boxes of all components and can exclude them accordingly. You wouldn’t want to repaint the entire windows just because a button state changed.
If a component is opaque, the area behind it does not need to be redrawn, allowing JUCE to apply further optimisations. In the other case, if a component is not opaque, invalidating it leads to repaint on all the components behind it.
Invalidation (meaning “something needs to be redrawn”) can have many reasons. If you change the bounding box of your position indicator, this will at least invalidate the area where it was before, and the new area where it’s at now. All components overlapping these areas must be redrawn. Because of this it’s important that the paint code of the component behind the position indicator is fast.
I’d avoid “paintOverChildren”, it sabotages the entire idea of minimizing the areas to repaint. The best you can do is construct your layout in a way that prevents as many unnecessary repaints as possible. If only a single pixel of a component needs to be redrawn, the entire code in it’s paint function needs to run, and that can get expensive quickly.
You can enable repaint debugging in the projucer (JUCE_ENABLE_REPAINT_DEBUGGING) to have JUCE colorize all components randomly each time they’re redrawn, this will give you a visual idea of what’s going on in terms of invalidations.
Ok, I get what you mean. So does this mean, no matter how I go about doing it the entire waveform will need to be drawn each time the vertical indicator moves? Since that region will be invalidated and will always overlap with the waveform component.
Ok. But in my situation, where multiple moving indicators are getting repainted over a waveform viewer, that would not work. Correct me if I’m wrong but my conclusion is that the way JUCE’s invalidation system works means that I will have to repaint the waveform each cycle no matter what.
It’s a bit hypothetical without actually benchmarking it and knowing more about all the context, but if you need to draw this waveform as a background a lot, maybe don’t rasterize the path each time, instead buffer it to an image.