Как синхронизировать ЦП и ГП с помощью забора в DirectX/Direct3D 12?

Я начинаю изучать Direct3D 12 и с трудом понимаю синхронизацию CPU-GPU. Насколько я понимаю, забор (ID3D12Fence) - это не более чем значение UINT64 (unsigned long long), используемое в качестве счетчика. Но его методы меня смущают. Ниже приведена часть исходного кода примера D3D12.(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);
    }
}

Насколько я понимаю, эта часть пытается «сбросить» очередь команд, что в основном заставляет ЦП ждать GPU, пока он не достигнет заданного «значения Fence», чтобы ЦП и GPU имели одинаковое значение ограничения.

В. Если этот Signal() является функцией, которая позволяет графическому процессору обновлять значение границы внутри заданного ID3D12Fence, зачем нужно это значение mCurrentFence?

Согласно Microsoft Doc, он говорит: «Обновляет забор до указанного значения». Какое указанное значение? Мне нужно «Получить последнее значение списка завершенных команд», а не устанавливать или указывать. Для чего это указанное значение?

Мне кажется, так и должно быть

// 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)
{
...
}

если m_Fence->GetCompletedValue() равно 0,

if (0 < 1)

Графический процессор не обработал список команд (индекс 0), тогда ЦП должен ждать, пока ГП не выполнит его. Тогда имеет смысл вызывать SetEventOnCompletion, WaitForSingleObject и т.д.

if (1 < 1)

Графический процессор завершил список команд (индекс 0), поэтому ЦП не нужно ждать.

Увеличить значение mCurrentFence там, где выполняется список команд.

mCommandQueue->ExecuteCommandLists(_countof(cmdsLists), cmdsLists);
mCurrentFence++;

person YoonSeok OH    schedule 24.10.2019    source источник


Ответы (2)


mCommandQueue->Signal(mFence.Get(), mCurrentFence) устанавливает значение ограничения на mCurrentFence, как только все ранее поставленные в очередь команды в очереди команд были выполнены. В этом случае «указанное значение» — это mCurrentFence.

Когда вы начинаете, значение забора и mCurrentFence устанавливается на 0. Затем mCurrentFence устанавливается на 1. Затем мы делаем mCommandQueue->Signal(mFence.Get(), 1), который устанавливает забор на 1, как только все было выполнено в этой очереди. Наконец, мы вызываем mFence->SetEventOnCompletion(1, eventHandle), а затем WaitForSingleObject, чтобы дождаться, пока граница не будет установлена ​​на 1.

Замените 1 на 2 для следующей итерации и так далее.

Обратите внимание, что mCommandQueue->Signal является неблокирующей операцией и не сразу устанавливает значение ограничения, а только после выполнения всех других команд GPU. Вы можете предположить, что m_Fence->GetCompletedValue() < mCurrentFence всегда верно в этом примере.

зачем нужно это значение mCurrentFence?

Я предполагаю, что это не обязательно необходимо, но вы избегаете дополнительного вызова API, отслеживая таким образом значение забора. В этом случае вы также можете сделать:

// retrieve last value of the fence and increment by one (Additional API call)
auto nextFence = mFence->GetCompletedValue() + 1;
ThrowIfFailed(mCommandQueue->Signal(mFence.Get(), nextFence));

// Wait until the GPU has completed commands up to this fence point.
if(mFence->GetCompletedValue() < nextFence)
{
    HANDLE eventHandle = CreateEventEx(nullptr, false, false, EVENT_ALL_ACCESS);  
    ThrowIfFailed(mFence->SetEventOnCompletion(nextFence, eventHandle));
    WaitForSingleObject(eventHandle, INFINITE);
    CloseHandle(eventHandle);
}
person Felix Brüll    schedule 27.10.2019
comment
Как подход к разделению части отправки и части ожидания, можно ли использовать код, как показано ниже? - person YoonSeok OH; 27.10.2019
comment
void SynchronizeWithGPU() { if (mFence->GetCompletedValue() ‹ m_nextFence) { HANDLE eventHandle = CreateEventEx(nullptr, false, false, EVENT_ALL_ACCESS); ThrowIfFailed(mFence->SetEventOnCompletion(m_nextFence, eventHandle)); WaitForSingleObject (обработчик события, БЕСКОНЕЧНО); CloseHandle (событиеHandle); } } - person YoonSeok OH; 27.10.2019
comment
и поместите сигнальную часть рядом с mCommandQueue-›ExecuteCommandLists()? Кажется, это обеспечит больший промежуток времени до тех пор, пока GPU не выполнит команду Signal, поскольку сигнал не обрабатывается немедленно. - person YoonSeok OH; 27.10.2019
comment
Мне кажется, все в порядке. - person Felix Brüll; 27.10.2019

В дополнение к ответу Феликса:

Отслеживание значения ограничения (например, mCurrentFence) полезно для ожидания более конкретных точек в очереди команд.

Например, предположим, что мы используем эту настройку:

ComPtr<ID3D12CommandQueue> queue;
ComPtr<ID3D12Fence> queueFence;
UINT64 fenceVal = 0;

UINT64 incrementFence()
{
    fenceVal++;
    queue->Signal(queueFence.Get(), fenceVal); // CHECK HRESULT
    return fenceVal;
}

void waitFor(UINT64 fenceVal, DWORD timeout = INFINITE)
{
    if (queueFence->GetCompletedValue() < fenceVal)
    {
        queueFence->SetEventOnCompletion(fenceVal, fenceEv); // CHECK HRESULT
        WaitForSingleObject(fenceEv, timeout);
    }
}

Затем мы можем сделать следующее (псевдо):

SUBMIT COMMANDS 1
cmds1Complete = incrementFence();
    .
    . <- CPU STUFF
    .
SUBMIT COMMANDS 2
cmds2Complete = incrementFence();
    .
    . <- CPU STUFF
    .
waitFor(cmds1Complete)
    .
    . <- CPU STUFF (that needs COMMANDS 1 to be complete,
      but COMMANDS 2 is NOT required to be completed [but also could be])
    .
waitFor(cmds2Complete)
    .
    . <- EVERYTHING COMPLETE
    .

Поскольку мы отслеживаем fenceVal, у нас также может быть функция flush, которая просто ожидает отслеживаемого fenceVal (в отличие от значения, возвращаемого из incrementFence). самое последнее значение (поэтому, как сказал Феликс, он просто сохраняет вызов API):

void flushCmdQueue()
{
    waitFor(incrementFence());
}

Этот пример несколько сложнее, чем первоначальная проблема, однако я думаю, что это важно, когда вы спрашиваете об отслеживании mCurrentFence.

person nedb    schedule 18.03.2020