I actually did try that, but the image rendering took even more CPU (and I used drawImageAt to ensure no image resizing). Is it possible setBufferedToImage would still be helpful?
But in that case wouldn’t something like
auto visibleRect = g.getClipBounds().getIntersection(viewPort->getViewArea())
make more sense? ![]()
I’ve had success using createComponentSnapshot() to grab an image of the component so that the whole component doesn’t need to be recalculated when another component above it would repaint (a playhead).
I used this on an envelope with an JUCE path. And then I modified the paint function to draw the image if the envelope hasn’t changed since last paint.
I tried bufferedToImage before that but it wouldn’t do anything and I needed my component not be opaque.
Edit:
int scale = g.getInternalContext().getPhysicalPixelScaleFactor();
juce::Rectangle<int> bounds = getLocalBounds();
cachedImage = createComponentSnapshot(bounds, false, scale);
If necessary, consider offloading the task of rendering the path to an image onto a separate thread. It’s crucial to conduct benchmarking to pinpoint the performance bottleneck. Additionally, examine whether the component responsible for drawing the image is scaling it, as this could trigger unwanted resizing. For larger resolutions, consider incorporating OpenGL for rendering efficiency, as the performance often depends on the total number of pixels being processed.
I found that setBufferedToImage is working pretty well in my case. There is one problem though. Currently, I have the indicators in a separate overlay component like you suggested. At 60hz I call repaint on the overlay component and it paints the indicators. The problem is that my waveform component is being repainted even when no keys are being pressed and no indicators are being drawn. Does calling paint on a component automatically invalidate that entire region? I can tell this is what’s happening because removing the 60hz overlay.repaint() call stops the waveform paint from also calling every frame.
Edit: I just check before calling repaint now, although I am still curious about this because I was under the impression from jcomusic that JUCE checked which pixels paint affects.
The check should be outside paint, eg
If (condition) repaint();
This is what I assume would happen but in practice it seems to just repaint the whole thing in many cases, not just a small region.
You can manually call repaint on a certain region though.
Hi!
@Ben:
Does calling paint on a component automatically invalidate that entire region?
→ yes
@evanberard:
There can be ripple effects from the structure of the UI, if more repaints are going on then there should be, no way around debugging I guess…
Some more general rambling on the topic, in the hope that it might provide more insight:
Well, JUCE tries to avoid to paint unnecessary areas, but this has its limits.
I’m assuming that there is this small area on top of a larger component containing the waveform image that needs to be redrawn. Since the paint function of the waveform component only has one paint() for the entire size of the component, the only thing JUCE can do is apply a clipping mask, let’s assume this to be rectangular as well. When painting a rectangular image, without any transforms or anything, the image drawing code can look at the clipping mask, and draw only the relevant section of the image. This would be as efficient as it gets.*
But, if this paint code draws a path instead, there is no way around rasterising the entire path, and comparisons to the clipping mask can at best skip some of the actual accesses related to writing and antialiasing the pixels.
From what I’ve seen of the JUCE internals, usually the compromises are good, in the sense that they don’t try to overoptimize, and the fact that at every stage here, a stack of affine transforms, alpha blending, and what not can be at play, imposes some limits on what can be done efficiently in terms of optimization in the generic case. In other words, if checking whether a pixel has to be drawn is more expensive than just drawing the pixel and discarding it late based to the clipping mask, then there is no benefit in doing additional branching and special case handling.
*(
I’m not certain how well this works in practice, but if the JUCE functionality you’re using doesn’t do this to an optimal extend, you could do this yourself. I think it has been suggested before in this thread.
There is another thing at play when it comes to invalidation regions. Invalidation rectangles are actually a feature that Windows offers, on operating system level. The WIN32 api sends messages to your application, telling it which regions need to be redrawn. This is what happens if you move a portion of your window beyond screen boundaries and back, for example. JUCE hooks into this system, which makes sense.
What’s important here is that there can be multiple of those “please repaint this region” messages in a very short time period, exceeding the screen refresh rate. JUCE recently made some improvements to handling this to avoid painting the same components multiple times for one single refresh. As far as I know, there are also mechanisms that combine invalidation rectangles into larger ones to make the process more efficient. The details are complicated and I can’t remember all of them from the back of my head, I’m mostly mentioning it to give you an idea where to look if you want to gain more understanding of how all of this works, to be able to optimize better.
tldr:
When I’m optimising such scenarios, I count calls to the paint functions (some cleverly placed DBG(“paint on XYZ”) will do), to see if there are multiple paint calls when there should only be one, or none. In addition, I place some more DBG(“yzx”) on events that cause or could cause a repaint. that’s usually conclusive already. For animation, I don’t use AnimatedAppComponent, I hook up the timers myself so I can start/stop them and check conditions before issuing repaint() calls.
Then, I microbenchmark the time it takes to paint the components in question too. Best practices aside, each case is unique, no way around diving into it deeply ![]()
Yeah I didn’t see any way of getting around having to repaint the entire path.
I did this as well - adding debugs and watching them closely to make sure there’s no double repaints. I basically have a flag called shouldRepaintEntirely that I set to true in every possible place where an interaction would change the underlying component (eg. hovering over a point, dragging, etc).
If it should repaint entirely, it sets a flag currentlyCaching to true and then calls createComponentSnapshot which runs through the paint function and saves the component to a cached image.
Meanwhile there’s a playhead on a 60hz timer that would absolutely shred the CPU if it was drawing over the raw path instead of an image version of it.
In the paint function if currentlyCaching is true, it draws all the elements of the envelope, otherwise it only draws the cached image version and the playhead. Saved me a solid 40% of CPU.
//==============================================================================
void EnvelopeEditor::repaintEntirely()
{
shouldRedrawEnvelope = true;
repaint();
}
//==============================================================================
void EnvelopeEditor::repaintPlayheadOnly()
{
if (playhead.showPlayhead)
repaint();
}
//==============================================================================
void EnvelopeEditor::paintEnvelope(juce::Graphics& g)
{
if (shouldRedrawEnvelope || !useCachedGraphics)
{
if (!currentlyCaching && useCachedGraphics)
{
startCaching(g);
}
if (currentlyCaching || !useCachedGraphics)
{
if (!backgroundColour.isTransparent())
g.fillAll(backgroundColour);
grid.paintGridLines(g);
path.paintPath(g);
dots.paintDots(g);
dots.paintSecondaryDots(g);
tooltip.paintTooltips();
}
}
if (!currentlyCaching || !useCachedGraphics)
{
if (cachedImage.isValid())
g.drawImage(cachedImage, getLocalBounds().toFloat());
if (playhead.showPlayhead)
{
playhead.calculatePlayheadBounds();
playhead.paintPlayhead(g);
}
}
}
//==============================================================================
void EnvelopeEditor::startCaching(juce::Graphics& g)
{
currentlyCaching = true;
int scale;
scale = g.getInternalContext().getPhysicalPixelScaleFactor();
juce::Rectangle<int> bounds = getLocalBounds();
cachedImage = createComponentSnapshot(bounds, false, scale);
currentlyCaching = false;
shouldRedrawEnvelope = false;
repaint();
}
//==============================================================================
void EnvelopeEditor::paint(juce::Graphics& g)
{
if (isVisible())
paintEnvelope(g);
}
This paint function is super weird and confusing but it works ¯\(ツ)/¯
Basically I call repaintEntirely() wherever there’s an interaction or event that changes the envelope itself. Otherwise the timer function calls repaintPlayheadOnly() many times.
I could optimize more by only repainting a region of the cached image when the playhead is over it, but then it became a bit too much of a hassle so didn’t bother.
CPU now sits around 16% instead of the 60%+ I was getting before. I can live with 16% haha
60% CPU to draw the path of a waveform doesn’t sound right.
@Ben and @evanberard: Perhaps you could share the code which you are using to create the path? Just to rule out that the problem is there.
It’s just a lineTo() for every pixel on the screen. With the Y coordinate coming from an interpolation function. The high CPU comes from recomputing it 60 times a second. Waveform paths also tend to be just a simple lineTo for every pixel.
Before:
After:
//==============================================================================
void Path::paintPath(juce::Graphics& g)
{
double y = editor.env.getYfromX(0.0);
p.clear();
auto inset = editor.inset;
p.startNewSubPath(inset + 0.0, inset + (1.0 - y) * editor.height);
for (int i = 0; i <= editor.width; i++)
{
double x = editor.convertToEnvelopeCoordX(i);
y = editor.env.getYfromX(x);
p.lineTo(inset + i, inset + (1.0 - y) * editor.height);
}
g.setColour(pathColour);
g.strokePath(p, juce::PathStrokeType(lineWidth));
}
Every time paintPath() is called, the path is being recreated. Instead you could update it in another function which you call only when necessary. That way, within paintPath(), only strokePath() needs to be called (this might be what @benvining was hinting at).
Another significant optimization would be not to call lineTo() for every pixel. You could probably get away with every third, perhaps even every fifth pixel, without any perceptible difference.
Every time
paintPath()is called, the path is being recreated. Instead you could update it in another function which you call only when necessary. That way, withinpaintPath(), onlystrokePath()needs to be called (this might be what @benvining was hinting at).
I tried this, and while definitely a great optimization, it still comes in more expensive than just painting an image version.
Keep in mind I am testing with a larger than normal envelope to optimize the worst case where the window is blown up pretty large by the user
Here’s y fixed to 0.5 as a quick example:
But the path is still much larger than it needs to be if you create it by calling lineTo() for every pixel. What happens if you change that?
Good question. I believe I tested caching the path itself but I don’t remember the result. I’ll give it a shot ![]()
I’ll give this a try too. Might help CPU spikes when the user is moving the points. But that’s way less of an issue compared to consistently high CPU
Edit @aamf:
I added if (! pathIsInitialized) to the beginning of the function and then set pathIsInitialized to true so the lineTo() functions only ever get called on the first paint
The result is about the same CPU usage. Maybe 1 or 2 percentage points down. Just seems like drawing a path in JUCE is expensive when called many times
Maybe using image and setPixelAt would be quicker.
For drawing the playhead on top? Imo that would save a lot of CPU because redrawing an image 60 times a second still takes up a fair amount of CPU as well. Just gotta make sure to figure out a good way erase the previous playhead location ![]()
I meant just to render the playhead but ofc that would be even more efficient.
You can test this simply by changing the loop as so. This will skip pixels and give you a much smaller path. Does it have any positive effect?
for (int i = 0; i <= editor.width; i += 4)



