Fence-Based 资源管理

演示如何通过围栏跟踪 GPU 进度来管理资源数据生命周期。 内存可以有效地与围栏一起使用,以仔细管理内存中可用空间的可用性,例如在上传堆的环形缓冲区实现中。

环形缓冲区方案

下面是应用对上传堆内存的罕见需求的示例。

环形缓冲区是管理上传堆的一种方法。 环形缓冲区保存接下来几个帧所需的数据。 应用维护当前数据输入指针和帧偏移队列来记录每个帧以及该帧的资源数据的起始偏移量。

应用基于缓冲区创建环形缓冲区,以便将数据上传到每个帧的 GPU。 目前已呈现帧 2,环缓冲区环绕帧 4 的数据,帧 5 所需的所有数据都存在,帧 6 所需的大型常量缓冲区需要子分配。

图 1:应用尝试为常量缓冲区进行子分配,但发现可用内存不足。

此环形缓冲区中的可用内存不足

图 2:通过围栏轮询,应用发现帧 3 已呈现,帧偏移队列随后更新,环缓冲区的当前状态随之而来-但是,可用内存仍然不够大,无法容纳常量缓冲区。

帧 3 呈现内存仍然不足

图 3:鉴于这种情况,CPU 会阻止自身(通过围栏等待),直到呈现帧 4,从而释放为帧 4 分配的内存子资源。

渲染帧 4 释放更多的环形缓冲区

图 4:现在可用内存足以容纳常量缓冲区,子分配成功;应用将大常量缓冲区数据复制到资源数据以前用于帧 3 和 4 的内存。 当前输入指针最终会更新。

现在环形缓冲区中的帧 6 有空间

如果应用实现环形缓冲区,则环形缓冲区必须足够大,才能应对资源数据大小的更糟糕的情况。

环形缓冲区示例

以下示例代码演示如何管理环形缓冲区,并注意处理围栏轮询和等待的子分配例程。 为简单起见,该示例使用NOT_SUFFICIENT_MEMORY来隐藏“堆中找到的足够可用内存”的详细信息,因为该逻辑(基于 FrameOffsetQueue内部的 m_pDataCur 和偏移量)与堆或围栏无关。 此示例被简化为牺牲帧速率而不是内存利用率。

请注意,环形缓冲区支持应是一种常用的方案;但是,堆设计并不排除其他用法,例如命令列表参数化和重复使用。

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

缓冲区中的 子分配