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;
}