I'm beginning learning Direct3D 12 and having difficulty in understanding CPU-GPU synchronization. As far as I understand, fence (ID3D12Fence) is no more than a UINT64(unsigned long long) value used as counter. But its methods confuse me. The below is a part of source code from D3D12 example.(https://github.com/d3dcoder/d3d12book)
void D3DApp::FlushCommandQueue()
{
// Advance the fence value to mark commands up to this fence point.
mCurrentFence++;
// Add an instruction to the command queue to set a new fence point. Because we
// are on the GPU timeline, the new fence point won't be set until the GPU finishes
// processing all the commands prior to this Signal().
ThrowIfFailed(mCommandQueue->Signal(mFence.Get(), mCurrentFence));
// Wait until the GPU has completed commands up to this fence point.
if(mFence->GetCompletedValue() < mCurrentFence)
{
HANDLE eventHandle = CreateEventEx(nullptr, false, false, EVENT_ALL_ACCESS);
// Fire event when GPU hits current fence.
ThrowIfFailed(mFence->SetEventOnCompletion(mCurrentFence, eventHandle));
// Wait until the GPU hits current fence event is fired.
WaitForSingleObject(eventHandle, INFINITE);
CloseHandle(eventHandle);
}
}
As far as I understand, this part is trying to 'Flush' the command queue, which is basically making CPU wait for GPU until it reaches to given 'Fence value' so that CPU and GPU have identical fence value.
Q. If this Signal() is a function that lets GPU to update the fence value inside given ID3D12Fence, why is that mCurrentFence value needed?
According to Microsoft Doc, it says "Updates a fence to a specified value." What specified value? What I need is "Get Last Completed Command List Value", not set or specify. What is this specified value for?
To me, it seems it has to be like
// Suppose mCurrentFence is 1 after submitting 1 command list (Index 0), and the thread reached to here for the FIRST time
ThrowIfFailed(mCommandQueue->Signal(mFence.Get()));
// At this point Fence value inside mFence is updated
if (m_Fence->GetCompletedValue() < mCurrentFence)
{
...
}
if m_Fence->GetCompletedValue() is 0,
if (0 < 1)
GPU hasn't operated the command list (Index 0), then CPU has to wait until GPU follows up. Then it makes sense calling SetEventOnCompletion, WaitForSingleObject, etc.
if (1 < 1)
GPU has completed the command list (Index 0), so CPU does not need to wait.
Increment mCurrentFence somewhere where command list is executed.
mCommandQueue->ExecuteCommandLists(_countof(cmdsLists), cmdsLists);
mCurrentFence++;
mCommandQueue->Signal(mFence.Get(), mCurrentFence)
sets the fence value tomCurrentFence
as soon as all previously queued commands on the command queue have been executed. In this case, the "specified value" is mCurrentFence.When you start, both, the value of the fence and mCurrentFence are set to 0. Next, mCurrentFence is set to 1. Then we do
mCommandQueue->Signal(mFence.Get(), 1)
which sets the fence to 1 as soon as everything was executed on that queue. Finally we callmFence->SetEventOnCompletion(1, eventHandle)
followed byWaitForSingleObject
to wait until the fence gets set to 1.Replace 1 with 2 for the next iteration and so on.
Note that
mCommandQueue->Signal
is a nonblocking operation and does not immediately set the value of the fence, only after all other gpu commands have been executed. You can assume thatm_Fence->GetCompletedValue() < mCurrentFence
is always true in this example.why is that mCurrentFence value needed?
I suppose it is not necessarily needed, but you avoid an additional API call by keeping track of the fence value this way. In this case you could also do: