기록공간

[DirectX 12] Direct3D 초기화 - 1 본문

DirectX/기초

[DirectX 12] Direct3D 초기화 - 1

입코딩 2020. 3. 10. 18:57
반응형

본격적으로 Direct3D를 초기화하는 방법을 살펴보자. 꽤 길기 때문에 2파트로 나누어 작성하였다. 또한 앞서 살펴봤던 기본지식들을 모두 알고 있다는 가정하에 진행하려고 한다.

 

초기화 과정은 다음과 같은 단계들로 구성된다.

 

  1. D3D12CreateDevice 함수를 이용해서 ID3D12Device를 생성한다.

  2. ID3D12Fence 객체를 생성하고 서술자들의 크기를 얻는다.

  3. 4X MSAA(다중 표본화) 품질 수준 지원 여부를 점검한다.

  4. 명령 큐와 명령 리스트 할당자, 그리고 주 명령 리스트를 생성한다.

  5. 스왑 체인을 서술하고 생성한다.

  6. 응용 프로그램에 필요한 서술자 힙들을 생성한다.

  7. 후면 버퍼의 크기를 설정하고, 후면 버퍼에 대한 렌더 타깃 뷰(RTV)를 생성한다.

  8. 깊이 스텐실 버퍼를 생성하고, 그와 연관된 깊이 스텐실 뷰를 생성한다.

  9. 뷰포트와 가위 판정용 사각형들을 설정한다.

(앞으로 나올 모든 코드의 자세한 내용은 https://github.com/d3dcoder/d3d12book

Chapter 4를 참고)

장치 생성

Direct3D 초기화는 Direct3D 12 장치(ID3D12Device)를 생성하는 것으로 시작한다. 장치는 디스플레이 어댑터(GPU)를 나타내는 객체이다. 일반적으로 디스플레이 어댑터는 물리적인 3차원 그래픽 하드웨어 장치이지만, 하드웨어 그래픽 기능성을 흉내 내는 소프트웨어 디스플레이 어댑터(예를 들면 WARP 어댑터)도 존재한다. Direct3D 12 장치는 기능 지원 점검에 쓰이며, 자원이나  뷰, 명령 목록 등 다른 모든 Direct3D 인터페이스 객체들의 생성에도 쓰인다. 

 

장치를 생성할 때에는 다음과 같은 함수를 사용한다.

 

pAdapter, MinimumFeatureLevel, riid로 들어가는 DXGI, D3D_FEATURE_LEVEL, GUID는 모두 앞서 설명한 바 있기 때문에 넘어간다. 

 

알맞는 값을 매개변수로 집어넣고 생성될 D3D12Device 인터페이스를 받을 포인터를 ppDevice위치에 넣어주면 된다. 

 

다음은 이 함수의 호출 예이다. 

bool D3DApp::InitDirect3D()
{
#if defined(DEBUG) || defined(_DEBUG) 
	// D3D12 디버그층 활성화
{
	ComPtr<ID3D12Debug> debugController;
	ThrowIfFailed(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController)));
	debugController->EnableDebugLayer();
}
#endif

	ThrowIfFailed(CreateDXGIFactory1(IID_PPV_ARGS(&mdxgiFactory)));

	// 하드웨어 어댑터를 나타내는 장치를 생성해본다.
	HRESULT hardwareResult = D3D12CreateDevice(
		nullptr,             // NULL이면 기본 어댑터
		D3D_FEATURE_LEVEL_11_0,
		IID_PPV_ARGS(&md3dDevice));

	// 실패 했다면 WARP 어댑터를 나타내는 장치를 생성한다.
	if(FAILED(hardwareResult))
	{
		ComPtr<IDXGIAdapter> pWarpAdapter;
		ThrowIfFailed(mdxgiFactory->EnumWarpAdapter(IID_PPV_ARGS(&pWarpAdapter)));

		ThrowIfFailed(D3D12CreateDevice(
			pWarpAdapter.Get(),
			D3D_FEATURE_LEVEL_11_0,
			IID_PPV_ARGS(&md3dDevice)));
	}

	// 뒷부분 생략...
}

디버그 모드 빌드를 위해 우선 디버그 층을 활성화 했음을 주목하자. 디버그층(Debug layer)이 활성화되어 있으면 Direct3D는 추가적인 디버깅을 활성해서, VC++의 출력창에 다음과 같은 형태의 디버그 메시지를 보낸다.

// D3D12 ERROR: ID3D12CommandList::Reset: Reset fails because the command list was not closed.

또한, 첫 D3D12CreateDevice 호출이 실패하면 한발 물러나서 소프트웨어 어댑터인 WARP를 나타내는 장치를 생성한다는 점도 주목하자. WARP(Windows Advanced Rasterization Platform)는 Windows에서 제공하는 고급 래스터화 플랫폼이다.

 

WARP 어댑터에 대한 장치를 생성하려면 그 전에 IDXGIFactory4의 EnumWarpAdapter 메서드를 호출해주어야 한다. 이렇게 해야 디스플레이 어댑터 나열 시 WARP 어댑터가 나타난다.

ComPtr<IDXGIFactory4> mdxgiFactory;
CreateDXGIFactory1(IID_PPV_ARGS(&mdxgiFactory));
mdxgiFactory->EnumWarpAdapter(
   IID_PPV_ARGS(&pWarpAdapter));

mdxgiFactory 객체는 스왑 체인을 생성할 때에도 쓰인다. (이는 스왑 체인이 Direct3D가 아니라 DXGI의 일부이기 떄문이다)

 

울타리(펜스) 생성과 서술자 크기 얻기

CPU와 GPU의 동기화를 위한 펜스 객체를 생성한다. 또한, 이후에 필요한 서술자들의 크기도 미리 조회해서 설정해 둔다. 서술자 크기는 GPU마다 다를 수 있으므로, 이처럼 실행시점에서 적절한 메서드를 호출해서 알아내야 한다. 나중에 서술자 크기가 필요할 때 바로 사용할 수 있도록, 크기들을 적절한 멤버 변수들에 저장해 둔다.

bool D3DApp::InitDirect3D()
{
	// 장치생성 부분 생략...

	// 펜스객체를 생성
	ThrowIfFailed(md3dDevice->CreateFence(0, D3D12_FENCE_FLAG_NONE,
		IID_PPV_ARGS(&mFence)));

	// 렌더 타깃 뷰 서술자 크기
	mRtvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
	// 깊이 스텐실 뷰 서술자 크기
	mDsvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_DSV);
	// 상수 버퍼 서술자 크기
	mCbvSrvUavDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);

	// 뒷부분 생략...
}

 

4X MSAA 품질 수준 지원 점검

4X MSAA를 기본으로 선택한 이유는 비용이 그리 크지 않으면서도 화질이 많이 개선된다는 점과 모든 Direct3D 11급 장치가 모든 렌더 대상 형식에서 4X MSAA를 지원한다는 점 때문이다. 현재 장치의 기능 수준(D3D_FEATURE_LEVEL)이 Direct3D 11이상임을 확인했다면 4X MSAA 지원 여부는 따로 확인할 필요가 없지만, 여기서는 지원 여부를 명시적으로 점검한다.

bool D3DApp::InitDirect3D()
{
	// 앞부분 생략...
    
	// 다중 샘플링 설정 구조체
	D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS msQualityLevels;
	msQualityLevels.Format = mBackBufferFormat;
	msQualityLevels.SampleCount = 4;	// 4x MSAA
	msQualityLevels.Flags = D3D12_MULTISAMPLE_QUALITY_LEVELS_FLAG_NONE;
	msQualityLevels.NumQualityLevels = 0;
    
	// 후면 버퍼 4X MSAA 지원 여부 검사
	ThrowIfFailed(md3dDevice->CheckFeatureSupport(
		D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS,
		&msQualityLevels,
		sizeof(msQualityLevels)));

	m4xMsaaQuality = msQualityLevels.NumQualityLevels;
	assert(m4xMsaaQuality > 0 && "Unexpected MSAA quality level.");
    
	// 뒷부분 생략...
}

4X MSAA가 항상 지원되므로, 반환된 품질 수준(m4xMsaaQuality)은 항상 0보다 커야 한다. 위의 코드는 이 점을 assert를 이용하여 확인한다.

 

명령 큐와 명령 리스트 생성

명령 큐를 대표하는 인터페이스는 ID3D12CommandQueue이고 명령 할당자를 대표하는 인터페이스는 ID3D12CommandAllocator, 명령 리스트를 대표하는 인터페이스는ID3D12Graphics- CommandList이다. 다음 함수는 명령 큐와 명령 할당자, 명령 리스트를 생성하는 방법을 보여준다.

bool D3DApp::InitDirect3D()
{
    // 앞부분 생략...
    
    // 명령에 필요한 객체들 생성
    CreateCommandObjects();
    CreateSwapChain();
    CreateRtvAndDsvDescriptorHeaps();

	return true;
}

