/* ============================================================================== This file is part of the JUCE framework. Copyright (c) Raw Material Software Limited JUCE is an open source framework subject to commercial or open source licensing. By downloading, installing, or using the JUCE framework, or combining the JUCE framework with any other source code, object code, content or any other copyrightable work, you agree to the terms of the JUCE End User Licence Agreement, and all incorporated terms including the JUCE Privacy Policy and the JUCE Website Terms of Service, as applicable, which will bind you. If you do not agree to the terms of these agreements, we will not license the JUCE framework to you, and you must discontinue the installation or download process and cease use of the JUCE framework. JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/ JUCE Privacy Policy: https://juce.com/juce-privacy-policy JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/ Or: You may also use this code under the terms of the AGPLv3: https://www.gnu.org/licenses/agpl-3.0.en.html THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. ============================================================================== */ namespace juce { class NativeReadOnlyDataReleaser : public Image::BitmapData::BitmapDataReleaser { public: NativeReadOnlyDataReleaser (Image::PixelFormat pixelFormat, int lineStride, int w, int h, ComSmartPtr deviceContextIn, ComSmartPtr sourceBitmap, Point offset) : bitmap (Direct2DBitmap::createBitmap (deviceContextIn, pixelFormat, { (UINT32) w, (UINT32) h }, lineStride, D2D1_BITMAP_OPTIONS_CPU_READ | D2D1_BITMAP_OPTIONS_CANNOT_DRAW)) { const D2D1_POINT_2U destPoint { 0, 0 }; const Rectangle fullRect { w, h }; const auto sourceRect = D2DUtilities::toRECT_U (fullRect.getIntersection (fullRect.withPosition (offset))); if (auto hr = bitmap->CopyFromBitmap (&destPoint, sourceBitmap, &sourceRect); FAILED (hr)) return; D2D1_MAPPED_RECT mappedRect{}; bitmap->Map (D2D1_MAP_OPTIONS_READ, &mappedRect); data = mappedRect.bits; pitch = mappedRect.pitch; } ~NativeReadOnlyDataReleaser() override { bitmap->Unmap(); } auto getData() const { return data; } auto getPitch() const { return pitch; } private: ComSmartPtr bitmap; BYTE* data = nullptr; UINT32 pitch = 0; }; class SoftwareDataReleaser : public Image::BitmapData::BitmapDataReleaser { public: SoftwareDataReleaser (std::unique_ptr r, Image backupIn, ComSmartPtr nativeBitmapIn, Image::BitmapData::ReadWriteMode modeIn, D2D1_RECT_U targetRectIn) : oldReleaser (std::move (r)), backup (std::move (backupIn)), nativeBitmap (nativeBitmapIn), targetRect (targetRectIn), mode (modeIn) { } static void flushImage (Image softwareImage, ComSmartPtr native, D2D1_RECT_U target) { if (softwareImage.getFormat() == Image::PixelFormat::RGB) softwareImage = softwareImage.convertedToFormat (Image::PixelFormat::ARGB); const Image::BitmapData bitmapData { softwareImage, (int) target.left, (int) target.top, (int) (target.right - target.left), (int) (target.bottom - target.top), Image::BitmapData::readOnly }; const auto hr = native->CopyFromMemory (&target, bitmapData.data, (UINT32) bitmapData.lineStride); jassertquiet (SUCCEEDED (hr)); } ~SoftwareDataReleaser() override { // Ensure that writes to the backup bitmap have been flushed before reading from it oldReleaser = nullptr; if (mode != Image::BitmapData::ReadWriteMode::readOnly) flushImage (backup, nativeBitmap, targetRect); } private: std::unique_ptr oldReleaser; Image backup; ComSmartPtr nativeBitmap; D2D1_RECT_U targetRect{}; Image::BitmapData::ReadWriteMode mode{}; }; ComSmartPtr Direct2DPixelData::createAdapterBitmap() const { auto bitmap = Direct2DBitmap::createBitmap (context, pixelFormat, { (UINT32) width, (UINT32) height }, getLineStride(), D2D1_BITMAP_OPTIONS_TARGET); // The bitmap may be slightly too large due // to DPI scaling, so fill it with transparent black if (bitmap == nullptr || ! clearImage) return bitmap; context->SetTarget (bitmap); context->BeginDraw(); context->Clear(); context->EndDraw(); context->SetTarget (nullptr); return bitmap; } void Direct2DPixelData::createDeviceResources() { if (adapter == nullptr) adapter = directX->adapters.getDefaultAdapter(); if (context == nullptr) context = Direct2DDeviceContext::createContext (adapter); if (nativeBitmap == nullptr) { nativeBitmap = createAdapterBitmap(); if (backup.isValid()) SoftwareDataReleaser::flushImage (backup, nativeBitmap, { 0, 0, (UINT32) width, (UINT32) height }); } } void Direct2DPixelData::initBitmapDataReadOnly (Image::BitmapData& bitmap, int x, int y) { const auto pixelStride = getPixelStride(); const auto lineStride = getLineStride(); const auto offset = (size_t) x * (size_t) pixelStride + (size_t) y * (size_t) lineStride; bitmap.pixelFormat = pixelFormat; bitmap.pixelStride = pixelStride; bitmap.lineStride = lineStride; bitmap.size = (size_t) (height * lineStride) - offset; JUCE_TRACE_LOG_D2D_IMAGE_MAP_DATA; auto releaser = std::make_unique (pixelFormat, lineStride, width, height, context, getAdapterD2D1Bitmap(), Point { x, y }); bitmap.data = releaser->getData(); bitmap.lineStride = (int) releaser->getPitch(); bitmap.dataReleaser = std::move (releaser); } auto Direct2DPixelData::make (Image::PixelFormat formatToUse, int widthIn, int heightIn, bool clearImageIn, DxgiAdapter::Ptr adapterIn) -> Ptr { return new Direct2DPixelData (formatToUse, widthIn, heightIn, clearImageIn, adapterIn); } auto Direct2DPixelData::fromDirect2DBitmap (ComSmartPtr bitmap) -> Ptr { const auto size = bitmap->GetPixelSize(); return new Direct2DPixelData { Image::ARGB, (int) size.width, (int) size.height, false, nullptr }; } Direct2DPixelData::Direct2DPixelData (Image::PixelFormat f, int widthIn, int heightIn, bool clear, DxgiAdapter::Ptr adapterIn) : ImagePixelData (f, widthIn, heightIn), clearImage (clear), adapter (adapterIn != nullptr ? adapterIn : directX->adapters.getDefaultAdapter()) { directX->adapters.addListener (*this); } Direct2DPixelData::~Direct2DPixelData() { directX->adapters.removeListener (*this); } std::unique_ptr Direct2DPixelData::createLowLevelContext() { sendDataChangeMessage(); return std::make_unique (this); } void Direct2DPixelData::initialiseBitmapData (Image::BitmapData& bitmap, int x, int y, Image::BitmapData::ReadWriteMode mode) { JUCE_TRACE_LOG_D2D_IMAGE_MAP_DATA; // The native format matches the JUCE format, and there's no need to write from CPU->GPU, so // map the GPU memory as read-only and return that. if (mode == Image::BitmapData::ReadWriteMode::readOnly && pixelFormat != Image::PixelFormat::RGB) { initBitmapDataReadOnly (bitmap, x, y); return; } // The native format does not match the JUCE format, or the user wants to read the current state of the image. // If the user wants to read the image, then we'll need to copy it to CPU memory. if (mode != Image::BitmapData::ReadWriteMode::writeOnly) { // Store the previous width and height, and set up the BitmapData to cover the entire image area const auto oldW = std::exchange (bitmap.width, width); const auto oldH = std::exchange (bitmap.height, height); // Map the image as read-only. initBitmapDataReadOnly (bitmap, 0, 0); // Copy the mapped image to CPU memory in the correct format. backup = BitmapDataDetail::convert (bitmap, SoftwareImageType{}); // Unmap the image (important, the BitmapData is reused later on). bitmap.dataReleaser = {}; // Reset the initial width and height bitmap.width = oldW; bitmap.height = oldH; } // If the user doesn't want to read from the image, then we may need to create a blank image that they can write to. if (! backup.isValid()) backup = Image { SoftwareImageType{}.create (pixelFormat, width, height, clearImage) }; // Redirect the BitmapData to our backup software image. backup.getPixelData()->initialiseBitmapData (bitmap, x, y, mode); // When this dataReleaser is destroyed, then if the mode is not read-only, image data will be copied // from the software image to GPU memory. bitmap.dataReleaser = std::make_unique (std::move (bitmap.dataReleaser), backup, getAdapterD2D1Bitmap(), mode, D2D1_RECT_U { (UINT32) x, (UINT32) y, (UINT32) width, (UINT32) height }); } void Direct2DPixelData::flushToSoftwareBackup() { backup = SoftwareImageType{}.convert (Image { this }); } ImagePixelData::Ptr Direct2DPixelData::clone() { auto cloned = make (pixelFormat, width, height, false, nullptr); if (cloned == nullptr) return {}; cloned->backup = backup.createCopy(); const D2D1_POINT_2U destinationPoint { 0, 0 }; const auto sourceRectU = D2DUtilities::toRECT_U (Rectangle { width, height }); const auto sourceD2D1Bitmap = getAdapterD2D1Bitmap(); const auto destinationD2D1Bitmap = cloned->getAdapterD2D1Bitmap(); if (sourceD2D1Bitmap == nullptr || destinationD2D1Bitmap == nullptr) return {}; if (const auto hr = destinationD2D1Bitmap->CopyFromBitmap (&destinationPoint, sourceD2D1Bitmap, &sourceRectU); FAILED (hr)) { jassertfalse; return {}; } return cloned; } void Direct2DPixelData::applyGaussianBlurEffect (float radius, Image& result) { // The result must be a separate image! jassert (result.getPixelData() != this); if (context == nullptr) { result = {}; return; } ComSmartPtr effect; if (const auto hr = context->CreateEffect (CLSID_D2D1GaussianBlur, effect.resetAndGetPointerAddress()); FAILED (hr) || effect == nullptr) { result = {}; return; } effect->SetInput (0, getAdapterD2D1Bitmap()); effect->SetValue (D2D1_GAUSSIANBLUR_PROP_STANDARD_DEVIATION, radius / 3.0f); const auto tie = [] (const auto& x) { return std::tuple (x.pixelFormat, x.width, x.height); }; const auto originalPixelData = dynamic_cast (result.getPixelData()); if (originalPixelData == nullptr || tie (*this) != tie (*originalPixelData)) result = Image { make (pixelFormat, width, height, false, adapter) }; const auto outputPixelData = dynamic_cast (result.getPixelData()); if (outputPixelData == nullptr) { result = {}; return; } outputPixelData->createDeviceResources(); auto outputDataContext = outputPixelData->context; if (outputDataContext == nullptr) { result = {}; return; } outputDataContext->SetTarget (outputPixelData->getAdapterD2D1Bitmap()); outputDataContext->BeginDraw(); outputDataContext->Clear(); outputDataContext->DrawImage (effect); outputDataContext->EndDraw(); outputDataContext->SetTarget (nullptr); } void Direct2DPixelData::applySingleChannelBoxBlurEffect (int radius, Image& result) { // The result must be a separate image! jassert (result.getPixelData() != this); if (context == nullptr) { 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 = context->CreateEffect (CLSID_D2D1ConvolveMatrix, effect.resetAndGetPointerAddress()); FAILED (hr) || effect == nullptr) { result = {}; return; } 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) { result = {}; return; } begin->SetInput (0, getAdapterD2D1Bitmap()); const auto originalPixelData = dynamic_cast (result.getPixelData()); if (originalPixelData == nullptr || std::tuple (Image::SingleChannel, width, height) != std::tuple (originalPixelData->pixelFormat, originalPixelData->width, originalPixelData->height)) result = Image { make (Image::SingleChannel, width, height, false, adapter) }; const auto outputPixelData = dynamic_cast (result.getPixelData()); if (outputPixelData == nullptr) { result = {}; return; } outputPixelData->createDeviceResources(); auto outputDataContext = outputPixelData->context; if (outputDataContext == nullptr) { result = {}; return; } outputDataContext->SetTarget (outputPixelData->getAdapterD2D1Bitmap()); outputDataContext->BeginDraw(); outputDataContext->Clear(); outputDataContext->DrawImage (end); outputDataContext->EndDraw(); outputDataContext->SetTarget (nullptr); } std::unique_ptr Direct2DPixelData::createType() const { return std::make_unique(); } void Direct2DPixelData::adapterCreated (DxgiAdapter::Ptr) { } void Direct2DPixelData::adapterRemoved (DxgiAdapter::Ptr) { adapter = nullptr; context = nullptr; nativeBitmap = nullptr; } ComSmartPtr Direct2DPixelData::getAdapterD2D1Bitmap() { createDeviceResources(); return nativeBitmap; } //============================================================================== ImagePixelData::Ptr NativeImageType::create (Image::PixelFormat format, int width, int height, bool clearImage) const { SharedResourcePointer directX; if (directX->adapters.getFactory() == nullptr) { // Make sure the DXGI factory exists // // The caller may be trying to create an Image from a static variable; if this is a DLL, then this is // probably called from DllMain. You can't create a DXGI factory from DllMain, so fall back to a // software image. return new SoftwarePixelData { format, width, height, clearImage }; } return Direct2DPixelData::make (format, width, height, clearImage, nullptr); } //============================================================================== //============================================================================== #if JUCE_UNIT_TESTS class Direct2DImageUnitTest final : public UnitTest { public: Direct2DImageUnitTest() : UnitTest ("Direct2DImageUnitTest", UnitTestCategories::graphics) { compareFunctions[{ Image::RGB, Image::RGB }] = [] (uint8* rgb1, uint8* rgb2) { return rgb1[0] == rgb2[0] && rgb1[1] == rgb2[1] && rgb1[2] == rgb2[2]; }; compareFunctions[{ Image::RGB, Image::ARGB }] = [] (uint8* rgb, uint8* argb) { // Compare bytes directly to avoid alpha premultiply issues return rgb[0] == argb[0] && // blue rgb[1] == argb[1] && // green rgb[2] == argb[2]; // red }; compareFunctions[{ Image::RGB, Image::SingleChannel }] = [] (uint8*, uint8* singleChannel) { return *singleChannel == 0xff; }; compareFunctions[{ Image::ARGB, Image::RGB }] = [] (uint8* argb, uint8* rgb) { // Compare bytes directly to avoid alpha premultiply issues return argb[0] == rgb[0] && argb[1] == rgb[1] && argb[2] == rgb[2]; }; compareFunctions[{ Image::ARGB, Image::ARGB }] = [] (uint8* argb1, uint8* argb2) { return *reinterpret_cast (argb1) == *reinterpret_cast (argb2); }; compareFunctions[{ Image::ARGB, Image::SingleChannel }] = [] (uint8* argb, uint8* singleChannel) { return argb[3] = *singleChannel; }; compareFunctions[{ Image::SingleChannel, Image::RGB }] = [] (uint8* singleChannel, uint8* rgb) { auto alpha = *singleChannel; return rgb[0] == alpha && rgb[1] == alpha && rgb[2] == alpha; }; compareFunctions[{ Image::SingleChannel, Image::ARGB }] = [] (uint8* singleChannel, uint8* argb) { return *singleChannel = argb[3]; }; compareFunctions[{ Image::SingleChannel, Image::SingleChannel }] = [] (uint8* singleChannel1, uint8* singleChannel2) { return *singleChannel1 == *singleChannel2; }; } void runTest() override { beginTest ("Direct2DImageUnitTest"); random = getRandom(); for (auto format : formats) compareSameFormat (format); testFormatConversion(); } Rectangle randomRectangleWithin (Rectangle container) noexcept { auto x = random.nextInt (container.getWidth() - 2); auto y = random.nextInt (container.getHeight() - 2); auto w = random.nextInt (container.getHeight() - x); auto h = random.nextInt (container.getWidth() - y); h = jmax (h, 1); w = jmax (w, 1); return Rectangle { x, y, w, h }; } void compareSameFormat (Image::PixelFormat format) { auto softwareImage = Image { SoftwareImageType{}.create (format, 100, 100, true) }; { Graphics g { softwareImage }; g.fillCheckerBoard (softwareImage.getBounds().toFloat(), 21.0f, 21.0f, makeRandomColor(), makeRandomColor()); } auto direct2DImage = NativeImageType{}.convert (softwareImage); compareImages (softwareImage, direct2DImage, compareFunctions[{ softwareImage.getFormat(), direct2DImage.getFormat() }]); checkReadWriteModes (softwareImage); checkReadWriteModes (direct2DImage); } void compareImages (Image& image1, Image& image2, std::function compareBytes) { { // BitmapData width & height should match Rectangle area = randomRectangleWithin (image1.getBounds()); Image::BitmapData data1 { image1, area.getX(), area.getY(), area.getWidth(), area.getHeight(), Image::BitmapData::ReadWriteMode::readOnly }; Image::BitmapData data2 { image2, area.getX(), area.getY(), area.getWidth(), area.getHeight(), Image::BitmapData::ReadWriteMode::readOnly }; expect (data1.width == data2.width); expect (data1.height == data2.height); } { // Bitmap data should match after ImageType::convert Image::BitmapData data1 { image1, Image::BitmapData::ReadWriteMode::readOnly }; Image::BitmapData data2 { image2, Image::BitmapData::ReadWriteMode::readOnly }; for (int y = 0; y < data1.height; ++y) { auto line1 = data1.getLinePointer (y); auto line2 = data2.getLinePointer (y); for (int x = 0; x < data1.width; ++x) { expect (compareBytes (line1, line2), "Failed comparing format " + String { image1.getFormat() } + " to " + String { image2.getFormat() }); line1 += data1.pixelStride; line2 += data2.pixelStride; } } } { // Subsection data should match // Should be able to have two different BitmapData objects simultaneously for the same source image Rectangle area1 = randomRectangleWithin (image1.getBounds()); Rectangle area2 = randomRectangleWithin (image1.getBounds()); Image::BitmapData data1 { image1, Image::BitmapData::ReadWriteMode::readOnly }; Image::BitmapData data2a { image2, area1.getX(), area1.getY(), area1.getWidth(), area1.getHeight(), Image::BitmapData::ReadWriteMode::readOnly }; Image::BitmapData data2b { image2, area2.getX(), area2.getY(), area2.getWidth(), area2.getHeight(), Image::BitmapData::ReadWriteMode::readOnly }; auto compareSubsection = [&] (Image::BitmapData& subsection1, Image::BitmapData& subsection2, Rectangle area) { for (int y = 0; y < area.getHeight(); ++y) { auto line1 = subsection1.getLinePointer (y + area.getY()); auto line2 = subsection2.getLinePointer (y); for (int x = 0; x < area.getWidth(); ++x) { expect (compareBytes (line1 + (x + area.getX()) * subsection1.pixelStride, line2 + x * subsection2.pixelStride)); } } }; compareSubsection (data1, data2a, area1); compareSubsection (data1, data2b, area2); } } void checkReadWriteModes (Image& image) { // Check read and write modes int x = random.nextInt (image.getWidth()); auto writeColor = makeRandomColor().withAlpha (1.0f); auto expectedColor = writeColor; switch (image.getFormat()) { case Image::SingleChannel: { auto alpha = writeColor.getAlpha(); expectedColor = Colour { alpha, alpha, alpha, alpha }; break; } case Image::RGB: case Image::ARGB: break; case Image::UnknownFormat: default: jassertfalse; break; } { Image::BitmapData data { image, Image::BitmapData::ReadWriteMode::writeOnly }; for (int y = 0; y < data.height; ++y) data.setPixelColour (x, y, writeColor); } { Image::BitmapData data { image, Image::BitmapData::ReadWriteMode::readOnly }; for (int y = 0; y < data.height; ++y) { auto color = data.getPixelColour (x, y); expect (color == expectedColor); } } } void testFormatConversion() { for (auto sourceFormat : formats) { for (auto destFormat : formats) { auto softwareStartImage = Image { SoftwareImageType {}.create (sourceFormat, 100, 100, true) }; { Graphics g { softwareStartImage }; g.fillCheckerBoard (softwareStartImage.getBounds().toFloat(), 21.0f, 21.0f, makeRandomColor(), makeRandomColor()); } auto convertedSoftwareImage = softwareStartImage.convertedToFormat (destFormat); compareImages (softwareStartImage, convertedSoftwareImage, compareFunctions[{ sourceFormat, destFormat }]); auto direct2DImage = NativeImageType {}.convert (softwareStartImage); compareImages (softwareStartImage, direct2DImage, compareFunctions[{ sourceFormat, sourceFormat }]); auto convertedDirect2DImage = direct2DImage.convertedToFormat (destFormat); compareImages (softwareStartImage, convertedDirect2DImage, compareFunctions[{ sourceFormat, destFormat }]); } } } Colour makeRandomColor() { uint8 red = (uint8) random.nextInt (255); uint8 green = (uint8) random.nextInt (255); uint8 blue = (uint8) random.nextInt (255); uint8 alpha = (uint8) random.nextInt (255); return Colour { red, green, blue, alpha }; } Random random; std::array const formats { Image::RGB, Image::ARGB, Image::SingleChannel }; std::map, std::function> compareFunctions; }; static Direct2DImageUnitTest direct2DImageUnitTest; #endif } // namespace juce