Управление ресурсами на основе границ

Показывает, как управлять жизненным циклом данных ресурсов путем отслеживания хода выполнения GPU с помощью ограждений. Память можно эффективно использовать повторно с ограждениями, которые тщательно управляют доступностью свободного места в памяти, например в реализации кольцевого буфера для кучи отправки.

Сценарий кольцевого буфера

Ниже приведен пример, в котором приложение испытывает редкий спрос на память кучи отправки.

Кольцевой буфер — это один из способов управления кучей отправки. Кольцевой буфер содержит данные, необходимые для следующих нескольких кадров. Приложение поддерживает текущий указатель ввода данных и очередь смещения кадра для записи каждого кадра и начального смещения данных ресурсов для этого кадра.

Приложение создает кольцевой буфер на основе буфера для передачи данных в GPU для каждого кадра. В настоящее время кадр 2 отрисовывается, кольцевой буфер обтекает данные для кадра 4, все данные, необходимые для кадра 5, присутствуют, а большой буфер констант, необходимый для кадра 6, необходимо выделить.

Рис. 1 . Приложение пытается выделить часть для буфера констант, но находит недостаточно свободной памяти.

Недостаточно свободной памяти в этом кольцевом буфере

Рис. 2 . При опросе ограждения приложение обнаруживает, что кадр 3 был отрисован, затем обновляется очередь смещения кадра, после чего следует текущее состояние кольцевого буфера, однако свободная память по-прежнему недостаточно велика для размещения буфера констант.

по-прежнему недостаточно памяти после отрисовки кадра 3

Рис. 3 . Учитывая ситуацию, ЦП блокирует себя (через ограждение ожидания), пока кадр 4 не будет отрисован, что освобождает память, выделенную для кадра 4.

Отрисовка кадра 4 освобождает больше кольцевого буфера

Рис. 4 . Теперь объем свободной памяти достаточно велик для буфера констант, а выделение подсети выполняется успешно; приложение копирует данные буфера больших констант в память, которая ранее использовалась данными ресурсов для кадров 3 и 4. Текущий указатель ввода, наконец, обновлен.

теперь есть место из кадра 6 в кольцевом буфере

Если приложение реализует кольцевой буфер, кольцевой буфер должен быть достаточно большим, чтобы справиться с худшим сценарием размеров данных ресурсов.

Пример кольцевого буфера

В следующем примере кода показано, как можно управлять кольцевым буфером, обращая внимание на подпрограмму подраспределения, которая обрабатывает опрос и ожидание ограждения. Для простоты в примере используется NOT_SUFFICIENT_MEMORY для скрытия сведений о том, что "недостаточно свободной памяти в куче", так как эта логика (на основе m_pDataCur и смещения внутри FrameOffsetQueue) не тесно связана с кучами или ограждениями. Выборка упрощена, чтобы пожертвовать частотой кадров вместо использования памяти.

Обратите внимание, что поддержка кольцевого буфера, как ожидается, будет популярным сценарием. однако при проектировании кучи не исключается другое использование, например параметризация списка команд и повторное использование.

struct FrameResourceOffset
{
    UINT frameIndex;
    UINT8* pResourceOffset;
};
std::queue<FrameResourceOffset> frameOffsetQueue;

void DrawFrame()
{
    float vertices[] = ...;
    UINT verticesOffset = 0;
    ThrowIfFailed(
        SetDataToUploadHeap(
            vertices, sizeof(float), sizeof(vertices) / sizeof(float), 
            4, // Max alignment requirement for vertex data is 4 bytes.
            verticesOffset
            ));

    float constants[] = ...;
    UINT constantsOffset = 0;
    ThrowIfFailed(
        SetDataToUploadHeap(
            constants, sizeof(float), sizeof(constants) / sizeof(float), 
            D3D12_CONSTANT_BUFFER_DATA_PLACEMENT_ALIGNMENT,
            constantsOffset
            ));

    // Create vertex buffer views for the new binding model. 
    // Create constant buffer views for the new binding model. 
    // ...

    commandQueue->Execute(commandList);
    commandQueue->AdvanceFence();
}

HRESULT SuballocateFromHeap(SIZE_T uSize, UINT uAlign)
{
    if (NOT_SUFFICIENT_MEMORY(uSize, uAlign))
    {
        // Free up resources for frames processed by GPU; see Figure 2.
        UINT lastCompletedFrame = commandQueue->GetLastCompletedFence();
        FreeUpMemoryUntilFrame( lastCompletedFrame );

        while ( NOT_SUFFICIENT_MEMORY(uSize, uAlign)
            && !frameOffsetQueue.empty() )
        {
            // Block until a new frame is processed by GPU, then free up more memory; see Figure 3.
            UINT nextGPUFrame = frameOffsetQueue.front().frameIndex;
            commandQueue->SetEventOnFenceCompletion(nextGPUFrame, hEvent);
            WaitForSingleObject(hEvent, INFINITE);
            FreeUpMemoryUntilFrame( nextGPUFrame );
        }
    }

    if (NOT_SUFFICIENT_MEMORY(uSize, uAlign))
    {
        // Apps need to create a new Heap that is large enough for this resource.
        return E_HEAPNOTLARGEENOUGH;
    }
    else
    {
        // Update current data pointer for the new resource.
        m_pDataCur = reinterpret_cast<UINT8*>(
            Align(reinterpret_cast<SIZE_T>(m_pHDataCur), uAlign)
            );

        // Update frame offset queue if this is the first resource for a new frame; see Figure 4.
        UINT currentFrame = commandQueue->GetCurrentFence();
        if ( frameOffsetQueue.empty()
            || frameOffsetQueue.back().frameIndex < currentFrame )
        {
            FrameResourceOffset offset = {currentFrame, m_pDataCur};
            frameOffsetQueue.push(offset);
        }

        return S_OK;
    }
}

void FreeUpMemoryUntilFrame(UINT lastCompletedFrame)
{
    while ( !frameOffsetQueue.empty() 
        && frameOffsetQueue.first().frameIndex <= lastCompletedFrame )
    {
        frameOffsetQueue.pop();
    }
}

ID3D12Fence

Подраспределение в буферах