목차
- 인사말
- 참조
- 솔루션의 구조
- D3D12HelloTriangle
- D3D12HelloBundle
- D3D12HelloFrameBuffering
- D3D12HelloConstantBuffers
- D3D12HelloTexture
- 부록: 마무리
인사말
이번에 MS의 DirectX12의 샘플 소스코드를 심층적으로 분석하는 카테고리를 만들어 첫 글을 쓴다.
개인적으로 이 활동과 글이 나의 좋은 성장요소가 되었으면 좋겠다.
또한, 최종적으로는 MS에서 샘플로 풀어둔 "MiniEngine"을 분석함으로써 그 동안 책과 일반적인 강의에서는 배울수 없었던 다양한 고급 렌더링기법에 관하여 자습하고자 한다.
우선 오늘 쓸 글의 내용은 MS에서 공개한 DirectX12 샘플프로젝트들의 골격과 디자인을 알아보기 위해 가장 쉬운 솔루션을 분석할 것이다.
마지막으로 이 글들은 불필요한 내용들을 적지는 않았지만,
나의 기준에서 생각보다 상세하게 기술했기 때문에 심층이라는 단어를 감히 사용하고자 하며 본 분석을 보는 대상독자들이 winapi와 directx12를 살짝이상 터득한 상태라고 가정한 상태에서 시작할것이며 독자개인의 학습에 관하여 알려준다는 느낌보다는 내가 한번더 정리한다는 개념으로 쓴거라, 독자에게는 크게 와닿지 않는 부분이 존재할 수 있으나, 확실히 전체소스코드와 나의 글을 쭉 따라가다보면 어느정도 궁금증이 해소되는 부분이 많이 존재할거라 믿는다.
참조
https://github.com/microsoft/DirectX-Graphics-Samples/tree/master/Samples/Desktop/D3D12HelloWorld
GitHub - microsoft/DirectX-Graphics-Samples: This repo contains the DirectX Graphics samples that demonstrate how to build graph
This repo contains the DirectX Graphics samples that demonstrate how to build graphics intensive applications on Windows. - GitHub - microsoft/DirectX-Graphics-Samples: This repo contains the Direc...
github.com
솔루션의 구조
우선 HelloWorld의 솔루션은 다음과 같이 여러 부속 프로젝트들로 나뉜다.
우선 나의 글에서는 HelloWindow예제는 생략하고 바로 HelloTriangle예제부터 시작하겠다.
어차피 HelloWindow에 존재하는 윈도우 메세지활용 로직이 HelloTriangle에 모두 포함되어있기 때문이다.
그러면 차례대로 예제의 핵심 부분만을 찝으면서 분석해 나갈것이며,
개인적으로 생각했을때 불필요하다거나 중복되는 내용은 생략하겠다.
D3D12HelloTriangle
우선 모든 프로젝트를 분석할때에는 세부적인내용보다는 거시적인 관점에서 프로젝트의 흐름을 파악하는게 중요한것같다. 따라서 우선 이 프로젝트의 구조를 보면 다음과 같다.
기타 계산을 도와주는 헬퍼 모듈과, 메인함수가 포함된 소스파일, 또 3개의 클래스를 가진다.
우선 이 솔루션에서는 DXSample, Win32Application, Main.cpp, DXSampleHelper.h는 비슷한 코드를 가지기에
특별한 변경사항이 없다면 한번만 분석하고 넘어가겠다.
Main.cpp
//*********************************************************
//
// Copyright (c) Microsoft. All rights reserved.
// This code is licensed under the MIT License (MIT).
// THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
// ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
// IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
// PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//*********************************************************
#include "stdafx.h"
#include "D3D12HelloTriangle.h"
_Use_decl_annotations_
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int nCmdShow)
{
D3D12HelloTriangle sample(1280, 720, L"D3D12 Hello Triangle");
return Win32Application::Run(&sample, hInstance, nCmdShow);
}
메인함수에서는 크게 분석하기 어려운내용은 없다고생각한다.
이때 우선 D3D12HelloTriangle은 일단 무시하고, Win32Application쪽으로 넘어가보자.
Win32Application
우선 Win32Application의 헤더파일에서는
//*********************************************************
//
// Copyright (c) Microsoft. All rights reserved.
// This code is licensed under the MIT License (MIT).
// THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
// ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
// IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
// PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//*********************************************************
#pragma once
#include "DXSample.h"
class DXSample;
class Win32Application
{
public:
static int Run(DXSample* pSample, HINSTANCE hInstance, int nCmdShow);
static HWND GetHwnd() { return m_hwnd; }
protected:
static LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
private:
static HWND m_hwnd;
};
다음과 같이 DXSample이 전방선언 되어있으며, 공통적으로 모든 메서드가 전역으로 되어있다.
그말은 즉슨 외부에서도 객체를 만들지않고도 이 클래스의 메서드들을 사용이 가능하다는 소리이다.
Main.cpp에서 봤다시피 Run에서 모든 윈도우관련 모든 로직이 작동할것으로 예측된다.
// Parse the command line parameters
int argc;
LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(), &argc);
pSample->ParseCommandLineArgs(argv, argc);
LocalFree(argv);
우선 Run메서드에서 처음 메인함수들의 인자들을 받아와서 ParseCommandLineArgs라는곳으로 보낸다.
이 코드의 의미는 만약 해당 exe파일을 cmd에서 실행시켰을때, 명령 인자들을 처리하는 부분이다.
RECT windowRect = { 0, 0, static_cast<LONG>(pSample->GetWidth()), static_cast<LONG>(pSample->GetHeight()) };
AdjustWindowRect(&windowRect, WS_OVERLAPPEDWINDOW, FALSE);
위 코드는 혼동하면 안되는것이 윈도우의 크기를 조절하는것이 아닌 클라이언트의 크기를 조정한다.
그럼 두개가 뭐가 다르냐, 만약 윈도우의 크기만 조절한다면, 실제 렌더링했을때 가끔 오른쪽하단의 픽셀영역이 짤려보일것이다. 이건 나의 경험담이라서 꼭 이야기 해주고싶었다.
m_hwnd = CreateWindow(
windowClass.lpszClassName,
pSample->GetTitle(),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
windowRect.right - windowRect.left,
windowRect.bottom - windowRect.top,
nullptr, // We have no parent window.
nullptr, // We aren't using menus.
hInstance,
pSample);
위 부분은 알다시피 윈도우를 만들어주는 부분인데, 사실 나는 이 코드를 처음봤을때 처음 안 부분이
마지막에 pSample을 넘겨준것이다.
이는 메세지 프로시저 콜백함수에서 WM_CREATE메세지를 받았을때 lParam에 값이 저장되는데
LPCREATESTRUCT를 통해 값을 받아올수있다.
뒤에 좀더 내용이 있지만, 나머지는 엄청 쉬운부분이라고 판단하여 패스하겠다.
// Main message handler for the sample.
LRESULT CALLBACK Win32Application::WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
DXSample* pSample = reinterpret_cast<DXSample*>(GetWindowLongPtr(hWnd, GWLP_USERDATA));
switch (message)
{
case WM_CREATE:
{
// Save the DXSample* passed in to CreateWindow.
LPCREATESTRUCT pCreateStruct = reinterpret_cast<LPCREATESTRUCT>(lParam);
SetWindowLongPtr(hWnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(pCreateStruct->lpCreateParams));
}
return 0;
case WM_KEYDOWN:
if (pSample)
{
pSample->OnKeyDown(static_cast<UINT8>(wParam));
}
return 0;
case WM_KEYUP:
if (pSample)
{
pSample->OnKeyUp(static_cast<UINT8>(wParam));
}
return 0;
case WM_PAINT:
if (pSample)
{
pSample->OnUpdate();
pSample->OnRender();
}
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
// Handle any messages the switch statement didn't.
return DefWindowProc(hWnd, message, wParam, lParam);
}
다음으로는 윈도우 프로시저 함수인데, 위에서 말했다시피 pSample을 WM_CREATE메세지를 통해 실시간으로 받아오고있다.
또한 nullptr참조를 방지하기 위하여, pSample포인터는 항상 값이 들어있는 상태에서만 사용하기로 한다.
DXSample
virtual void OnInit() = 0;
virtual void OnUpdate() = 0;
virtual void OnRender() = 0;
virtual void OnDestroy() = 0;
DXSample클래스는 앞으로 실제 D3D로직이 실행될 클래스들의 부모클래스가 될것이며,
그러한 클래스들에 다형성을 부여하고자, 위와같이 일부 메서드들은 가상함수로 구현되어있다.
다음으로 DXSample::GetHardwareAdapter 함수는 DXGIFactory COM객체를 통하여, 가장 최상의 퍼포먼스를 보여줄 GPU를 가리키는 포인터를 탐색하여, 반환한다.
for (
UINT adapterIndex = 0;
SUCCEEDED(factory6->EnumAdapterByGpuPreference(
adapterIndex,
requestHighPerformanceAdapter == true ? DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE : DXGI_GPU_PREFERENCE_UNSPECIFIED,
IID_PPV_ARGS(&adapter)));
++adapterIndex)
{
DXGI_ADAPTER_DESC1 desc;
adapter->GetDesc1(&desc);
if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE)
{
// Don't select the Basic Render Driver adapter.
// If you want a software adapter, pass in "/warp" on the command line.
continue;
}
// Check to see whether the adapter supports Direct3D 12, but don't create the
// actual device yet.
if (SUCCEEDED(D3D12CreateDevice(adapter.Get(), D3D_FEATURE_LEVEL_11_0, _uuidof(ID3D12Device), nullptr)))
{
break;
}
}
최상의 퍼포먼스를 가지는 GPU의 포인터는 위와 같이 구해주어, adapter에 저장된다.
이때 사용한 메서드로 EnumAdpaterByGpuPreference가 사용되었는데, MSDN문서에서 보다시피 2번째 매개변수를 기준으로 모든 gpu 어댑터를 순회한다.
만약 두번째 매개변수가 DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE 일경우 제일 높은 성능을 자랑하는 GPU가 인덱스 0에 온다.
이때 해당 GPU어댑터를 가리키는 객체를 D3D12CreateDevice를 통해 유효한 값을 가리키는지 확인한 후(Device는 만들지 않는다) 해당 adapter를 밖으로 넘겨준다.
D3D12HelloTriangle
class D3D12HelloTriangle : public DXSample
{
public:
D3D12HelloTriangle(UINT width, UINT height, std::wstring name);
virtual void OnInit();
virtual void OnUpdate();
virtual void OnRender();
virtual void OnDestroy();
private:
static const UINT FrameCount = 2;
struct Vertex
{
XMFLOAT3 position;
XMFLOAT4 color;
};
// Pipeline objects.
CD3DX12_VIEWPORT m_viewport;
CD3DX12_RECT m_scissorRect;
ComPtr<IDXGISwapChain3> m_swapChain;
ComPtr<ID3D12Device> m_device;
ComPtr<ID3D12Resource> m_renderTargets[FrameCount];
ComPtr<ID3D12CommandAllocator> m_commandAllocator;
ComPtr<ID3D12CommandQueue> m_commandQueue;
ComPtr<ID3D12RootSignature> m_rootSignature;
ComPtr<ID3D12DescriptorHeap> m_rtvHeap;
ComPtr<ID3D12PipelineState> m_pipelineState;
ComPtr<ID3D12GraphicsCommandList> m_commandList;
UINT m_rtvDescriptorSize;
// App resources.
ComPtr<ID3D12Resource> m_vertexBuffer;
D3D12_VERTEX_BUFFER_VIEW m_vertexBufferView;
// Synchronization objects.
UINT m_frameIndex;
HANDLE m_fenceEvent;
ComPtr<ID3D12Fence> m_fence;
UINT64 m_fenceValue;
void LoadPipeline();
void LoadAssets();
void PopulateCommandList();
void WaitForPreviousFrame();
};
우선 정점버퍼에 넘겨울 Vertex구조체가 정의되어있고, GPU의 현재 처리부분을 CPU와 동기화하기 위한 Fence가 들어있는데, 사실 이 부분은 현재로써 크게 신경쓸 필요가없지만 밑에서 볼 FrameBuffering예제에서는 좀 중요하게 다뤄지며,
밑에서는 더 세세하게 분석할 것이다.
또한, 스왑체인을 통해 화면에 그려질 렌더타겟 텍스쳐를 2개준비한다.
D3D12에서는 11에서보다 좀 더 리소스의 사용용도를 명확하게 구분지을 필요가있는데,
이때 렌더타겟 리소스는 화면에 뿌려줄 픽셀행렬이라고 생각하면 될것이다.
이 데모에서는 FrameCount를 스왑체인의 갯수와 동일하게 설정했다.
프로그램을 초기화 하는 부분에서는 우선 D3D12HelloTriangle::LoadPipeline() 가 호출된다.
#if defined(_DEBUG)
// Enable the debug layer (requires the Graphics Tools "optional feature").
// NOTE: Enabling the debug layer after device creation will invalidate the active device.
{
ComPtr<ID3D12Debug> debugController;
if (SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController))))
{
debugController->EnableDebugLayer();
// Enable additional debug layers.
dxgiFactoryFlags |= DXGI_CREATE_FACTORY_DEBUG;
}
}
#endif
이 부분은 디버깅할때 D3D12나 DXGI계층에서 만약 런타임 에러가 났을경우 프로그래머에게 알려주는 계층을 생성한다.
중요하다.
if (m_useWarpDevice)
{
ComPtr<IDXGIAdapter> warpAdapter;
ThrowIfFailed(factory->EnumWarpAdapter(IID_PPV_ARGS(&warpAdapter)));
ThrowIfFailed(D3D12CreateDevice(
warpAdapter.Get(),
D3D_FEATURE_LEVEL_11_0,
IID_PPV_ARGS(&m_device)
));
}
else
{
ComPtr<IDXGIAdapter1> hardwareAdapter;
GetHardwareAdapter(factory.Get(), &hardwareAdapter);
ThrowIfFailed(D3D12CreateDevice(
hardwareAdapter.Get(),
D3D_FEATURE_LEVEL_11_0,
IID_PPV_ARGS(&m_device)
));
}
보다시피 만약 적절한 GPU를 탐색하지 못했다면 이때 warpAdapter라는것을 하드웨어 어댑터대신 사용하는데
이것은 Windows에서 기본으로 지원하는 소프트웨어 어댑터이다.
// This sample does not support fullscreen transitions.
ThrowIfFailed(factory->MakeWindowAssociation(Win32Application::GetHwnd(), DXGI_MWA_NO_ALT_ENTER));
ThrowIfFailed(swapChain.As(&m_swapChain));
m_frameIndex = m_swapChain->GetCurrentBackBufferIndex();
첫번째줄의 코드가 이 프로그램이 포커싱되어있는상태에서 ALT+ENTER를 눌렀을때 원래는 전체화면으로 바뀌게 되는데
이 데모에서는 전체화면에 대한 버퍼크기변환을 고려하지 않았기 때문에 원래 DXGI에서 전체화면에 대해 처리되었던 부분을 Window메세지루프에서 처리하기로 바꾸는 부분이다.
또한 위에서 스왑체인의 객체의 버전을 바꾸고있다.
LoadPipeline에서 나머지 부분은 서술자힙만들고 뷰 생성해주고 이런 기본적인 내용이라서 그냥 패스하겠다.
다음은 D3D12HelloTriangle::LoadAssets()을 살펴볼것이다.
이 함수에서는 우선 RootSignature를 만들어준뒤, 셰이더를 컴파일하여 그래픽파이프라인 상태객체를 생성한다.
// Create the command list.
ThrowIfFailed(m_device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, m_commandAllocator.Get(), m_pipelineState.Get(), IID_PPV_ARGS(&m_commandList)));
// Command lists are created in the recording state, but there is nothing
// to record yet. The main loop expects it to be closed, so close it now.
ThrowIfFailed(m_commandList->Close());
그 다음 명령목록을 만들어주는데, 여기서 주의할 점이 크게 명령할것이 없다면 바로 Close를 호출해주어야한다.
왜냐하면 막 생성된 명령목록은 기본적으로 열린상태이다.
// Create the vertex buffer.
{
// Define the geometry for a triangle.
Vertex triangleVertices[] =
{
{ { 0.0f, 0.25f * m_aspectRatio, 0.0f }, { 1.0f, 0.0f, 0.0f, 1.0f } },
{ { 0.25f, -0.25f * m_aspectRatio, 0.0f }, { 0.0f, 1.0f, 0.0f, 1.0f } },
{ { -0.25f, -0.25f * m_aspectRatio, 0.0f }, { 0.0f, 0.0f, 1.0f, 1.0f } }
};
const UINT vertexBufferSize = sizeof(triangleVertices);
// Note: using upload heaps to transfer static data like vert buffers is not
// recommended. Every time the GPU needs it, the upload heap will be marshalled
// over. Please read up on Default Heap usage. An upload heap is used here for
// code simplicity and because there are very few verts to actually transfer.
ThrowIfFailed(m_device->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
D3D12_HEAP_FLAG_NONE,
&CD3DX12_RESOURCE_DESC::Buffer(vertexBufferSize),
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(&m_vertexBuffer)));
// Copy the triangle data to the vertex buffer.
UINT8* pVertexDataBegin;
CD3DX12_RANGE readRange(0, 0); // We do not intend to read from this resource on the CPU.
ThrowIfFailed(m_vertexBuffer->Map(0, &readRange, reinterpret_cast<void**>(&pVertexDataBegin)));
memcpy(pVertexDataBegin, triangleVertices, sizeof(triangleVertices));
m_vertexBuffer->Unmap(0, nullptr);
// Initialize the vertex buffer view.
m_vertexBufferView.BufferLocation = m_vertexBuffer->GetGPUVirtualAddress();
m_vertexBufferView.StrideInBytes = sizeof(Vertex);
m_vertexBufferView.SizeInBytes = vertexBufferSize;
}
위 부분은 실제 우리가 그려줄 삼각형의 정점내용이 담긴 정점버퍼를 만들어주는데,
이 데모에서는 UploadBuffer만을 만들어 주었지만,
DefaultBuffer를 같이 만들어주어서, UpdateSubResources를 통해 업로드버퍼에서 일반버퍼로 복사해주는 방식을 추천한다.
이 방법이 장기적으로 봤을때는 성능관리 측면에서 좋다고 한다.
다음으로는 내가 이 Triangle데모에서 제일 중요하다고 여기는 부분이다.
// Create synchronization objects and wait until assets have been uploaded to the GPU.
{
ThrowIfFailed(m_device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&m_fence)));
m_fenceValue = 1;
// Create an event handle to use for frame synchronization.
m_fenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
if (m_fenceEvent == nullptr)
{
ThrowIfFailed(HRESULT_FROM_WIN32(GetLastError()));
}
// Wait for the command list to execute; we are reusing the same command
// list in our main loop but for now, we just want to wait for setup to
// complete before continuing.
WaitForPreviousFrame();
}
해당 부분은 GPU와 CPU의 작업동기화를 위한 Fence객체와 그의 부속물을 초기화하는 부분이다.
그러면 코드분석의 일관성을 위해 바로 WaitForPreviousFrame메서드의 내용을 보자.
void D3D12HelloTriangle::WaitForPreviousFrame()
{
// WAITING FOR THE FRAME TO COMPLETE BEFORE CONTINUING IS NOT BEST PRACTICE.
// This is code implemented as such for simplicity. The D3D12HelloFrameBuffering
// sample illustrates how to use fences for efficient resource usage and to
// maximize GPU utilization.
// Signal and increment the fence value.
const UINT64 fence = m_fenceValue;
ThrowIfFailed(m_commandQueue->Signal(m_fence.Get(), fence));
m_fenceValue++;
// Wait until the previous frame is finished.
if (m_fence->GetCompletedValue() < fence)
{
ThrowIfFailed(m_fence->SetEventOnCompletion(fence, m_fenceEvent));
WaitForSingleObject(m_fenceEvent, INFINITE);
}
m_frameIndex = m_swapChain->GetCurrentBackBufferIndex();
}
주석에서도 보다시피, 그리 좋은 방법은 아니다. 이 Triangle데모에서는 억지로 CPU와 GPU의 동기화를 맞추는
FrameBuffering데모에서 이에 관하여 좀 더 나은 전략을 분석할 수 있다.
우선 Signal은 두번째 매개변수로 받은 값을 받아 해당 값만큼의 위치에 Fence를 설치한다.
이 말이 쉽게 와닿지 않을것같아서 그림을 그렸다.
이런식으로 Signal은 GPU상에 존재하는 명령목록을 통해 받아온 작업들 사이에 Fence를 두는데
GPU가 해당 FenceValue에 도달한다면 이를 Fence객체에 통지하여, CPU가 다음 프레임을 진행할 수 있도록 동기화한다.
따라서 해당 방식에서도 알다시피 CPU가 놀게되는 사태가 벌어지기때문에 비효율적인 코드라고 말할수 있겠다.
이에 대한 개선안은 마지막으로 말하지만 FrameBuffering데모에서 분석하면서 해법을 알아볼것이다.
마지막으로 렌더링 구간이다.
// Record all the commands we need to render the scene into the command list.
PopulateCommandList();
// Execute the command list.
ID3D12CommandList* ppCommandLists[] = { m_commandList.Get() };
m_commandQueue->ExecuteCommandLists(_countof(ppCommandLists), ppCommandLists);
// Present the frame.
ThrowIfFailed(m_swapChain->Present(1, 0));
WaitForPreviousFrame();
PopulateCommandList라는 메서드를 호출한뒤, 명령목록을 모두 실제로 실행해주고, 스왑체인의 present를 통해 렌더타겟 리소스를 화면에 뿌려주고있다.
마지막에는 위에서 알아본 WaitForPreviousFrame() 메서드를 호출함으로써 GPU의 작업이 다 끝날때까지 CPU가 기다려줌으로써 동기화를 해주는 부분이다.
마지막으로 이 데모는 PopulateCommandList메서드의 내용을 확인한다음 마치겠다.
void D3D12HelloTriangle::PopulateCommandList()
{
// Command list allocators can only be reset when the associated
// command lists have finished execution on the GPU; apps should use
// fences to determine GPU execution progress.
ThrowIfFailed(m_commandAllocator->Reset());
// However, when ExecuteCommandList() is called on a particular command
// list, that command list can then be reset at any time and must be before
// re-recording.
ThrowIfFailed(m_commandList->Reset(m_commandAllocator.Get(), m_pipelineState.Get()));
// Set necessary state.
m_commandList->SetGraphicsRootSignature(m_rootSignature.Get());
m_commandList->RSSetViewports(1, &m_viewport);
m_commandList->RSSetScissorRects(1, &m_scissorRect);
// Indicate that the back buffer will be used as a render target.
m_commandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_renderTargets[m_frameIndex].Get(), D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET));
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(m_rtvHeap->GetCPUDescriptorHandleForHeapStart(), m_frameIndex, m_rtvDescriptorSize);
m_commandList->OMSetRenderTargets(1, &rtvHandle, FALSE, nullptr);
// Record commands.
const float clearColor[] = { 0.0f, 0.2f, 0.4f, 1.0f };
m_commandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);
m_commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
m_commandList->IASetVertexBuffers(0, 1, &m_vertexBufferView);
m_commandList->DrawInstanced(3, 1, 0, 0);
// Indicate that the back buffer will now be used to present.
m_commandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_renderTargets[m_frameIndex].Get(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT));
ThrowIfFailed(m_commandList->Close());
}
이 함수에서 특히 눈여겨봐야할것은 두 부분이 있는데
우선, 명령목록 할당자는 자신이 가리키는 명령목록의 모든 작업이 끝나야만 새로운 작업을 시작할 수 있다.
이 개념은 후에 FrameBuffering에서 중요한 요소로 작동된다.
또한 자원방벽(ResourceBarrier)이 현재 백버퍼의 인덱스(m_frameIndex)를 기준으로 자원상태를 바꿔주는 부분도 눈여겨볼만 하다.
D3D12HelloBundle
이번에는 Bundle 이라는 개념을 학습하기 위한 데모를 분석해보겠다.
번들은 이번에 내가 분석하면서 처음 깨달은 개념중 하나인데,
쉽게 요약해서 말하자면 특정 드로우콜명령집합을 모아둔 인스턴스 명령목록이다.
따라서 사전에 미리 만들어두고, 실제 렌더링을 할때 GraphicsCommandList의 ExecuteBundle을 통해 특정 드로우콜명령집합들을 한번에 호출이 가능하다.
이거는 나에게 개인적으로 진짜로 흥미가 있는 개념이다.
기존 HelloTriangle데모에서 몇가지만 수정하면 되었다.
우선 아래과 같이 번들에 맞는 CommandQueue와 CommandList를 생성한다.
ComPtr<ID3D12CommandAllocator> m_commandAllocator;
ComPtr<ID3D12CommandAllocator> m_bundleAllocator;
ComPtr<ID3D12GraphicsCommandList> m_commandList;
ComPtr<ID3D12GraphicsCommandList> m_bundle;
ThrowIfFailed(m_device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_BUNDLE, IID_PPV_ARGS(&m_bundleAllocator)));
// Create and record the bundle.
{
ThrowIfFailed(m_device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_BUNDLE, m_bundleAllocator.Get(), m_pipelineState.Get(), IID_PPV_ARGS(&m_bundle)));
m_bundle->SetGraphicsRootSignature(m_rootSignature.Get());
m_bundle->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
m_bundle->IASetVertexBuffers(0, 1, &m_vertexBufferView);
m_bundle->DrawInstanced(3, 1, 0, 0);
ThrowIfFailed(m_bundle->Close());
}
일반적으로 생성된 명령큐+목록과 다른 특이한 점은 플래그가 D3D12_COMMAND_LIST_TYPE_BUNDLE 로 설정해야한다.
또한, 위의 소스코드에서 보다시피 드로우콜들을 생성할때 바로 넣어준다.
이제 이 드로우콜들을 간단하게 바로 실행하기위해서는 렌더링쪽에서는 이런식으로 코드가 만들어졌다.
// Execute the commands stored in the bundle.
m_commandList->ExecuteBundle(m_bundle.Get());
렌더링 중간부분이다.
이런식으로 일반 명령목록에 다음 명령을 제출하면, 우리가 위에서 Bundle을 생성할때 미리 넣어둔 드로우콜들이 즉각 실행된다.
D3D12HelloFrameBuffering
이 예제또한 HelloTriangle데모에서 몇가지가 바뀌었다.
우선 눈여겨봐야할 점이 CommandAllocator와 FenceValue를 다중으로 관리하고 있다는 점이다.
이것은 렌더타겟의 백버퍼의 갯수랑은 무관하지만, 이 데모에서는 그냥 같이 통일해준듯 하다.
ThrowIfFailed(m_device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, m_commandAllocators[m_frameIndex].Get(), m_pipelineState.Get(), IID_PPV_ARGS(&m_commandList)));
위 코드는 해당 데모에서 명령목록을 생성하는 부분인데, 사실 나는 1000페이지책을 3회독하고 강의까지 들은상태임에도 불구하고, 이러한 미세한 부분을 놓치고있었다.
여기서 명령목록에 연결된 할당자는 여러개인데 왜 목록은 하나만 만들지? 라는 의문이 갑자기 들었는데
이때 m_commandAllocators[m_frameIndex]로 설정함을 볼수있다. 이것은 우선 기본적으로 연결된 할당자를 위로 설정함을 뜻하고, 나중에 렌더링을 하기전에 GraphicsCommandList::Reset을 통해 할당자를 교차하면서 바꿔줄수있다.
되게 기본적이고 생소하더라도 나에게는 도움이 되었던것같다. 진짜로..
// Create synchronization objects and wait until assets have been uploaded to the GPU.
{
ThrowIfFailed(m_device->CreateFence(m_fenceValues[m_frameIndex], D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&m_fence)));
m_fenceValues[m_frameIndex]++;
// Create an event handle to use for frame synchronization.
m_fenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
if (m_fenceEvent == nullptr)
{
ThrowIfFailed(HRESULT_FROM_WIN32(GetLastError()));
}
// Wait for the command list to execute; we are reusing the same command
// list in our main loop but for now, we just want to wait for setup to
// complete before continuing.
WaitForGpu();
}
// Wait for pending GPU work to complete.
void D3D12HelloFrameBuffering::WaitForGpu()
{
// Schedule a Signal command in the queue.
ThrowIfFailed(m_commandQueue->Signal(m_fence.Get(), m_fenceValues[m_frameIndex]));
// Wait until the fence has been processed.
ThrowIfFailed(m_fence->SetEventOnCompletion(m_fenceValues[m_frameIndex], m_fenceEvent));
WaitForSingleObjectEx(m_fenceEvent, INFINITE, FALSE);
// Increment the fence value for the current frame.
m_fenceValues[m_frameIndex]++;
}
위 부분은 이제 Fence객체를 만들고, 초기화단계에서 GPU와 CPU를 동기화까지 해주는 부분이다.
대충 해당 데모에서 프레임당 Fence상태를 보면 이런식으로 이해하면 좋을듯싶다.
우선 위 그림은 밑에서 렌더링작업을 같이 설명할때 다시 와서 보면 그렇구나 라고 깨달을것이다.
// Render the scene.
void D3D12HelloFrameBuffering::OnRender()
{
// Record all the commands we need to render the scene into the command list.
PopulateCommandList();
// Execute the command list.
ID3D12CommandList* ppCommandLists[] = { m_commandList.Get() };
m_commandQueue->ExecuteCommandLists(_countof(ppCommandLists), ppCommandLists);
// Present the frame.
ThrowIfFailed(m_swapChain->Present(1, 0));
MoveToNextFrame();
}
실제 렌더링작업에서는 다른건 다 필요없고 MoveToNextFrame() 메서드만 보면된다.
왜냐하면 다른건 다 그리기명령이고 해당 메서드만 프레임버퍼링에 관련된 함수이기 때문이다.
// Prepare to render the next frame.
void D3D12HelloFrameBuffering::MoveToNextFrame()
{
// Schedule a Signal command in the queue.
const UINT64 currentFenceValue = m_fenceValues[m_frameIndex];
ThrowIfFailed(m_commandQueue->Signal(m_fence.Get(), currentFenceValue));
// Update the frame index.
m_frameIndex = m_swapChain->GetCurrentBackBufferIndex();
// If the next frame is not ready to be rendered yet, wait until it is ready.
if (m_fence->GetCompletedValue() < m_fenceValues[m_frameIndex])
{
ThrowIfFailed(m_fence->SetEventOnCompletion(m_fenceValues[m_frameIndex], m_fenceEvent));
WaitForSingleObjectEx(m_fenceEvent, INFINITE, FALSE);
}
// Set the fence value for the next frame.
m_fenceValues[m_frameIndex] = currentFenceValue + 1;
}
D3D12HelloFrameBuffering::MoveToNextFrame() 메서드의 내용은 이제 CPU에서 한 프레임씩 처리하면서
CommandList에 담긴 명령들을 CommandQueue에 제출하고 난뒤, CPU의 일들을 한번에 FrameCount(2)만큼 먼저 처리하면서 GPU가 그리기작업을 쭉 따라가는 식이다.
// If the next frame is not ready to be rendered yet, wait until it is ready.
if (m_fence->GetCompletedValue() < m_fenceValues[m_frameIndex])
{
ThrowIfFailed(m_fence->SetEventOnCompletion(m_fenceValues[m_frameIndex], m_fenceEvent));
WaitForSingleObjectEx(m_fenceEvent, INFINITE, FALSE);
}
위 코드내용을 좀 의역하자면 만약 GPU가 아직 처리중인 작업이 있는데도 이전 프레임의 CPU작업속도보다 느리면 GPU의 그리기작업이 끝날때까지 기다리겠다는 의미이다.
쉽게 말해서 m_fence->GetCompletedValue()가 GPU의 현재 프레임의 작업수치라고 보면되고, m_fenceValues[m_frameIndex]가 CPU의 이전 프레임의 작업수치라고 보면된다.
또한, FenceValue의 업데이트는 ID3D12CommandQueue::Signal의 두번째 매개변수를 통해 이루어진다.
한마디로 그냥 쉽게 생각하면되는데 CPU가 먼저 CommandQueue에게 이 시점이되면 해당 frameValue값으로 Fence객체에게 값을 넘겨주라고하고, GPU가 렌더링을 마치고나면 Signal의 두번쨰매개변수를 통해 Fence객체에게 실제로 통보하여 GetCompletedValue를 업데이트하는 방식이다.
D3D12HelloConstantBuffers
우선 RootSignature부분부터 바로 시작하겠다.
CD3DX12_DESCRIPTOR_RANGE1 ranges[1];
CD3DX12_ROOT_PARAMETER1 rootParameters[1];
ranges[0].Init(D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 1, 0, 0, D3D12_DESCRIPTOR_RANGE_FLAG_DATA_STATIC);
rootParameters[0].InitAsDescriptorTable(1, &ranges[0], D3D12_SHADER_VISIBILITY_VERTEX);
// Allow input layout and deny uneccessary access to certain pipeline stages.
D3D12_ROOT_SIGNATURE_FLAGS rootSignatureFlags =
D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT |
D3D12_ROOT_SIGNATURE_FLAG_DENY_HULL_SHADER_ROOT_ACCESS |
D3D12_ROOT_SIGNATURE_FLAG_DENY_DOMAIN_SHADER_ROOT_ACCESS |
D3D12_ROOT_SIGNATURE_FLAG_DENY_GEOMETRY_SHADER_ROOT_ACCESS |
D3D12_ROOT_SIGNATURE_FLAG_DENY_PIXEL_SHADER_ROOT_ACCESS;
CD3DX12_VERSIONED_ROOT_SIGNATURE_DESC rootSignatureDesc;
rootSignatureDesc.Init_1_1(_countof(rootParameters), rootParameters, 0, nullptr, rootSignatureFlags);
ComPtr<ID3DBlob> signature;
ComPtr<ID3DBlob> error;
ThrowIfFailed(D3DX12SerializeVersionedRootSignature(&rootSignatureDesc, featureData.HighestVersion, &signature, &error));
이 부분은 RootSignature를 생성하는 부분인데, 우선 서술자범위객체를 통해서 우리가 실제로 사용할 자원들을 정의하고 있다. 여기서 우리는 CBV버퍼 하나를 Vertex버퍼에서만 사용하기위해 이렇게 만든것으로 보인다.
그다음
// Create the constant buffer.
{
const UINT constantBufferSize = sizeof(SceneConstantBuffer); // CB size is required to be 256-byte aligned.
ThrowIfFailed(m_device->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
D3D12_HEAP_FLAG_NONE,
&CD3DX12_RESOURCE_DESC::Buffer(constantBufferSize),
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(&m_constantBuffer)));
// Describe and create a constant buffer view.
D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc = {};
cbvDesc.BufferLocation = m_constantBuffer->GetGPUVirtualAddress();
cbvDesc.SizeInBytes = constantBufferSize;
m_device->CreateConstantBufferView(&cbvDesc, m_cbvHeap->GetCPUDescriptorHandleForHeapStart());
// Map and initialize the constant buffer. We don't unmap this until the
// app closes. Keeping things mapped for the lifetime of the resource is okay.
CD3DX12_RANGE readRange(0, 0); // We do not intend to read from this resource on the CPU.
ThrowIfFailed(m_constantBuffer->Map(0, &readRange, reinterpret_cast<void**>(&m_pCbvDataBegin)));
memcpy(m_pCbvDataBegin, &m_constantBufferData, sizeof(m_constantBufferData));
}
이 부분은 실제로 상수버퍼가 들어가 Resource의 공간을 GPU에 할당받은 후, 해당 자원에 대한 용도를 서술자힙에 보내주는 코드부분이다.
또한, Map을 통해 실제 상수버퍼에 값까지 넣고있다.
D3D12는 전통적으로 자원에 대한 뷰들을 서술자 힙이라는 공간에 넣어둔다.
이제 반드시 유의할 사항이 상수버퍼의 크기는 256바이트로 정렬된 값이어야만 한다.
이제 실제 렌더링하는 부분인 PopulateCommandList쪽을 살펴보면 다음과 같이 서술자힙과 루트시그니쳐를 등록해주는 부분이 꼭 필요하다.
// Set necessary state.
m_commandList->SetGraphicsRootSignature(m_rootSignature.Get());
ID3D12DescriptorHeap* ppHeaps[] = { m_cbvHeap.Get() };
m_commandList->SetDescriptorHeaps(_countof(ppHeaps), ppHeaps);
m_commandList->SetGraphicsRootDescriptorTable(0, m_cbvHeap->GetGPUDescriptorHandleForHeapStart());
또한, 이 데모에서는 D3D12HelloConstBuffers::OnUpdate() 메서드를 통해 매 프레임 상수버퍼의 값을 수정해주고있다.
// Update frame-based values.
void D3D12HelloConstBuffers::OnUpdate()
{
const float translationSpeed = 0.005f;
const float offsetBounds = 1.25f;
m_constantBufferData.offset.x += translationSpeed;
if (m_constantBufferData.offset.x > offsetBounds)
{
m_constantBufferData.offset.x = -offsetBounds;
}
memcpy(m_pCbvDataBegin, &m_constantBufferData, sizeof(m_constantBufferData));
}
이전에 생성해준 상수버퍼의 자원메모리주소를 가리키는곳으로 메모리복사함수인 memcpy를 통해 상수버퍼의 값을 매 프레임마다 업데이트 해주고있다.
D3D12HelloTexture
우선 이 데모에서는 LoadPipeline 메서드에서 다음과 같이 셰이더리소스를 목적으로하는 서술자힙을 생성하고 있다.
D3D12_DESCRIPTOR_HEAP_DESC::Flags가 SHADER_VISIBLE이라면 해당 리소스가 그래픽파이프라인에 접근할 수 있다는 의미이다.
// Describe and create a shader resource view (SRV) heap for the texture.
D3D12_DESCRIPTOR_HEAP_DESC srvHeapDesc = {};
srvHeapDesc.NumDescriptors = 1;
srvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
srvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
ThrowIfFailed(m_device->CreateDescriptorHeap(&srvHeapDesc, IID_PPV_ARGS(&m_srvHeap)));
m_rtvDescriptorSize = m_device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
CD3DX12_DESCRIPTOR_RANGE1 ranges[1];
ranges[0].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 0, 0, D3D12_DESCRIPTOR_RANGE_FLAG_DATA_STATIC);
CD3DX12_ROOT_PARAMETER1 rootParameters[1];
rootParameters[0].InitAsDescriptorTable(1, &ranges[0], D3D12_SHADER_VISIBILITY_PIXEL);
또한 RootSignature를 생성할 때에는 전처럼 CBV가아닌 SRV로 생성하며, 픽셀셰이더에서만 사용한다고 명시한다.
// Note: ComPtr's are CPU objects but this resource needs to stay in scope until
// the command list that references it has finished executing on the GPU.
// We will flush the GPU at the end of this method to ensure the resource is not
// prematurely destroyed.
ComPtr<ID3D12Resource> textureUploadHeap;
// Create the texture.
{
// Describe and create a Texture2D.
D3D12_RESOURCE_DESC textureDesc = {};
textureDesc.MipLevels = 1;
textureDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
textureDesc.Width = TextureWidth;
textureDesc.Height = TextureHeight;
textureDesc.Flags = D3D12_RESOURCE_FLAG_NONE;
textureDesc.DepthOrArraySize = 1;
textureDesc.SampleDesc.Count = 1;
textureDesc.SampleDesc.Quality = 0;
textureDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
ThrowIfFailed(m_device->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
D3D12_HEAP_FLAG_NONE,
&textureDesc,
D3D12_RESOURCE_STATE_COPY_DEST,
nullptr,
IID_PPV_ARGS(&m_texture)));
const UINT64 uploadBufferSize = GetRequiredIntermediateSize(m_texture.Get(), 0, 1);
// Create the GPU upload buffer.
ThrowIfFailed(m_device->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
D3D12_HEAP_FLAG_NONE,
&CD3DX12_RESOURCE_DESC::Buffer(uploadBufferSize),
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(&textureUploadHeap)));
// Copy data to the intermediate upload heap and then schedule a copy
// from the upload heap to the Texture2D.
std::vector<UINT8> texture = GenerateTextureData();
D3D12_SUBRESOURCE_DATA textureData = {};
textureData.pData = &texture[0];
textureData.RowPitch = TextureWidth * TexturePixelSize;
textureData.SlicePitch = textureData.RowPitch * TextureHeight;
UpdateSubresources(m_commandList.Get(), m_texture.Get(), textureUploadHeap.Get(), 0, 0, 1, &textureData);
m_commandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_texture.Get(), D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE));
// Describe and create a SRV for the texture.
D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
srvDesc.Format = textureDesc.Format;
srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
srvDesc.Texture2D.MipLevels = 1;
m_device->CreateShaderResourceView(m_texture.Get(), &srvDesc, m_srvHeap->GetCPUDescriptorHandleForHeapStart());
}
// Close the command list and execute it to begin the initial GPU setup.
ThrowIfFailed(m_commandList->Close());
ID3D12CommandList* ppCommandLists[] = { m_commandList.Get() };
m_commandQueue->ExecuteCommandLists(_countof(ppCommandLists), ppCommandLists);
위 코드는 본격적으로 Texture를 목적으로하는 TextureWidth * TextureHeight크기의 텍스쳐버퍼를 만든다.
또, 기본 리소스방벽상태가 COPY_DEST인데 이것은 다른 리소스를 복사해올때 목적지(붙여넣기)가 되는 버퍼가 자신이 된다는 의미이다. 또는 그런 목적으로 사용한다는 버퍼 사용용도 명시이다.
또한 GenerateTextureData(); 매서드를 통해 임의로 R8G8B8A8포맷으로 픽셀값이 들어간 TextureWidth * TextureHeight크기의 바이너리 배열을 얻은후, 해당 바이너리값을 UpdateSubresources(업로드힙)을 통해 실제 텍스쳐버퍼에 업로드한다. 우선 RowPitch는 한 행의 크기를 입력하면된다.
R8G8B8A8로 총 32bits이므로 4 * 텍스쳐의 가로길이 를 입력하면되며,
SlicePitch는 여기에 텍스쳐의 세로길이를 곱하면서 전체 텍스쳐의 크기를 입력하면 된다.
또한 중요한 점이, 이것은 CommandList를 사용하므로, 끝나고 명령을 Queue에 보내야 한다.
마지막으로 렌더링을 하는 부분은
// Set necessary state.
m_commandList->SetGraphicsRootSignature(m_rootSignature.Get());
ID3D12DescriptorHeap* ppHeaps[] = { m_srvHeap.Get() };
m_commandList->SetDescriptorHeaps(_countof(ppHeaps), ppHeaps);
m_commandList->SetGraphicsRootDescriptorTable(0, m_srvHeap->GetGPUDescriptorHandleForHeapStart());
이런식으로 RootSignature와 같이 서술자힙을 업데이트해주면 된다.
부록: 마무리
이번 포스팅은 여러모로 세부지식을 튜닝하는데 도움이 되었던것같다.
확실히 최대한 자세하게 쓰려고 노력은 했으나, 그것도 한계가 있다고 판단하기도했고 이 글을 보는 독자들을 가르친다는 느낌보다는 그냥 내가 분석하고 중요하다고 생각되는부분만을 위주로 글로 옮겼기때문에 사람차이에 따라 좀 난해해보일수도있다고 생각이 든다.
다음으로 분석할 샘플주제도 미리 생각해두었다. 다음 샘플, 또는 그 경험들이 나에게 좋은 경험과 지식의 축적으로 이어졌으면 좋겠다.
'분석 > D3D12 MS 샘플 심층분석' 카테고리의 다른 글
[ MS샘플 심층분석 ] D3D12Fullscreen (0) | 2023.01.25 |
---|