Skip to content

Commit b6a352c

Browse files
GPUUploadManager: fix potential issue with stale pages in ScheduleBufferUpdate
1 parent deb956d commit b6a352c

4 files changed

Lines changed: 38 additions & 12 deletions

File tree

Graphics/GraphicsTools/include/GPUUploadManagerImpl.hpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,12 +148,12 @@ class GPUUploadManagerImpl final : public ObjectBase<IGPUUploadManager>
148148
// Returns the number of pending operations.
149149
size_t GetNumPendingOps() const { return m_NumPendingOps.load(std::memory_order_acquire); }
150150

151+
// Returns true if the page is sealed for new writes.
152+
bool IsSealed() const { return (m_State.load(std::memory_order_acquire) & SEALED_BIT) != 0; }
153+
151154
#ifdef DILIGENT_DEBUG
152155
// Returns the number of active writers. This is used for testing and debugging purposes.
153156
Uint32 DbgGetWriterCount() const { return m_State.load(std::memory_order_acquire) & WRITER_MASK; }
154-
155-
// Returns true if the page is sealed for new writes. This is used for testing and debugging purposes.
156-
bool DbgIsSealed() const { return (m_State.load(std::memory_order_acquire) & SEALED_BIT) != 0; }
157157
#endif
158158

159159
void ReleaseStagingBuffer(IDeviceContext* pContext);

Graphics/GraphicsTools/interface/GPUUploadManager.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,8 @@ DILIGENT_BEGIN_INTERFACE(IGPUUploadManager, IObject)
135135
void* pCallbackData DEFAULT_VALUE(nullptr)) PURE;
136136

137137
/// Retrieves GPU upload manager statistics.
138+
///
139+
/// The method must not be called concurrently with RenderThreadUpdate().
138140
VIRTUAL void METHOD(GetStats)(THIS_
139141
GPUUploadManagerStats REF Stats) CONST PURE;
140142
};

Graphics/GraphicsTools/src/GPUUploadManagerImpl.cpp

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ bool GPUUploadManagerImpl::Page::ScheduleBufferUpdate(IBuffer*
234234

235235
void GPUUploadManagerImpl::Page::ExecutePendingOps(IDeviceContext* pContext, Uint64 FenceValue)
236236
{
237-
VERIFY(DbgIsSealed(), "Page must be sealed before executing pending operations");
237+
VERIFY(IsSealed(), "Page must be sealed before executing pending operations");
238238
VERIFY(DbgGetWriterCount() == 0, "All writers must finish before executing pending operations");
239239

240240
if (m_pData != nullptr && !m_PersistentMapped)
@@ -284,7 +284,7 @@ void GPUUploadManagerImpl::Page::Reset(IDeviceContext* pContext)
284284

285285
bool GPUUploadManagerImpl::Page::TryEnqueue()
286286
{
287-
VERIFY(DbgIsSealed(), "Page must be sealed before it can be enqueued for execution");
287+
VERIFY(IsSealed(), "Page must be sealed before it can be enqueued for execution");
288288
VERIFY(DbgGetWriterCount() == 0, "All writers must finish before the page can be enqueued");
289289

290290
bool Expected = false;
@@ -412,14 +412,38 @@ void GPUUploadManagerImpl::ScheduleBufferUpdate(IDeviceContext* pC
412412
continue;
413413
}
414414

415-
const bool UpdateScheduled = Writer.ScheduleBufferUpdate(pDstBuffer, DstOffset, NumBytes, pSrcData, Callback, pCallbackData);
416-
if (Writer.EndWriting() == Page::WritingStatus::LastWriterSealed)
415+
auto EndWriting = [&]() {
416+
if (Writer.EndWriting() == Page::WritingStatus::LastWriterSealed)
417+
{
418+
// We were the last writer - enqueue the page whether the update was successfully
419+
// scheduled or not, because the page is sealed and can't be written to anymore.
420+
TryEnqueuePage(P);
421+
}
422+
};
423+
424+
// Prevent ABA-style bug when pages are reset and reused
425+
// * Thread T1 loads P0 as current, then gets descheduled before TryBeginWriting().
426+
// * Render thread swaps current to P1, seals P0, sees it has 0 ops, calls Reset(nullptr)
427+
// which clears SEALED_BIT, and pushes P0 to FreePages
428+
// * T1 resumes and calls P0->TryBeginWriting() and succeeds (because SEALED_BIT is now cleared).
429+
// * T1 schedules an update into a page that is not current and is simultaneously sitting in the free list,
430+
// potentially being popped/installed elsewhere.
417431
{
418-
// We were the last writer - enqueue the page whether the update was successfully scheduled or not,
419-
// because the page is sealed and can't be written to anymore.
420-
TryEnqueuePage(P);
432+
Page* CurNow = m_pCurrentPage.load(std::memory_order_acquire);
433+
// Validate that the page is either:
434+
// * still current OR
435+
// * already sealed (meaning it got rotated after we began writing, which is fine because writer count protects it)
436+
if (P != CurNow && !P->IsSealed())
437+
{
438+
EndWriting();
439+
// Reload current and retry
440+
continue;
441+
}
421442
}
422443

444+
const bool UpdateScheduled = Writer.ScheduleBufferUpdate(pDstBuffer, DstOffset, NumBytes, pSrcData, Callback, pCallbackData);
445+
EndWriting();
446+
423447
if (UpdateScheduled)
424448
{
425449
break;
@@ -492,7 +516,7 @@ bool GPUUploadManagerImpl::TryRotatePage(IDeviceContext* pContext, Page* Expecte
492516

493517
bool GPUUploadManagerImpl::TryEnqueuePage(Page* P)
494518
{
495-
VERIFY_EXPR(P->DbgIsSealed());
519+
VERIFY_EXPR(P->IsSealed());
496520
if (P->TryEnqueue())
497521
{
498522
if (P->GetNumPendingOps() > 0)

Tests/DiligentCoreTest/src/GraphicsTools/GPUUploadManagerPageTest.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ TEST(GPUUploadManagerPageTest, ParallelTryBeginWriting)
166166
LOG_INFO_MESSAGE("Total writes: ", TotalWrites.load(), " out of ", kNumThreads * kNumIterations);
167167

168168
EXPECT_EQ(NumPagesAlreadySealed.load(), kNumThreads - 1) << "Only one thread should be able to seal the page";
169-
EXPECT_EQ(NumLastWriters.load(), 1) << "Only one thread should be the last writer";
169+
EXPECT_EQ(NumLastWriters.load(), 1u) << "Only one thread should be the last writer";
170170
}
171171

172172

0 commit comments

Comments
 (0)