Alternatives to AffineTransform::scale for UI scaling?

I have been using AffineTransform::scale to scale the main component of my plugins for some time now, and the solution is convenient and pretty elegant.
This is doing wonder with my existing plugins, but I am now working on a plugin with much busier UI, and I am facing problems with no obvious (to me) solutions:

  • When setBufferedToImage is used the UI (and especially the text) gets quite blurry with most non trivial scale factors. This is very noticable on text with non retina (100% desktop scale) displays. This can also affect moving components, with sporadic “bouncing” effects, up and down one pixel from one x position to next. This seems to be due to the rounding in StandardCachedComponentImage, but tweaking it only seems to displace the issue from one ratio to another.

  • On MacOS with retina displays I am experiencing slow downs (high CPU usage) with some scale factors and UI height combinations (eg odd main component height combined with 125% UI scale factor and 200% desktop scale, IIRC). This is less noticeable with JUCE 7, but it is still there.

I am now considering leaving the comfort of AffineTransform::scale for good, and start scalling each and every component manually, including fonts, borders, menus, etc.
Has anyone here tried doing this, and… is there an alternative?
I’d like to know before I start mutilating all my components :smiley:

1 Like

I scale each component of my plugin and it works well. For an EQ plugin (with spectrums and dynamic response curves), I get a much lower CPU usage by removing (nearly) all setBufferedToImage. Thanks to the Direct2D feature, I obtain good performance on both macOS and Windows.

However, when the UI size is small, I can still find some “bouncing” effects of some components as I can only assign integer bounds to them.

2 Likes

Try positioning your components with AffineTransform translations instead of setting the component bounds.

Matt

1 Like

I scale each component of my plugin and it works well.
You mean using AffineTransform?

For components with static content I tend to rely a lot on setBufferedToImage and do heavy stuff in paint rather that in resized or when the scale changes. I’ll have to refactor a bunch of things if I want to remove it and maintain good results…

I tried the Direct2D backend again today. It does work very well, and I have a few things to report to @matt :slight_smile:

Thanks for the kind words; keep the reports coming.

Matt

1 Like

Hey @fuo, have you gotten anywhere with this in the past few days? I’ve encountered a similar issue myself and spent a long time tracking down the cause of the blurry text. My first solution for that was to forcibly invalidate all buffered images in the event of a window resize:

inline void invalidateAllCachedImages(Component* inComponent)
{

    for (Component* c : inComponent->getChildren()) {
        invalidateAllCachedImages(c);
    }

    if (inComponent->getCachedComponentImage() != nullptr) {
        inComponent->getCachedComponentImage()->invalidateAll();
    }
}

Unfortunately, that didn’t end up working, lending credence to the idea that it’s something to do with the way CachedComponentImages paint. If you’ve made any advances on these I’d be very grateful if you could share them!

Hi @officialnsa

I reworked my code to cache a few calculations and switched to the direct2D branch, and it looks like I might not need setBufferedToImage in the future (finger crossed).

I think the cache is already invalided when the scale is changed.
The problem is that for many scale ratios the image size multiplied by the scale ratio does not exactly correspond to the size of the component, because of rounding values, so the image is scaled and gets blurry (and it also probably slower than it would if no rescaling was needed).
I don’t think there is an easy way around it unfortunately.

You might try caching an image of your components yourself, but then you will also need to monitor any change in scale.
Here is a snapshot method I used for another purpose, when facing a similar problem of blurry images when using the createComponentSnapshot method.

// derived from Component::createComponentSnapshot (Juce 7.0.7)
// differences:
//    - uses scaleFactor directly for the affine transfrom instead of the rounded version
//    - ceil instead of round for image w and h
//    - use isOpaque instead of flag
//    - optional PixelFormat argument to avoid relying on isOpaque, or needeing to convert it afterward
juce::Image createComponentSnapshot(juce::Component* component, juce::Rectangle<int> areaToGrab, bool clipImageToComponentBounds, float scaleFactor, std::optional<juce::Image::PixelFormat> pixelFormat = std::nullopt) {
	auto r = areaToGrab;

	if (clipImageToComponentBounds)
		r = r.getIntersection (component->getLocalBounds());

	if (r.isEmpty())
		return {};

	auto w = (int)ceil(scaleFactor * (float)r.getWidth());
	auto h = (int)ceil(scaleFactor * (float)r.getHeight());

	juce::Image image(pixelFormat.value_or(component->isOpaque() ? juce::Image::RGB : juce::Image::ARGB), w, h, true);

	juce::Graphics g(image);

	if (scaleFactor)
		g.addTransform(juce::AffineTransform::scale(scaleFactor));

	g.setOrigin(-r.getPosition());

	component->paintEntireComponent(g, true);


	return image;
}

// scale-accurate screenshot of the entier bounds of a component
juce::Image getScreenshot(juce::Component* component, std::optional<juce::Image::PixelFormat> pixelFormat = std::nullopt) {
	return createComponentSnapshot(
		component,
		component->getLocalBounds(),
		false,
		(float)juce::Desktop::getInstance().getDisplays().getDisplayForRect(component->getScreenBounds())->scale * juce::Component::getApproximateScaleFactorForComponent(component),
		pixelFormat
	);
}

