펜스를 통해 GPU 진행률을 추적하여 리소스 데이터 수명 범위를 관리하는 방법을 보여 줍니다. 메모리는 업로드 힙에 대한 링 버퍼 구현과 같이 메모리에서 사용 가능한 공간의 가용성을 신중하게 관리하는 펜스와 함께 효과적으로 다시 사용할 수 있습니다.
링 버퍼 시나리오
다음은 앱이 업로드 힙 메모리에 대한 드문 수요를 경험하는 예제입니다.
링 버퍼는 업로드 힙을 관리하는 한 가지 방법입니다. 링 버퍼는 다음 몇 프레임에 필요한 데이터를 보유합니다. 앱은 현재 데이터 입력 포인터와 해당 프레임에 대한 리소스 데이터의 각 프레임 및 시작 오프셋을 기록하는 프레임 오프셋 큐를 유지 관리합니다.
앱은 버퍼를 기반으로 링 버퍼를 만들어 각 프레임의 GPU에 데이터를 업로드합니다. 현재 프레임 2가 렌더링되고, 링 버퍼가 프레임 4의 데이터를 둘러싸고, 프레임 5에 필요한 모든 데이터가 있으며, 프레임 6에 필요한 큰 상수 버퍼를 하위 할당해야 합니다.
그림 1: 앱이 상수 버퍼에 대해 하위 할당을 시도하지만 사용 가능한 메모리가 부족한 것을 찾습니다.
이 링 버퍼
그림 2 : 펜스 폴링을 통해 앱은 프레임 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();
}
}
관련 항목