1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-09 23:34:20 +00:00

Direct2D: Simplify threading of swapchain presentation

Previously, IDXGISwapChain::Present was called on a background thread,
which made it difficult to avoid race conditions. e.g. during a
live-resize of a window, we would occasionally draw old incomplete
frames instead of new frames at the correct size.

The new approach moves the Present call to the main thread via
AsyncUpdater. We attempt to present whenever the swap event wakes, and
whenever a frame is drawn. Only a single Present call may be made after
the swap event wakes. Subsequent Present calls will be ignored until the
next time the swap event wakes.
This commit is contained in:
reuk 2024-10-02 19:06:25 +01:00
parent c7f7a7c1bb
commit c57041e5bc
No known key found for this signature in database
GPG key ID: FCB43929F012EE5C
3 changed files with 58 additions and 323 deletions

View file

@ -595,7 +595,7 @@ public:
const auto paintAreas = getPaintAreas();
const auto paintBounds = paintAreas.getBounds();
if (! getFrameSize().intersects (paintBounds) || paintBounds.isEmpty())
if (! getFrameSize().intersects (paintBounds) || paintBounds.isEmpty() || paintAreas.isEmpty())
return nullptr;
// Is Direct2D ready to paint?

View file

@ -35,250 +35,44 @@
namespace juce
{
class Presentation
{
public:
auto getPresentationBitmap() const
{
jassert (presentationBitmap != nullptr);
return presentationBitmap;
}
auto getPresentationBitmap (const Rectangle<int>& swapSize, ComSmartPtr<ID2D1DeviceContext1> context)
{
if (presentationBitmap != nullptr)
{
const auto size = presentationBitmap->GetPixelSize();
if (size.width != (uint32) swapSize.getWidth() || size.height != (uint32) swapSize.getHeight())
presentationBitmap = nullptr;
}
if (presentationBitmap == nullptr)
{
presentationBitmap = Direct2DBitmap::createBitmap (context,
Image::ARGB,
{ (uint32) swapSize.getWidth(), (uint32) swapSize.getHeight() },
D2D1_BITMAP_OPTIONS_TARGET);
}
return presentationBitmap;
}
void setPaintAreas (RectangleList<int> areas)
{
paintAreas = std::move (areas);
}
const RectangleList<int>& getPaintAreas() const
{
return paintAreas;
}
private:
ComSmartPtr<ID2D1Bitmap> presentationBitmap;
RectangleList<int> paintAreas;
};
class PresentationQueue
{
public:
Presentation* lockFront()
{
const std::scoped_lock lock { mutex };
displaying = std::exchange (readyToDisplay, nullptr);
return displaying;
}
void unlockFront()
{
const std::scoped_lock lock { mutex };
displaying = nullptr;
}
Presentation* lockBack()
{
const std::scoped_lock lock { mutex };
preparing = [&]() -> Presentation*
{
for (auto& p : presentations)
if (&p != displaying && &p != readyToDisplay)
return &p;
return nullptr;
}();
return preparing;
}
void unlockBack()
{
{
const std::scoped_lock lock { mutex };
if (preparing == nullptr)
return;
if (readyToDisplay != nullptr)
{
// Copy the dirty regions from the newest presentation over the top of the 'ready'
// presentation, then combine dirty regions.
// We're effectively combining several frames of dirty regions into one, until
// the screen update catches up.
for (const auto& area : preparing->getPaintAreas())
{
const auto destPoint = D2DUtilities::toPOINT_2U (area.getPosition());
const auto sourceRect = D2DUtilities::toRECT_U (area);
readyToDisplay->getPresentationBitmap()->CopyFromBitmap (&destPoint, preparing->getPresentationBitmap(), &sourceRect);
}
auto areas = readyToDisplay->getPaintAreas();
areas.add (preparing->getPaintAreas());
readyToDisplay->setPaintAreas (std::move (areas));
}
else
{
readyToDisplay = std::exchange (preparing, nullptr);
}
}
SetEvent (wakeEvent.getHandle());
}
HANDLE getWakeEvent() const
{
return wakeEvent.getHandle();
}
private:
WindowsScopedEvent wakeEvent;
std::mutex mutex;
std::array<Presentation, 2> presentations;
Presentation* preparing = nullptr;
Presentation* readyToDisplay = nullptr;
Presentation* displaying = nullptr;
};
template <auto lock, auto unlock>
class PresentationQueueLock
{
public:
PresentationQueueLock() = default;
explicit PresentationQueueLock (PresentationQueue& q)
: queue (&q),
presentation (queue != nullptr ? (queue->*lock)() : nullptr)
{
}
~PresentationQueueLock()
{
if (queue != nullptr)
(queue->*unlock)();
}
PresentationQueueLock (PresentationQueueLock&& other) noexcept
: queue (std::exchange (other.queue, nullptr)),
presentation (std::exchange (other.presentation, nullptr))
{
}
PresentationQueueLock& operator= (PresentationQueueLock&& other) noexcept
{
PresentationQueueLock { std::move (other) }.swap (*this);
return *this;
}
PresentationQueueLock (const PresentationQueueLock&) = delete;
PresentationQueueLock& operator= (const PresentationQueueLock&) = delete;
Presentation* getPresentation() const { return presentation; }
private:
void swap (PresentationQueueLock& other) noexcept
{
std::swap (other.queue, queue);
std::swap (other.presentation, presentation);
}
PresentationQueue* queue = nullptr;
Presentation* presentation = nullptr;
};
using BackBufferLock = PresentationQueueLock<&PresentationQueue::lockBack, &PresentationQueue::unlockBack>;
using FrontBufferLock = PresentationQueueLock<&PresentationQueue::lockFront, &PresentationQueue::unlockFront>;
struct Direct2DHwndContext::HwndPimpl : public Direct2DGraphicsContext::Pimpl
{
private:
struct SwapChainThread
struct SwapChainThread : private AsyncUpdater
{
SwapChainThread (Direct2DHwndContext::HwndPimpl& ownerIn,
ComSmartPtr<ID2D1Multithread> multithreadIn)
explicit SwapChainThread (Direct2DHwndContext::HwndPimpl& ownerIn)
: owner (ownerIn),
multithread (multithreadIn),
swapChainEventHandle (ownerIn.swap.swapChainEvent->getHandle())
{
}
~SwapChainThread()
~SwapChainThread() override
{
cancelPendingUpdate();
SetEvent (quitEvent.getHandle());
thread.join();
}
BackBufferLock getFreshPresentation()
{
return BackBufferLock (queue);
}
void notify()
{
SetEvent (queue.getWakeEvent());
}
private:
Direct2DHwndContext::HwndPimpl& owner;
PresentationQueue queue;
ComSmartPtr<ID2D1Multithread> multithread;
HANDLE swapChainEventHandle = nullptr;
WindowsScopedEvent quitEvent;
std::thread thread { [&] { threadLoop(); } };
void handleAsyncUpdate() override
{
owner.swapEventReceived = true;
owner.present();
}
void threadLoop()
{
Thread::setCurrentThreadName ("JUCE D2D swap chain thread");
bool swapChainReady = false;
const auto serviceSwapChain = [&]
{
if (! swapChainReady)
return;
FrontBufferLock frontBufferLock { queue };
auto* frontBuffer = frontBufferLock.getPresentation();
if (frontBuffer == nullptr)
return;
JUCE_D2DMETRICS_SCOPED_ELAPSED_TIME (owner.owner.metrics, swapChainThreadTime);
{
ScopedMultithread scopedMultithread { multithread };
owner.present (frontBuffer, 0);
}
swapChainReady = false;
};
for (;;)
{
const HANDLE handles[] { swapChainEventHandle, quitEvent.getHandle(), queue.getWakeEvent() };
const HANDLE handles[] { swapChainEventHandle, quitEvent.getHandle() };
const auto waitResult = WaitForMultipleObjects ((DWORD) std::size (handles), handles, FALSE, INFINITE);
@ -286,20 +80,13 @@ private:
{
case WAIT_OBJECT_0:
{
swapChainReady = true;
serviceSwapChain();
triggerAsyncUpdate();
break;
}
case WAIT_OBJECT_0 + 1:
return;
case WAIT_OBJECT_0 + 2:
{
serviceSwapChain();
break;
}
case WAIT_FAILED:
default:
jassertfalse;
@ -312,14 +99,22 @@ private:
SwapChain swap;
ComSmartPtr<ID2D1DeviceContext1> deviceContext;
std::unique_ptr<SwapChainThread> swapChainThread;
BackBufferLock presentation;
std::optional<CompositionTree> compositionTree;
// Areas that must be repainted during the next paint call, between startFrame/endFrame
RectangleList<int> deferredRepaints;
// Areas that have been updated in the backbuffer, but not presented
RectangleList<int> dirtyRegionsInBackBuffer;
std::vector<RECT> dirtyRectangles;
int64 lastFinishFrameTicks = 0;
HWND hwnd = nullptr;
// Set to true after the swap event is signalled, indicating that we're allowed to try presenting
// a new frame.
bool swapEventReceived = false;
bool prepare() override
{
const auto adapter = directX->adapters.getAdapterForHwnd (hwnd);
@ -352,7 +147,7 @@ private:
}
if (! swapChainThread && swap.swapChainEvent.has_value())
swapChainThread = std::make_unique<SwapChainThread> (*this, directX->getD2DMultithread());
swapChainThread = std::make_unique<SwapChainThread> (*this);
if (! compositionTree.has_value())
compositionTree = CompositionTree::create (adapter->dxgiDevice, hwnd, swap.chain);
@ -388,19 +183,15 @@ private:
if (auto now = Time::getHighResolutionTicks(); Time::highResolutionTicksToSeconds (now - lastFinishFrameTicks) < 0.001)
return false;
if (presentation.getPresentation() == nullptr)
if (swapChainThread != nullptr)
presentation = swapChainThread->getFreshPresentation();
// Paint if:
// resources are allocated
// deferredRepaints has areas to be painted
// the swap chain thread is ready
bool ready = Pimpl::checkPaintReady();
ready &= swap.canPaint();
ready &= swap.buffer != nullptr;
ready &= compositionTree.has_value();
ready &= ! getPaintAreas().isEmpty();
ready &= presentation.getPresentation() != nullptr;
return ready;
}
@ -444,16 +235,13 @@ public:
ComSmartPtr<ID2D1Image> getDeviceContextTarget() const override
{
if (auto* p = presentation.getPresentation())
return p->getPresentationBitmap (swap.getSize(), getDeviceContext());
return {};
return swap.buffer;
}
bool setSize (Rectangle<int> size)
void setSize (Rectangle<int> size)
{
if (size == swap.getSize() || size.isEmpty())
return false;
return;
// Require the entire window to be repainted
deferredRepaints = size;
@ -464,18 +252,11 @@ public:
if (auto dc = getDeviceContext())
{
ScopedMultithread scopedMultithread { directX->getD2DMultithread() };
auto hr = swap.resize (size, dc);
jassert (SUCCEEDED (hr));
if (FAILED (hr))
teardown();
if (swapChainThread)
swapChainThread->notify();
}
return true;
}
void addDeferredRepaint (Rectangle<int> deferredRepaint)
@ -497,103 +278,58 @@ public:
// If a new frame is starting, clear deferredAreas in case repaint is called
// while the frame is being painted to ensure the new areas are painted on the
// next frame
JUCE_TRACE_LOG_D2D_PAINT_CALL (etw::direct2dHwndPaintStart, owner.getFrameId());
presentation.getPresentation()->setPaintAreas (getPaintAreas());
dirtyRegionsInBackBuffer.add (deferredRepaints);
deferredRepaints.clear();
JUCE_TRACE_LOG_D2D_PAINT_CALL (etw::direct2dHwndPaintStart, owner.getFrameId());
return savedState;
}
HRESULT finishFrame() override
{
const ScopeGuard scope { [this]
{
presentation = {};
lastFinishFrameTicks = Time::getHighResolutionTicks();
} };
return Pimpl::finishFrame();
const auto result = Pimpl::finishFrame();
present();
lastFinishFrameTicks = Time::getHighResolutionTicks();
return result;
}
void present (Presentation* paintedPresentation, uint32 flags)
void present()
{
if (paintedPresentation == nullptr)
return;
// Fill out the array of dirty rectangles
// Compare paintAreas to the swap chain buffer area. If the rectangles in paintAreas are contained
// by the swap chain buffer area, then mark those rectangles as dirty. DXGI will only keep the dirty rectangles from the
// current buffer and copy the clean area from the previous buffer.
// The buffer needs to be completely filled before using dirty rectangles. The dirty rectangles need to be contained
// within the swap chain buffer.
JUCE_D2DMETRICS_SCOPED_ELAPSED_TIME (owner.metrics, present1Duration);
// Allocate enough memory for the array of dirty rectangles
const auto areas = paintedPresentation->getPaintAreas();
paintedPresentation->setPaintAreas ({});
if (swap.buffer == nullptr || dirtyRegionsInBackBuffer.isEmpty() || ! swapEventReceived)
return;
dirtyRectangles.resize ((size_t) areas.getNumRectangles());
// Allocate enough memory for the array of dirty rectangles
dirtyRectangles.resize ((size_t) dirtyRegionsInBackBuffer.getNumRectangles());
// Fill the array of dirty rectangles, intersecting each paint area with the swap chain buffer
DXGI_PRESENT_PARAMETERS presentParameters{};
presentParameters.pDirtyRects = dirtyRectangles.data();
presentParameters.DirtyRectsCount = 0;
if (swap.state == SwapChain::State::bufferFilled)
auto const swapChainSize = swap.getSize();
for (const auto& area : dirtyRegionsInBackBuffer)
{
auto* dirtyRectangle = dirtyRectangles.data();
auto const swapChainSize = swap.getSize();
for (const auto& area : areas)
{
// If this paint area contains the entire swap chain, then
// no need for dirty rectangles
if (area.contains (swapChainSize))
{
presentParameters.DirtyRectsCount = 0;
break;
}
// Intersect this paint area with the swap chain buffer
auto intersection = area.getIntersection (swapChainSize);
if (intersection.isEmpty())
{
// Can't clip to an empty rectangle
continue;
}
D2D1_POINT_2U destPoint { (uint32) intersection.getX(), (uint32) intersection.getY() };
D2D1_RECT_U sourceRect { (uint32) intersection.getX(),
(uint32) intersection.getY(),
(uint32) intersection.getRight(),
(uint32) intersection.getBottom() };
swap.buffer->CopyFromBitmap (&destPoint, paintedPresentation->getPresentationBitmap(), &sourceRect);
// Add this intersected paint area to the dirty rectangle array (scaled for DPI)
*dirtyRectangle = D2DUtilities::toRECT (intersection);
dirtyRectangle++;
presentParameters.DirtyRectsCount++;
}
presentParameters.pDirtyRects = dirtyRectangles.data();
}
if (presentParameters.DirtyRectsCount == 0)
{
D2D1_POINT_2U destPoint { 0, 0 };
if (auto bitmap = paintedPresentation->getPresentationBitmap())
swap.buffer->CopyFromBitmap (&destPoint, bitmap, nullptr);
if (const auto intersection = area.getIntersection (swapChainSize); ! intersection.isEmpty())
presentParameters.pDirtyRects[presentParameters.DirtyRectsCount++] = D2DUtilities::toRECT (intersection);
}
// Present the freshly painted buffer
const auto hr = swap.chain->Present1 (swap.presentSyncInterval, swap.presentFlags | flags, &presentParameters);
const auto hr = swap.chain->Present1 (swap.presentSyncInterval, swap.presentFlags, &presentParameters);
jassertquiet (SUCCEEDED (hr));
// The buffer is now completely filled and ready for dirty rectangles for the next frame
swap.state = SwapChain::State::bufferFilled;
if (FAILED (hr))
return;
// We managed to present a frame, so we should avoid rendering anything or calling
// present again until that frame has been shown on-screen.
swapEventReceived = false;
// There's nothing waiting to be displayed in the backbuffer.
dirtyRegionsInBackBuffer.clear();
JUCE_TRACE_LOG_D2D_PAINT_CALL (etw::direct2dHwndPaintEnd, owner.getFrameId());
}

View file

@ -501,7 +501,6 @@ public:
idle,
chainAllocated,
bufferAllocated,
bufferFilled
};
State state = State::idle;
};