From c37c18c5b42430f0ba13cdf1dc9ce52e883948ee Mon Sep 17 00:00:00 2001 From: Anthony Nicholls Date: Thu, 30 Oct 2025 16:21:06 +0000 Subject: [PATCH] macOS: Prevent visual glitches in images CGImages created from a juce Image may be drawn after or during changes to the underlying data. This change copies the required data into a new buffer to ensure the CGImage data is always independent from the juce image data. --- .../native/juce_CoreGraphicsContext_mac.mm | 183 ++++++++++-------- 1 file changed, 98 insertions(+), 85 deletions(-) diff --git a/modules/juce_graphics/native/juce_CoreGraphicsContext_mac.mm b/modules/juce_graphics/native/juce_CoreGraphicsContext_mac.mm index ef5c31f67b..48ab9f8199 100644 --- a/modules/juce_graphics/native/juce_CoreGraphicsContext_mac.mm +++ b/modules/juce_graphics/native/juce_CoreGraphicsContext_mac.mm @@ -45,25 +45,35 @@ public: using Ptr = ReferenceCountedObjectPtr; CoreGraphicsPixelData (const Image::PixelFormat format, int w, int h, bool clearImage) - : ImagePixelData (format, w, h) + : ImagePixelData (format, w, h), + pixelStride (format == Image::RGB ? 3 : ((format == Image::ARGB) ? 4 : 1)), + lineStride ((pixelStride * jmax (1, width) + 3) & ~3), + // SDK version 10.14+ intermittently requires a bit of extra space + // at the end of the image data so we add an extra line stride. This + // feels like something has gone wrong in Apple's code. + data ((size_t) lineStride * (size_t) (jmax (1, height) + 1), clearImage) { - pixelStride = format == Image::RGB ? 3 : ((format == Image::ARGB) ? 4 : 1); - lineStride = (pixelStride * jmax (1, width) + 3) & ~3; + const auto colourSpaceName = (format == Image::SingleChannel) ? kCGColorSpaceGenericGrayGamma2_2 + : kCGColorSpaceSRGB; - auto numComponents = (size_t) lineStride * (size_t) jmax (1, height); + const detail::ColorSpacePtr colourSpace { CGColorSpaceCreateWithName (colourSpaceName) }; - // SDK version 10.14+ intermittently requires a bit of extra space - // at the end of the image data. This feels like something has gone - // wrong in Apple's code. - numComponents += (size_t) lineStride; + context.reset (CGBitmapContextCreate (data.getData(), + (size_t) width, + (size_t) height, + 8, + (size_t) lineStride, + colourSpace.get(), + getCGImageFlags (format))); + } - imageData->data.allocate (numComponents, clearImage); - - auto colourSpace = detail::ColorSpacePtr { CGColorSpaceCreateWithName ((format == Image::SingleChannel) ? kCGColorSpaceGenericGrayGamma2_2 - : kCGColorSpaceSRGB) }; - - context.reset (CGBitmapContextCreate (imageData->data, (size_t) width, (size_t) height, 8, (size_t) lineStride, - colourSpace.get(), getCGImageFlags (format))); + CoreGraphicsPixelData (const CoreGraphicsPixelData& other) + : CoreGraphicsPixelData (other.pixelFormat, other.width, other.height, false) + // Don't try to recreate the CIContext here. It's expensive to do so, + // therefore it's best to leave it null and recreate it lazily. + { + jassert (data.getSize() == other.data.getSize()); + data.copyFrom (other.data.getData(), 0, other.data.getSize()); } ~CoreGraphicsPixelData() override @@ -78,10 +88,13 @@ public: return std::make_unique (context.get(), height); } - void initialiseBitmapData (Image::BitmapData& bitmap, int x, int y, Image::BitmapData::ReadWriteMode mode) override + void initialiseBitmapData (Image::BitmapData& bitmap, + int x, + int y, + Image::BitmapData::ReadWriteMode mode) override { const auto offset = (size_t) (x * pixelStride + y * lineStride); - bitmap.data = imageData->data + offset; + bitmap.data = (uint8*) data.getData() + offset; bitmap.size = (size_t) (lineStride * height) - offset; bitmap.pixelFormat = pixelFormat; bitmap.lineStride = lineStride; @@ -96,12 +109,14 @@ public: ImagePixelData::Ptr clone() override { - auto im = new CoreGraphicsPixelData (pixelFormat, width, height, false); - memcpy (im->imageData->data, imageData->data, (size_t) (lineStride * height)); + auto im = new CoreGraphicsPixelData (*this); return *im; } - std::unique_ptr createType() const override { return std::make_unique(); } + std::unique_ptr createType() const override + { + return std::make_unique(); + } void applyGaussianBlurEffectInArea (Rectangle area, float radius) override { @@ -114,7 +129,8 @@ public: } //============================================================================== - static CFUniquePtr getCachedImageRef (const Image& juceImage, CGColorSpaceRef colourSpace) + static CFUniquePtr getCachedImageRef (const Image& juceImage, + CGColorSpaceRef colourSpace) { auto cgim = std::invoke ([&]() -> CFUniquePtr { @@ -129,62 +145,36 @@ public: const Image::BitmapData srcData (juceImage, Image::BitmapData::readOnly); - const auto usableSize = jmin ((size_t) srcData.lineStride * (size_t) srcData.height, srcData.size); - CFUniquePtr data (CFDataCreate (nullptr, (const UInt8*) srcData.data, (CFIndex) usableSize)); - detail::DataProviderPtr provider { CGDataProviderCreateWithCFData (data.get()) }; - - return CFUniquePtr { CGImageCreate ((size_t) srcData.width, - (size_t) srcData.height, - 8, - (size_t) srcData.pixelStride * 8, - (size_t) srcData.lineStride, - colourSpace, - getCGImageFlags (juceImage.getFormat()), - provider.get(), - nullptr, - true, - kCGRenderingIntentDefault) }; + return createCGImage (srcData.data, + srcData.size, + (size_t) srcData.width, + (size_t) srcData.height, + (size_t) srcData.pixelStride, + (size_t) srcData.lineStride, + colourSpace, + getCGImageFlags (juceImage.getFormat())); } //============================================================================== - detail::ContextPtr context; - detail::ImagePtr cachedImageRef; - NSUniquePtr ciContext; - struct ImageDataContainer final : public ReferenceCountedObject - { - ImageDataContainer() = default; - - using Ptr = ReferenceCountedObjectPtr; - HeapBlock data; - }; CFUniquePtr getCGImage (CGColorSpaceRef colourSpace) { - if (cachedImageRef != nullptr) - return CFUniquePtr { CGImageRetain (cachedImageRef.get()) }; - - const Image::BitmapData srcData { Image { this }, Image::BitmapData::readOnly }; - - detail::DataProviderPtr provider { CGDataProviderCreateWithData (new ImageDataContainer::Ptr (imageData), - srcData.data, - srcData.size, - [] (void * __nullable info, const void*, size_t) { delete (ImageDataContainer::Ptr*) info; }) }; - - cachedImageRef.reset (CGImageCreate ((size_t) srcData.width, - (size_t) srcData.height, - 8, - (size_t) srcData.pixelStride * 8, - (size_t) srcData.lineStride, - colourSpace, getCGImageFlags (pixelFormat), provider.get(), - nullptr, true, kCGRenderingIntentDefault)); + if (cachedImageRef == nullptr) + { + cachedImageRef = createCGImage (data.getData(), + data.getSize(), + (size_t) width, + (size_t) height, + (size_t) pixelStride, + (size_t) lineStride, + colourSpace, + CGBitmapContextGetBitmapInfo (context.get())); + } return CFUniquePtr { CGImageRetain (cachedImageRef.get()) }; } - ImageDataContainer::Ptr imageData = new ImageDataContainer(); - int pixelStride, lineStride; - NativeExtensions getNativeExtensions() override { struct Wrapped @@ -213,6 +203,31 @@ public: } private: + static CFUniquePtr createCGImage (void* data, + size_t size, + size_t imageWidth, + size_t imageHeight, + size_t imagePixelStride, + size_t imageLineStride, + CGColorSpaceRef imageColourSpace, + CGBitmapInfo bitmapInfo) + { + CFUniquePtr cfData (CFDataCreate (nullptr, (const UInt8*) data, (CFIndex) size)); + detail::DataProviderPtr provider { CGDataProviderCreateWithCFData (cfData.get()) }; + + return CFUniquePtr { CGImageCreate (imageWidth, + imageHeight, + 8, + imagePixelStride * 8, + imageLineStride, + imageColourSpace, + bitmapInfo, + provider.get(), + nullptr, + true, + kCGRenderingIntentDefault) }; + } + template bool applyFilterInArea (Rectangle area, BuildFilter&& buildFilter) { @@ -263,14 +278,21 @@ private: static CGBitmapInfo getCGImageFlags (const Image::PixelFormat& format) { - #if JUCE_BIG_ENDIAN - return format == Image::ARGB ? ((uint32_t) kCGImageAlphaPremultipliedFirst | (uint32_t) kCGBitmapByteOrder32Big) : kCGBitmapByteOrderDefault; - #else - return format == Image::ARGB ? ((uint32_t) kCGImageAlphaPremultipliedFirst | (uint32_t) kCGBitmapByteOrder32Little) : kCGBitmapByteOrderDefault; - #endif + if (format != Image::ARGB) + return kCGImageByteOrderDefault; + + return (uint32_t) kCGImageAlphaPremultipliedFirst + | (uint32_t) kCGBitmapByteOrder32Host; } - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CoreGraphicsPixelData) + int pixelStride; + int lineStride; + detail::ContextPtr context; + MemoryBlock data; + detail::ImagePtr cachedImageRef; + NSUniquePtr ciContext; + + JUCE_LEAK_DETECTOR (CoreGraphicsPixelData) }; ImagePixelData::Ptr NativeImageType::create (Image::PixelFormat format, int width, int height, bool clearImage) const @@ -1108,7 +1130,10 @@ Image juce_loadWithCoreImage (InputStream& input) auto provider = detail::DataProviderPtr { CGDataProviderCreateWithData (new MemoryBlockHolder::Ptr (memBlockHolder), memBlockHolder->block.getData(), memBlockHolder->block.getSize(), - [] (void * __nullable info, const void*, size_t) { delete (MemoryBlockHolder::Ptr*) info; }) }; + [] (void * __nullable info, const void*, size_t) + { + delete (MemoryBlockHolder::Ptr*) info; + }) }; if (auto imageSource = CFUniquePtr (CGImageSourceCreateWithDataProvider (provider.get(), nullptr))) { @@ -1160,18 +1185,6 @@ Image juce_loadWithCoreImage (InputStream& input) } #endif -Image juce_createImageFromCIImage (CIImage*, int, int); -Image juce_createImageFromCIImage (CIImage* im, int w, int h) -{ - auto cgImage = new CoreGraphicsPixelData (Image::ARGB, w, h, false); - - CIContext* cic = [CIContext contextWithCGContext: cgImage->context.get() options: nil]; - [cic drawImage: im inRect: CGRectMake (0, 0, w, h) fromRect: CGRectMake (0, 0, w, h)]; - CGContextFlush (cgImage->context.get()); - - return Image (*cgImage); -} - CGImageRef juce_createCoreGraphicsImage (const Image& juceImage, CGColorSpaceRef colourSpace) { return CoreGraphicsPixelData::getCachedImageRef (juceImage, colourSpace).release();