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:
parent
c7f7a7c1bb
commit
c57041e5bc
3 changed files with 58 additions and 323 deletions
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -501,7 +501,6 @@ public:
|
|||
idle,
|
||||
chainAllocated,
|
||||
bufferAllocated,
|
||||
bufferFilled
|
||||
};
|
||||
State state = State::idle;
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue