1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-10 23:44:24 +00:00

ImagePixelData: Update signatures of functions providing whole-image effects

This commit is contained in:
reuk 2025-01-14 19:57:04 +00:00
parent 8cb8c5e572
commit ada2c88b03
No known key found for this signature in database
12 changed files with 587 additions and 398 deletions

View file

@ -1,5 +1,33 @@
# JUCE breaking changes
# develop
## Change
The signatures of virtual functions ImagePixelData::applyGaussianBlurEffect()
and ImagePixelData::applySingleChannelBoxBlurEffect() have changed.
ImageEffects::applyGaussianBlurEffect() and
ImageEffects::applySingleChannelBoxBlurEffect() have been removed.
**Possible Issues**
User code overriding or calling these functions will fail to compile.
**Workaround**
The blur functions now operate within a specified area of the image. Update
overriding implementations accordingly. Instead of using the ImageEffects
static functions, call the corresponding ImagePixelData member functions
directly.
**Rationale**
The blur functions had a 'temporary storage' parameter which was not
particularly useful in practice, so this has been removed. Moving the
functionality of the ImageEffects static members directly into corresponding
member functions of ImagePixelData simplifies the public API.
# Version 8.0.5
## Change

View file

@ -48,10 +48,8 @@ void DropShadow::drawForImage (Graphics& g, const Image& srcImage) const
if (! srcImage.isValid())
return;
Image blurred;
ImageEffects::applySingleChannelBoxBlurEffect (radius,
srcImage.convertedToFormat (Image::SingleChannel),
blurred);
auto blurred = srcImage.convertedToFormat (Image::SingleChannel);
blurred.getPixelData()->applySingleChannelBoxBlurEffect (radius);
g.setColour (colour);
g.drawImageAt (blurred, offset.x, offset.y, true);
@ -76,11 +74,10 @@ void DropShadow::drawForPath (Graphics& g, const Path& path) const
(float) (offset.y - area.getY())));
}
Image blurred;
ImageEffects::applySingleChannelBoxBlurEffect (radius, pathImage, blurred);
pathImage.getPixelData()->applySingleChannelBoxBlurEffect (radius);
g.setColour (colour);
g.drawImageAt (blurred, area.getX(), area.getY(), true);
g.drawImageAt (pathImage, area.getX(), area.getY(), true);
}
}

View file

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

View file

@ -78,7 +78,6 @@ private:
float radius = 2.0f;
Colour colour { Colours::white };
Point<int> offset;
Image cachedImage;
JUCE_LEAK_DETECTOR (GlowEffect)
};

View file

