1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-09 23:34:20 +00:00

Image: Add new backup-extensions interface to support images with no main-memory backup

This commit is contained in:
reuk 2025-01-14 19:59:18 +00:00
parent 2fbb72d960
commit 55e5e2082c
No known key found for this signature in database
7 changed files with 265 additions and 30 deletions

View file

@ -49,6 +49,8 @@ void DropShadow::drawForImage (Graphics& g, const Image& srcImage) const
return;
auto blurred = srcImage.convertedToFormat (Image::SingleChannel);
blurred.setBackupEnabled (false);
blurred.getPixelData()->applySingleChannelBoxBlurEffect (radius);
g.setColour (colour);
@ -66,6 +68,7 @@ void DropShadow::drawForPath (Graphics& g, const Path& path) const
if (area.getWidth() > 2 && area.getHeight() > 2)
{
Image pathImage { Image::SingleChannel, area.getWidth(), area.getHeight(), true };
pathImage.setBackupEnabled (false);
{
Graphics g2 (pathImage);

View file

@ -48,7 +48,10 @@ void GlowEffect::setGlowProperties (float newRadius, Colour newColour, Point<int
void GlowEffect::applyEffect (Image& image, Graphics& g, float scaleFactor, float alpha)
{
auto blurred = image.createCopy();
blurred.getPixelData()->applyGaussianBlurEffect (radius * scaleFactor);
blurred.setBackupEnabled (false);
if (auto ptr = blurred.getPixelData())
ptr->applyGaussianBlurEffect (radius * scaleFactor);
g.setColour (colour.withMultipliedAlpha (alpha));
g.drawImageAt (blurred, offset.x, offset.y, true);

View file

@ -747,6 +747,20 @@ bool Image::BitmapData::convertFrom (const BitmapData& source)
return BitmapDataDetail::convert (source, *this);
}
bool Image::setBackupEnabled (bool enabled)
{
if (auto ptr = image)
{
if (auto* ext = ptr->getBackupExtensions())
{
ext->setBackupEnabled (enabled);
return true;
}
}
return false;
}
//==============================================================================
void Image::clear (const Rectangle<int>& area, Colour colourToClearTo)
{

View file

@ -298,6 +298,15 @@ public:
*/
void desaturate();
/** This is a shorthand for dereferencing the internal ImagePixelData's BackupExtensions
and calling setBackupEnabled() if the extensions exist.
@returns true if the extensions exist and the backup flag was updated, or false otherwise
@see ImagePixelDataBackupExtensions::setBackupEnabled()
*/
bool setBackupEnabled (bool);
//==============================================================================
/** Retrieves a section of an image as raw pixel data, so it can be read or written to.
@ -446,6 +455,105 @@ private:
JUCE_LEAK_DETECTOR (Image)
};
//==============================================================================
/**
The methods on this interface allow clients of ImagePixelData to query and control
the automatic-backup process from graphics memory to main memory, if this mechanism is
relevant and supported.
Some image types (Direct2D, OpenGL) are backed by textures that live in graphics memory.
Such textures are quick to display, but they will be lost if the graphics device goes away.
Normally, a backup of the texture will be kept in main memory, so that the image can still
be used even if any graphics device goes away. While this has the benefit that programs are
automatically resilient to graphics devices going away, it also incurs some performance
overhead, because the texture content must be copied back to main memory after each
modification.
For performance-sensitive applications it can be beneficial to disable the automatic sync
behaviour, and to sync manually instead, which can be achieved using the methods of this type.
The following table shows how to interpret the results of this type's member functions.
needsBackup() | canBackup() | meaning
--------------------------------------------------------------------------------------------
true | true | the main-memory copy of the image is outdated, but there's an
| | up-to-date copy in graphics memory
| |
true | false | although main memory is out-of-date, the most recent copy of
| | the image in graphics memory has been lost
| |
false | true | main memory is up-to-date with graphics memory; graphics
| | memory is still available;
| |
false | false | main memory has an up-to-date copy of the image, but the most
| | recent copy of the image in graphics memory has been lost
@tags{Graphics}
*/
class ImagePixelDataBackupExtensions
{
public:
/** The automatic image backup mechanism can be disabled by passing false to this function, or
enabled by passing true.
If you disable automatic backup for a particular image, make sure you test that your
software behaves as expected when graphics devices are disconnected. One easy way to test
this on Windows is to use your program over a remote desktop session, and to end and
re-start the session while the image is being displayed.
The most common scenario where this flag is useful is when drawing single-use images.
e.g. for a drop shadow or other effect, the following series of steps might be carried
out on each paint call:
- Create a path that matches the shadowed component's outline
- Draw the path into a temporary image
- Blur the temporary image
- Draw the temporary image into some other context
- (destroy the temporary image)
In this case, where the image is created, modified, used, and destroyed in quick succession,
there's no need to keep a resilient backup of the image around, so it's reasonable to call
setBackupEnabled(false) after constructing the image.
@see isBackupEnabled(), backupNow()
*/
virtual void setBackupEnabled (bool) = 0;
/** @see setBackupEnabled(), backupNow() */
virtual bool isBackupEnabled() const = 0;
/** This function will attempt to make the image resilient to graphics device disconnection by
copying from graphics memory to main memory.
By default, backups happen automatically, so there's no need to call this function unless
auto-backup has been disabled on this image.
Flushing may fail if the graphics device goes away before its memory can be read.
If needsBackup() returns false, then backupNow() will always return true without doing any
work.
@returns true if the main-memory copy of the image is up-to-date, or false otherwise.
@see setBackupEnabled(), isBackupEnabled()
*/
virtual bool backupNow() = 0;
/** Returns true if the main-memory copy of the image is out-of-date, false if it's up-to-date.
@see canBackup()
*/
virtual bool needsBackup() const = 0;
/** Returns if there is an up-to-date copy of this image in graphics memory, or false otherwise.
@see needsBackup()
*/
virtual bool canBackup() const = 0;
protected:
// Not intended for virtual destruction
~ImagePixelDataBackupExtensions() = default;
};
//==============================================================================
/**
@ -463,6 +571,8 @@ private:
class JUCE_API ImagePixelData : public ReferenceCountedObject
{
public:
using BackupExtensions = ImagePixelDataBackupExtensions;
ImagePixelData (Image::PixelFormat, int width, int height);
~ImagePixelData() override;
@ -474,6 +584,13 @@ public:
virtual Ptr clone() = 0;
/** Creates an instance of the type of this image. */
virtual std::unique_ptr<ImageType> createType() const = 0;
/** Returns a raw pointer to an instance of ImagePixelDataBackupExtensions if this ImagePixelData
provides this extension, or nullptr otherwise.
*/
virtual BackupExtensions* getBackupExtensions() { return nullptr; }
virtual const BackupExtensions* getBackupExtensions() const { return nullptr; }
/** Initialises a BitmapData object. */
virtual void initialiseBitmapData (Image::BitmapData&, int x, int y, Image::BitmapData::ReadWriteMode) = 0;
/** Returns the number of Image objects which are currently referring to the same internal

View file

@ -199,9 +199,9 @@ static bool readFromDirect2DBitmap (ComSmartPtr<ID2D1DeviceContext1> context,
target->pixelFormat,
(int) size.width,
(int) size.height } };
Image::BitmapData srcBitmap { srcImage, Image::BitmapData::readOnly };
Image::BitmapData dstBitmap { Image { target }, Image::BitmapData::writeOnly };
BitmapDataDetail::convert (srcBitmap, dstBitmap);
Image::BitmapData dstData { Image { target }, Image::BitmapData::writeOnly };
dstData.convertFrom ({ srcImage, Image::BitmapData::readOnly });
return true;
}
@ -225,9 +225,12 @@ static ImagePixelData::Ptr readFromDirect2DBitmap (ComSmartPtr<ID2D1DeviceContex
return result;
}
Direct2DPixelDataPages::Direct2DPixelDataPages (ComSmartPtr<ID2D1Bitmap1> bitmap,
//==============================================================================
Direct2DPixelDataPages::Direct2DPixelDataPages (ImagePixelDataBackupExtensions* parent,
ComSmartPtr<ID2D1Bitmap1> bitmap,
ImagePixelData::Ptr image)
: backingData (image),
: parentBackupExtensions (parent),
backingData (image),
pages { Page { bitmap, {} } },
upToDate (true)
{
@ -235,10 +238,12 @@ Direct2DPixelDataPages::Direct2DPixelDataPages (ComSmartPtr<ID2D1Bitmap1> bitmap
jassert (image->createType()->getTypeID() == SoftwareImageType{}.getTypeID());
}
Direct2DPixelDataPages::Direct2DPixelDataPages (ComSmartPtr<ID2D1Device1> device,
Direct2DPixelDataPages::Direct2DPixelDataPages (ImagePixelDataBackupExtensions* parent,
ComSmartPtr<ID2D1Device1> device,
ImagePixelData::Ptr image,
State initialState)
: backingData (image),
: parentBackupExtensions (parent),
backingData (image),
pages (makePages (device, backingData, initialState == State::cleared)),
upToDate (initialState != State::unsuitableToRead)
{
@ -246,11 +251,24 @@ Direct2DPixelDataPages::Direct2DPixelDataPages (ComSmartPtr<ID2D1Device1> device
jassert (image->createType()->getTypeID() == SoftwareImageType{}.getTypeID());
}
auto Direct2DPixelDataPages::getPagesWithoutSync() const -> Span<const Page>
{
// Accessing page data which is out-of-date!
jassert (upToDate);
return pages;
}
auto Direct2DPixelDataPages::getPages() -> Span<const Page>
{
if (std::exchange (upToDate, true))
const ScopeGuard scope { [this] { upToDate = true; } };
if (upToDate)
return pages;
// We need to make sure that the parent image is up-to-date, otherwise we'll end up
// fetching outdated image data.
parentBackupExtensions->backupNow();
auto sourceToUse = backingData->pixelFormat == Image::RGB
? Image { backingData }.convertedToFormat (Image::ARGB)
: Image { backingData };
@ -287,7 +305,7 @@ Direct2DPixelData::Direct2DPixelData (ComSmartPtr<ID2D1Device1> device,
ComSmartPtr<ID2D1Bitmap1> page)
: Direct2DPixelData (readFromDirect2DBitmap (Direct2DDeviceContext::create (device), page), State::drawn)
{
pagesForDevice.emplace (device, Direct2DPixelDataPages { page, backingData });
pagesForDevice.emplace (device, Direct2DPixelDataPages { this, page, backingData });
}
Direct2DPixelData::Direct2DPixelData (Image::PixelFormat formatToUse, int w, int h, bool clearIn)
@ -310,6 +328,10 @@ bool Direct2DPixelData::createPersistentBackup (ComSmartPtr<ID2D1Device1> device
return false;
}
// If the backup is not outdated, then it must be up-to-date
if (state != State::outdated)
return true;
const auto iter = deviceHint != nullptr
? pagesForDevice.find (deviceHint)
: std::find_if (pagesForDevice.begin(),
@ -334,13 +356,15 @@ bool Direct2DPixelData::createPersistentBackup (ComSmartPtr<ID2D1Device1> device
return false;
}
const auto result = readFromDirect2DBitmap (context, pages.getPages().front().bitmap, backingData);
state = State::drawn;
const auto result = readFromDirect2DBitmap (context, pages.getPagesWithoutSync().front().bitmap, backingData);
state = result ? State::drawn : State::outdated;
return result;
}
auto Direct2DPixelData::getIteratorForDevice (ComSmartPtr<ID2D1Device1> device)
{
mostRecentDevice = device;
if (device == nullptr)
return pagesForDevice.end();
@ -377,6 +401,11 @@ auto Direct2DPixelData::getIteratorForDevice (ComSmartPtr<ID2D1Device1> device)
case State::drawing:
jassertfalse;
return Pages::State::unsuitableToRead;
// If this is hit, the pages will need to be synced through main memory before they are
// suitable for reading.
case State::outdated:
return Pages::State::unsuitableToRead;
}
// Unhandled switch case?
@ -384,7 +413,7 @@ auto Direct2DPixelData::getIteratorForDevice (ComSmartPtr<ID2D1Device1> device)
return Pages::State::unsuitableToRead;
}();
const auto pair = pagesForDevice.emplace (device, Pages { device, backingData, initialState });
const auto pair = pagesForDevice.emplace (device, Pages { this, device, backingData, initialState });
return pair.first;
}
@ -408,7 +437,10 @@ struct Direct2DPixelData::Context : public Direct2DImageContext
endFrame();
self->createPersistentBackup (D2DUtilities::getDeviceForContext (getDeviceContext()));
self->state = State::outdated;
if (self->sync)
self->createPersistentBackup (D2DUtilities::getDeviceForContext (getDeviceContext()));
}
Ptr self;
@ -422,12 +454,18 @@ auto Direct2DPixelData::createNativeContext() -> std::unique_ptr<Context>
sendDataChangeMessage();
const auto adapter = directX->adapters.getDefaultAdapter();
const auto device = std::invoke ([this]() -> ComSmartPtr<ID2D1Device1>
{
if (mostRecentDevice != nullptr)
return mostRecentDevice;
if (adapter == nullptr)
return nullptr;
const auto adapter = directX->adapters.getDefaultAdapter();
const auto device = adapter->direct2DDevice;
if (adapter == nullptr)
return nullptr;
return adapter->direct2DDevice;
});
if (device == nullptr)
return nullptr;
@ -526,11 +564,17 @@ void Direct2DPixelData::initialiseBitmapData (Image::BitmapData& bitmap,
int y,
Image::BitmapData::ReadWriteMode mode)
{
// If this is hit, there's already another BitmapData or Graphics context active on this
// image. Only one BitmapData or Graphics context may be active on an Image at a time.
jassert (state != State::drawing);
// If we're about to read from the image, and the main-memory copy of the image is outdated,
// then we must force a backup so that we can return up-to-date data
if (mode != Image::BitmapData::writeOnly && state == State::outdated)
createPersistentBackup (nullptr);
backingData->initialiseBitmapData (bitmap, x, y, mode);
// If we're only reading, then we can assume that the bitmap data was flushed to the software
// image directly after it was last modified by d2d, so we can just use the BitmapData
// initialised by the backing data.
// If we're writing, then we'll need to update our textures next time we try to use them, so
// mark them as outdated.
if (mode == Image::BitmapData::readOnly)
@ -538,12 +582,9 @@ void Direct2DPixelData::initialiseBitmapData (Image::BitmapData& bitmap,
struct Releaser : public Image::BitmapData::BitmapDataReleaser
{
Releaser (std::unique_ptr<BitmapDataReleaser> wrappedIn, Direct2DPixelData::Ptr selfIn)
Releaser (std::unique_ptr<BitmapDataReleaser> wrappedIn, Ptr selfIn)
: wrapped (std::move (wrappedIn)), self (std::move (selfIn))
{
// If this is hit, there's already another BitmapData or Graphics context active on this
// image. Only one BitmapData or Graphics context may be active on an Image at a time.
jassert (self->state != State::drawing);
self->state = State::drawing;
}
@ -556,7 +597,7 @@ void Direct2DPixelData::initialiseBitmapData (Image::BitmapData& bitmap,
}
std::unique_ptr<BitmapDataReleaser> wrapped;
Direct2DPixelData::Ptr self;
Ptr self;
};
bitmap.dataReleaser = std::make_unique<Releaser> (std::move (bitmap.dataReleaser), this);
@ -714,6 +755,34 @@ auto Direct2DPixelData::getPagesForDevice (ComSmartPtr<ID2D1Device1> device) ->
return getIteratorForDevice (device)->second.getPages();
}
void Direct2DPixelData::setBackupEnabled (bool x)
{
sync = x;
}
bool Direct2DPixelData::isBackupEnabled() const
{
return sync;
}
bool Direct2DPixelData::backupNow()
{
return createPersistentBackup (nullptr);
}
bool Direct2DPixelData::needsBackup() const
{
return state == State::outdated;
}
bool Direct2DPixelData::canBackup() const
{
return std::any_of (pagesForDevice.begin(), pagesForDevice.end(), [] (const auto& pair)
{
return pair.second.isUpToDate();
});
}
//==============================================================================
ImagePixelData::Ptr NativeImageType::create (Image::PixelFormat format, int width, int height, bool clearImage) const
{

View file

@ -69,7 +69,7 @@ public:
/* Creates a single page containing the provided bitmap and main-memory storage, marking the
hardware data as up-to-date.
*/
Direct2DPixelDataPages (ComSmartPtr<ID2D1Bitmap1>, ImagePixelData::Ptr);
Direct2DPixelDataPages (ImagePixelDataBackupExtensions*, ComSmartPtr<ID2D1Bitmap1>, ImagePixelData::Ptr);
/* Allocates hardware storage for the provided software bitmap.
Depending on the initial state, will:
@ -77,7 +77,7 @@ public:
- mark the GPU images as up-to-date, or
- clear the GPU images, then mark them as up-to-date
*/
Direct2DPixelDataPages (ComSmartPtr<ID2D1Device1>, ImagePixelData::Ptr, State);
Direct2DPixelDataPages (ImagePixelDataBackupExtensions*, ComSmartPtr<ID2D1Device1>, ImagePixelData::Ptr, State);
/* Returns all pages included in this set.
This will be called before reading from the pages (e.g. when drawing them).
@ -86,6 +86,9 @@ public:
*/
Span<const Page> getPages();
/** Returns all pages without first syncing from main memory. */
Span<const Page> getPagesWithoutSync() const;
/* Marks this set as needing to be updated from the software image.
We don't actually do the copy until the next time that we need to read the hardware pages.
This is to avoid redundant copies in the common case that pages are only drawn on a single
@ -102,6 +105,7 @@ public:
}
private:
ImagePixelDataBackupExtensions* parentBackupExtensions = nullptr;
ImagePixelData::Ptr backingData;
std::vector<Direct2DPixelDataPage> pages;
bool upToDate = false;
@ -126,7 +130,8 @@ private:
render target are marked as outdated.
*/
class Direct2DPixelData : public ImagePixelData,
private DxgiAdapterListener
private DxgiAdapterListener,
private ImagePixelDataBackupExtensions
{
public:
using Ptr = ReferenceCountedObjectPtr<Direct2DPixelData>;
@ -196,6 +201,9 @@ public:
return ! pages.empty() ? pages.front().bitmap : nullptr;
}
BackupExtensions* getBackupExtensions() override { return this; }
const BackupExtensions* getBackupExtensions() const override { return this; }
private:
enum class State
{
@ -203,6 +211,7 @@ private:
initiallyCleared,
drawing,
drawn,
outdated,
};
Direct2DPixelData (ImagePixelData::Ptr, State);
@ -232,17 +241,28 @@ private:
template <typename Fn>
bool applyEffectInArea (Rectangle<int>, Fn&&);
void setBackupEnabled (bool) override;
bool isBackupEnabled() const override;
bool backupNow() override;
bool needsBackup() const override;
bool canBackup() const override;
void adapterCreated (DxgiAdapter::Ptr) override {}
void adapterRemoved (DxgiAdapter::Ptr adapter) override
{
if (adapter != nullptr)
pagesForDevice.erase (adapter->direct2DDevice);
if (mostRecentDevice == adapter->direct2DDevice)
mostRecentDevice = nullptr;
}
SharedResourcePointer<DirectX> directX;
ImagePixelData::Ptr backingData;
ComSmartPtr<ID2D1Device1> mostRecentDevice;
std::map<ComSmartPtr<ID2D1Device1>, Pages> pagesForDevice;
State state;
bool sync = true;
JUCE_LEAK_DETECTOR (Direct2DPixelData)
};

View file

@ -55,10 +55,18 @@ struct StandardCachedComponentImage : public CachedComponentImage
jmax (1, imageBounds.getWidth()),
jmax (1, imageBounds.getHeight()),
! owner.isOpaque());
image.setBackupEnabled (false);
validArea.clear();
}
// If the cached image is outdated but cannot be backed-up, this indicates that the graphics
// device holding the most recent copy of the cached image has gone away. Therefore, we've
// effectively lost the contents of the cache, and we must repaint the entire component.
if (auto ptr = image.getPixelData())
if (auto* extensions = ptr->getBackupExtensions())
if (extensions->needsBackup() && ! extensions->canBackup())
validArea.clear();
if (! validArea.containsRectangle (compBounds))
{
Graphics imG (image);
@ -81,6 +89,7 @@ struct StandardCachedComponentImage : public CachedComponentImage
validArea = compBounds;
// TODO(reuk) test dragging/sharing between multiple graphics devices
g.setColour (Colours::black.withAlpha (owner.getAlpha()));
g.drawImageTransformed (image, AffineTransform::scale ((float) compBounds.getWidth() / (float) imageBounds.getWidth(),
(float) compBounds.getHeight() / (float) imageBounds.getHeight()), false);