As mentionned in my first post I tried to use the same rouding method in StandardCachedComponentImage but it did not fix the problem.

1 Like

I’ve run into an issue which is related to this topic. I also use the AffineTransform::scale in a container component to scale the GUI of my plugin, as described in several forum topics. The background is a bitmap image and I have an animated component, its cyclic repaint() is triggered by a Timer. I’d like to avoid that the whole bitmap gets repainted in every cycle. Setting the animated component to opaque solves the problem as long as the scale factor is 1.0. But as soon as I change the scale to a value different from 1.0, the ComponentHelpers::clipObscuredRegions in the Component::paintComponentAndChildren won’t detect my opaque area due to a float-to-int rounding error, therefore the whole bitmap will be repainted.
Has anybody else had the same problem? Scaling the GUI with AffineTransform::scale is nice and easy, I’d like to avoid calculating the scaled sizes and positions in each resized() and paint() functions if it’s possible… But of course I do not want to keep painting the whole background at 60 Hz.

Would I be correct in thinking your animated component is a child of a background component that has setBufferedToImage (true) called on it? If I’m mistaken then everything below may be irrelevant, please let me know.

If a component has children then they will become part of the buffered image too, see the docs.

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, it can use this buffer rather than actually calling
the paint() method.

Parts of the buffer are invalidated when repaint() is called on this component
or its children. The buffer is then repainted at the next paint() callback.

When you mark the child component as opaque JUCE realises it only needs to update that part of the image, which means it only needs to redraw your animated component. When there is a transform applied there could be some interpolation between edges of components so it will need to draw the background component in order to properly update the image.

What you probably want to do, is to not add the animated component as a child component, instead add it as a sibling component.

If you can use the latest version of JUCE we actually have a demo of this in the DemoRunner under GUI >> ComponentDiagnosticsDemo. Once you select the demo you will be able to compare a number of components, select Expensive (buffered to image) with child and Expensive (buffered to image) with sibling. I’ve included a video below.

To go from child to sibling components it might look something like this…

Before

class AnimatedComponent : public juce::Component,
                          private juce::Timer
{
public:
    AnimatedComponent()
    {
        startTimerHz (60);
    }

    ~AnimatedComponent()
    {
        stopTimer();
    }

    void paint (juce::Graphics& g) override
    {
        // implement something that animates here
    }

private:
   void timerCallback() override
   {
        repaint();
   }
};

class BackgroundComponent : public juce::Component
{
public:
    BackgroundComponent()
    {
        addAndMakeVisible (animatedComponent);
        setBufferedToImage (true);
    }

    void paint (juce::Graphics& g) override
    {
        // implement some expensive background drawing here
    }

    void resized() override
    {
        animatedComponent.setSize (/* set it's size to whatever here */);
    }

private:
    AnimatedComponent animatedComponent;
};

After

// Use the same AnimatedComponent class

class BackgroundComponent : public juce::Component
{
public:
    BackgroundComponent()
    {
        // don't need to add a child component any more
        setBufferedToImage (true);
    }

    void paint (juce::Graphics& g) override
    {
        // implement some expensive background drawing here
    }
};

// this component will not implement any drawing
class ContainerComponent : public juce::Component
{
public:
    ContainerComponent()
    {
        addAndMakeVisible (background);
        // note because the animatedComponent is after the background,
        // it will still be on top just as it would if it were a child of background
        addAndMakeVisible (animatedComponent);
    }

    void resized() override
    {
        background.setBounds (getLocalBounds());
        animatedComponent.setBounds (/* set it's size to whatever here */);
    }

private:
    BackgroundComponent background;
    AnimatedComponent animatedComponent;
}

In the example above ContainerComponent would be acting like your BackgroundComponent does now. In general I recommend that components either

  • Do drawing
  • Have children

That is, there wouldn’t normally be any components that do both. This way when you call setBufferedToImage (true) on a component you shouldn’t have to worry about any child components because there won’t be any!

Also if you are able to check out the latest version of JUCE we’ve spent some time improving drawing performance around these kinds of areas. Something of note, if you have any components you are marking as opaque, if they are not actually obscuring other components entirely, then I suggest not marking them opaque any more. In general only use opaque when you measure it to be a performance boost (I realise you did in your case but once you make the changes suggested above I doubt you’ll see a performance boost any more).

Hope that helps.

1 Like

Thank you very much for the quick response!
My animated component is a child of a background component, but I did not call setBufferedToImage (true), I only tried to set the opaque flag.
I use JUCE 8.0.12. I’m going to have a look at the demo and try your suggested solution!

OK then if you don’t want the background to keep redrawing then you probably do want to use setBufferedToImage (true), but only once you reorganise your components as suggested.

The proposed solution works fine, thank you so much!

1 Like