drawImage Issue : Graphics of the DAW starts lagging after adding a few instances of my plugin

Hello folks,

I have a really big problem and I ran out of solutions.
I have 8 or 9 images that I am using in my plugin GUI.
They are very small in size. The background image is a jpeg file. It is 500x700 pixels. and 200KB.
Others are png, less than this both in file size and dimensions.

When I try to load only one instance of my plugin, everything works perfectly. With two instances it is still ok. But after 3 or 4 instances it starts lagging the meters and counter of the DAW. After a few more instances, the labels of the plugin start slowing down. The more instances I add, the more lagging occurs.

I was considering using OpenGL but looks like there will be some problems in the future since it is deprecated.

To be honest I don’t know what others are doing but it is obvious that they know something that I don’t know yet :smiley:

I know that I have a few options.

  1. filmstrips for all different angles of knobs.
  2. using only one image and rotating it with drawImageTransformed.
  3. Image+ generic graphics methods of the juce
  4. …

Which one of these methods would work faster?

Currently, I use two images. One is a plain pic of the knob without the shadow. The other is the shadow(a transparent png file).

I keep the shadow fixed and I just rotate the knob image. I am using the same logic for all of my knobs.

I tried to use the XCode profile option to see the CPU consumption and it appears that It is because of the drawImage method calls.

Here is the code of my knobs. I am just customizing the slider’s look and feel.
Could you please have a look at the code to see if there is anything wrong?
Do you have any suggestions?

BlackKnob.h file

#include <JuceHeader.h>

//=================================================

class BlackKnob  : public juce::Component,
               public juce::LookAndFeel_V4,
               public juce::Label::Listener
{
public:
BlackKnob();
~BlackKnob() override;

void paint (juce::Graphics&) override;
void resized() override;

void drawRotarySlider(Graphics& g, int x, int y, int width,
                      int height, float sliderPos,
                      float rotaryStartAngle, float rotaryEndAngle,
                      Slider& slider) override;
void triggerClick();
private:
juce::Slider::SliderLayout getSliderLayout (Slider&) override;
Label* createSliderTextBox (Slider& slider) override;
Label *l;

/** Called when a Label's text has changed. */
 void labelTextChanged (Label* labelThatHasChanged) override;

//these will define the size of the knob.
float widthOfSlider=60;
float heightOfSlider=80;

double firstClickTime;
int turn = 0;
int isClicked = 0;
long ShowLabelBaseTime=0;

Font arial{"Arial",15,Font::plain};

Image knobImage,knobShadow;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BlackKnob)
};

BlackKnob.cpp

#include <JuceHeader.h>

#include "BlackKnob.h"

//==============================================

BlackKnob::BlackKnob()

{
knobImage = ImageCache::getFromMemory(BinaryData::blackKnob_png, 
                                      BinaryData::blackKnob_pngSize);
knobShadow = ImageCache::getFromMemory(BinaryData::shadow_png,
                                       BinaryData::shadow_pngSize);
}

BlackKnob::~BlackKnob()
{
}

void BlackKnob::paint (juce::Graphics& g)
{
}

void BlackKnob::resized()
{
}

 void BlackKnob::drawRotarySlider(Graphics& g, int x, int y, int width,
                                  int height, float sliderPos,
                                  float rotaryStartAngle, float rotaryEndAngle,
                                  Slider& slider)
{
// the size of the images are little bit larger than the knobs.
knobImage= knobImage.rescaled(width, width);
knobImage.duplicateIfShared();
knobShadow = knobShadow.rescaled(knobImage.getWidth(), knobImage.getHeight());
knobShadow.duplicateIfShared();
slider.setTextBoxStyle(juce::Slider::TextBoxBelow, false, width, height*0.4f);

Rectangle <float> knobArea(x, y, width, width);
float halfOfDiameter = jmin((float)width,(float)height);
halfOfDiameter=halfOfDiameter/2.0f;
 float angle = rotaryStartAngle + (sliderPos * (rotaryEndAngle - rotaryStartAngle)) +2.2;
// I am trying to draw the image at the top of the knob area. 
// The label will be at the buttom. 
//It works fine for me.
g.setOrigin(0, -height*0.1f);
g.drawImageTransformed(knobImage,
 AffineTransform::rotation(angle, halfOfDiameter, halfOfDiameter));

g.drawImage(knobShadow, knobArea);
g.setOrigin(0, +height*0.1f);

//  this section just checks some mouse events and label events to display 
 // the label for a short period of time.

 if (((Time::getMillisecondCounter() - firstClickTime) > 500) &&
  !(slider.isMouseOverOrDragging() || l->isMouseOverOrDragging()) &&
  !(slider.isMouseButtonDownAnywhere() || l->isMouseButtonDownAnywhere()) &&
   turn == 1 &&
   !l->isBeingEdited())
  {
  turn = 0;
  isClicked = 0;
  firstClickTime = Time::getMillisecondCounter();
  }

 if ((slider.isMouseOverOrDragging()  ||
 l->isMouseOverOrDragging())  &&
 turn==0)
 {
 isClicked = 1;
 firstClickTime = Time::getMillisecondCounter();
 turn = 1;
 }

if(isClicked==1)
 {  ShowLabelBaseTime=Time::getMillisecondCounter(); }

if(Time::getMillisecondCounter()-ShowLabelBaseTime>800 &&isClicked==0)
{  l->setVisible(false); }
 else
 {  l->setVisible(true);  }

    slider.setVelocityModeParameters(0.01, 0,0.1, true);// velocity mode has modified
 }

 void BlackKnob::triggerClick()

 {
  isClicked=1;
 firstClickTime = Time::getMillisecondCounter();
  turn = 1;
 }

Slider::SliderLayout BlackKnob::getSliderLayout (Slider&) 
{
 Slider::SliderLayout layout;
 layout.sliderBounds = Rectangle<int> (0, 0, widthOfSlider, heightOfSlider);
 layout.textBoxBounds = Rectangle<int> (0, heightOfSlider*0.7f, widthOfSlider, heightOfSlider*0.3f);
 return layout;
 }

void BlackKnob::labelTextChanged (Label* labelThatHasChanged)
{
  DBG("changed");
 }

 Label* BlackKnob::createSliderTextBox (Slider& slider)
 {
       l = LookAndFeel_V3::createSliderTextBox (slider);
       l->setColour(Label::backgroundColourId, Colours::black);
       l->setColour (Label::textColourId, Colours::white.withAlpha (1.0f));
       l->setFont(arial);
       l->addListener(this);
       return l;
 }

So every single time a dial gets drawn, you:

  1. rescale it
  2. then copy it (duplicate if shared)

Then you do the same again for the shadow, and then you draw it transformed.

Why not skip all these steps and just draw it? The rescaling and duplication is completely unnecessary and slows everything down needlessly.

3 Likes

Hello,
Thank you for your answer.

I reviewed my code and made a small change.I moved this section from drawRotarySlider method to constructor.

My constructor looks like this now.

But still profiling tools tells me that the 40% of the cpu is being used by drawImageTransformed.
Every time it gets into the drawslider, it rotates the image and then prints. I don’t know if there is a method that just rotate the image and returns the rotated image. That way maybe i can keep the rotated image and call drawImageTransformed when a rotation is needed. Looks like it takes a lot of cpu to rotate these small images. Do you have any other suggestions ? :slight_smile:

drawImageAt is the most performant method for drawing images, but it needs you to make sure the image is already rotated and transformed. in your component’s resized you could already calculate the transformation so only the rotation is left in paint. if you use setBufferedImage(true) on your slider it will also reduce paint calls in general and only repaint when repaint was explicitely called on it. that is useful because if you control in which cases the parameterAttachment repaints you can reduce it to moments only when the parameter value has changes enough for a new paint call to make a visual difference

1 Like

Thank you for your answer :pray:

void Graphics::drawImageAt (const Image& imageToDraw, int x, int y, 
bool fillAlphaChannel) const
{
     drawImageTransformed (imageToDraw,
                      AffineTransform::translation ((float) x, (float) y),
                      fillAlphaChannel);
}

void Graphics::drawImage (const Image& imageToDraw, 
                          Rectangle<float> targetArea,
                          RectanglePlacement placementWithinTarget, bool fillAlphaChannelWithCurrentBrush) const
{
if (imageToDraw.isValid())
    drawImageTransformed (imageToDraw,
                          placementWithinTarget.getTransformToFit (imageToDraw.getBounds().toFloat(), targetArea),
                          fillAlphaChannelWithCurrentBrush);
}