@ -173,10 +173,10 @@ namespace BitmapDataDetail
};
};
static void convert (const Image::BitmapData& src, Image::BitmapData& dest)
static bool convert (const Image::BitmapData& src, Image::BitmapData& dest)
{
jassert (src.width == dest.width);
jassert (src.height == dest.height);
if (std::tuple (src.width, src.height) != std::tuple (dest.width, dest.height))
return false;
static constexpr auto converterFnTable = ConverterFnTable<RGB, ARGB, A>{};
@ -190,19 +190,76 @@ namespace BitmapDataDetail
if (auto* converter = converterFnTable.getConverterFor (src.pixelFormat, dest.pixelFormat))
converter (src, dest, dest.width, dest.height);
}
return true;
}
static Image convert (const Image::BitmapData& src, const ImageType& type)
{
Image result (type.create (src.pixelFormat, src.width, src.height, false));
{
Image::BitmapData dest (result, Image::BitmapData::writeOnly);
BitmapDataDetail::convert (src, dest);
}
Image::BitmapData (result, Image::BitmapData::writeOnly).convertFrom (src);
return result;
}
static void blurDataTriplets (uint8* d, int num, const int delta) noexcept
{
uint32 last = d[0];
d[0] = (uint8) ((d[0] + d[delta] + 1) / 3);
d += delta;
num -= 2;
do
{
const uint32 newLast = d[0];
d[0] = (uint8) ((last + d[0] + d[delta] + 1) / 3);
d += delta;
last = newLast;
}
while (--num > 0);
d[0] = (uint8) ((last + d[0] + 1) / 3);
}
static void blurSingleChannelImage (uint8* const data, const int w, const int h,
const int lineStride, const int repetitions) noexcept
{
jassert (w > 2 && h > 2);
for (int y = 0; y < h; ++y)
for (int i = repetitions; --i >= 0;)
blurDataTriplets (data + lineStride * y, w, 1);
for (int x = 0; x < w; ++x)
for (int i = repetitions; --i >= 0;)
blurDataTriplets (data + x, h, lineStride);
}
template <class PixelType>
struct PixelIterator
{
template <class PixelOperation>
static void iterate (const Image::BitmapData& data, const PixelOperation& pixelOp)
{
for (int y = 0; y < data.height; ++y)
for (int x = 0; x < data.width; ++x)
pixelOp (*reinterpret_cast<PixelType*> (data.getPixelPointer (x, y)));
}
};
template <class PixelOperation>
static void performPixelOp (const Image::BitmapData& data, const PixelOperation& pixelOp)
{
switch (data.pixelFormat)
{
case Image::ARGB: PixelIterator<PixelARGB> ::iterate (data, pixelOp); break;
case Image::RGB: PixelIterator<PixelRGB> ::iterate (data, pixelOp); break;
case Image::SingleChannel: PixelIterator<PixelAlpha>::iterate (data, pixelOp); break;
case Image::UnknownFormat:
default: jassertfalse; break;
}
}
}
class SubsectionPixelData : public ImagePixelData
@ -250,10 +307,35 @@ public:
std::unique_ptr<ImageType> createType() const override { return sourceImage->createType(); }
void applySingleChannelBoxBlurEffectInArea (Rectangle<int> b, int radius) override
{
sourceImage->applySingleChannelBoxBlurEffectInArea (getIntersection (b), radius);
}
void applyGaussianBlurEffectInArea (Rectangle<int> b, float radius) override
{
sourceImage->applyGaussianBlurEffectInArea (getIntersection (b), radius);
}
void multiplyAllAlphasInArea (Rectangle<int> b, float amount) override
{
sourceImage->multiplyAllAlphasInArea (getIntersection (b), amount);
}
void desaturateInArea (Rectangle<int> b) override
{
sourceImage->desaturateInArea (getIntersection (b));
}
/* as we always hold a reference to image, don't double count */
int getSharedCount() const noexcept override { return getReferenceCount() + sourceImage->getSharedCount() - 1; }
private:
Rectangle<int> getIntersection (Rectangle<int> b) const
{
return area.getIntersection (b + area.getTopLeft());
}
friend class Image;
const ImagePixelData::Ptr sourceImage;
const Rectangle<int> area;
@ -284,14 +366,40 @@ int ImagePixelData::getSharedCount() const noexcept
return getReferenceCount();
}
void ImagePixelData::applyGaussianBlurEffect ([[maybe_unused]] float radius, Image& result)
void ImagePixelData::applySingleChannelBoxBlurEffectInArea (Rectangle<int> bounds, int radius)
{
result = {};
if (pixelFormat == Image::SingleChannel)
{
const Image::BitmapData bm (Image { this }, bounds, Image::BitmapData::readWrite);
BitmapDataDetail::blurSingleChannelImage (bm.data, bm.width, bm.height, bm.lineStride, 2 * radius);
}
}
void ImagePixelData::applySingleChannelBoxBlurEffect ([[maybe_unused]] int radius, juce::Image &result)
void ImagePixelData::applyGaussianBlurEffectInArea (Rectangle<int> bounds, float radius)
{
result = {};
ImageConvolutionKernel blurKernel (roundToInt (radius * 2.0f));
blurKernel.createGaussianBlur (radius);
Image target { this };
blurKernel.applyToImage (target, Image { this }.createCopy(), bounds);
}
void ImagePixelData::multiplyAllAlphasInArea (Rectangle<int> b, float amount)
{
if (pixelFormat == Image::ARGB || pixelFormat == Image::SingleChannel)
{
const Image::BitmapData destData (Image { this }, b, Image::BitmapData::readWrite);
BitmapDataDetail::performPixelOp (destData, [&] (auto& p) { p.multiplyAlpha (amount); });
}
}
void ImagePixelData::desaturateInArea (Rectangle<int> b)
{
if (pixelFormat == Image::ARGB || pixelFormat == Image::RGB)
{
const Image::BitmapData destData (Image { this }, b, Image::BitmapData::readWrite);
BitmapDataDetail::performPixelOp (destData, [] (auto& p) { p.desaturate(); });
}
}
//==============================================================================
@ -634,6 +742,11 @@ void Image::BitmapData::setPixelColour (int x, int y, Colour colour) const noexc
}
}
bool Image::BitmapData::convertFrom (const BitmapData& source)
{
return BitmapDataDetail::convert (source, *this);
}
//==============================================================================
void Image::clear (const Rectangle<int>& area, Colour colourToClearTo)
{
@ -680,46 +793,16 @@ void Image::multiplyAlphaAt (int x, int y, float multiplier)
}
}
template <class PixelType>
struct PixelIterator
{
template <class PixelOperation>
static void iterate (const Image::BitmapData& data, const PixelOperation& pixelOp)
{
for (int y = 0; y < data.height; ++y)
for (int x = 0; x < data.width; ++x)
pixelOp (*reinterpret_cast<PixelType*> (data.getPixelPointer (x, y)));
}
};
template <class PixelOperation>
static void performPixelOp (const Image::BitmapData& data, const PixelOperation& pixelOp)
{
switch (data.pixelFormat)
{
case Image::ARGB: PixelIterator<PixelARGB> ::iterate (data, pixelOp); break;
case Image::RGB: PixelIterator<PixelRGB> ::iterate (data, pixelOp); break;
case Image::SingleChannel: PixelIterator<PixelAlpha>::iterate (data, pixelOp); break;
case Image::UnknownFormat:
default: jassertfalse; break;
}
}
void Image::multiplyAllAlphas (float amountToMultiplyBy)
{
jassert (hasAlphaChannel());
const BitmapData destData (*this, 0, 0, getWidth(), getHeight(), BitmapData::readWrite);
performPixelOp (destData, [&] (auto& p) { p.multiplyAlpha (amountToMultiplyBy); });
if (auto ptr = image)
ptr->multiplyAllAlphas (amountToMultiplyBy);
}
void Image::desaturate()
{
if (isARGB() || isRGB())
{
const BitmapData destData (*this, 0, 0, getWidth(), getHeight(), BitmapData::readWrite);
performPixelOp (destData, [] (auto& p) { p.desaturate(); });
}
if (auto ptr = image)
ptr->desaturate();
}
void Image::createSolidAreaMask (RectangleList<int>& result, float alphaThreshold) const
@ -842,111 +925,6 @@ void Image::moveImageSection (int dx, int dy,
}
}
void ImageEffects::applyGaussianBlurEffect (float radius, const Image& input, Image& result)
{
auto image = input.getPixelData();
if (image == nullptr)
{
result = {};
return;
}
auto copy = result;
image->applyGaussianBlurEffect (radius, copy);
if (copy.isValid())
{
result = std::move (copy);
return;
}
const auto tie = [] (const auto& x) { return std::tuple (x.getFormat(), x.getWidth(), x.getHeight()); };
if (tie (input) != tie (result))
result = Image { input.getFormat(), input.getWidth(), input.getHeight(), false };
ImageConvolutionKernel blurKernel (roundToInt (radius * 2.0f));
blurKernel.createGaussianBlur (radius);
blurKernel.applyToImage (result, input, result.getBounds());
}
static void blurDataTriplets (uint8* d, int num, const int delta) noexcept
{
uint32 last = d[0];
d[0] = (uint8) ((d[0] + d[delta] + 1) / 3);
d += delta;
num -= 2;
do
{
const uint32 newLast = d[0];
d[0] = (uint8) ((last + d[0] + d[delta] + 1) / 3);
d += delta;
last = newLast;
}
while (--num > 0);
d[0] = (uint8) ((last + d[0] + 1) / 3);
}
static void blurSingleChannelImage (uint8* const data, const int width, const int height,
const int lineStride, const int repetitions) noexcept
{
jassert (width > 2 && height > 2);
for (int y = 0; y < height; ++y)
for (int i = repetitions; --i >= 0;)
blurDataTriplets (data + lineStride * y, width, 1);
for (int x = 0; x < width; ++x)
for (int i = repetitions; --i >= 0;)
blurDataTriplets (data + x, height, lineStride);
}
static void blurSingleChannelImage (Image& image, int radius)
{
const Image::BitmapData bm (image, Image::BitmapData::readWrite);
blurSingleChannelImage (bm.data, bm.width, bm.height, bm.lineStride, 2 * radius);
}
void ImageEffects::applySingleChannelBoxBlurEffect (int radius, const Image& input, Image& result)
{
auto image = input.getPixelData();
if (image == nullptr)
{
result = {};
return;
}
auto copy = result;
image->applySingleChannelBoxBlurEffect (radius, copy);
if (copy.isValid())
{
result = std::move (copy);
return;
}
const auto inputConfig = std::tuple (Image::SingleChannel, input.getWidth(), input.getHeight());
const auto outputConfig = std::tuple (result.getFormat(), result.getWidth(), result.getHeight());
if (inputConfig != outputConfig)
result = Image { Image::SingleChannel, input.getWidth(), input.getHeight(), false };
{
Image::BitmapData source { input, Image::BitmapData::readOnly };
Image::BitmapData dest { result, Image::BitmapData::writeOnly };
BitmapDataDetail::convert (source, dest);
}
blurSingleChannelImage (result, radius);
}
//==============================================================================
#if JUCE_ALLOW_STATIC_NULL_VARIABLES

