Working with Image Objects

TL;DR

Turn off backup mode for procedurally generated Image objects for a big performance boost.

Turning off backup mode for images only affects Direct2D images.

juce::Image temporaryImage{ juce::Image::ARGB, 100, 100, false }; 
temporaryImage.setBackupEnabled(false);

{
   juce::Graphics g{ temporaryImage }; 
   g.fillAll(juce::Colours::hotpink); 
} 

JUCE Image Storage

A JUCE Image is a container for a two-dimensional bitmap of pixels. Internally, an Image stores a pointer to an ImagePixelData object, which represents either a pure software image or some platform-specific image format, such as a CoreGraphics image.

Until recently, the actual pixel data was stored in CPU RAM. This meant that accessing the pixel data was easy, but the CPU had to perform any rasterization (i.e., filling or painting the bitmap).

With Direct2D, pixel data can be stored in GPU VRAM. The performance benefits of keeping your image data in VRAM are significant; however, there are a few limitations:

  • GPU textures have a maximum size that depends on the specific GPU. This means that a very large Image cannot be kept as a single Direct2D bitmap.
  • GPU textures can be lost without warning when the GPU resets (for example, when changing your screen DPI scaling)
  • Reading a GPU texture back to CPU RAM can be slow
  • Modifying the Image pixels may cause the data to be backed up to CPU RAM, incurring a performance hit.

The JUCE renderer solves the first issue with a virtual texturing system. A single Image object may actually hold multiple Direct2D bitmaps internally, displayed as adjacent tiles so that they appear as one large contiguous bitmap. This allows bitmaps of effectively unlimited size.

The renderer handles the second issue by keeping a software backup in CPU RAM of the Image data. If the GPU resets, the bitmap in VRAM is automatically restored from the software backup. The downside of this approach is that the creating the backup can take a significant amount of time, depending on the size of the image.

Many of the performance issues reported on the forum are directly related to the performance penalty of the software backup. Fortunately, this is easily resolved using the ImagePixelDataBackupExtensions class, which gives you control over if and when an Image is backed up to CPU RAM.

At the moment, only Direct2D images make use of ImagePixelDataBackupExtensions; other image types are not affected.

Image Painting Performance

Typically, you are painting onto a window or a JUCE Image.

Let’s refer to the window or the Image being painted as the target. A window is always a painting target. An Image can be a painting target, a painting source, or both.

In other words, you can paint an Image onto another Image, or you can paint an Image onto a window.

With Direct2D, using an Image as a source is very fast. However, painting to an Image target can be slow if you are painting to a Direct2D Image and software backup is enabled.

Here’s the golden rule:

Any Image that is frequently repainted as a target should be set to “no backup” mode.

Example

Does your paint handler look something like this?

juce::Image proceduralImage{ juce::Image::ARGB, 100, 100, true };
float animatedSize = 50.0f;

void paint(Graphics& g) override
{
    g.fillAll(juce::Colours::black);

    proceduralImage.clear();

    {
        juce::Graphics ig{ proceduralImage };
        ig.setColour(juce::Colours::orange);

        juce::Rectangle<float> r{ animatedSize, animatedSize };
        ig.fillEllipse(r.withCentre(getLocalBounds().getCentre().toFloat()));
    }

    g.drawImageAt(proceduralImage, 0, 0);
}

In that case, the target image (proceduralImage) should be set to “no backup” mode. Note the helper method that creates the image and disables backup mode.

static juce::Image createTransientImage(int width, int height)
{
    juce::Image transientImage{ juce::Image::ARGB, 100, 100, true };
    transientImage.setBackupEnabled(false);
    return transientImage;
}

auto proceduralImage = createTransientImage(100, 100);

Checking for Lost Image Data

Of course, if you disable the backup, there’s no backup! In many cases, this is fine since you’re just going to be repainting the Image anyhow.

If you do need to check if the GPU bitmap data has been lost and needs to be repainted, you can use the Image backup extensions.

if (auto ptr = image.getPixelData())
   if (auto* extensions = ptr->getBackupExtensions())   
      if (extensions->needsBackup() && ! extensions->canBackup())

The syntax is somewhat verbose, so I recommend using a helper function:

bool imageNeedsRepaint(const Image& image) 
{
  if (auto ptr = image.getPixelData())
    if (auto* ext = ptr->getBackupExtensions())
      return ext->needsBackup() && ! ext->canBackup();
  return false;
}
10 Likes

Thanks for your work. It makes a huge difference and solves many performance problems on Windows.

As a developer who uses macOS as his main development system, I can only guess what the result will be on Windows and add those flags.

If I understand correctly, we also don’t know the moment when Direct2D flushes the image. It can happen randomly at any time and is not predictable.
I know we have this helper method to find out if the image is still there, but I already fear some subtle, hard-to-reproduce graphic bugs that happen randomly.

I wonder if it would be possible to generalize this behaviour and force a flush of the GPU image after drawing it on macOS and Windows directly in the framework code, when setBufferedImage(false) is set. To guarantee a fixed, deterministic behavior that is the same on all platforms?

Hi @kunz-

You’re referring to when GPU image data is unexpectedly lost and needs to be restored? Yes, that can happen unpredictably.

It doesn’t happen often. Here’s a few circumstances where it might happen:

  • Changing Windows display DPI settings
  • Enabling or disabling a secondary graphics adapter
  • Connecting or disconnecting an external graphics adapter (Thunderbolt, etc)
  • Graphics card reset hotkey (Windows+Ctrl+Shift+B)
  • Simulated GPU reset using DXCap: Handling device removal

In practice, I don’t think it’s that big a deal. The image backup system built into JUCE will automatically restore any lost images. If you disable image backup for performance reasons, you are likely repainting that image frequently anyhow so losing the previous image data is unimportant.

Matt

Hello @matt

Thanks for clarifying. Then this happens only on user-triggered actions. One more question: Does it keep the image when I move the plugin to a second monitor?

It could also happen if there’s a hardware fault, driver problem, high levels of sunspot activity, UFOs, having a bad hair day, or for any other arbitrary reason.

That’s a good question, actually. I think Windows virtualizes the images between adapters, but that needs to be tested.

Matt

Got you. We have to check if the image is still there when using this flag, when the animation is not continuous.