From ada2c88b033a0077089c560194e2b3a776f6ca14 Mon Sep 17 00:00:00 2001 From: reuk Date: Tue, 14 Jan 2025 19:57:04 +0000 Subject: [PATCH] ImagePixelData: Update signatures of functions providing whole-image effects --- BREAKING_CHANGES.md | 28 ++ .../effects/juce_DropShadowEffect.cpp | 11 +- .../juce_graphics/effects/juce_GlowEffect.cpp | 5 +- .../juce_graphics/effects/juce_GlowEffect.h | 1 - modules/juce_graphics/images/juce_Image.cpp | 280 ++++++------ modules/juce_graphics/images/juce_Image.h | 104 ++--- modules/juce_graphics/juce_graphics.cpp | 2 +- .../native/juce_CoreGraphicsContext_mac.mm | 88 +++- .../juce_Direct2DImageContext_windows.cpp | 5 + .../juce_Direct2DImageContext_windows.h | 2 + .../native/juce_Direct2DImage_windows.cpp | 424 ++++++++++-------- .../native/juce_Direct2DImage_windows.h | 35 +- 12 files changed, 587 insertions(+), 398 deletions(-) diff --git a/BREAKING_CHANGES.md b/BREAKING_CHANGES.md index 327f5c31a3..576c115bc7 100644 --- a/BREAKING_CHANGES.md +++ b/BREAKING_CHANGES.md @@ -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 diff --git a/modules/juce_graphics/effects/juce_DropShadowEffect.cpp b/modules/juce_graphics/effects/juce_DropShadowEffect.cpp index 73a1b31b5c..165e45d944 100644 --- a/modules/juce_graphics/effects/juce_DropShadowEffect.cpp +++ b/modules/juce_graphics/effects/juce_DropShadowEffect.cpp @@ -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); } } diff --git a/modules/juce_graphics/effects/juce_GlowEffect.cpp b/modules/juce_graphics/effects/juce_GlowEffect.cpp index 347241c66c..7b263db655 100644 --- a/modules/juce_graphics/effects/juce_GlowEffect.cpp +++ b/modules/juce_graphics/effects/juce_GlowEffect.cpp @@ -47,10 +47,11 @@ void GlowEffect::setGlowProperties (float newRadius, Colour newColour, PointapplyGaussianBlurEffect (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); diff --git a/modules/juce_graphics/effects/juce_GlowEffect.h b/modules/juce_graphics/effects/juce_GlowEffect.h index f2cd2e2a91..8dc67a387f 100644 --- a/modules/juce_graphics/effects/juce_GlowEffect.h +++ b/modules/juce_graphics/effects/juce_GlowEffect.h @@ -78,7 +78,6 @@ private: float radius = 2.0f; Colour colour { Colours::white }; Point offset; - Image cachedImage; JUCE_LEAK_DETECTOR (GlowEffect) }; diff --git a/modules/juce_graphics/images/juce_Image.cpp b/modules/juce_graphics/images/juce_Image.cpp index 6fecff1d16..895fdfa3c7 100644 --- a/modules/juce_graphics/images/juce_Image.cpp +++ b/modules/juce_graphics/images/juce_Image.cpp @@ -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{}; @@ -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 + struct PixelIterator + { + template + 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 (data.getPixelPointer (x, y))); + } + }; + + template + static void performPixelOp (const Image::BitmapData& data, const PixelOperation& pixelOp) + { + switch (data.pixelFormat) + { + case Image::ARGB: PixelIterator ::iterate (data, pixelOp); break; + case Image::RGB: PixelIterator ::iterate (data, pixelOp); break; + case Image::SingleChannel: PixelIterator::iterate (data, pixelOp); break; + case Image::UnknownFormat: + default: jassertfalse; break; + } + } } class SubsectionPixelData : public ImagePixelData @@ -250,10 +307,35 @@ public: std::unique_ptr createType() const override { return sourceImage->createType(); } + void applySingleChannelBoxBlurEffectInArea (Rectangle b, int radius) override + { + sourceImage->applySingleChannelBoxBlurEffectInArea (getIntersection (b), radius); + } + + void applyGaussianBlurEffectInArea (Rectangle b, float radius) override + { + sourceImage->applyGaussianBlurEffectInArea (getIntersection (b), radius); + } + + void multiplyAllAlphasInArea (Rectangle b, float amount) override + { + sourceImage->multiplyAllAlphasInArea (getIntersection (b), amount); + } + + void desaturateInArea (Rectangle 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 getIntersection (Rectangle b) const + { + return area.getIntersection (b + area.getTopLeft()); + } + friend class Image; const ImagePixelData::Ptr sourceImage; const Rectangle 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 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 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 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 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& area, Colour colourToClearTo) { @@ -680,46 +793,16 @@ void Image::multiplyAlphaAt (int x, int y, float multiplier) } } -template -struct PixelIterator -{ - template - 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 (data.getPixelPointer (x, y))); - } -}; - -template -static void performPixelOp (const Image::BitmapData& data, const PixelOperation& pixelOp) -{ - switch (data.pixelFormat) - { - case Image::ARGB: PixelIterator ::iterate (data, pixelOp); break; - case Image::RGB: PixelIterator ::iterate (data, pixelOp); break; - case Image::SingleChannel: PixelIterator::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& 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 diff --git a/modules/juce_graphics/images/juce_Image.h b/modules/juce_graphics/images/juce_Image.h index fe597a1c4e..7ef7d9b11f 100644 --- a/modules/juce_graphics/images/juce_Image.h +++ b/modules/juce_graphics/images/juce_Image.h @@ -357,6 +357,15 @@ public: /** Returns the size of the bitmap. */ Rectangle getBounds() const noexcept { return Rectangle (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 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 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 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 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 diff --git a/modules/juce_graphics/juce_graphics.cpp b/modules/juce_graphics/juce_graphics.cpp index df258afbbe..251c4332b7 100644 --- a/modules/juce_graphics/juce_graphics.cpp +++ b/modules/juce_graphics/juce_graphics.cpp @@ -52,7 +52,7 @@ //============================================================================== #if JUCE_MAC #import - #include + #include #include #elif JUCE_WINDOWS diff --git a/modules/juce_graphics/native/juce_CoreGraphicsContext_mac.mm b/modules/juce_graphics/native/juce_CoreGraphicsContext_mac.mm index 6c7022c349..0010ddbf5f 100644 --- a/modules/juce_graphics/native/juce_CoreGraphicsContext_mac.mm +++ b/modules/juce_graphics/native/juce_CoreGraphicsContext_mac.mm @@ -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 createType() const override { return std::make_unique(); } + void applyGaussianBlurEffectInArea (Rectangle area, float radius) override + { + const auto buildFilter = [radius] + { + return [CIFilter filterWithName: @"CIGaussianBlur" + withInputParameters: @{ kCIInputRadiusKey: [NSNumber numberWithFloat: radius] }]; + }; + applyFilterInArea (area, buildFilter); + } + + void applySingleChannelBoxBlurEffectInArea (Rectangle area, int radius) override + { + const auto buildFilter = [radius] + { + return [CIFilter filterWithName: @"CIBoxBlur" + withInputParameters: @{ kCIInputRadiusKey: [NSNumber numberWithFloat: (float) radius] }]; + }; + applyFilterInArea (area, buildFilter); + } + + void multiplyAllAlphasInArea (Rectangle 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 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; struct ImageDataContainer final : public ReferenceCountedObject { @@ -161,6 +202,49 @@ public: int pixelStride, lineStride; private: + template + bool applyFilterInArea (Rectangle 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(); diff --git a/modules/juce_graphics/native/juce_Direct2DImageContext_windows.cpp b/modules/juce_graphics/native/juce_Direct2DImageContext_windows.cpp index eafa0e43d7..84a265e189 100644 --- a/modules/juce_graphics/native/juce_Direct2DImageContext_windows.cpp +++ b/modules/juce_graphics/native/juce_Direct2DImageContext_windows.cpp @@ -94,6 +94,11 @@ Direct2DImageContext::Direct2DImageContext (ComSmartPtr con Direct2DImageContext::~Direct2DImageContext() = default; +ComSmartPtr Direct2DImageContext::getDeviceContext() const +{ + return getPimpl()->getDeviceContext(); +} + Direct2DGraphicsContext::Pimpl* Direct2DImageContext::getPimpl() const noexcept { return pimpl.get(); diff --git a/modules/juce_graphics/native/juce_Direct2DImageContext_windows.h b/modules/juce_graphics/native/juce_Direct2DImageContext_windows.h index 085aaf0b7e..8d24fb0774 100644 --- a/modules/juce_graphics/native/juce_Direct2DImageContext_windows.h +++ b/modules/juce_graphics/native/juce_Direct2DImageContext_windows.h @@ -44,6 +44,8 @@ public: ~Direct2DImageContext() override; + ComSmartPtr getDeviceContext() const; + private: struct ImagePimpl; std::unique_ptr pimpl; diff --git a/modules/juce_graphics/native/juce_Direct2DImage_windows.cpp b/modules/juce_graphics/native/juce_Direct2DImage_windows.cpp index a0fcf12cc9..ed62746f46 100644 --- a/modules/juce_graphics/native/juce_Direct2DImage_windows.cpp +++ b/modules/juce_graphics/native/juce_Direct2DImage_windows.cpp @@ -155,6 +155,15 @@ static bool readFromDirect2DBitmap (ComSmartPtr context, return {}; } + // Unimplemented, should never be called + void applyGaussianBlurEffectInArea (Rectangle, float) override { jassertfalse; } + // Unimplemented, should never be called + void applySingleChannelBoxBlurEffectInArea (Rectangle, int) override { jassertfalse; } + // Unimplemented, should never be called + void multiplyAllAlphasInArea (Rectangle, float) override { jassertfalse; } + // Unimplemented, should never be called + void desaturateInArea (Rectangle) 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 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 device) { if (device == nullptr) @@ -341,6 +388,77 @@ auto Direct2DPixelData::getIteratorForDevice (ComSmartPtr device) return pair.first; } +struct Direct2DPixelData::Context : public Direct2DImageContext +{ + Context (Ptr selfIn, + ComSmartPtr context, + ComSmartPtr 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 +{ + 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 (this, context, pages.front().bitmap); +} + std::unique_ptr Direct2DPixelData::createLowLevelContext() { if (state == State::drawing) @@ -390,86 +508,17 @@ std::unique_ptr Direct2DPixelData::createLowLevelContex return std::make_unique(); } - 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 context, - ComSmartPtr 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 storedContext; - ComSmartPtr storedTarget; - Ptr self; - ImagePixelData::Ptr backup; - }; - - return std::make_unique (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 (std::move (bitmap.dataReleaser), this); } -void Direct2DPixelData::applyGaussianBlurEffect (float radius, Image& result) +template +bool Direct2DPixelData::applyEffectInArea (Rectangle 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 target; + context->GetTarget (target.resetAndGetPointerAddress()); - if (context == nullptr || maxSize < width || maxSize < height) - { - result = {}; - return; - } + if (target == nullptr) + return false; - ComSmartPtr 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 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 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 { - 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 begin, end; - - for (auto horizontal : { false, true }) - { - for (auto i = 0; i < radius; ++i) + ComSmartPtr effect; + if (const auto hr = dc->CreateEffect (CLSID_D2D1GaussianBlur, effect.resetAndGetPointerAddress()); + FAILED (hr) || effect == nullptr) { - ComSmartPtr 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 b, int radius) +{ + applyEffectInArea (b, [&] (auto dc, auto input) -> ComSmartPtr + { + constexpr FLOAT kernel[] { 1.0f / 9.0f, 2.0f / 9.0f, 3.0f / 9.0f, 2.0f / 9.0f, 1.0f / 9.0f }; + + ComSmartPtr 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 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 b, float value) +{ + applyEffectInArea (b, [&] (auto dc, auto input) -> ComSmartPtr { - result = {}; - return; - } + ComSmartPtr 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 b) +{ + applyEffectInArea (b, [&] (auto dc, auto input) -> ComSmartPtr + { + ComSmartPtr 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 device) -> Span diff --git a/modules/juce_graphics/native/juce_Direct2DImage_windows.h b/modules/juce_graphics/native/juce_Direct2DImage_windows.h index fa70036adb..a088be2ccf 100644 --- a/modules/juce_graphics/native/juce_Direct2DImage_windows.h +++ b/modules/juce_graphics/native/juce_Direct2DImage_windows.h @@ -96,6 +96,11 @@ public: upToDate = false; } + bool isUpToDate() const + { + return upToDate; + } + private: ImagePixelData::Ptr backingData; std::vector 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, float) override; + void applySingleChannelBoxBlurEffectInArea (Rectangle, int) override; + void multiplyAllAlphasInArea (Rectangle, float) override; + void desaturateInArea (Rectangle) 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); + /* 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 deviceHint); + + struct Context; + std::unique_ptr createNativeContext(); + + template + bool applyEffectInArea (Rectangle, Fn&&); + void adapterCreated (DxgiAdapter::Ptr) override {} void adapterRemoved (DxgiAdapter::Ptr adapter) override {