From 589d9940ede39fc98c51f7b97920fdde8259a7a2 Mon Sep 17 00:00:00 2001 From: reuk Date: Tue, 3 Sep 2024 18:47:32 +0100 Subject: [PATCH] Direct2D: Add support for bitmaps spanning multiple texture pages --- modules/juce_graphics/images/juce_Image.cpp | 3 + .../juce_Direct2DGraphicsContext_windows.cpp | 247 +++-- .../juce_Direct2DHwndContext_windows.cpp | 4 +- .../native/juce_Direct2DImage_windows.cpp | 872 +++++++++++------- .../native/juce_Direct2DImage_windows.h | 178 +++- .../native/juce_DirectX_windows.h | 23 +- .../native/juce_Windowing_windows.cpp | 2 +- 7 files changed, 873 insertions(+), 456 deletions(-) diff --git a/modules/juce_graphics/images/juce_Image.cpp b/modules/juce_graphics/images/juce_Image.cpp index a33fe03a52..112bcce29e 100644 --- a/modules/juce_graphics/images/juce_Image.cpp +++ b/modules/juce_graphics/images/juce_Image.cpp @@ -80,6 +80,9 @@ public: { } + Rectangle getSubsection() const { return area; } + ImagePixelData::Ptr getSourcePixelData() const { return sourceImage; } + std::unique_ptr createLowLevelContext() override { auto g = sourceImage->createLowLevelContext(); diff --git a/modules/juce_graphics/native/juce_Direct2DGraphicsContext_windows.cpp b/modules/juce_graphics/native/juce_Direct2DGraphicsContext_windows.cpp index 61f8479c75..388cbe6df1 100644 --- a/modules/juce_graphics/native/juce_Direct2DGraphicsContext_windows.cpp +++ b/modules/juce_graphics/native/juce_Direct2DGraphicsContext_windows.cpp @@ -345,9 +345,9 @@ public: const auto d2d1Bitmap = [&] { if (auto direct2DPixelData = dynamic_cast (fillType.image.getPixelData())) - if (auto bitmap = direct2DPixelData->getAdapterD2D1Bitmap()) - if (bitmap->GetPixelFormat().format == DXGI_FORMAT_B8G8R8A8_UNORM) - return bitmap; + if (const auto page = direct2DPixelData->getFirstPageForContext (context)) + if (page->GetPixelFormat().format == DXGI_FORMAT_B8G8R8A8_UNORM) + return page; JUCE_D2DMETRICS_SCOPED_ELAPSED_TIME (Direct2DMetricsHub::getInstance()->imageContextMetrics, createBitmapTime); @@ -1162,11 +1162,20 @@ void Direct2DGraphicsContext::clipToImageAlpha (const Image& sourceImage, const if (auto deviceContext = getPimpl()->getDeviceContext()) { + const auto maxDim = (int) deviceContext->GetMaximumBitmapSize(); + + if (sourceImage.getWidth() > maxDim || sourceImage.getHeight() > maxDim) + { + // The Direct2D renderer doesn't currently support clipping to very large images + jassertfalse; + return; + } + // Is this a Direct2D image already? ComSmartPtr d2d1Bitmap; if (auto direct2DPixelData = dynamic_cast (sourceImage.getPixelData())) - d2d1Bitmap = direct2DPixelData->getAdapterD2D1Bitmap(); + d2d1Bitmap = direct2DPixelData->getFirstPageForContext (deviceContext); if (! d2d1Bitmap) { @@ -1430,78 +1439,108 @@ void Direct2DGraphicsContext::strokePath (const Path& p, const PathStrokeType& s deviceContext->DrawGeometry (geometry, brush, strokeType.getStrokeThickness(), strokeStyle); } -void Direct2DGraphicsContext::drawImage (const Image& image, const AffineTransform& transform) +void Direct2DGraphicsContext::drawImage (const Image& imageIn, const AffineTransform& transform) { JUCE_D2DMETRICS_SCOPED_ELAPSED_TIME (metrics, drawImageTime) JUCE_SCOPED_TRACE_EVENT_FRAME (etw::drawImage, etw::direct2dKeyword, getFrameId()); - if (image.isNull()) + if (imageIn.isNull()) return; applyPendingClipList(); if (auto deviceContext = getPimpl()->getDeviceContext()) { - // Is this a Direct2D image already with the correct format? - ComSmartPtr d2d1Bitmap; + auto image = NativeImageType{}.convert (imageIn); + Direct2DPixelData* nativeBitmap = nullptr; Rectangle imageClipArea; - if (auto direct2DPixelData = dynamic_cast (image.getPixelData())) + const auto imageTransform = currentState->currentTransform.getTransformWith (transform); + + if (auto* subsectionPixelData = dynamic_cast (image.getPixelData())) { - d2d1Bitmap = direct2DPixelData->getAdapterD2D1Bitmap(); + if (auto direct2DPixelData = dynamic_cast (subsectionPixelData->getSourcePixelData().get())) + { + nativeBitmap = direct2DPixelData; + imageClipArea = subsectionPixelData->getSubsection(); + } + } + else if (auto direct2DPixelData = dynamic_cast (image.getPixelData())) + { + nativeBitmap = direct2DPixelData; imageClipArea = { direct2DPixelData->width, direct2DPixelData->height }; } - - if (! d2d1Bitmap || d2d1Bitmap->GetPixelFormat().format != DXGI_FORMAT_B8G8R8A8_UNORM) + else { - JUCE_D2DMETRICS_SCOPED_ELAPSED_TIME (Direct2DMetricsHub::getInstance()->imageContextMetrics, createBitmapTime); - - d2d1Bitmap = Direct2DBitmap::toBitmap (image, deviceContext, Image::ARGB); - imageClipArea = image.getBounds(); + // This shouldn't happen, we converted the image to a native type already + jassertfalse; } - if (d2d1Bitmap) + if (! nativeBitmap) { - auto sourceRectF = D2DUtilities::toRECT_F (imageClipArea); - - auto imageTransform = currentState->currentTransform.getTransformWith (transform); - - if (imageTransform.isOnlyTranslation()) - { - auto destinationRect = D2DUtilities::toRECT_F (imageClipArea.toFloat() + Point { imageTransform.getTranslationX(), imageTransform.getTranslationY() }); - - deviceContext->DrawBitmap (d2d1Bitmap, - &destinationRect, - currentState->fillType.getOpacity(), - currentState->interpolationMode, - &sourceRectF, - {}); - - return; - } - - if (D2DHelpers::isTransformAxisAligned (imageTransform)) - { - auto destinationRect = D2DUtilities::toRECT_F (imageClipArea.toFloat().transformedBy (imageTransform)); - - deviceContext->DrawBitmap (d2d1Bitmap, - &destinationRect, - currentState->fillType.getOpacity(), - currentState->interpolationMode, - &sourceRectF, - {}); - return; - } - - ScopedTransform scopedTransform { *getPimpl(), currentState, transform }; - deviceContext->DrawBitmap (d2d1Bitmap, - nullptr, - currentState->fillType.getOpacity(), - currentState->interpolationMode, - &sourceRectF, - {}); + jassertfalse; + return; } + + auto drawTiles = [&] (const auto& pixelData, auto&& getRect) + { + for (const auto& page : pixelData->getPagesForContext (deviceContext)) + { + const auto pageBounds = page.getBounds(); + const auto intersection = pageBounds.toFloat().getIntersection (imageClipArea.toFloat()); + + if (intersection.isEmpty()) + continue; + + const auto src = intersection - pageBounds.getPosition().toFloat(); + const auto dst = getRect (intersection - imageClipArea.getPosition().toFloat()); + const auto [srcConverted, dstConverted] = std::tuple (D2DUtilities::toRECT_F (src), + D2DUtilities::toRECT_F (dst)); + + if (nativeBitmap->pixelFormat == Image::SingleChannel) + { + const auto lastColour = currentState->colourBrush->GetColor(); + const auto lastMode = deviceContext->GetAntialiasMode(); + + currentState->colourBrush->SetColor (D2D1::ColorF (1.0f, 1.0f, 1.0f, currentState->fillType.getOpacity())); + deviceContext->SetAntialiasMode (D2D1_ANTIALIAS_MODE_ALIASED); + deviceContext->FillOpacityMask (page.bitmap, + currentState->colourBrush, + dstConverted, + srcConverted); + + deviceContext->SetAntialiasMode (lastMode); + currentState->colourBrush->SetColor (lastColour); + } + else + { + deviceContext->DrawBitmap (page.bitmap, + dstConverted, + currentState->fillType.getOpacity(), + currentState->interpolationMode, + srcConverted, + {}); + } + } + }; + + if (imageTransform.isOnlyTranslation() || D2DHelpers::isTransformAxisAligned (imageTransform)) + { + drawTiles (nativeBitmap, [&] (auto intersection) + { + return intersection.transformedBy (imageTransform); + }); + + return; + } + + ScopedTransform scopedTransform { *getPimpl(), currentState, transform }; + + drawTiles (nativeBitmap, [] (auto intersection) + { + return intersection; + }); } } @@ -1787,4 +1826,102 @@ Direct2DGraphicsContext::ScopedTransform::~ScopedTransform() pimpl.resetDeviceContextTransform(); } +//============================================================================== +//============================================================================== + +#if JUCE_UNIT_TESTS + +class Direct2DGraphicsContextTests : public UnitTest +{ +public: + Direct2DGraphicsContextTests() : UnitTest ("Direct2D Graphics Context", UnitTestCategories::graphics) {} + + void runTest() override + { + const auto imageWidth = 1 << 15; + const auto imageHeight = 128; + Image largeImageSoftware { Image::RGB, imageWidth, imageHeight, false, SoftwareImageType{} }; + + { + Graphics g { largeImageSoftware }; + g.setGradientFill ({ Colours::red, 0, 0, Colours::cyan, (float) largeImageSoftware.getWidth(), 0, false }); + g.fillAll(); + } + + constexpr auto targetDim = 512; + + const auto largeImageNative = NativeImageType{}.convert (largeImageSoftware); + const auto subsection = largeImageNative.getClippedImage (largeImageNative.getBounds().withSizeKeepingCentre (1 << 14, 64)); + + beginTest ("Render large images"); + { + for (const auto& imageToDraw : { largeImageNative, subsection }) + { + const AffineTransform transformsToTest[] + { + {}, + AffineTransform::translation ((float) targetDim - (float) imageToDraw.getWidth(), 0), + AffineTransform::translation (0, (float) targetDim - (float) imageToDraw.getHeight()), + AffineTransform::scale ((float) targetDim / imageWidth), + AffineTransform::scale ((float) targetDim / imageWidth) + .followedBy (AffineTransform::translation (32, 64)), + AffineTransform::scale (1.1f), + AffineTransform::scale ((float) targetDim / imageWidth, + (float) targetDim / imageHeight), + AffineTransform::rotation (MathConstants::pi * 0.25f), + AffineTransform::rotation (MathConstants::pi * 0.25f, imageWidth * 0.5f, 0) + .followedBy (AffineTransform::translation (-imageWidth * 0.5f, 0)), + }; + + for (const auto& transform : transformsToTest) + { + Image targetNative { Image::RGB, targetDim, targetDim, true, NativeImageType{} }; + Image targetSoftware { Image::RGB, targetDim, targetDim, true, SoftwareImageType{} }; + + for (auto& image : { &targetNative, &targetSoftware }) + { + Graphics g { *image }; + g.drawImageTransformed (imageToDraw, transform); + } + + compareImages (targetNative, targetSoftware); + } + } + } + } + + void compareImages (const Image& a, const Image& b) + { + expect (a.getBounds() == b.getBounds()); + + const Image::BitmapData bitmapA { a, Image::BitmapData::readOnly }; + const Image::BitmapData bitmapB { b, Image::BitmapData::readOnly }; + + int64_t accumulatedError{}; + int64_t numSamples{}; + + for (auto y = 0; y < a.getHeight(); y += 16) + { + for (auto x = 0; x < a.getWidth(); x += 16) + { + const auto expected = bitmapA.getPixelColour (x, y); + const auto actual = bitmapB.getPixelColour (x, y); + + for (auto& fn : { &Colour::getRed, &Colour::getGreen, &Colour::getBlue, &Colour::getAlpha }) + { + accumulatedError += ((int64_t) (actual.*fn)() - (int64_t) (expected.*fn)()); + ++numSamples; + } + } + } + + const auto averageError = (double) accumulatedError / (double) numSamples; + expect (std::abs (averageError) < 1.0); + } +}; + +static Direct2DGraphicsContextTests direct2DGraphicsContextTests; + +#endif + } // namespace juce diff --git a/modules/juce_graphics/native/juce_Direct2DHwndContext_windows.cpp b/modules/juce_graphics/native/juce_Direct2DHwndContext_windows.cpp index ff26fb5f69..d02ef1ff8e 100644 --- a/modules/juce_graphics/native/juce_Direct2DHwndContext_windows.cpp +++ b/modules/juce_graphics/native/juce_Direct2DHwndContext_windows.cpp @@ -649,9 +649,7 @@ public: if (const auto hr = snapshot->CopyFromBitmap (&p, swap.buffer, &sourceRect); FAILED (hr)) return {}; - const Image result { Direct2DPixelData::fromDirect2DBitmap (directX->adapters.getAdapterForHwnd (hwnd), - context, - snapshot) }; + const Image result { new Direct2DPixelData { context, snapshot } }; swap.chain->Present (0, DXGI_PRESENT_DO_NOT_WAIT); diff --git a/modules/juce_graphics/native/juce_Direct2DImage_windows.cpp b/modules/juce_graphics/native/juce_Direct2DImage_windows.cpp index 41347d1fc5..655452e247 100644 --- a/modules/juce_graphics/native/juce_Direct2DImage_windows.cpp +++ b/modules/juce_graphics/native/juce_Direct2DImage_windows.cpp @@ -35,305 +35,410 @@ namespace juce { -class NativeReadOnlyDataReleaser : public Image::BitmapData::BitmapDataReleaser +Rectangle Direct2DPixelDataPage::getBounds() const { -public: - NativeReadOnlyDataReleaser (Image::PixelFormat pixelFormat, - int w, - int h, - ComSmartPtr deviceContextIn, - ComSmartPtr sourceBitmap, - Point offset) - : bitmap (Direct2DBitmap::createBitmap (deviceContextIn, - pixelFormat, - { (UINT32) w, (UINT32) h }, - 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 (native == nullptr) - return; - - 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 }, - 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; + return bitmap != nullptr ? D2DUtilities::rectFromSize (bitmap->GetPixelSize()).withPosition (topLeft) + : Rectangle{}; } -void Direct2DPixelData::createDeviceResources() +//============================================================================== +static ComSmartPtr getDeviceForContext (ComSmartPtr context) { - if (adapter == nullptr) - adapter = directX->adapters.getDefaultAdapter(); + if (context == nullptr) + return {}; + + ComSmartPtr device; + context->GetDevice (device.resetAndGetPointerAddress()); + return device.getInterface(); +} + +static std::vector makePages (ComSmartPtr device, + ImagePixelData::Ptr backingData, + bool needsClear) +{ + if (device == nullptr || backingData == nullptr) + { + jassertfalse; + return {}; + } + + // We create a new context rather than reusing an existing one, because we'll run into problems + // if we call BeginDraw/EndDraw on a context that's already doing its own drawing + const auto context = Direct2DDeviceContext::create (device); if (context == nullptr) - context = Direct2DDeviceContext::create (adapter); - - if (nativeBitmap == nullptr) { - nativeBitmap = createAdapterBitmap(); - - if (backup.isValid()) - SoftwareDataReleaser::flushImage (backup, nativeBitmap, { 0, 0, (UINT32) width, (UINT32) height }); + jassertfalse; + return {}; } -} -void Direct2DPixelData::initBitmapDataReadOnly (Image::BitmapData& bitmap, int x, int y) -{ - const auto pixelStride = getPixelStride(); - const auto lineStride = getLineStride(); + const auto maxDim = (size_t) context->GetMaximumBitmapSize(); + std::vector result; - 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; + const auto width = (size_t) backingData->width; + const auto height = (size_t) backingData->height; + const auto pixelFormat = backingData->pixelFormat; - JUCE_TRACE_LOG_D2D_IMAGE_MAP_DATA; + for (size_t h = 0; h < height; h += maxDim) + { + const auto tileHeight = (UINT32) jmin (maxDim, height - h); - auto releaser = std::make_unique (pixelFormat, - width, - height, - context, - getAdapterD2D1Bitmap(), - Point { x, y }); - bitmap.data = releaser->getData(); - bitmap.lineStride = (int) releaser->getPitch(); - bitmap.dataReleaser = std::move (releaser); -} + for (size_t w = 0; w < width; w += maxDim) + { + const auto tileWidth = (UINT32) jmin (maxDim, width - w); -auto Direct2DPixelData::make (Image::PixelFormat formatToUse, - int widthIn, - int heightIn, - bool clearImageIn, - DxgiAdapter::Ptr adapterIn) -> Ptr -{ - return new Direct2DPixelData (formatToUse, widthIn, heightIn, clearImageIn, adapterIn); -} + const auto bitmap = Direct2DBitmap::createBitmap (context, + pixelFormat, + D2D1::SizeU (tileWidth, tileHeight), + D2D1_BITMAP_OPTIONS_TARGET); + + jassert (bitmap != nullptr); + + if (needsClear) + { + context->SetTarget (bitmap); + context->BeginDraw(); + context->Clear(); + context->EndDraw(); + } + + result.push_back (Direct2DPixelDataPage { bitmap, { (int) w, (int) h } }); + } + } -auto Direct2DPixelData::fromDirect2DBitmap (DxgiAdapter::Ptr adapterIn, - ComSmartPtr contextIn, - ComSmartPtr bitmapIn) -> Ptr -{ - const auto size = bitmapIn->GetPixelSize(); - Ptr result = new Direct2DPixelData { Image::ARGB, (int) size.width, (int) size.height, false, nullptr }; - result->adapter = adapterIn; - result->context = contextIn; - result->nativeBitmap = bitmapIn; return result; } -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()) +/* Maps the content of the provided bitmap and copies it into target, which should be a software + bitmap. +*/ +static bool readFromDirect2DBitmap (ComSmartPtr context, + ComSmartPtr bitmap, + ImagePixelData::Ptr target) { + if (bitmap == nullptr || context == nullptr || target == nullptr) + return false; + + const auto size = bitmap->GetPixelSize(); + + if (std::tuple (target->width, target->height) != std::tuple ((int) size.width, (int) size.height)) + { + // Mismatched sizes, unable to read D2D image back into software image! + jassertfalse; + return false; + } + + const auto readableBitmap = Direct2DBitmap::createBitmap (context, + target->pixelFormat, + size, + D2D1_BITMAP_OPTIONS_CPU_READ | D2D1_BITMAP_OPTIONS_CANNOT_DRAW); + + const auto dstPoint = D2D1::Point2U(); + const auto srcRect = D2DUtilities::toRECT_U (D2DUtilities::rectFromSize (size)); + readableBitmap->CopyFromBitmap (&dstPoint, bitmap, &srcRect); + + // This is only used to construct a read-only BitmapData backed by a texture for conversion to a + // software image + struct TexturePixelData : public ImagePixelData + { + TexturePixelData (ComSmartPtr bitmapIn, Image::PixelFormat format, int w, int h) + : ImagePixelData (format, w, h), + bitmap (bitmapIn) + { + } + + std::unique_ptr createLowLevelContext() override + { + jassertfalse; // This should never be called + return {}; + } + + Ptr clone() override + { + jassertfalse; // This should never be called + return {}; + } + + std::unique_ptr createType() const override + { + jassertfalse; // This should never be called + return {}; + } + + void initialiseBitmapData (Image::BitmapData& bd, int x, int y, Image::BitmapData::ReadWriteMode mode) override + { + if (mode != Image::BitmapData::readOnly) + { + // This type only supports read-only access + jassertfalse; + return; + } + + struct Releaser : public Image::BitmapData::BitmapDataReleaser + { + explicit Releaser (ComSmartPtr toUnmapIn) : toUnmap (toUnmapIn) {} + ~Releaser() override { toUnmap->Unmap(); } + ComSmartPtr toUnmap; + }; + + D2D1_MAPPED_RECT mapped{}; + bitmap->Map (D2D1_MAP_OPTIONS_READ, &mapped); + const auto dataEnd = mapped.bits + bitmap->GetPixelSize().height * mapped.pitch; + + bd.pixelFormat = pixelFormat; + bd.pixelStride = pixelFormat == Image::SingleChannel ? 1 : 4; + bd.lineStride = (int) mapped.pitch; + bd.data = mapped.bits + x * bd.pixelStride + y * (int) mapped.pitch; + bd.size = (size_t) (dataEnd - bd.data); + bd.dataReleaser = std::make_unique (bitmap); + } + + ComSmartPtr bitmap; + }; + + Image srcImage { new TexturePixelData { readableBitmap, + target->pixelFormat, + (int) size.width, + (int) size.height } }; + Image::BitmapData srcBitmap { srcImage, Image::BitmapData::readOnly }; + Image::BitmapData dstBitmap { Image { target }, Image::BitmapData::writeOnly }; + BitmapDataDetail::convert (srcBitmap, dstBitmap); + + return true; +} + +/* Returns new software bitmap storage with content matching the provided hardware bitmap. */ +static ImagePixelData::Ptr readFromDirect2DBitmap (ComSmartPtr context, + ComSmartPtr bitmap) +{ + if (bitmap == nullptr) + return {}; + + const auto size = bitmap->GetPixelSize(); + const auto result = SoftwareImageType{}.create (Image::ARGB, + (int) size.width, + (int) size.height, + false); + + if (result == nullptr || ! readFromDirect2DBitmap (context, bitmap, result)) + return {}; + + return result; +} + +Direct2DPixelDataPages::Direct2DPixelDataPages (ComSmartPtr bitmap, + ImagePixelData::Ptr image) + : backingData (image), + pages { Page { bitmap, {} } }, + upToDate (true) +{ + // The backup image must be a software image + jassert (image->createType()->getTypeID() == SoftwareImageType{}.getTypeID()); +} + +Direct2DPixelDataPages::Direct2DPixelDataPages (ComSmartPtr context, + ImagePixelData::Ptr image, + State initialState) + : backingData (image), + pages (makePages (getDeviceForContext (context), backingData, initialState == State::cleared)), + upToDate (initialState != State::unsuitableToRead) +{ + // The backup image must be a software image + jassert (image->createType()->getTypeID() == SoftwareImageType{}.getTypeID()); +} + +auto Direct2DPixelDataPages::getPages() -> Span +{ + if (std::exchange (upToDate, true)) + return pages; + + auto sourceToUse = backingData->pixelFormat == Image::RGB + ? Image { backingData }.convertedToFormat (Image::ARGB) + : Image { backingData }; + + for (const auto& page : pages) + { + const auto pageBounds = page.getBounds(); + const Image::BitmapData bitmapData { sourceToUse, + pageBounds.getX(), + pageBounds.getY(), + pageBounds.getWidth(), + pageBounds.getHeight(), + Image::BitmapData::readOnly }; + + const auto target = D2DUtilities::toRECT_U (pageBounds.withZeroOrigin()); + const auto hr = page.bitmap->CopyFromMemory (&target, bitmapData.data, (UINT32) bitmapData.lineStride); + jassertquiet (SUCCEEDED (hr)); + } + + return pages; +} + +//============================================================================== +Direct2DPixelData::Direct2DPixelData (ImagePixelData::Ptr ptr, State initialState) + : ImagePixelData { ptr->pixelFormat, ptr->width, ptr->height }, + backingData (ptr), + state (initialState) +{ + jassert (backingData->createType()->getTypeID() == SoftwareImageType{}.getTypeID()); directX->adapters.addListener (*this); } +Direct2DPixelData::Direct2DPixelData (ComSmartPtr context, + ComSmartPtr page) + : Direct2DPixelData (readFromDirect2DBitmap (context, page), State::suitableToRead) +{ + if (const auto device1 = getDeviceForContext (context)) + pagesForDevice.emplace (device1, Direct2DPixelDataPages { page, backingData }); +} + +Direct2DPixelData::Direct2DPixelData (Image::PixelFormat formatToUse, int w, int h, bool clearIn) + : Direct2DPixelData { SoftwareImageType{}.create (formatToUse, w, h, clearIn), + clearIn ? State::cleared : State::unsuitableToRead } +{ +} + Direct2DPixelData::~Direct2DPixelData() { directX->adapters.removeListener (*this); } +auto Direct2DPixelData::getIteratorForContext (ComSmartPtr context) +{ + const auto device1 = getDeviceForContext (context); + + if (device1 == nullptr) + return pagesForDevice.end(); + + const auto iter = pagesForDevice.find (device1); + + if (iter != pagesForDevice.end()) + return iter; + + const auto initialState = [&] + { + switch (state) + { + // If our image is currently cleared, then the initial state of the page should also + // be cleared. + case State::cleared: + return State::cleared; + + // If our image holds junk, then it must be written before first read, which means + // that the cached pages must also be written before first read. Don't mark the new + // pages as needing a sync yet - there's a chance that we'll render directly into + // the new pages, in which case copying the initial state from the software image + // would be unnecessary and wasteful. + case State::unsuitableToRead: + return State::suitableToRead; + + // If the software image has been written with valid data, then we need to preserve + // this data when reading or writing (e.g. to a subsection, or with transparency) + // to the new pages, so mark the new pages as needing a sync before first access. + case State::suitableToRead: + return State::unsuitableToRead; + } + + return State::unsuitableToRead; + }(); + + const auto pair = pagesForDevice.emplace (device1, Pages { context, backingData, initialState }); + return pair.first; +} + std::unique_ptr Direct2DPixelData::createLowLevelContext() { sendDataChangeMessage(); - struct FlushingContext : public Direct2DImageContext - { - explicit FlushingContext (Direct2DPixelData::Ptr p) - : Direct2DImageContext (p->context, p->getAdapterD2D1Bitmap(), Rectangle { p->width, p->height }), - ptr (startFrame (1.0f) ? p : nullptr) - { - } + const auto adapter = directX->adapters.getDefaultAdapter(); - ~FlushingContext() override - { - if (ptr == nullptr) - return; - - endFrame(); - ptr->flushToSoftwareBackup(); - } - - Direct2DPixelData::Ptr ptr; - }; - - 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) + if (adapter == nullptr) return {}; - cloned->backup = backup.createCopy(); + const auto context = Direct2DDeviceContext::create (adapter); - 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) + if (context == nullptr) return {}; - if (const auto hr = destinationD2D1Bitmap->CopyFromBitmap (&destinationPoint, sourceD2D1Bitmap, &sourceRectU); FAILED (hr)) + const auto maxSize = (int) context->GetMaximumBitmapSize(); + + if (maxSize < width || maxSize < height) + { + for (auto& pair : pagesForDevice) + pair.second.markOutdated(); + + return backingData->createLowLevelContext(); + } + + const auto iter = getIteratorForContext (context); + jassert (iter != pagesForDevice.end()); + + const auto pages = iter->second.getPages(); + + if (pages.empty() || pages.front().bitmap == nullptr) { jassertfalse; return {}; } - return cloned; + // 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(); + + // The low level context will eventually write valid image data to this image. + state = State::suitableToRead; + + struct FlushingContext : public Direct2DImageContext + { + FlushingContext (SoftwarePixelData::Ptr backupIn, + ComSmartPtr context, + ComSmartPtr target) + : Direct2DImageContext (context, target, D2DUtilities::rectFromSize (target->GetPixelSize())), + storedContext (context), + storedTarget (target), + backup (startFrame (1.0f) ? backupIn : nullptr) + { + } + + ~FlushingContext() override + { + if (backup == nullptr) + return; + + endFrame(); + readFromDirect2DBitmap (storedContext, storedTarget, backup); + } + + ComSmartPtr storedContext; + ComSmartPtr storedTarget; + SoftwarePixelData::Ptr backup; + }; + + return std::make_unique (backingData, context, pages.front().bitmap); +} + +void Direct2DPixelData::initialiseBitmapData (Image::BitmapData& bitmap, + int x, + int y, + Image::BitmapData::ReadWriteMode mode) +{ + backingData->initialiseBitmapData (bitmap, x, y, mode); + + // If we're only reading, then we can assume that the bitmap data was flushed to the software + // image directly after it was last modified by d2d, so we can just use the BitmapData + // initialised by the backing data. + // If we're writing, then we'll need to update our textures next time we try to use them, so + // mark them as outdated. + if (mode == Image::BitmapData::readOnly) + return; + + // We're writing to the image data, so mark this image as having valid image data. + state = State::suitableToRead; + + for (auto& pair : pagesForDevice) + pair.second.markOutdated(); } void Direct2DPixelData::applyGaussianBlurEffect (float radius, Image& result) @@ -341,7 +446,18 @@ void Direct2DPixelData::applyGaussianBlurEffect (float radius, Image& result) // The result must be a separate image! jassert (result.getPixelData() != this); - if (context == nullptr) + const auto adapter = directX->adapters.getDefaultAdapter(); + + if (adapter == nullptr) + { + result = {}; + return; + } + + const auto context = Direct2DDeviceContext::create (adapter); + const auto maxSize = (int) context->GetMaximumBitmapSize(); + + if (context == nullptr || maxSize < width || maxSize < height) { result = {}; return; @@ -355,38 +471,21 @@ void Direct2DPixelData::applyGaussianBlurEffect (float radius, Image& result) return; } - effect->SetInput (0, getAdapterD2D1Bitmap()); + effect->SetInput (0, getFirstPageForContext (context)); 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()); + const auto outputPixelData = Direct2DBitmap::createBitmap (context, + Image::ARGB, + D2D1::SizeU ((UINT32) width, (UINT32) height), + D2D1_BITMAP_OPTIONS_TARGET); - if (originalPixelData == nullptr || tie (*this) != tie (*originalPixelData)) - result = Image { make (pixelFormat, width, height, false, adapter) }; + context->SetTarget (outputPixelData); + context->BeginDraw(); + context->Clear(); + context->DrawImage (effect); + context->EndDraw(); - 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); + result = Image { new Direct2DPixelData { context, outputPixelData } }; } void Direct2DPixelData::applySingleChannelBoxBlurEffect (int radius, Image& result) @@ -394,7 +493,18 @@ void Direct2DPixelData::applySingleChannelBoxBlurEffect (int radius, Image& resu // The result must be a separate image! jassert (result.getPixelData() != this); - if (context == nullptr) + const auto adapter = directX->adapters.getDefaultAdapter(); + + if (adapter == nullptr) + { + result = {}; + return; + } + + const auto context = Direct2DDeviceContext::create (adapter); + const auto maxSize = (int) context->GetMaximumBitmapSize(); + + if (context == nullptr || maxSize < width || maxSize < height) { result = {}; return; @@ -410,7 +520,7 @@ void Direct2DPixelData::applySingleChannelBoxBlurEffect (int radius, Image& resu { ComSmartPtr effect; if (const auto hr = context->CreateEffect (CLSID_D2D1ConvolveMatrix, effect.resetAndGetPointerAddress()); - FAILED (hr) || effect == nullptr) + FAILED (hr) || effect == nullptr) { result = {}; return; @@ -439,58 +549,25 @@ void Direct2DPixelData::applySingleChannelBoxBlurEffect (int radius, Image& resu return; } - begin->SetInput (0, getAdapterD2D1Bitmap()); + begin->SetInput (0, getFirstPageForContext (context)); - const auto originalPixelData = dynamic_cast (result.getPixelData()); + const auto outputPixelData = Direct2DBitmap::createBitmap (context, + Image::ARGB, + D2D1::SizeU ((UINT32) width, (UINT32) height), + D2D1_BITMAP_OPTIONS_TARGET); - 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) }; + context->SetTarget (outputPixelData); + context->BeginDraw(); + context->Clear(); + context->DrawImage (end); + context->EndDraw(); - 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); + result = Image { new Direct2DPixelData { context, outputPixelData } }; } -std::unique_ptr Direct2DPixelData::createType() const +auto Direct2DPixelData::getPagesForContext (ComSmartPtr context) -> Span { - 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; + return getIteratorForContext (context)->second.getPages(); } //============================================================================== @@ -508,7 +585,7 @@ ImagePixelData::Ptr NativeImageType::create (Image::PixelFormat format, int widt return new SoftwarePixelData { format, width, height, clearImage }; } - return Direct2DPixelData::make (format, width, height, clearImage, nullptr); + return new Direct2DPixelData (format, width, height, clearImage); } //============================================================================== @@ -574,30 +651,109 @@ public: void runTest() override { - beginTest ("Direct2DImageUnitTest"); - random = getRandom(); - for (auto format : formats) - compareSameFormat (format); + constexpr auto multiPageSize = (1 << 14) + 512 + 3; - testFormatConversion(); + beginTest ("Direct2DImageUnitTest"); + { + for (const auto size : { 100, multiPageSize }) + { + for (auto format : formats) + { + compareSameFormat (format, size, 32); + compareSameFormat (format, 32, size); + } + + testFormatConversion (size, 32); + testFormatConversion (32, size); + } + } + + beginTest ("Ensure data parity across mapped page boundaries"); + { + const auto adapterToUse = directX->adapters.getDefaultAdapter(); + const auto contextToUse = Direct2DDeviceContext::create (adapterToUse); + + for (auto sourceFormat : formats) + { + Image softwareImage { SoftwareImageType{}.create (sourceFormat, multiPageSize, 32, true) }; + + { + const Image::BitmapData bitmap { softwareImage, Image::BitmapData::writeOnly }; + + for (int y = 0; y < bitmap.height; y++) + { + auto line = bitmap.getLinePointer (y); + + for (int x = 0; x < bitmap.lineStride; x++) + line[x] = (uint8_t) jmap (x, 0, bitmap.lineStride, 0, 256); + } + } + + for (auto destFormat : formats) + { + auto d2dImage = NativeImageType{}.convert (softwareImage) + .convertedToFormat (destFormat); + + const auto maxPageBounds = [&] + { + if (auto* data = dynamic_cast (d2dImage.getPixelData())) + if (auto pages = data->getPagesForContext (contextToUse); ! pages.empty()) + return pages.front().getBounds(); + + return Rectangle{}; + }(); + + const auto boundarySize = softwareImage.getHeight(); + const auto pageBoundary = softwareImage.getBounds().getIntersection ({ maxPageBounds.getWidth() - boundarySize / 2, + 0, + boundarySize, + boundarySize }); + + const Image::BitmapData data1 { softwareImage, + pageBoundary.getX(), + pageBoundary.getY(), + pageBoundary.getWidth(), + pageBoundary.getHeight(), + Image::BitmapData::ReadWriteMode::readOnly }; + const Image::BitmapData data2 { d2dImage, + pageBoundary.getX(), + pageBoundary.getY(), + pageBoundary.getWidth(), + pageBoundary.getHeight(), + Image::BitmapData::ReadWriteMode::readOnly }; + + auto f = compareFunctions.at ({ data1.pixelFormat, data2.pixelFormat }); + + for (int y = 0; y < data1.height; y++) + { + for (int x = 0; x < data1.width; x++) + { + auto p1 = data1.getPixelPointer (x, y); + auto p2 = data2.getPixelPointer (x, y); + + expect (f (p1, p2)); + } + } + } + } + } } 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 }; + const auto w = random.nextInt ({ 1, container.getWidth() }); + const auto h = random.nextInt ({ 1, container.getHeight() }); + const auto x = random.nextInt ({ container.getX(), container.getRight() - w }); + const auto y = random.nextInt ({ container.getY(), container.getBottom() - h }); + + return { x, y, w, h }; } - void compareSameFormat (Image::PixelFormat format) + void compareSameFormat (Image::PixelFormat format, int width, int height) { - auto softwareImage = Image { SoftwareImageType{}.create (format, 100, 100, true) }; + auto softwareImage = Image { SoftwareImageType{}.create (format, width, height, true) }; { Graphics g { softwareImage }; g.fillCheckerBoard (softwareImage.getBounds().toFloat(), 21.0f, 21.0f, makeRandomColor(), makeRandomColor()); @@ -713,27 +869,25 @@ public: } } - void testFormatConversion() + void testFormatConversion (int width, int height) { for (auto sourceFormat : formats) { for (auto destFormat : formats) { - auto softwareStartImage = Image { SoftwareImageType {}.create (sourceFormat, 100, 100, true) }; + Image softwareStartImage { SoftwareImageType {}.create (sourceFormat, width, height, true) }; { Graphics g { softwareStartImage }; g.fillCheckerBoard (softwareStartImage.getBounds().toFloat(), 21.0f, 21.0f, makeRandomColor(), makeRandomColor()); } - auto convertedSoftwareImage = softwareStartImage.convertedToFormat (destFormat); + auto convertedSoftwareImage = softwareStartImage.convertedToFormat (destFormat); compareImages (softwareStartImage, convertedSoftwareImage, compareFunctions[{ sourceFormat, destFormat }]); - auto direct2DImage = NativeImageType {}.convert (softwareStartImage); - + auto direct2DImage = NativeImageType{}.convert (softwareStartImage); compareImages (softwareStartImage, direct2DImage, compareFunctions[{ sourceFormat, sourceFormat }]); auto convertedDirect2DImage = direct2DImage.convertedToFormat (destFormat); - compareImages (softwareStartImage, convertedDirect2DImage, compareFunctions[{ sourceFormat, destFormat }]); } } @@ -741,14 +895,14 @@ public: Colour makeRandomColor() { - uint8 red = (uint8) random.nextInt (255); + uint8 red = (uint8) random.nextInt (255); uint8 green = (uint8) random.nextInt (255); - uint8 blue = (uint8) random.nextInt (255); + uint8 blue = (uint8) random.nextInt (255); uint8 alpha = (uint8) random.nextInt (255); return Colour { red, green, blue, alpha }; } - + SharedResourcePointer directX; Random random; std::array const formats { Image::RGB, Image::ARGB, Image::SingleChannel }; std::map, std::function> compareFunctions; diff --git a/modules/juce_graphics/native/juce_Direct2DImage_windows.h b/modules/juce_graphics/native/juce_Direct2DImage_windows.h index f5c875d669..4da93a16d2 100644 --- a/modules/juce_graphics/native/juce_Direct2DImage_windows.h +++ b/modules/juce_graphics/native/juce_Direct2DImage_windows.h @@ -35,60 +35,176 @@ namespace juce { +/* A single bitmap that represents a subsection of a virtual bitmap. */ +struct Direct2DPixelDataPage +{ + /* The bounds of the stored bitmap inside the virtual bitmap. */ + Rectangle getBounds() const; + + /* The stored subsection bitmap. */ + ComSmartPtr bitmap; + + /* The top-left position of this virtual bitmap inside the virtual bitmap. */ + Point topLeft; +}; + +/* A set of pages that together represent a full virtual bitmap. + All pages in the set always share the same resource context. + Additionally, stores a reference to a software-backed bitmap, the content of which will + be copied to the pages when necessary in order to ensure that the software- and hardware-backed + bitmaps match. +*/ +class Direct2DPixelDataPages +{ +public: + using Page = Direct2DPixelDataPage; + + enum class State + { + unsuitableToRead, // Image data is outdated + suitableToRead, // Image data is up-to-date with the backing data + cleared, // Implies suitableToRead + }; + + /* Creates a single page containing the provided bitmap and main-memory storage, marking the + hardware data as up-to-date. + */ + Direct2DPixelDataPages (ComSmartPtr, ImagePixelData::Ptr); + + /* Allocates hardware storage for the provided software bitmap. + Depending on the initial state, will: + - mark the GPU images as needing to be copied from main memory before they are next accessed, or + - mark the GPU images as up-to-date, or + - clear the GPU images, then mark them as up-to-date + */ + Direct2DPixelDataPages (ComSmartPtr, ImagePixelData::Ptr, State); + + /* Returns all pages included in this set. + This will be called before reading from the pages (e.g. when drawing them). + Therefore, this function will check whether the hardware data is out-of-date and + copy from the software image if necessary before returning. + */ + Span getPages(); + + /* Marks this set as needing to be updated from the software image. + We don't actually do the copy until the next time that we need to read the hardware pages. + This is to avoid redundant copies in the common case that pages are only drawn on a single + device at a time. + */ + void markOutdated() + { + upToDate = false; + } + +private: + ImagePixelData::Ptr backingData; + std::vector pages; + bool upToDate = false; +}; + +/* Pixel data type providing accelerated access to cached Direct2D textures. + + Direct2D bitmaps are device-dependent resources, but frequently a computer will + have multiple devices, e.g. if there are several GPUs available which is common for laptops. + In order to support a fast image type that can be drawn by any one of the available devices, + we store a software bitmap which acts as the source-of-truth, and cache per-device hardware + bitmaps alongside it. The caching mechanism tries to minimise the amount of redundant work. + + When attempting to access hardware bitmaps, we first check the cache to see whether we've + previously allocated bitmaps for the requested device, and only create bitmaps if none already + exist. + + We only copy from the software backup to hardware memory immediately before accessing the + bitmaps for a particular device, and then only if that hardware bitmap is outdated. All + hardware bitmaps are marked as outdated when a writeable BitmapData is created for the current + PixelData. When creating a low-level graphics context, all hardware bitmaps other than the + render target are marked as outdated. +*/ class Direct2DPixelData : public ImagePixelData, private DxgiAdapterListener { public: using Ptr = ReferenceCountedObjectPtr; + using Page = Direct2DPixelDataPage; + using Pages = Direct2DPixelDataPages; + using State = Pages::State; - static Ptr make (Image::PixelFormat formatToUse, - int w, - int h, - bool clearImageIn, - DxgiAdapter::Ptr adapterIn); + /* Creates image storage, taking ownership of the provided bitmap. + This will immediately copy the content of the image to the software backup, so that the + image can still be drawn if original device goes away. + */ + Direct2DPixelData (ComSmartPtr, ComSmartPtr); - static Ptr fromDirect2DBitmap (DxgiAdapter::Ptr, - ComSmartPtr, - ComSmartPtr); + /* Creates software image storage of the requested size. */ + Direct2DPixelData (Image::PixelFormat, int, int, bool); ~Direct2DPixelData() override; + /* Creates new software image storage with content matching the content of this image. + Does not copy any hardware resources. + */ + ImagePixelData::Ptr clone() override + { + return new Direct2DPixelData (backingData->clone(), State::suitableToRead); + } + + std::unique_ptr createType() const override + { + return std::make_unique(); + } + + /* Creates a graphics context that will use the default device to draw into hardware bitmaps + for that device. When the context is destroyed, the rendered hardware bitmap will be copied + back to software storage. + + This PixelData may hold device resources for devices other than the default device. In that + case, the other device resources will be marked as outdated, to ensure that they are updated + from the software backup before they are next accessed. + */ std::unique_ptr createLowLevelContext() override; - void initialiseBitmapData (Image::BitmapData& bitmap, int x, int y, Image::BitmapData::ReadWriteMode mode) override; + /* Provides access to the software image storage. - ImagePixelData::Ptr clone() override; + If the bitmap data provides write access, then all device resources will be marked as + outdated, to ensure that they are updated from the software backup before they are next + accessed. + */ + void initialiseBitmapData (Image::BitmapData&, int, int, Image::BitmapData::ReadWriteMode) override; void applyGaussianBlurEffect (float radius, Image& result) override; void applySingleChannelBoxBlurEffect (int radius, Image& result) override; - std::unique_ptr createType() const 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 + through the Direct2D API will have unpredictable results. + If you want to render into this image using D2D, call createLowLevelContext. + */ + Span getPagesForContext (ComSmartPtr); - DxgiAdapter::Ptr getAdapter() const { return adapter; } - ComSmartPtr getAdapterD2D1Bitmap(); - - void flushToSoftwareBackup(); + /* Utility function that just returns a pointer to the bitmap for the first page returned from + getPagesForContext. + */ + ComSmartPtr getFirstPageForContext (ComSmartPtr context) + { + const auto pages = getPagesForContext (context); + return ! pages.empty() ? pages.front().bitmap : nullptr; + } private: - Direct2DPixelData (Image::PixelFormat, int, int, bool, DxgiAdapter::Ptr); + Direct2DPixelData (ImagePixelData::Ptr, State); + auto getIteratorForContext (ComSmartPtr); - int getPixelStride() const { return pixelFormat == Image::SingleChannel ? 1 : 4; } - int getLineStride() const { return (getPixelStride() * jmax (1, width) + 3) & ~3; } - - void adapterCreated (DxgiAdapter::Ptr) override; - void adapterRemoved (DxgiAdapter::Ptr) override; - - void initBitmapDataReadOnly (Image::BitmapData&, int, int); - - ComSmartPtr createAdapterBitmap() const; - void createDeviceResources(); + void adapterCreated (DxgiAdapter::Ptr) override {} + void adapterRemoved (DxgiAdapter::Ptr adapter) override + { + if (adapter != nullptr) + pagesForDevice.erase (adapter->direct2DDevice); + } SharedResourcePointer directX; - const bool clearImage; - Image backup; - DxgiAdapter::Ptr adapter; - ComSmartPtr context; - ComSmartPtr nativeBitmap; + ImagePixelData::Ptr backingData; + std::map, Pages> pagesForDevice; + State state; JUCE_LEAK_DETECTOR (Direct2DPixelData) }; diff --git a/modules/juce_graphics/native/juce_DirectX_windows.h b/modules/juce_graphics/native/juce_DirectX_windows.h index 0c2e23eb9f..1959a7ea44 100644 --- a/modules/juce_graphics/native/juce_DirectX_windows.h +++ b/modules/juce_graphics/native/juce_DirectX_windows.h @@ -322,6 +322,8 @@ struct D2DUtilities static Point toPoint (POINT p) noexcept { return { p.x, p.y }; } static POINT toPOINT (Point p) noexcept { return { p.x, p.y }; } + static D2D1_POINT_2U toPOINT_2U (Point p) { return D2D1::Point2U ((UINT32) p.x, (UINT32) p.y); } + static D2D1_COLOR_F toCOLOR_F (Colour c) { return { c.getFloatRed(), c.getFloatGreen(), c.getFloatBlue(), c.getFloatAlpha() }; @@ -331,21 +333,23 @@ struct D2DUtilities { return { transform.mat00, transform.mat10, transform.mat01, transform.mat11, transform.mat02, transform.mat12 }; } + + static Rectangle rectFromSize (D2D1_SIZE_U s) + { + return { (int) s.width, (int) s.height }; + } }; //============================================================================== struct Direct2DDeviceContext { - static ComSmartPtr create (DxgiAdapter::Ptr adapter) + static ComSmartPtr create (ComSmartPtr device) { - if (adapter == nullptr) - return {}; - ComSmartPtr result; - if (const auto hr = adapter->direct2DDevice->CreateDeviceContext (D2D1_DEVICE_CONTEXT_OPTIONS_ENABLE_MULTITHREADED_OPTIMIZATIONS, - result.resetAndGetPointerAddress()); - FAILED (hr)) + if (const auto hr = device->CreateDeviceContext (D2D1_DEVICE_CONTEXT_OPTIONS_ENABLE_MULTITHREADED_OPTIMIZATIONS, + result.resetAndGetPointerAddress()); + FAILED (hr)) { jassertfalse; return {}; @@ -358,6 +362,11 @@ struct Direct2DDeviceContext return result; } + static ComSmartPtr create (DxgiAdapter::Ptr adapter) + { + return adapter != nullptr ? create (adapter->direct2DDevice) : nullptr; + } + Direct2DDeviceContext() = delete; }; diff --git a/modules/juce_gui_basics/native/juce_Windowing_windows.cpp b/modules/juce_gui_basics/native/juce_Windowing_windows.cpp index 7a1a7de954..57eefd8a1f 100644 --- a/modules/juce_gui_basics/native/juce_Windowing_windows.cpp +++ b/modules/juce_gui_basics/native/juce_Windowing_windows.cpp @@ -5313,7 +5313,7 @@ private: Image getImage() const { - return Image { Direct2DPixelData::fromDirect2DBitmap (adapter, deviceContext, bitmap) }; + return Image { new Direct2DPixelData { deviceContext, bitmap } }; } ComSmartPtr getBitmap() const