View file

@ -357,6 +357,15 @@ public:
/** Returns the size of the bitmap. */
Rectangle<int> getBounds() const noexcept { return Rectangle<int> (width, height); }
/** Attempts to copy the contents of src into this bitmap data.
Returns true on success, and false otherwise.
The source BitmapData must be readable, and the destination (current) BitmapData must
be writeable. This function cannot check for this precondition, so you must ensure this
yourself!
*/
bool convertFrom (const Image::BitmapData& src);
uint8* data; /**< The raw pixel data, packed according to the image's pixel format. */
size_t size; /**< The number of valid/allocated bytes after data. May be smaller than "lineStride * height" if this is a section of a larger image. */
PixelFormat pixelFormat; /**< The format of the data. */
@ -477,26 +486,58 @@ public:
This blur applies to all channels of the input image. It may be more expensive to
calculate than a box blur, but should produce higher-quality results.
Implementations should attempt to re-use the storage provided in the result out-parameter
when possible.
If native blurs are unsupported, or if creating a blur fails for any other reason,
the result out-parameter will be reset to an invalid image.
The default implementation will modify the image pixel-by-pixel on the CPU, which will be slow.
Native image types may provide optimised implementations.
*/
virtual void applyGaussianBlurEffect (float radius, Image& result);
virtual void applyGaussianBlurEffectInArea (Rectangle<int> bounds, float radius);
/** @see applyGaussianBlurEffectInArea() */
void applyGaussianBlurEffect (float radius)
{
applyGaussianBlurEffectInArea ({ width, height }, radius);
}
/** Applies a native blur effect to this image, if available.
This is intended for blurring single-channel images, which is useful when rendering drop
shadows. This is implemented as several box-blurs in series. The results should be visually
similar to a Gaussian blur, but less accurate.
Implementations should attempt to re-use the storage provided in the result out-parameter
when possible.
If native blurs are unsupported, or if creating a blur fails for any other reason,
the result out-parameter will be reset to an invalid image.
The default implementation will modify the image pixel-by-pixel on the CPU, which will be slow.
Native image types may provide optimised implementations.
*/
virtual void applySingleChannelBoxBlurEffect (int radius, Image& result);
virtual void applySingleChannelBoxBlurEffectInArea (Rectangle<int> bounds, int radius);
/** @see applySingleChannelBoxBlurEffectInArea() */
void applySingleChannelBoxBlurEffect (int radius)
{
applySingleChannelBoxBlurEffectInArea ({ width, height }, radius);
}
/** Multiples all alpha-channel values in the image by the specified amount.
The default implementation will modify the image pixel-by-pixel on the CPU, which will be slow.
Native image types may provide optimised implementations.
*/
virtual void multiplyAllAlphasInArea (Rectangle<int> bounds, float amount);
/** @see multiplyAllAlphasInArea() */
void multiplyAllAlphas (float amount)
{
multiplyAllAlphasInArea ({ width, height }, amount);
}
/** Changes all the colours to be shades of grey, based on their current luminosity.
The default implementation will modify the image pixel-by-pixel on the CPU, which will be slow.
Native image types may provide optimised implementations.
*/
virtual void desaturateInArea (Rectangle<int> bounds);
/** @see desaturateInArea() */
void desaturate()
{
desaturateInArea ({ width, height });
}
/** The pixel format of the image data. */
const Image::PixelFormat pixelFormat;
@ -588,43 +629,4 @@ public:
int getTypeID() const override;
};
//==============================================================================
/**
Utility functions for applying effects to images. These effects may or may not
be hardware-accelerated.
@tags{Graphics}
*/
struct ImageEffects
{
ImageEffects() = delete;
/** Applies a blur to this image, placing the blurred image in the result out-parameter.
This will attempt to call the applyGaussianBlurEffect() member of the input image's
underlying ImagePixelData, which will use hardware acceleration if available. If this
fails, then a software blur will be applied instead.
This blur applies to all channels of the input image. It may be more expensive to
calculate than a box blur, but should produce higher-quality results.
If result is already the correct size, then its storage will be reused directly.
Otherwise, new storage may be allocated for the blurred image.
*/
static void applyGaussianBlurEffect (float radius, const Image& input, Image& result);
/** Applies a blur to this image, placing the blurred image in the result out-parameter.
This will attempt to call the applySingleChannelBoxBlurEffect() member of the input image's
underlying ImagePixelData, which will use hardware acceleration if available. If this
fails, then a software blur will be applied instead.
This kind of blur is only capable of blurring single-channel images, which is useful when
rendering drop shadows. The blur is implemented as several box-blurs in series. The results
should be visually similar to a Gaussian blur, but less accurate.
If result is already the correct size, then its storage will be reused directly.
Otherwise, new storage may be allocated for the blurred image.
*/
static void applySingleChannelBoxBlurEffect (int radius, const Image& input, Image& result);
};
} // namespace juce

View file

@ -52,7 +52,7 @@
//==============================================================================
#if JUCE_MAC
#import <QuartzCore/QuartzCore.h>
#include <CoreImage/CIRenderDestination.h>
#include <CoreImage/CoreImage.h>
#include <CoreText/CTFont.h>
#elif JUCE_WINDOWS

View file

@ -60,8 +60,8 @@ public:
auto colourSpace = detail::ColorSpacePtr { CGColorSpaceCreateWithName ((format == Image::SingleChannel) ? kCGColorSpaceGenericGrayGamma2_2
: kCGColorSpaceSRGB) };
context = detail::ContextPtr { CGBitmapContextCreate (imageData->data, (size_t) width, (size_t) height, 8, (size_t) lineStride,
colourSpace.get(), getCGImageFlags (format)) };
context.reset (CGBitmapContextCreate (imageData->data, (size_t) width, (size_t) height, 8, (size_t) lineStride,
colourSpace.get(), getCGImageFlags (format)));
}
~CoreGraphicsPixelData() override
@ -101,6 +101,46 @@ public:
std::unique_ptr<ImageType> createType() const override { return std::make_unique<NativeImageType>(); }
void applyGaussianBlurEffectInArea (Rectangle<int> area, float radius) override
{
const auto buildFilter = [radius]
{
return [CIFilter filterWithName: @"CIGaussianBlur"
withInputParameters: @{ kCIInputRadiusKey: [NSNumber numberWithFloat: radius] }];
};
applyFilterInArea (area, buildFilter);
}
void applySingleChannelBoxBlurEffectInArea (Rectangle<int> area, int radius) override
{
const auto buildFilter = [radius]
{
return [CIFilter filterWithName: @"CIBoxBlur"
withInputParameters: @{ kCIInputRadiusKey: [NSNumber numberWithFloat: (float) radius] }];
};
applyFilterInArea (area, buildFilter);
}
void multiplyAllAlphasInArea (Rectangle<int> area, float amount) override
{
const auto buildFilter = [amount]
{
return [CIFilter filterWithName: @"CIColorMatrix"
withInputParameters: @{ @"inputAVector": [CIVector vectorWithX: 0 Y: 0 Z: 0 W: amount] }];
};
applyFilterInArea (area, buildFilter);
}
void desaturateInArea (Rectangle<int> area) override
{
const auto buildFilter = []
{
return [CIFilter filterWithName: @"CIColorControls"
withInputParameters: @{ kCIInputSaturationKey: [NSNumber numberWithFloat: 0] }];
};
applyFilterInArea (area, buildFilter);
}
//==============================================================================
static CGImageRef getCachedImageRef (const Image& juceImage, CGColorSpaceRef colourSpace)
{
@ -148,6 +188,7 @@ public:
//==============================================================================
detail::ContextPtr context;
detail::ImagePtr cachedImageRef;
NSUniquePtr<CIContext> ciContext;
struct ImageDataContainer final : public ReferenceCountedObject
{
@ -161,6 +202,49 @@ public:
int pixelStride, lineStride;
private:
template <typename BuildFilter>
bool applyFilterInArea (Rectangle<int> area, BuildFilter&& buildFilter)
{
// This function might be called on the OpenGL rendering thread, or some other background
// thread that doesn't necessarily have an autorelease pool in scope.
// Note that buildFilter is called within this pool, to ensure that the filter is released
// upon leaving the pool's scope.
JUCE_AUTORELEASEPOOL
{
auto* filter = buildFilter();
if (filter == nullptr || context == nullptr)
return false;
const ImagePtr content { CGBitmapContextCreateImage (context.get()) };
if (content == nullptr)
return false;
const auto cgArea = makeCGRect (area);
auto* ciImage = [[CIImage imageWithCGImage: content.get()] imageByCroppingToRect: cgArea];
if (ciImage == nullptr)
return false;
if (ciContext == nullptr)
ciContext.reset ([[CIContext contextWithCGContext: context.get() options: nullptr] retain]);
if (ciContext == nullptr)
return false;
[filter setValue: ciImage forKey: kCIInputImageKey];
auto* output = [filter outputImage];
if (output == nullptr)
return false;
CGContextClearRect (context.get(), cgArea);
[ciContext.get() drawImage: output inRect: cgArea fromRect: cgArea];
return true;
}
}
void freeCachedImageRef()
{
cachedImageRef.reset();

View file

@ -94,6 +94,11 @@ Direct2DImageContext::Direct2DImageContext (ComSmartPtr<ID2D1DeviceContext1> con
Direct2DImageContext::~Direct2DImageContext() = default;
ComSmartPtr<ID2D1DeviceContext1> Direct2DImageContext::getDeviceContext() const
{
return getPimpl()->getDeviceContext();
}
Direct2DGraphicsContext::Pimpl* Direct2DImageContext::getPimpl() const noexcept
{
return pimpl.get();

View file

@ -44,6 +44,8 @@ public:
~Direct2DImageContext() override;
ComSmartPtr<ID2D1DeviceContext1> getDeviceContext() const;
private:
struct ImagePimpl;
std::unique_ptr<ImagePimpl> pimpl;

View file

@ -155,6 +155,15 @@ static bool readFromDirect2DBitmap (ComSmartPtr<ID2D1DeviceContext1> context,
return {};
}
// Unimplemented, should never be called
void applyGaussianBlurEffectInArea (Rectangle<int>, float) override { jassertfalse; }
// Unimplemented, should never be called
void applySingleChannelBoxBlurEffectInArea (Rectangle<int>, int) override { jassertfalse; }
// Unimplemented, should never be called
void multiplyAllAlphasInArea (Rectangle<int>, float) override { jassertfalse; }
// Unimplemented, should never be called
void desaturateInArea (Rectangle<int>) override { jassertfalse; }
void initialiseBitmapData (Image::BitmapData& bd, int x, int y, Image::BitmapData::ReadWriteMode mode) override
{
if (mode != Image::BitmapData::readOnly)
@ -292,6 +301,44 @@ Direct2DPixelData::~Direct2DPixelData()
directX->adapters.removeListener (*this);
}
bool Direct2DPixelData::createPersistentBackup (ComSmartPtr<ID2D1Device1> deviceHint)
{
if (state == State::drawing)
{
// Creating a backup while the image is being modified would leave the backup in an invalid state
jassertfalse;
return false;
}
const auto iter = deviceHint != nullptr
? pagesForDevice.find (deviceHint)
: std::find_if (pagesForDevice.begin(),
pagesForDevice.end(),
[] (const auto& pair) { return pair.second.isUpToDate(); });
if (iter == pagesForDevice.end())
{
// There's no up-to-date image in graphics memory, so the graphics device probably got
// removed, dropping our image data. The image data is irrevocably lost!
jassertfalse;
return false;
}
auto& [device, pages] = *iter;
const auto context = Direct2DDeviceContext::create (device);
if (context == nullptr)
{
// Unable to create a device context to read the image data
jassertfalse;
return false;
}
const auto result = readFromDirect2DBitmap (context, pages.getPages().front().bitmap, backingData);
state = State::drawn;
return result;
}
auto Direct2DPixelData::getIteratorForDevice (ComSmartPtr<ID2D1Device1> device)
{
if (device == nullptr)
@ -341,6 +388,77 @@ auto Direct2DPixelData::getIteratorForDevice (ComSmartPtr<ID2D1Device1> device)
return pair.first;
}
struct Direct2DPixelData::Context : public Direct2DImageContext
{
Context (Ptr selfIn,
ComSmartPtr<ID2D1DeviceContext1> context,
ComSmartPtr<ID2D1Bitmap1> target)
: Direct2DImageContext (context, target, D2DUtilities::rectFromSize (target->GetPixelSize())),
self (selfIn),
frameStarted (startFrame (1.0f))
{
if (frameStarted)
self->state = State::drawing;
}
~Context() override
{
if (! frameStarted)
return;
endFrame();
self->createPersistentBackup (D2DUtilities::getDeviceForContext (getDeviceContext()));
}
Ptr self;
bool frameStarted = false;
};
auto Direct2DPixelData::createNativeContext() -> std::unique_ptr<Context>
{
if (state == State::drawing)
return nullptr;
sendDataChangeMessage();
const auto adapter = directX->adapters.getDefaultAdapter();
if (adapter == nullptr)
return nullptr;
const auto device = adapter->direct2DDevice;
if (device == nullptr)
return nullptr;
const auto context = Direct2DDeviceContext::create (device);
if (context == nullptr)
return nullptr;
const auto maxSize = (int) context->GetMaximumBitmapSize();
if (maxSize < width || maxSize < height)
return nullptr;
const auto iter = getIteratorForDevice (device);
jassert (iter != pagesForDevice.end());
const auto pages = iter->second.getPages();
if (pages.empty() || pages.front().bitmap == nullptr)
return nullptr;
// Every page *other than the page we're about to render onto* will need to be updated from the
// software image before it is next read.
for (auto i = pagesForDevice.begin(); i != pagesForDevice.end(); ++i)
if (i != iter)
i->second.markOutdated();
return std::make_unique<Context> (this, context, pages.front().bitmap);
}
std::unique_ptr<LowLevelGraphicsContext> Direct2DPixelData::createLowLevelContext()
{
if (state == State::drawing)
@ -390,86 +508,17 @@ std::unique_ptr<LowLevelGraphicsContext> Direct2DPixelData::createLowLevelContex
return std::make_unique<InertContext>();
}
sendDataChangeMessage();
if (auto ptr = createNativeContext())
return ptr;
const auto invalidateAllAndReturnSoftwareContext = [this]
{
// If this is hit, something has gone wrong when trying to create a Direct2D renderer,
// and we're about to fall back to a software renderer instead.
jassertfalse;
// If this is hit, something has gone wrong when trying to create a Direct2D renderer,
// and we're about to fall back to a software renderer instead.
jassertfalse;
for (auto& pair : pagesForDevice)
pair.second.markOutdated();
for (auto& pair : pagesForDevice)
pair.second.markOutdated();
return backingData->createLowLevelContext();
};
const auto adapter = directX->adapters.getDefaultAdapter();
if (adapter == nullptr)
return invalidateAllAndReturnSoftwareContext();
const auto device = adapter->direct2DDevice;
if (device == nullptr)
return invalidateAllAndReturnSoftwareContext();
const auto context = Direct2DDeviceContext::create (device);
if (context == nullptr)
return invalidateAllAndReturnSoftwareContext();
const auto maxSize = (int) context->GetMaximumBitmapSize();
if (maxSize < width || maxSize < height)
return invalidateAllAndReturnSoftwareContext();
const auto iter = getIteratorForDevice (device);
jassert (iter != pagesForDevice.end());
const auto pages = iter->second.getPages();
if (pages.empty() || pages.front().bitmap == nullptr)
return invalidateAllAndReturnSoftwareContext();
// Every page *other than the page we're about to render onto* will need to be updated from the
// software image before it is next read.
for (auto i = pagesForDevice.begin(); i != pagesForDevice.end(); ++i)
if (i != iter)
i->second.markOutdated();
struct FlushingContext : public Direct2DImageContext
{
FlushingContext (Ptr selfIn,
ComSmartPtr<ID2D1DeviceContext1> context,
ComSmartPtr<ID2D1Bitmap1> target)
: Direct2DImageContext (context, target, D2DUtilities::rectFromSize (target->GetPixelSize())),
storedContext (context),
storedTarget (target),
self (selfIn),
backup (startFrame (1.0f) ? selfIn->backingData : nullptr)
{
if (backup != nullptr)
self->state = State::drawing;
}
~FlushingContext() override
{
if (backup == nullptr)
return;
endFrame();
readFromDirect2DBitmap (storedContext, storedTarget, backup);
self->state = State::drawn;
}
ComSmartPtr<ID2D1DeviceContext1> storedContext;
ComSmartPtr<ID2D1Bitmap1> storedTarget;
Ptr self;
ImagePixelData::Ptr backup;
};
return std::make_unique<FlushingContext> (this, context, pages.front().bitmap);
return backingData->createLowLevelContext();
}
void Direct2DPixelData::initialiseBitmapData (Image::BitmapData& bitmap,
@ -513,138 +562,151 @@ void Direct2DPixelData::initialiseBitmapData (Image::BitmapData& bitmap,
bitmap.dataReleaser = std::make_unique<Releaser> (std::move (bitmap.dataReleaser), this);
}
void Direct2DPixelData::applyGaussianBlurEffect (float radius, Image& result)
template <typename Fn>
bool Direct2DPixelData::applyEffectInArea (Rectangle<int> area, Fn&& configureEffect)
{
// The result must be a separate image!
jassert (result.getPixelData().get() != this);
const auto internalGraphicsContext = createNativeContext();
const auto adapter = directX->adapters.getDefaultAdapter();
if (adapter == nullptr)
if (internalGraphicsContext == nullptr)
{
result = {};
return;
// Something when wrong while trying to create a device context with this image as a target
jassertfalse;
return false;
}
const auto device = adapter->direct2DDevice;
const auto context = internalGraphicsContext->getDeviceContext();
if (device == nullptr)
return;
if (context == nullptr)
return false;
const auto context = Direct2DDeviceContext::create (device);
const auto maxSize = (int) context->GetMaximumBitmapSize();
ComSmartPtr<ID2D1Image> target;
context->GetTarget (target.resetAndGetPointerAddress());
if (context == nullptr || maxSize < width || maxSize < height)
{
result = {};
return;
}
if (target == nullptr)
return false;
ComSmartPtr<ID2D1Effect> effect;
if (const auto hr = context->CreateEffect (CLSID_D2D1GaussianBlur, effect.resetAndGetPointerAddress());
FAILED (hr) || effect == nullptr)
{
result = {};
return;
}
const auto size = D2D1::SizeU ((UINT32) area.getWidth(), (UINT32) area.getHeight());
effect->SetInput (0, getFirstPageForDevice (device));
effect->SetValue (D2D1_GAUSSIANBLUR_PROP_STANDARD_DEVIATION, radius / 3.0f);
ComSmartPtr<ID2D1Bitmap> copy;
context->CreateBitmap (size,
D2D1::BitmapProperties (context->GetPixelFormat()),
copy.resetAndGetPointerAddress());
const auto outputPixelData = Direct2DBitmap::createBitmap (context,
Image::ARGB,
D2D1::SizeU ((UINT32) width, (UINT32) height),
D2D1_BITMAP_OPTIONS_TARGET);
if (copy == nullptr)
return false;
context->SetTarget (outputPixelData);
context->BeginDraw();
context->Clear();
context->DrawImage (effect);
context->EndDraw();
const auto rect = D2DUtilities::toRECT_U (area);
copy->CopyFromRenderTarget (nullptr, context, &rect);
result = Image { new Direct2DPixelData { device, outputPixelData } };
const auto effect = configureEffect (context, copy);
if (effect == nullptr)
return false;
const auto destPoint = D2D1::Point2F ((float) area.getX(), (float) area.getY());
context->PushAxisAlignedClip (D2DUtilities::toRECT_F (area), D2D1_ANTIALIAS_MODE_ALIASED);
context->DrawImage (effect,
&destPoint,
nullptr,
D2D1_INTERPOLATION_MODE_NEAREST_NEIGHBOR,
D2D1_COMPOSITE_MODE_SOURCE_COPY);
context->PopAxisAlignedClip();
return true;
}
void Direct2DPixelData::applySingleChannelBoxBlurEffect (int radius, Image& result)
void Direct2DPixelData::applyGaussianBlurEffectInArea (Rectangle<int> b, float radius)
{
// The result must be a separate image!
jassert (result.getPixelData().get() != this);
const auto adapter = directX->adapters.getDefaultAdapter();
if (adapter == nullptr)
applyEffectInArea (b, [&] (auto dc, auto input) -> ComSmartPtr<ID2D1Effect>
{
result = {};
return;
}
const auto device = adapter->direct2DDevice;
if (device == nullptr)
return;
const auto context = Direct2DDeviceContext::create (device);
const auto maxSize = (int) context->GetMaximumBitmapSize();
if (context == nullptr || maxSize < width || maxSize < height)
{
result = {};
return;
}
constexpr FLOAT kernel[] { 1.0f / 9.0f, 2.0f / 9.0f, 3.0f / 9.0f, 2.0f / 9.0f, 1.0f / 9.0f };
ComSmartPtr<ID2D1Effect> begin, end;
for (auto horizontal : { false, true })
{
for (auto i = 0; i < radius; ++i)
ComSmartPtr<ID2D1Effect> effect;
if (const auto hr = dc->CreateEffect (CLSID_D2D1GaussianBlur, effect.resetAndGetPointerAddress());
FAILED (hr) || effect == nullptr)
{
ComSmartPtr<ID2D1Effect> effect;
if (const auto hr = context->CreateEffect (CLSID_D2D1ConvolveMatrix, effect.resetAndGetPointerAddress());
FAILED (hr) || effect == nullptr)
{
result = {};
return;
}
return nullptr;
}
effect->SetValue (D2D1_CONVOLVEMATRIX_PROP_KERNEL_SIZE_X, (UINT32) (horizontal ? std::size (kernel) : 1));
effect->SetValue (D2D1_CONVOLVEMATRIX_PROP_KERNEL_SIZE_Y, (UINT32) (horizontal ? 1 : std::size (kernel)));
effect->SetValue (D2D1_CONVOLVEMATRIX_PROP_KERNEL_MATRIX, kernel);
effect->SetInput (0, input);
effect->SetValue (D2D1_GAUSSIANBLUR_PROP_STANDARD_DEVIATION, radius / 3.0f);
return effect;
});
}
if (begin == nullptr)
void Direct2DPixelData::applySingleChannelBoxBlurEffectInArea (Rectangle<int> b, int radius)
{
applyEffectInArea (b, [&] (auto dc, auto input) -> ComSmartPtr<ID2D1Effect>
{
constexpr FLOAT kernel[] { 1.0f / 9.0f, 2.0f / 9.0f, 3.0f / 9.0f, 2.0f / 9.0f, 1.0f / 9.0f };
ComSmartPtr<ID2D1Effect> begin, end;
for (auto horizontal : { false, true })
{
for (auto i = 0; i < roundToInt (radius); ++i)
{
begin = effect;
end = effect;
}
else
{
effect->SetInputEffect (0, end);
end = effect;
ComSmartPtr<ID2D1Effect> effect;
if (const auto hr = dc->CreateEffect (CLSID_D2D1ConvolveMatrix, effect.resetAndGetPointerAddress());
FAILED (hr) || effect == nullptr)
{
// Unable to create effect!
jassertfalse;
return nullptr;
}
effect->SetValue (D2D1_CONVOLVEMATRIX_PROP_KERNEL_SIZE_X, (UINT32) (horizontal ? std::size (kernel) : 1));
effect->SetValue (D2D1_CONVOLVEMATRIX_PROP_KERNEL_SIZE_Y, (UINT32) (horizontal ? 1 : std::size (kernel)));
effect->SetValue (D2D1_CONVOLVEMATRIX_PROP_KERNEL_MATRIX, kernel);
if (begin == nullptr)
{
begin = effect;
end = effect;
}
else
{
effect->SetInputEffect (0, end);
end = effect;
}
}
}
}
if (begin == nullptr)
begin->SetInput (0, input);
return end;
});
}
void Direct2DPixelData::multiplyAllAlphasInArea (Rectangle<int> b, float value)
{
applyEffectInArea (b, [&] (auto dc, auto input) -> ComSmartPtr<ID2D1Effect>
{
result = {};
return;
}
ComSmartPtr<ID2D1Effect> effect;
if (const auto hr = dc->CreateEffect (CLSID_D2D1Opacity, effect.resetAndGetPointerAddress());
FAILED (hr) || effect == nullptr)
{
return nullptr;
}
begin->SetInput (0, getFirstPageForDevice (device));
effect->SetInput (0, input);
effect->SetValue (D2D1_OPACITY_PROP_OPACITY, value);
return effect;
});
}
const auto outputPixelData = Direct2DBitmap::createBitmap (context,
Image::ARGB,
D2D1::SizeU ((UINT32) width, (UINT32) height),
D2D1_BITMAP_OPTIONS_TARGET);
void Direct2DPixelData::desaturateInArea (Rectangle<int> b)
{
applyEffectInArea (b, [&] (auto dc, auto input) -> ComSmartPtr<ID2D1Effect>
{
ComSmartPtr<ID2D1Effect> effect;
if (const auto hr = dc->CreateEffect (CLSID_D2D1Saturation, effect.resetAndGetPointerAddress());
FAILED (hr) || effect == nullptr)
{
return nullptr;
}
context->SetTarget (outputPixelData);
context->BeginDraw();
context->Clear();
context->DrawImage (end);
context->EndDraw();
result = Image { new Direct2DPixelData { device, outputPixelData } };
effect->SetInput (0, input);
effect->SetValue (D2D1_SATURATION_PROP_SATURATION, 0.0f);
return effect;
});
}
auto Direct2DPixelData::getPagesForDevice (ComSmartPtr<ID2D1Device1> device) -> Span<const Page>

View file

@ -96,6 +96,11 @@ public:
upToDate = false;
}
bool isUpToDate() const
{
return upToDate;
}
private:
ImagePixelData::Ptr backingData;
std::vector<Direct2DPixelDataPage> pages;
@ -170,8 +175,10 @@ public:
*/
void initialiseBitmapData (Image::BitmapData&, int, int, Image::BitmapData::ReadWriteMode) override;
void applyGaussianBlurEffect (float radius, Image& result) override;
void applySingleChannelBoxBlurEffect (int radius, Image& result) override;
void applyGaussianBlurEffectInArea (Rectangle<int>, float) override;
void applySingleChannelBoxBlurEffectInArea (Rectangle<int>, int) override;
void multiplyAllAlphasInArea (Rectangle<int>, float) override;
void desaturateInArea (Rectangle<int>) override;
/* This returns image data that is suitable for use when drawing with the provided context.
This image data should be treated as a read-only view - making modifications directly
@ -201,6 +208,30 @@ private:
Direct2DPixelData (ImagePixelData::Ptr, State);
auto getIteratorForDevice (ComSmartPtr<ID2D1Device1>);
/* Attempts to copy the content of the corresponding texture in graphics storage into
persistent software storage.
The argument specifies the device holding the texture that should be backed up.
Passing null will instead search through all devices to find which device has the most
recent copy of the image data.
In most cases it is unnecessary to call this function directly.
Returns true on success, i.e. the backup is already up-to-date or the backup was updated
successfully.
Returns false on failure. The backup process may fail if the graphics storage became
unavailable for some reason, such as an external GPU being disconnected, or a remote desktop
session ending. If this happens, the image content is *irrevocably lost* and will need to
be recreated.
*/
bool createPersistentBackup (ComSmartPtr<ID2D1Device1> deviceHint);
struct Context;
std::unique_ptr<Context> createNativeContext();
template <typename Fn>
bool applyEffectInArea (Rectangle<int>, Fn&&);
void adapterCreated (DxgiAdapter::Ptr) override {}
void adapterRemoved (DxgiAdapter::Ptr adapter) override
{