Skip to content

Commit 09a0743

Browse files
committed
Fix double-pop of the execution tracking queue in Base::CommandQueueTracking, introduced by commit 1184cff ("Fix command list set execution state tracking")
The background execution-waiting thread does: 1. PopNextExecutingCommandListSet() // locks queue, pops front, waits on fence, Complete() 2. CompleteCommandListSetExecution() // locks queue again, pops front if it == the set just completed The set is already gone from the queue after the first call, so the second pop was meant to be a no-op. But between the two calls the queue mutex is released, and the main thread — which owns the same CommandListSet object for that frame buffer index — can come all the way around the frame ring and re-execute the very same object, pushing it back to the front. CompleteCommandListSetExecution then matched it by address and popped that new execution out of tracking. From there the failure is deterministic: the set is Executing but untracked, so WaitUntilCompleted(frame_index) finds nothing via IsExecutingOnFrameIndex() and returns without waiting, and the next Reset() on its command lists throws InvalidArgumentException<Rhi::CommandListState> — CommandList.cpp:98. Heavy CPU load widens the window: the background thread gets descheduled right after PopNextExecutingCommandListSet() returns, while the main thread runs a full 3-frame ring in well under a millisecond.
1 parent ff5c1f1 commit 09a0743

2 files changed

Lines changed: 26 additions & 14 deletions

File tree

Modules/Graphics/RHI/Base/Include/Methane/Graphics/Base/CommandQueueTracking.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ class CommandQueueTracking // NOSONAR - destructor is required
101101
Ptr<Rhi::ITimestampQueryPool> GetInitializedTimestampQueryPoolPtr() const;
102102
void CompleteExecutionSafely();
103103
void WaitForExecution() noexcept;
104+
bool IsExecutingCommandListsQueueEmpty() const;
104105
bool IsFrontListExecutingOnFrameIndex(const Opt<Data::Index>& frame_index) const noexcept;
105106
bool IsExecutingOnFrameIndex(const Opt<Data::Index>& frame_index) const noexcept;
106107

Modules/Graphics/RHI/Base/Sources/Methane/Graphics/Base/CommandQueueTracking.cpp

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,10 @@ void CommandQueueTracking::WaitForExecution() noexcept
142142
m_execution_waiting_condition_var.wait_for(lock, std::chrono::milliseconds(32),
143143
[this]
144144
{
145-
return !m_execution_waiting || !m_executing_command_lists.
146-
empty();
145+
// The executing command lists queue is modified by other threads
146+
// (see Execute() and ProcessExecutingCommandListSet()), so it is
147+
// read here under its own mutex rather than under the waiting mutex.
148+
return !m_execution_waiting || !IsExecutingCommandListsQueueEmpty();
147149
}
148150
);
149151

@@ -154,12 +156,10 @@ void CommandQueueTracking::WaitForExecution() noexcept
154156
m_name_changed = false;
155157
}
156158

157-
while (!m_executing_command_lists.empty())
159+
// The loop is driven by the pop result only: testing m_executing_command_lists.empty() here
160+
// would be an unsynchronized read of the queue concurrently modified by the other threads.
161+
while (const Ptr<CommandListSet> command_list_set_ptr = PopNextExecutingCommandListSet())
158162
{
159-
Ptr<CommandListSet> command_list_set_ptr = PopNextExecutingCommandListSet();
160-
if (!command_list_set_ptr)
161-
break;
162-
163163
CompleteCommandListSetExecution(*command_list_set_ptr);
164164
}
165165

@@ -182,6 +182,13 @@ void CommandQueueTracking::WaitForExecution() noexcept
182182
}
183183
}
184184

185+
bool CommandQueueTracking::IsExecutingCommandListsQueueEmpty() const
186+
{
187+
META_FUNCTION_TASK();
188+
std::scoped_lock lock_guard(m_executing_command_lists_mutex);
189+
return m_executing_command_lists.empty();
190+
}
191+
185192
bool CommandQueueTracking::IsFrontListExecutingOnFrameIndex(const Opt<Data::Index>& frame_index) const noexcept
186193
{
187194
const Ptr<CommandListSet> command_list_set_ptr = m_executing_command_lists.front();
@@ -251,15 +258,19 @@ Ptr<CommandListSet> CommandQueueTracking::PopNextExecutingCommandListSet()
251258
return command_list_set_ptr;
252259
}
253260

254-
void CommandQueueTracking::CompleteCommandListSetExecution(CommandListSet& executing_command_list_set)
261+
void CommandQueueTracking::CompleteCommandListSetExecution(CommandListSet&)
255262
{
256263
META_FUNCTION_TASK();
257-
std::unique_lock lock_guard(m_executing_command_lists_mutex);
258-
if (!m_executing_command_lists.empty() &&
259-
m_executing_command_lists.front().get() == std::addressof(executing_command_list_set))
260-
{
261-
m_executing_command_lists.pop();
262-
}
264+
// NOTE: the completed command list set was already popped out of the executing queue by
265+
// PopNextExecutingCommandListSet(), atomically with waiting for its completion, and it must NOT be
266+
// popped here once more. The executing command lists mutex is released by the time this method is
267+
// called, so the main thread may have already re-executed the very same command list set object for
268+
// the next frame with the same frame buffer index, pushing it back to the front of the queue.
269+
// Popping the queue front here used to drop that new execution out of tracking, after which
270+
// WaitUntilCompleted() did not wait for it (IsExecutingOnFrameIndex() found nothing to wait for)
271+
// and the following Reset() of its command lists threw an exception on the 'Executing' state.
272+
// This method is kept as an extension point for the native command queue implementations,
273+
// which need to release per-execution resources of the completed command list set.
263274
}
264275

265276
void CommandQueueTracking::ShutdownQueueExecution()

0 commit comments

Comments
 (0)