지금까지의 예제에서 사용해온 방식은 매 프레임마다 D3DApp::FlushCommandQueue()를 호출해 해당 프레임의 명령들이 모두 실행되기를 기다렸다. 이러한 방식은 비효율적이다.
- 한 프레임의 시작에서 CPU가 명령을 쓰는동안 GPU는 놀게된다.
- 한 프레임의 끝에서 GPU가 명령을 처리하는 동안 CPU는 놀게된다.
이 문제의 해결 방법은 매 프레임 CPU가 수정해야 하는 자원들을 순환 배열(circular array)로 관리하는 것이다. 이렇게 관리되는 자원들을 이 책에서는 프레임 자원(frame resource)라고 부르고, 일반적으로 사용한다.
순환 배열을 이용한 기법이란 프레임 n에서 CPU는 자원 배열을 훑으며 다음번 가용 프레임 자원(즉, GPU가 사용하지 않는 자원)을 찾는다. GPU가 이전 프레임을 처리하는 동안 CPU는 그런 가용 프레임 자원을 적절히 갱신하고 프레임 n을 위한 명령 목록들을 구축해서 제출한다.
순환 배열 기법을 간단히 말하면 GPU가 0번째 프레임에서 자원 a의 사용이 끝나면 CPU는 그 자원을 프레임 배열을 돌아다니며 그 자원을 쓸 예정인 프레임이 사용할 수 있도록 상태를 미리 갱신해준다. 그런 다음 프레임 n+1로 넘어가 같은 과정을 반복한다.
PassConstants 구조체: https://dandydot.tistory.com/29
7.3 패스별 상수 버퍼
7.1의 FrameResource
dandydot.tistory.com
#pragma once
#include "d3dUtil.h"
#include "MathHelper.h"
#include "UploadBuffer.h"
using namespace DirectX;
using Microsoft::WRL::ComPtr;
struct Vertex
{
XMFLOAT3 Pos;
XMFLOAT4 Color;
};
struct ObjectConstants
{
XMFLOAT4X4 World = MathHelper::Identity4x4();
};
struct PassConstants
{
// 변하지 않는 상수 버퍼(시야 행렬, 투영 행렬, 시점 위치 등) 자료들.
XMFLOAT4X4 View = MathHelper::Identity4x4();
XMFLOAT4X4 Proj = MathHelper::Identity4x4();
XMFLOAT4X4 InvView = MathHelper::Identity4x4();
XMFLOAT4X4 InvProj = MathHelper::Identity4x4();
XMFLOAT4X4 ViewProj = MathHelper::Identity4x4();
XMFLOAT4X4 InvViewProj = MathHelper::Identity4x4();
XMFLOAT3 EyePosW = { 0.0f, 0.0f, 0.0f };
float cbPerObjectPad1 = 0.0f;
XMFLOAT2 RenderTargetSize = { 0.0f,0.0f };
XMFLOAT2 InvRenderTargetSize = { 0.0f,0.0f };
float NearZ = 0.0f;
float FarZ = 0.0f;
float TotalTime = 0.0f;
float DeltaTime = 0.0f;
};
class FrameResource
{
public:
FrameResource(ID3D12Device* device, UINT passCount, UINT objectCount);
FrameResource(const FrameResource& rhs) = delete;
FrameResource& operator=(const FrameResource& rhs) = delete;
~FrameResource() = default;
public:
// 명령 할당자는 GPU가 명령을 다 처리한 후 재설정해야 한다. 하지만 CPU가
// 먼저 프레임을 순회하며 수정할 자원들을 재설정 할 예정이니 일 할
// 할당자 새로 채용
ComPtr<ID3D12CommandAllocator> mCommandAllocator;
// 상수버퍼는 자원을 참조하는 명령들을 GPU가 다 처리하고 나서 갱신해야
// 한다. 따라서 프레임마다 상수 버퍼를 새로 만들어야 한다.
std::unique_ptr<UploadBuffer<PassConstants>> PassCB = nullptr;
std::unique_ptr<UploadBuffer<ObjectConstants>> ObjectCB = nullptr;
// GPU가 아직 이 프레임에 들어갈 자원들을 사용하고 있는지 확인
UINT64 Fence = 0;
};
다음은 프레임 n에서 CPU가 하는 일을 보여준다.
void ShapesApp::Update(const GameTimer& timer)
{
// 순환적으로 자원 프레임 배열의 다음 원소에 접근.
mCurrFrameResourceIndex = (mCurrFrameResourceIndex + 1) % gNumFrameResource;
mCurrFrameResource = mFrameResource[mCurrFrameResourceIndex].get();
// GPU가 울타리 지점까지의 명령을 다 처리했는지 확인. 아직 처리중이면 기다림
if (mCurrFrameResource->Fence != 0 && mFence->GetCompletedValue() < mCurrFrameResource->Fence)
{
// 이벤트 핸들 생성
HANDLE eventHandle = CreateEventEx(nullptr, false, false, EVENT_ALL_ACCESS);
// 펜스가 특정 값에 도달할 때 발생할 이벤트 지정
ThrowIfFailed(mFence->SetEventOnCompletion(mCurrFrameResource->Fence, eventHandle));
WaitForSingleObject(eventHandle, INFINITE);
CloseHandle(eventHandle);
}
// [...] mCurrFrameResource의 자원들을 갱신한다.
}
void ShapesApp::Draw(const GameTimer& timer)
{
// [...] 이 프레임의 명령 목록들을 구축하고 제출함
mCurrFrameResource->Fence = ++mCurrentFence;
// 새 울타리 지점을 설정하는 명령(Signal()을 명령 대기열에 추가한다.
// GPU는 Signal() 이전 명령들을 전부 처리하기 전 까지 설정하지 않는다.
mCommandQueue->Signal(mFence.Get(), mCurrentFence);
// GPU가 아직 이전 프레임 명령을 처리하고 있을 수도 있지만 그 이전 프레임에 관한
// 자원들은 여기서 전혀 건드리지 않으므로 문제될건 없다.
}
'DirectX' 카테고리의 다른 글
7.3 패스별 상수 버퍼 (0) | 2022.01.06 |
---|---|
7.2 렌더 항목 (0) | 2022.01.06 |
6.10 기하구조 보조 구조체 (0) | 2022.01.03 |
6.9 파이프라인 상태 객체 (0) | 2022.01.03 |
6.8 래스터화기 상태 (0) | 2022.01.03 |