ComPtr<ID3D12CommandQueue> mCommandQueue;             // 명령 큐
ComPtr<ID3D12CommandAllocator> mDirectCmdListAlloc;   // 명령 리스트 할당자
ComPtr<ID3D12GraphicsCommandList> mCommandList;	      // 명령 리스트
void D3DApp::CreateCommandObjects()
{
	D3D12_COMMAND_QUEUE_DESC queueDesc = {};
	queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
	queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
    
        // 명령 큐 인터페이스 생성
	ThrowIfFailed(md3dDevice->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&mCommandQueue)));

        // 명령 할당자 인터페이스 생성
	ThrowIfFailed(md3dDevice->CreateCommandAllocator(
		D3D12_COMMAND_LIST_TYPE_DIRECT,
		IID_PPV_ARGS(mDirectCmdListAlloc.GetAddressOf())));

        // 명령 리스트 인터페이스 생성
	ThrowIfFailed(md3dDevice->CreateCommandList(
		0,
		D3D12_COMMAND_LIST_TYPE_DIRECT,
		mDirectCmdListAlloc.Get(), // 연관된 명령 할당자
		nullptr,                   // 초기 파이프라인 상태 객체
		IID_PPV_ARGS(mCommandList.GetAddressOf())));

	// 닫힌 상태로 시작한다. 이후 명령 리스트를 처음 참조할 때
	// Reset을 호출하는데, Reset을 호출하려면 명령 리스트가
	// 닫혀 있어야 하기 때문이다.
	mCommandList->Close();
}

CreateCommandList 호출 시 파이프라인 상태 객체 매개변수에 널 포인터를 지정했음에 주목하자. 이번 프로그램에서는 그 어떤 그리기 명령도 제출하지 않기 때문에 유효한 파이프라인 상태 객체를 지정하지 않아도 된다. 

 

스왑 체인의 서술과 생성

초기화 공정의 다음 단계는 스왑 체인을 생성하는 것이다. 이를 위해서는 우선 DXGI_SWAP_CHAIN_DESC 구조체 인스턴스의 멤버들을 지금 생성하고자 하는 교환 사슬에 맞게 설정해야 한다. 이 구조체의 정의는 다음과 같다.

 

더 자세한 정보가 필요하거나 추가적인 플래그와 옵션들에 대한 정보는 SDK 문서를 참조하자.

 

스왑 체인을 서술하는 구조체를 다 채웠으면, IDXGIFactory::CreateSwapChain 메서드를 호출해서 스왑 체인을 생성한다.

 

pDevice에는 ID3D12CommandQueue의 포인터, pDesc에는 앞서 채운 스왑 체인 서술 구조체의 포인터, 그리고 ppSwapChain에서 생성된 교환 사슬 인터페이스를 돌려준다.

 

다음 코드는 예제 프레임워크에서 스왑 체인을 생성하는 방법을 보여준다. 이 함수가 여러 번 호출되어도 문제가 없도록 설계되어 있음에 주목하자. 이 함수는 기존 스왑 체인을 먼저 해제한 후 새 스왑 체인을 생성한다. 덕분에 응용 프로그램은 이전과는 다른 설정으로 스왑 체인을 다시 생성할 수 있다. 

bool D3DApp::InitDirect3D()
{
    // 앞부분 생략...
    
    // 스왑체인 생성
    CreateSwapChain();
    CreateRtvAndDsvDescriptorHeaps();

    return true;
}

DXGI_FORMAT mBackBufferFormat = DXGI_FORMAT_R8G8B8A8_UNORM;
void D3DApp::CreateSwapChain()
{
    // 새 교환 사슬을 생성하기 전에 먼저 기존 교환 사슬을 해제
    mSwapChain.Reset();

    // 스왑 체인을 서술하는 구조체를 채운다.
    DXGI_SWAP_CHAIN_DESC sd;
    sd.BufferDesc.Width = mClientWidth;
    sd.BufferDesc.Height = mClientHeight;
    sd.BufferDesc.RefreshRate.Numerator = 60;
    sd.BufferDesc.RefreshRate.Denominator = 1;
    sd.BufferDesc.Format = mBackBufferFormat;
    sd.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
    sd.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;
    sd.SampleDesc.Count = m4xMsaaState ? 4 : 1;
    sd.SampleDesc.Quality = m4xMsaaState ? (m4xMsaaQuality - 1) : 0;
    sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
    sd.BufferCount = SwapChainBufferCount;
    sd.OutputWindow = mhMainWnd;
    sd.Windowed = true;
    sd.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
    sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;

    // 스왑 체인은 명령 리스트를 이용하여 방출(flush)을 수행한다.
    ThrowIfFailed(mdxgiFactory->CreateSwapChain(
		mCommandQueue.Get(),
		&sd, 
		mSwapChain.GetAddressOf()));
}

 

반응형
Comments