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

Direct2D: Add support for bitmaps spanning multiple texture pages

This commit is contained in:
reuk 2024-09-03 18:47:32 +01:00
parent 470ada4454
commit 589d9940ed
No known key found for this signature in database
GPG key ID: FCB43929F012EE5C
7 changed files with 873 additions and 456 deletions

View file

@ -80,6 +80,9 @@ public:
{
}
Rectangle<int> getSubsection() const { return area; }
ImagePixelData::Ptr getSourcePixelData() const { return sourceImage; }
std::unique_ptr<LowLevelGraphicsContext> createLowLevelContext() override
{
auto g = sourceImage->createLowLevelContext();

View file

@ -345,9 +345,9 @@ public:
const auto d2d1Bitmap = [&]
{
if (auto direct2DPixelData = dynamic_cast<Direct2DPixelData*> (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<ID2D1Bitmap> d2d1Bitmap;
if (auto direct2DPixelData = dynamic_cast<Direct2DPixelData*> (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<ID2D1Bitmap1> d2d1Bitmap;
auto image = NativeImageType{}.convert (imageIn);
Direct2DPixelData* nativeBitmap = nullptr;
Rectangle<int> imageClipArea;
if (auto direct2DPixelData = dynamic_cast<Direct2DPixelData*> (image.getPixelData()))
const auto imageTransform = currentState->currentTransform.getTransformWith (transform);
if (auto* subsectionPixelData = dynamic_cast<SubsectionPixelData*> (image.getPixelData()))
{
d2d1Bitmap = direct2DPixelData->getAdapterD2D1Bitmap();
if (auto direct2DPixelData = dynamic_cast<Direct2DPixelData*> (subsectionPixelData->getSourcePixelData().get()))
{
nativeBitmap = direct2DPixelData;
imageClipArea = subsectionPixelData->getSubsection();
}
}
else if (auto direct2DPixelData = dynamic_cast<Direct2DPixelData*> (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<float> { 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<float>::pi * 0.25f),
AffineTransform::rotation (MathConstants<float>::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

View file

@ -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);

File diff suppressed because it is too large Load diff

View file

@ -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<int> getBounds() const;
/* The stored subsection bitmap. */
ComSmartPtr<ID2D1Bitmap1> bitmap;
/* The top-left position of this virtual bitmap inside the virtual bitmap. */
Point<int> 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<ID2D1Bitmap1>, 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<ID2D1DeviceContext1>, 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<const Page> 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<Direct2DPixelDataPage> 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<Direct2DPixelData>;
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<ID2D1DeviceContext1>, ComSmartPtr<ID2D1Bitmap1>);
static Ptr fromDirect2DBitmap (DxgiAdapter::Ptr,
ComSmartPtr<ID2D1DeviceContext1>,
ComSmartPtr<ID2D1Bitmap1>);
/* 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<ImageType> createType() const override
{
return std::make_unique<NativeImageType>();
}
/* 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<LowLevelGraphicsContext> 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<ImageType> 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<const Page> getPagesForContext (ComSmartPtr<ID2D1DeviceContext1>);
DxgiAdapter::Ptr getAdapter() const { return adapter; }
ComSmartPtr<ID2D1Bitmap1> getAdapterD2D1Bitmap();
void flushToSoftwareBackup();
/* Utility function that just returns a pointer to the bitmap for the first page returned from
getPagesForContext.
*/
ComSmartPtr<ID2D1Bitmap1> getFirstPageForContext (ComSmartPtr<ID2D1DeviceContext1> 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<ID2D1DeviceContext1>);
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<ID2D1Bitmap1> createAdapterBitmap() const;
void createDeviceResources();
void adapterCreated (DxgiAdapter::Ptr) override {}
void adapterRemoved (DxgiAdapter::Ptr adapter) override
{
if (adapter != nullptr)
pagesForDevice.erase (adapter->direct2DDevice);
}
SharedResourcePointer<DirectX> directX;
const bool clearImage;
Image backup;
DxgiAdapter::Ptr adapter;
ComSmartPtr<ID2D1DeviceContext1> context;
ComSmartPtr<ID2D1Bitmap1> nativeBitmap;
ImagePixelData::Ptr backingData;
std::map<ComSmartPtr<ID2D1Device1>, Pages> pagesForDevice;
State state;
JUCE_LEAK_DETECTOR (Direct2DPixelData)
};

View file

@ -322,6 +322,8 @@ struct D2DUtilities
static Point<int> toPoint (POINT p) noexcept { return { p.x, p.y }; }
static POINT toPOINT (Point<int> p) noexcept { return { p.x, p.y }; }
static D2D1_POINT_2U toPOINT_2U (Point<int> 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<int> rectFromSize (D2D1_SIZE_U s)
{
return { (int) s.width, (int) s.height };
}
};
//==============================================================================
struct Direct2DDeviceContext
{
static ComSmartPtr<ID2D1DeviceContext1> create (DxgiAdapter::Ptr adapter)
static ComSmartPtr<ID2D1DeviceContext1> create (ComSmartPtr<ID2D1Device1> device)
{
if (adapter == nullptr)
return {};
ComSmartPtr<ID2D1DeviceContext1> 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<ID2D1DeviceContext1> create (DxgiAdapter::Ptr adapter)
{
return adapter != nullptr ? create (adapter->direct2DDevice) : nullptr;
}
Direct2DDeviceContext() = delete;
};

View file

@ -5313,7 +5313,7 @@ private:
Image getImage() const
{
return Image { Direct2DPixelData::fromDirect2DBitmap (adapter, deviceContext, bitmap) };
return Image { new Direct2DPixelData { deviceContext, bitmap } };
}
ComSmartPtr<ID2D1Bitmap1> getBitmap() const