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:

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