As far as i can see both call the drawImageTransformed.I tried to replace them but unfortunately profiling tool still tells me that it still cpu intensive. There is something going on but I still don’t found out the reason of the slowness :slight_smile:

Should i call “setBufferedImage(true):” in the look and feel class or in the drawrotaryImageMethod?

some of the things i mentioned are not possible with look and feel, like setting certain bounds in a component’s resized, or setBufferedImage, because it’s more of a component-specific thing. making custom components for parameters is more powerful than using look and feel on juce::Slider

1 Like

@reFX @Mrugalla
I see.
After your suggestions,
I stopped using the drawImage. Since it is also rescaling the image, it takes a lot of cpu power.
I rescale my images only once and then draw them with drawImageAt.

By doing this, i saved 17% . Now it takes 40%. Previously even two instances were slowing down the meters and counters. Now it starts after three or four instances. It is better but still too far from what i see in other plugins that have images. Some of them have a lot of images but still you can load 10 or 20 instances without any slowing down or rendering issues. For some them even 50 instances don’t slow down.

I checked the graphic class to see if there anything that i can use but looks like the draeImageAt is the simplest and least cpu consumer.

There must be something else that they are using. I will also try not to rotate the images. I will draw them with 0 degree angle(since drawImageAt does the same) to see if it is going to help or not. If it does, i will use filmstrip method instead of rotating one or two images.

Thank you for help :pray:

1 Like

another thing to note about juce::Graphics is that it lets you change the resamplingQuality, low medium and high. medium is the default quality and it looks almost the same as high, but low has a better performance and looks a bit more grainy, but also sharper. if the artefacts are not too unpleasant maybe it would help

Why do all the dials get drawn all the time? They should only be drawn when their values change. Are you repainting the whole UI on a timer or something like that?

1 Like

Well, the code that you see above is the look and feel of my knobs.

In my editor, I set the look and feel of the slider with this one. That is it. It calls the paint method itself. I mean if I tried to set a “mouseover condition” or anything else then during that time, the image would be invisible. The drawRotarySlider doesn’t stop painting. It calls the paint whenever it needs. I don’t invoke it.

I will try to go step by step and try to optimize it.
If it was possible. I would try to draw the image only once. After that, if the windows gain a key or mouse focus, ı would start calling the paint. The same thing for the knobs on the same plugın window, but the paint method is being called automatically.

It seems that you did some profiling and it pointed to one method specifically, which is used in your paint routine. But that’s only one part of the problem: you need, as @reFX pointed out, to be sure that your UI is painted only when needed, and only what moves is getting repainted.

You could use good old traces in your paint routine to see when it is called, or the convenient JUCE_IS_REPAINT_DEBUGGING_ACTIVE that’ll make your UI flash various colors when/where repainted.

Who knows you may realize that when 1 knob moves you are actually repainting everything. Or worse, everything gets repainted constantly for some reason.

I would bet on something like that: if you say that having N instances of your plugin, idle, impacts performance, it means that something is happening and using resources when it should not.

Edit: note that you could also be doing things you shouldn’t do on the message thread, or blocking it for some reason.

3 Likes

Keep in mind, if you have a meter on the left and the right side of the plugin, the redraw region will be merged to one big region on macOS, even if in the middle nothing is changed.

There is a workaround-flag you may want to try JUCE_COREGRAPHICS_RENDER_WITH_MULTIPLE_PAINT_CALLS

BTW JUCE_IS_REPAINT_DEBUGGING_ACTIVE show only the rectangles which are marked as dirty, the OS may choose bigger regions for redrawing

@dimbouche @reFX @Mrugalla @chkn

First of all thank you all for your help.
As you all mentioned, the problem was the number of the paint calls.

This helped a lot to see the components that are being repainted very often. Now every single animation that i am using, every single rotation or image change routine is waiting for a trigger or a value change to be repainted. Now over 15 instances are running with full animation and the meters are ok.

This will also help to make it better than it is now.I will try it.

You’re great folks! :pray:

1 Like

As far as I tested, JUCE_COREGRAPHICS_RENDER_WITH_MULTIPLE_PAINT_CALLS is of no help anymore sadly.
But in the Juce 6.1 release notes, they say:
We've also tightened up our CoreGraphics invalidation regions
I need to run some testing to see if it’s actually fixed!

Glad you got to the bottom of your issue @trengineer