펜스 기반 리소스 관리
펜스를 통해 GPU 진행 상황을 추적하여 리소스 데이터 수명을 관리하는 방법을 보여 줍니다. 업로드 힙을 위한 링 버퍼 구현과 같이 메모리에서 사용 가능한 공간의 가용성을 신중하게 관리하는 펜스를 통해 메모리를 효과적으로 다시 사용할 수 있습니다.
링 버퍼 시나리오
다음은 업로드 힙 메모리가 거의 필요하지 않은 앱의 예제입니다.
링 버퍼는 업로드 힙을 관리하는 한 가지 방법입니다. 링 버퍼는 다음 몇 개의 프레임에 필요한 데이터를 포함합니다. 앱은 현재 데이터 입력 포인터 및 프레임 오프셋 큐를 유지 관리하여 각 프레임 및 해당 프레임에 대한 리소스 데이터의 시작 오프셋을 기록합니다.
앱은 버퍼를 기반으로 링 버퍼를 만들어 각 프레임에 대한 데이터를 GPU에 업로드합니다. 현재 프레임 2가 렌더링되었고, 링 버퍼가 프레임 4의 데이터를 래핑하고, 프레임 5에 필요한 모든 데이터가 있으며, 프레임 6에 필요한 큰 상수 버퍼가 하위 할당되어야 합니다.
그림 1: 앱이 상수 버퍼에 대해 하위 할당하려고 하지만 사용 가능한 메모리가 부족합니다.
그림 2: 펜스 폴링을 통해 앱은 프레임 3이 렌더링되었고, 이후 프레임 오프셋 큐가 업데이트되고, 링 버퍼의 현재 상태가 뒤따름을 발견하지만, 사용 가능한 메모리가 상수 버퍼를 수용할 만큼 크지 않습니다.
그림 3: 해당 상황에서 CPU는 프레임 4가 렌더링되어 프레임 4에 하위 할당된 메모리가 해제될 때까지 펜스 대기를 통해 자신을 차단합니다.
그림 4: 이제 사용 가능한 메모리가 상수 버퍼에 사용할 만큼 충분히 크고 하위 할당이 성공합니다. 앱은 리소스 데이터가 이전에 프레임 3 및 4에 사용한 메모리에 큰 상수 버퍼 데이터를 복사합니다. 현재 입력 포인터가 마지막으로 업데이트됩니다.
앱이 링 버퍼를 구현하는 경우 링 버퍼는 리소스 데이터 크기가 더 나쁜 시나리오에 대처할 만큼 커야 합니다.
링 버퍼 샘플
다음 샘플 코드는 펜스 폴링 및 대기를 처리하는 하위 할당 루틴에 유의해서 링 버퍼를 관리하는 방법을 보여 줍니다. 간단히 하기 위해 샘플은 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();
}
}
관련 항목