@@ -234,7 +234,7 @@ bool GPUUploadManagerImpl::Page::ScheduleBufferUpdate(IBuffer*
234234
235235void 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
285285bool 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
493517bool 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 )
0 commit comments