DirectX 12 초기화와 렌더링 시스템 구축
DirectX 12는 Direct3D 11과 비교하여 근본적으로 다른 설계 철학을 가지고 있습니다. 하드웨어에 더 가까운 저수준 API를 제공함으로써 개발자에게 GPU 리소스와 명령 실행에 대한 명시적인 제어권을 부여합니다. 이를 통해 멀티스레드 렌더링 최적화, 세밀한 메모리 관리, CPU 오버헤드 최소화가 가능해집니다. 초기화 과정은 복잡하지만, 각 구성 요소의 역할과 상호작용을 이해하면 고성능 그래픽 엔진의 기반을 구축할 수 있습니다.
Device: GPU 통신의 핵심 인터페이스
DirectX 12 디바이스는 GPU와의 모든 상호작용을 관리하는 중앙 허브입니다. 디바이스 생성 과정은 여러 계층의 추상화를 거치며, 각 계층은 특정한 역할을 수행합니다.
순번 | 계층 | 역할과 책임 |
1 | 응용 프로그램 | 사용자 코드에서 GraphicDevice_DX12::CreateDevice() 함수를 호출하여 초기화 프로세스를 시작합니다. 이 레벨에서 디바이스 설정, 디버그 옵션, 어댑터 선택 등의 정책을 결정합니다. |
2 | D3D12 API | D3D12CreateDevice() 함수는 표준화된 DirectX 12 인터페이스를 제공합니다. 이 API는 GPU 제조사와 무관하게 일관된 프로그래밍 모델을 제공하며, 리소스 생성, 명령 제출, 동기화 등의 작업을 추상화합니다. |
3 | 사용자 모드 드라이버 | GPU 제조사(NVIDIA, AMD, Intel)가 제공하는 드라이버가 DirectX API 호출을 각 GPU의 네이티브 명령어로 변환합니다. 여기서 하드웨어 최적화와 성능 튜닝이 이루어집니다. |
4 | DXGI | DirectX Graphics Infrastructure는 그래픽 하드웨어 관리를 담당합니다. 어댑터 열거, 디스플레이 모드 쿼리, 스왑체인 생성 등 DirectX와 윈도우 시스템 간의 브리지 역할을 수행합니다. |
5 | 커널 모드 드라이버 | 운영체제 커널 레벨에서 작동하며, 하드웨어 명령을 최종적으로 GPU에 전달합니다. 메모리 보호, 프로세스 격리, 시스템 안정성을 보장하는 역할을 합니다. |
6 | 하드웨어(GPU) | 물리적인 그래픽 처리 장치가 명령을 실행하여 실제 렌더링 작업을 수행합니다. 정점 처리, 래스터화, 픽셀 셰이딩, 텍스처 샘플링 등이 여기서 이루어집니다. |
DXGI Factory: 하드웨어 탐색과 관리
if (FAILED(CreateDXGIFactory2(dxgiFactoryFlags, IID_PPV_ARGS(&mFactory))))
assert(NULL && "Create DXGI Factory Failed!");
C++
복사
DXGI Factory의 책임
DXGI Factory는 DirectX Graphics Infrastructure의 진입점으로, 시스템의 그래픽 하드웨어와 디스플레이 구성을 파악하는 첫 단계입니다. Factory 객체를 통해 수행할 수 있는 작업들은 다음과 같습니다.
기능 | 상세 설명 |
어댑터 열거 | EnumAdapters1() 함수로 시스템에 설치된 모든 GPU를 탐색합니다. 각 어댑터의 전용 비디오 메모리, 공유 메모리, PCI ID, 이름 등의 정보를 조회할 수 있습니다. 멀티 GPU 시스템에서는 성능이 가장 높은 어댑터를 선택하거나 특정 용도에 맞는 GPU를 지정할 수 있습니다. |
출력 장치 탐색 | IDXGIAdapter::EnumOutputs() 함수로 각 GPU에 연결된 모니터를 조회합니다. 멀티 모니터 환경에서 특정 디스플레이를 타겟팅하거나, 디스플레이 간 성능 특성을 고려한 최적화가 가능합니다. |
디스플레이 모드 조회 | IDXGIOutput::GetDisplayModeList() 함수로 지원 가능한 모든 해상도, 리프레시율, 픽셀 포맷 조합을 확인합니다. 이를 통해 사용자에게 적절한 그래픽 옵션을 제공하고, 최적의 전체화면 모드를 선택할 수 있습니다. |
스왑체인 생성 | CreateSwapChainForHwnd() 함수로 화면 출력을 위한 백버퍼 관리 객체를 생성합니다. 스왑체인은 더블 버퍼링 또는 트리플 버퍼링을 통해 화면 찢어짐 없는 부드러운 렌더링을 제공합니다. |
WARP 어댑터 접근 | EnumWarpAdapter() 함수로 CPU 기반 소프트웨어 렌더러에 접근합니다. GPU가 없는 서버 환경이나 원격 데스크톱 세션에서도 DirectX 12 애플리케이션을 실행할 수 있게 합니다. |
디버그 레이어 활성화
개발 과정에서 디버그 레이어는 필수적인 도구입니다. API 사용 오류, 리소스 누수, 잘못된 상태 전환, 성능 경고 등을 실시간으로 감지하고 Visual Studio 출력 창에 상세한 정보를 제공합니다.
#if defined(_DEBUG)
Microsoft::WRL::ComPtr<ID3D12Debug> debugController;
if (SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController))))
{
debugController->EnableDebugLayer();
// GPU 기반 검증 활성화 (선택적, 성능 저하 큼)
// debugController->SetEnableGPUBasedValidation(true);
dxgiFactoryFlags |= DXGI_CREATE_FACTORY_DEBUG;
}
#endif
C++
복사
디버그 레이어는 다음과 같은 오류를 탐지합니다.
•
잘못된 디스크립터 힙 인덱스 접근
•
리소스 배리어 누락 또는 중복
•
명령 리스트가 닫히지 않은 상태에서 제출
•
동기화 없이 GPU 리소스에 접근
•
메모리 누수와 리소스 해제 누락
어댑터 선택: 하드웨어 vs 소프트웨어
어댑터는 렌더링을 실행할 물리적 또는 가상의 GPU를 나타냅니다. DirectX 12는 유연한 어댑터 선택 메커니즘을 제공합니다.
하드웨어 어댑터 선택 전략
실제 GPU를 사용하여 고성능 렌더링을 수행합니다. 최적의 어댑터를 선택하는 기준은 다음과 같습니다.
•
전용 비디오 메모리(VRAM) 용량이 가장 큰 GPU
•
DirectX 12 Feature Level 12.0 이상을 지원하는 GPU
•
소프트웨어 어댑터(WARP)가 아닌 하드웨어 가속 GPU
•
외장 GPU가 내장 GPU보다 우선
void GetHardwareAdapter(IDXGIFactory4* factory, IDXGIAdapter1** adapter)
{
*adapter = nullptr;
IDXGIAdapter1* tempAdapter = nullptr;
SIZE_T maxDedicatedMemory = 0;
for (UINT i = 0; factory->EnumAdapters1(i, &tempAdapter) != DXGI_ERROR_NOT_FOUND; i++)
{
DXGI_ADAPTER_DESC1 desc;
tempAdapter->GetDesc1(&desc);
// 소프트웨어 어댑터 제외
if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE)
continue;
// D3D12 디바이스 생성 가능 여부 확인
if (SUCCEEDED(D3D12CreateDevice(tempAdapter, D3D_FEATURE_LEVEL_11_0, _uuidof(ID3D12Device), nullptr)))
{
// 전용 메모리가 가장 큰 GPU 선택
if (desc.DedicatedVideoMemory > maxDedicatedMemory)
{
maxDedicatedMemory = desc.DedicatedVideoMemory;
if (*adapter) (*adapter)->Release();
*adapter = tempAdapter;
(*adapter)->AddRef();
}
}
tempAdapter->Release();
}
}
C++
복사
WARP 어댑터: CPU 기반 렌더링
WARP(Windows Advanced Rasterization Platform)는 CPU를 사용하여 DirectX 11 수준의 모든 기능을 소프트웨어로 구현한 렌더러입니다.
if (mbUseWarpDevice)
{
Microsoft::WRL::ComPtr<IDXGIAdapter> warpAdapter;
if (SUCCEEDED(mFactory->EnumWarpAdapter(IID_PPV_ARGS(&warpAdapter))))
{
D3D12CreateDevice(warpAdapter.Get(), D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&mDevice));
}
}
C++
복사
WARP의 사용 사례:
•
원격 데스크톱이나 가상 머신 환경
•
서버 사이드 렌더링 (예: 게임 스트리밍 서비스)
•
자동화된 테스트 환경
•
개발 중 디버깅 목적
WARP는 정확성은 보장하지만 성능은 하드웨어 GPU 대비 10-100배 느립니다.
디바이스 생성과 Feature Level
선택한 어댑터를 기반으로 Direct3D 12 디바이스를 생성합니다.
if (FAILED(D3D12CreateDevice(
hardwareAdapter.Get(),
D3D_FEATURE_LEVEL_11_0,
IID_PPV_ARGS(&mDevice))))
{
assert(NULL && "Create Device with Hardware Adapter Failed!");
}
C++
복사
Feature Level의 의미와 선택
Feature Level은 GPU가 지원해야 하는 최소 기능 집합을 정의합니다. 이는 하위 호환성을 보장하면서도 최신 기능을 활용할 수 있게 해줍니다.
Feature Level | 주요 기능 |
11.0 | 컴퓨트 셰이더, DirectCompute, 테셀레이션, 멀티스레드 렌더링. 대부분의 DirectX 12 GPU가 지원하며 가장 넓은 호환성을 제공합니다. |
11.1 | 논리 연산, UAV 슬롯 확장, 16비트 인덱스 버퍼 개선. Windows 8 이상에서 지원됩니다. |
12.0 | 볼륨 타일 리소스, 보수적 래스터화, 래스터 순서 뷰. 최신 GPU 기능을 활용할 수 있습니다. |
12.1 | 셰이더 지정 스텐실 참조, 타입 UAV 로드, 리소스 힙 계층 3. 2016년 이후 GPU에서 지원됩니다. |
12.2 | 메쉬 셰이더, 샘플러 피드백, DirectX Raytracing 1.1. RTX 20 시리즈 이상의 최신 GPU 전용 기능입니다. |
디바이스 객체의 역할
생성된 디바이스는 GPU 리소스와 상호작용하는 모든 객체의 팩토리 역할을 합니다.
•
명령 객체 생성: Command Queue, Command Allocator, Command List
•
리소스 생성: 텍스처, 버퍼, 상수 버퍼, 구조화된 버퍼
•
뷰 생성: RTV, DSV, SRV, UAV, CBV
•
디스크립터 힙 관리: CPU/GPU 가시적 힙 생성
•
파이프라인 객체: Root Signature, PSO, 셰이더
•
동기화 객체: Fence, Event
•
쿼리 객체: 타임스탬프, 오클루전, 파이프라인 통계
Command Queue와 Swap Chain: 렌더링 파이프라인의 출발점
Command Queue는 CPU에서 GPU로 명령을 전달하는 비동기 파이프라인이며, Swap Chain은 렌더링 결과를 화면에 표시하는 버퍼 관리 시스템입니다. 이 두 구성 요소는 효율적인 프레임 렌더링의 핵심입니다.
단계 | 상세 설명 |
Command Queue 생성 | GPU 명령을 순차적으로 처리할 FIFO 큐를 생성합니다. CPU는 여러 스레드에서 동시에 Command List를 기록하고 큐에 제출할 수 있어, 멀티코어 CPU를 효율적으로 활용합니다. |
Swap Chain 구성 | 화면 출력을 위한 2개 이상의 백버퍼를 설정합니다. 더블 버퍼링은 화면 찢어짐을 방지하고, 트리플 버퍼링은 입력 지연을 최소화하면서 높은 프레임레이트를 유지합니다. |
윈도우 연결 | 생성한 Swap Chain을 윈도우 핸들(HWND)에 바인딩하여 렌더링 결과가 실제 화면에 출력되도록 합니다. 전체화면 모드와 창 모드 간 전환도 이 연결을 통해 관리됩니다. |
프레임 인덱스 관리 | GetCurrentBackBufferIndex()로 현재 렌더링할 백버퍼 인덱스를 확인합니다. 이 인덱스는 Present 호출 후 자동으로 변경되어 다음 프레임을 준비합니다. |
Command Queue: 비동기 명령 실행 관리
D3D12_COMMAND_QUEUE_DESC queueDesc = {};
queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
queueDesc.Priority = D3D12_COMMAND_QUEUE_PRIORITY_NORMAL;
queueDesc.NodeMask = 0;
if (FAILED(mDevice->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&mCommandQueue))))
assert(NULL && "Create Command Queue Failed!");
C++
복사
Command Queue의 동작 메커니즘
Command Queue는 생산자-소비자 패턴을 따릅니다. CPU(생산자)는 Command List에 렌더링 명령을 기록한 후 큐에 제출하고, GPU(소비자)는 큐에서 명령을 순서대로 꺼내어 실행합니다. 이러한 비동기 구조는 다음과 같은 이점을 제공합니다.
•
CPU-GPU 병렬성: CPU가 다음 프레임을 준비하는 동안 GPU는 현재 프레임을 렌더링
•
멀티스레드 명령 기록: 여러 CPU 코어가 동시에 Command List를 생성
•
프레임 파이프라이닝: 여러 프레임이 동시에 처리되어 전체 처리량 향상
Command Queue 타입별 특성
DirectX 12는 작업 유형에 따라 세 가지 큐 타입을 제공합니다.
타입 | 지원 작업 | 사용 사례 |
DIRECT | 그래픽 파이프라인(Draw), 컴퓨트 디스패치, 리소스 복사, 모든 명령 | 일반적인 렌더링 작업에 사용됩니다. 정점 처리, 래스터화, 픽셀 셰이딩을 포함한 전체 그래픽 파이프라인을 실행할 수 있습니다. |
COMPUTE | 컴퓨트 셰이더 디스패치, 리소스 복사 (그래픽 파이프라인 불가) | 물리 시뮬레이션, AI 추론, 포스트 프로세싱 등 컴퓨트 집약적 작업에 특화되어 있습니다. 그래픽 큐와 병렬로 실행하여 GPU 활용률을 극대화할 수 있습니다. |
COPY | 리소스 복사와 업로드만 가능 (가장 제한적) | 텍스처 스트리밍, 대용량 버퍼 전송, 비동기 리소스 로딩에 사용됩니다. DMA 엔진을 활용하여 다른 큐의 작업에 영향을 주지 않고 데이터를 전송할 수 있습니다. |
고급 렌더링 엔진에서는 이 세 가지 큐를 동시에 활용하여 작업을 병렬화합니다. 예를 들어:
•
DIRECT 큐: 메인 렌더링
•
COMPUTE 큐: 포스트 프로세싱, 파티클 시뮬레이션
•
COPY 큐: 다음 레벨의 텍스처 스트리밍
Swap Chain: 화면 출력과 동기화
버퍼링의 필요성과 문제 해결
단일 프레임 버퍼를 사용하면 심각한 시각적 아티팩트가 발생합니다.
화면 깜빡임(Flickering)
매 프레임마다 화면을 지우고 다시 그리는 과정에서, 지워진 상태가 모니터에 표시되면 검은 화면이 순간적으로 보입니다. 이는 렌더링이 느린 복잡한 장면에서 더욱 두드러집니다.
화면 찢어짐(Screen Tearing)
GPU가 새 프레임을 렌더링하는 도중 모니터의 수직 동기화(V-Sync) 신호가 발생하면, 이전 프레임의 일부와 새 프레임의 일부가 동시에 화면에 표시됩니다. 이로 인해 이미지가 수평선을 따라 찢어진 것처럼 보이는 현상이 발생합니다.
이는 특히 빠르게 움직이는 카메라나 객체가 있을 때 뚜렷하게 나타나며, 게임 경험을 크게 해칩니다.
더블 버퍼링 솔루션
두 개의 버퍼를 교대로 사용하여 이 문제를 해결합니다.
•
전면 버퍼(Front Buffer): 현재 모니터에 표시되고 있는 완성된 프레임을 담고 있습니다. 사용자가 보는 이미지는 항상 이 버퍼의 내용입니다.
•
후면 버퍼(Back Buffer): GPU가 다음 프레임을 렌더링하는 작업 공간입니다. 렌더링 도중에는 화면에 표시되지 않으므로 중간 상태가 사용자에게 노출되지 않습니다.
렌더링이 완료되면 모니터의 수직 귀선 기간(V-Blank) 동안 두 버퍼의 포인터를 교환합니다. 이를 플리핑(Flipping) 또는 프레젠트(Present)라고 합니다.
트리플 버퍼링의 장점
세 개의 버퍼를 사용하면 추가적인 이점을 얻을 수 있습니다.
•
입력 지연 감소: 다음 V-Sync를 기다리지 않고 즉시 렌더링 시작
•
프레임 드롭 방지: 프레임이 V-Sync 시간을 놓쳐도 다음 기회에 표시
•
더 부드러운 프레임레이트: 특히 가변 작업 부하에서 유리
Swap Chain 구조체 상세 설정
DXGI_SWAP_CHAIN_DESC1 swapChainDesc = {};
swapChainDesc.BufferCount = 2; // 더블 버퍼링
swapChainDesc.Width = application.GetWindow().GetWidth();
swapChainDesc.Height = application.GetWindow().GetHeight();
swapChainDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
swapChainDesc.Flags = DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT;
swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
swapChainDesc.SampleDesc.Count = 1;
swapChainDesc.SampleDesc.Quality = 0;
swapChainDesc.Scaling = DXGI_SCALING_STRETCH;
swapChainDesc.Stereo = FALSE;
swapChainDesc.AlphaMode = DXGI_ALPHA_MODE_UNSPECIFIED;
C++
복사
설정 항목 | 상세 설명과 선택 가이드 |
BufferCount | 백버퍼 개수를 지정합니다. 2는 더블 버퍼링으로 화면 찢어짐을 방지하며 메모리 효율적입니다. 3은 트리플 버퍼링으로 입력 지연을 최소화하고 가변 프레임레이트를 부드럽게 합니다. 경쟁 게임에는 2를, 시네마틱 게임에는 3을 권장합니다. |
Width, Height | 백버퍼의 해상도입니다. 일반적으로 윈도우 클라이언트 영역 크기와 동일하게 설정하지만, 내부 렌더링 해상도를 낮춰 성능을 향상시킬 수도 있습니다(업스케일링). |
Format | 픽셀 포맷을 정의합니다. R8G8B8A8_UNORM은 각 채널당 8비트(총 32비트)로 0-255 범위를 0.0-1.0으로 정규화합니다. HDR 렌더링에는 R10G10B10A2_UNORM 또는 R16G16B16A16_FLOAT을 사용합니다. |
Flags | FRAME_LATENCY_WAITABLE_OBJECT는 CPU가 GPU보다 너무 앞서가지 않도록 제어하는 이벤트 객체를 생성합니다. SetMaximumFrameLatency()와 함께 사용하여 프레임 지연을 제한할 수 있습니다. 이는 메모리 사용량을 줄이고 입력 응답성을 개선합니다. |
BufferUsage | RENDER_TARGET_OUTPUT은 백버퍼를 렌더 타겟으로 사용함을 명시합니다. 추가로 SHADER_INPUT을 지정하면 셰이더에서 이전 프레임을 읽을 수 있어 모션 블러나 TAA 구현에 유용합니다. |
SwapEffect | FLIP_DISCARD는 Windows 10 이상에서 권장되는 최신 플립 모델입니다. 효율적인 버퍼 교환, 낮은 지연시간, 가변 리프레시율(FreeSync/G-Sync) 지원을 제공합니다. 이전 프레임 내용이 필요 없는 일반적인 렌더링에 적합합니다. |
SampleDesc | 멀티샘플 안티앨리어싱(MSAA) 설정입니다. Count=1, Quality=0은 MSAA를 비활성화합니다. MSAA를 사용하려면 CheckFeatureSupport()로 지원 여부를 확인한 후 적절한 샘플 수(2, 4, 8)를 설정합니다. TAA나 FXAA 같은 포스트 프로세스 AA가 더 일반적입니다. |
Scaling | STRETCH는 백버퍼 크기와 윈도우 크기가 다를 때 비율에 맞춰 이미지를 늘립니다. NONE은 1:1 픽셀 매핑을 유지하며, 고 DPI 디스플레이에서 선명한 이미지를 제공합니다. |
Stereo | 스테레오스코픽 3D 렌더링 활성화 여부입니다. VR 헤드셋이나 3D 모니터가 아니면 FALSE로 설정합니다. VR은 별도의 VR API를 사용하므로 일반적으로 사용하지 않습니다. |
Swap Chain 생성 및 윈도우 연결
Microsoft::WRL::ComPtr<IDXGISwapChain1> swapChain;
HWND hwnd = application.GetWindow().GetHwnd();
if (FAILED(mFactory->CreateSwapChainForHwnd(
mCommandQueue.Get(), // Command Queue를 통해 렌더링 명령 실행
hwnd, // 윈도우 핸들
&swapChainDesc, // Swap Chain 설정
nullptr, // 전체화면 설정 (nullptr = 창모드)
nullptr, // 출력 제한 (nullptr = 기본 출력)
&swapChain)))
{
assert(NULL && "Create Swap Chain Failed!");
}
C++
복사
Swap Chain은 Command Queue와 윈도우를 연결하는 브리지입니다. 렌더링 명령이 큐를 통해 실행되고, 결과가 Swap Chain의 백버퍼에 그려진 후 Present 호출로 화면에 표시됩니다.
전체화면 전환 제어
// Alt+Enter 기본 전체화면 전환 비활성화
mFactory->MakeWindowAssociation(hwnd, DXGI_MWA_NO_ALT_ENTER);
C++
복사
DXGI는 기본적으로 Alt+Enter 키 조합으로 전체화면과 창모드를 자동 전환합니다. 그러나 게임 엔진에서는 해상도 변경, UI 스케일링, 입력 모드 전환 등을 함께 관리해야 하므로 자체 전체화면 로직을 구현하는 것이 일반적입니다.
SwapChain3 인터페이스로 업그레이드
swapChain.As(&mSwapChain);
mFrameIndex = mSwapChain->GetCurrentBackBufferIndex();
C++
복사
IDXGISwapChain3은 추가 기능을 제공합니다.
•
GetCurrentBackBufferIndex(): 현재 렌더링할 백버퍼 인덱스
•
CheckColorSpaceSupport(): HDR 색 공간 지원 확인
•
SetColorSpace1(): HDR 출력 활성화
•
ResizeBuffers1(): 버퍼 크기 변경 시 색 공간 유지
Descriptor Heap과 Render Target View
Descriptor는 GPU가 리소스에 접근하기 위한 메타데이터를 담고 있는 경량 구조체입니다. CPU의 포인터와 유사하지만 GPU가 이해할 수 있는 형식으로 인코딩되어 있습니다. Descriptor Heap은 이러한 Descriptor들을 연속된 메모리 블록에 저장하여 GPU가 효율적으로 접근할 수 있게 합니다.
Descriptor의 종류
DirectX 12는 다양한 리소스 타입에 대응하는 Descriptor를 제공합니다.
•
RTV (Render Target View): 렌더링 결과를 출력할 텍스처
•
DSV (Depth Stencil View): 깊이/스텐실 버퍼
•
SRV (Shader Resource View): 셰이더에서 읽기 전용으로 사용할 리소스
•
UAV (Unordered Access View): 셰이더에서 읽기/쓰기가 가능한 리소스
•
CBV (Constant Buffer View): 상수 버퍼
•
Sampler: 텍스처 샘플링 방식 정의
RTV Descriptor Heap 생성
D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc = {};
rtvHeapDesc.NumDescriptors = SWAP_CHAIN_FRAME_COUNT;
rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
rtvHeapDesc.NodeMask = 0;
if (FAILED(mDevice->CreateDescriptorHeap(&rtvHeapDesc, IID_PPV_ARGS(&mRTVHeap))))
assert(NULL && "Create RTV Descriptor Heap Failed!");
// Descriptor 크기는 GPU 아키텍처마다 다름
mRTVDescriptorSize = mDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
C++
복사
RTV Descriptor Heap은 렌더 타겟으로 사용할 텍스처들의 Descriptor를 저장합니다. Swap Chain의 각 백버퍼마다 하나의 RTV가 필요하므로, 최소한 BufferCount만큼의 Descriptor 슬롯이 필요합니다.
Descriptor Heap의 각 슬롯은 고정 크기를 가지며, 이 크기는 GPU 아키텍처에 따라 다릅니다. NVIDIA GPU에서는 32바이트, AMD GPU에서는 다른 크기일 수 있습니다. 따라서 GetDescriptorHandleIncrementSize()로 런타임에 크기를 조회해야 합니다.
Render Target View 생성과 배치
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(mRTVHeap->GetCPUDescriptorHandleForHeapStart());
for (UINT n = 0; n < SWAP_CHAIN_FRAME_COUNT; n++)
{
// Swap Chain에서 백버퍼 리소스 획득
if (FAILED(mSwapChain->GetBuffer(n, IID_PPV_ARGS(&mRenderTargets[n]))))
assert(NULL && "Get Swap Chain Buffer Failed!");
// RTV 생성 (Descriptor Heap의 n번째 슬롯에 저장)
mDevice->CreateRenderTargetView(mRenderTargets[n].Get(), nullptr, rtvHandle);
// 다음 Descriptor 위치로 이동
rtvHandle.Offset(1, mRTVDescriptorSize);
}
C++
복사
각 백버퍼에 대해 RTV를 생성하고 Descriptor Heap의 연속된 슬롯에 배치합니다. CreateRenderTargetView()의 두 번째 매개변수(nullptr)는 기본 RTV 설정을 사용함을 의미합니다. 특정 MIP 레벨이나 배열 슬라이스만 렌더링하려면 D3D12_RENDER_TARGET_VIEW_DESC 구조체를 제공할 수 있습니다.
렌더링 시에는 GetCurrentBackBufferIndex()로 현재 프레임 인덱스를 얻어 해당 RTV를 사용합니다.
Command Allocator와 Command List: 명령 기록 시스템
DirectX 12의 명령 기록 시스템은 두 가지 주요 객체로 구성됩니다.
Command Allocator: 메모리 관리자
Command Allocator는 Command List가 명령을 기록할 메모리를 할당하는 메모리 풀입니다. 각 프레임마다 독립적인 Allocator를 사용하여 멀티프레임 파이프라이닝을 지원합니다.
struct FrameContext
{
Microsoft::WRL::ComPtr<ID3D12CommandAllocator> CommandAllocator;
UINT64 FenceValue;
};
FrameContext mFrameContext[SWAP_CHAIN_FRAME_COUNT];
for (int i = 0; i < SWAP_CHAIN_FRAME_COUNT; i++)
{
if (FAILED(mDevice->CreateCommandAllocator(
D3D12_COMMAND_LIST_TYPE_DIRECT,
IID_PPV_ARGS(&mFrameContext[i].CommandAllocator))))
{
assert(NULL && "Create Command Allocator Failed!");
}
}
C++
복사
프레임별 Allocator가 필요한 이유
GPU는 비동기적으로 명령을 실행하므로, 이전 프레임의 명령이 아직 실행 중일 수 있습니다. 만약 단일 Allocator를 사용하면, 새 프레임을 기록할 때 이전 프레임의 메모리를 덮어쓰게 되어 GPU가 잘못된 명령을 실행하거나 크래시가 발생합니다.
프레임별 Allocator를 사용하면:
•
각 프레임이 독립적인 메모리 공간을 가짐
•
GPU가 이전 프레임을 처리하는 동안 CPU는 다음 프레임을 기록
•
3개의 Allocator를 순환하면 최대 3 프레임까지 파이프라이닝 가능
Command List: 명령 기록 객체
Command List는 실제 렌더링 명령을 기록하는 녹화 장치입니다.
if (FAILED(mDevice->CreateCommandList(
0, // GPU 노드 마스크 (단일 GPU = 0)
D3D12_COMMAND_LIST_TYPE_DIRECT, // 큐 타입과 일치
mFrameContext[0].CommandAllocator.Get(), // 초기 Allocator
nullptr, // 초기 PSO (nullptr 가능)
IID_PPV_ARGS(&mCommandList))))
{
assert(NULL && "Create Command List Failed!");
}
// Command List는 생성 시 열린 상태이므로 닫아야 함
mCommandList->Close();
C++
복사
Command List는 생성 시 "열린(Recording)" 상태입니다. 명령을 기록하기 전에는 닫아두고, 렌더링 시 Reset()으로 다시 열어서 사용합니다.
멀티스레드 명령 기록
DirectX 12의 강력한 기능 중 하나는 여러 스레드에서 동시에 Command List를 기록할 수 있다는 점입니다.
// 스레드 1: 지형 렌더링
void RenderTerrain(ID3D12GraphicsCommandList* cmdList) { ... }
// 스레드 2: 캐릭터 렌더링
void RenderCharacters(ID3D12GraphicsCommandList* cmdList) { ... }
// 스레드 3: UI 렌더링
void RenderUI(ID3D12GraphicsCommandList* cmdList) { ... }
// 메인 스레드: 모든 Command List를 Command Queue에 제출
ID3D12CommandList* cmdLists[] = { terrainCmdList, charactersCmdList, uiCmdList };
mCommandQueue->ExecuteCommandLists(3, cmdLists);
C++
복사
각 스레드는 자신만의 Command List와 Allocator를 사용하여 동시에 명령을 기록하고, 메인 스레드가 모든 List를 수집하여 한 번에 제출합니다.
Fence를 통한 CPU-GPU 동기화
Fence는 CPU와 GPU 간의 동기화를 위한 세마포어 메커니즘입니다. GPU가 특정 지점까지 명령 실행을 완료했는지 CPU가 확인할 수 있게 해줍니다.
Fence 객체 생성
UINT64 mFenceValue = 0;
HANDLE mFenceEvent = nullptr;
if (FAILED(mDevice->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&mFence))))
assert(NULL && "Create Fence Failed!");
mFenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
if (mFenceEvent == nullptr)
assert(NULL && "Create Fence Event Failed!");
C++
복사
Fence는 64비트 정수 카운터로 동작합니다. GPU가 특정 명령까지 실행을 완료하면 Fence 값을 업데이트하고, CPU는 이 값을 확인하여 동기화 지점을 판단합니다.
프레임 동기화 패턴
void WaitForPreviousFrame()
{
// 현재 Fence 값을 큐에 신호
const UINT64 currentFenceValue = mFenceValue;
mCommandQueue->Signal(mFence.Get(), currentFenceValue);
mFenceValue++;
// GPU가 신호를 완료했는지 확인
if (mFence->GetCompletedValue() < currentFenceValue)
{
// 아직 완료되지 않았으면 이벤트 대기
mFence->SetEventOnCompletion(currentFenceValue, mFenceEvent);
WaitForSingleObject(mFenceEvent, INFINITE);
}
// 다음 프레임 인덱스로 이동
mFrameIndex = mSwapChain->GetCurrentBackBufferIndex();
}
C++
복사
이 패턴의 동작 원리:
1.
Signal(): Command Queue에 Fence 값 업데이트 명령 삽입
2.
GPU가 이전 명령을 모두 실행한 후 Fence 값을 설정
3.
GetCompletedValue(): 현재 Fence 값 확인
4.
값이 예상보다 작으면 GPU가 아직 실행 중
5.
SetEventOnCompletion(): Fence 값 도달 시 이벤트 신호
6.
WaitForSingleObject(): 이벤트가 신호될 때까지 CPU 블록
멀티프레임 파이프라이닝
더 정교한 동기화는 각 프레임마다 고유한 Fence 값을 저장합니다.
void BeginFrame()
{
UINT frameIndex = mSwapChain->GetCurrentBackBufferIndex();
// 이 프레임의 Allocator를 재사용하기 전에 GPU 완료 대기
UINT64 fenceValue = mFrameContext[frameIndex].FenceValue;
if (mFence->GetCompletedValue() < fenceValue)
{
mFence->SetEventOnCompletion(fenceValue, mFenceEvent);
WaitForSingleObject(mFenceEvent, INFINITE);
}
// Allocator와 Command List 리셋
mFrameContext[frameIndex].CommandAllocator->Reset();
mCommandList->Reset(mFrameContext[frameIndex].CommandAllocator.Get(), nullptr);
}
void EndFrame()
{
UINT frameIndex = mSwapChain->GetCurrentBackBufferIndex();
// Command List 제출
mCommandList->Close();
ID3D12CommandList* cmdLists[] = { mCommandList.Get() };
mCommandQueue->ExecuteCommandLists(1, cmdLists);
// 이 프레임의 Fence 값 저장 및 신호
mFrameContext[frameIndex].FenceValue = mFenceValue;
mCommandQueue->Signal(mFence.Get(), mFenceValue);
mFenceValue++;
// Present
mSwapChain->Present(1, 0);
}
C++
복사
이 방식은 최대 SWAP_CHAIN_FRAME_COUNT개의 프레임이 동시에 파이프라인에 존재할 수 있게 하여 CPU와 GPU의 병렬성을 극대화합니다.
Root Signature와 Pipeline State Object
Root Signature: 셰이더 리소스 인터페이스
Root Signature는 셰이더가 접근할 리소스의 레이아웃을 정의하는 계약입니다. C++의 함수 시그니처와 유사하게, 셰이더가 기대하는 매개변수의 타입과 순서를 명시합니다.
Root Parameter 타입 | 상세 설명 |
Root Constant | 32비트 상수 값을 Root Signature에 직접 저장합니다. 최대 64 DWORD(256바이트)까지 허용되며, 가장 빠른 접근 속도를 제공합니다. 변환 행렬의 인덱스, 객체 ID, 시간 값 등 자주 변경되는 소량의 데이터에 적합합니다. Descriptor를 거치지 않으므로 오버헤드가 최소화됩니다. |
Root Descriptor | CBV, SRV, UAV 하나를 직접 가리키는 GPU 가상 주소를 저장합니다. Descriptor Heap을 거치지 않고 바로 리소스에 접근하므로 Descriptor Table보다 빠르지만, 단일 리소스만 참조할 수 있습니다. 동적으로 변경되는 상수 버퍼나 구조화된 버퍼에 사용합니다. |
Descriptor Table | Descriptor Heap의 특정 범위를 가리키는 포인터입니다. 여러 텍스처, 버퍼, 샘플러를 하나의 테이블로 묶어 효율적으로 관리할 수 있습니다. 가장 유연하고 일반적으로 사용되는 방식이며, 머티리얼 시스템이나 텍스처 배열에 적합합니다. |
Root Signature 생성 예제
CD3DX12_ROOT_PARAMETER rootParameters[3];
// Root Constant: 월드-뷰-프로젝션 행렬
rootParameters[0].InitAsConstants(16, 0); // 16 DWORDs (4x4 matrix), register b0
// Root Descriptor: 머티리얼 상수 버퍼
rootParameters[1].InitAsConstantBufferView(1); // register b1
// Descriptor Table: 텍스처와 샘플러
CD3DX12_DESCRIPTOR_RANGE ranges[2];
ranges[0].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 4, 0); // 4 textures, t0-t3
ranges[1].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SAMPLER, 2, 0); // 2 samplers, s0-s1
rootParameters[2].InitAsDescriptorTable(2, ranges);
CD3DX12_ROOT_SIGNATURE_DESC rootSigDesc;
rootSigDesc.Init(3, rootParameters, 0, nullptr, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);
// Root Signature를 바이너리로 시리얼라이즈
Microsoft::WRL::ComPtr<ID3DBlob> signature;
Microsoft::WRL::ComPtr<ID3DBlob> error;
D3D12SerializeRootSignature(&rootSigDesc, D3D_ROOT_SIGNATURE_VERSION_1, &signature, &error);
// Root Signature 생성
mDevice->CreateRootSignature(0, signature->GetBufferPointer(), signature->GetBufferSize(), IID_PPV_ARGS(&mRootSignature));
C++
복사
Pipeline State Object: 렌더링 파이프라인 상태
PSO는 렌더링 파이프라인의 거의 모든 상태를 단일 객체로 캡슐화합니다. DirectX 11에서는 상태를 개별적으로 변경했지만, DirectX 12는 전체 파이프라인 상태를 미리 컴파일하여 런타임 검증 오버헤드를 제거합니다.
PSO에 포함되는 상태:
•
셰이더: Vertex, Hull, Domain, Geometry, Pixel, Compute
•
입력 레이아웃: 정점 버퍼의 구조와 시맨틱
•
Root Signature: 리소스 바인딩 레이아웃
•
래스터라이저: 컬링 모드, 채우기 모드, 깊이 바이어스
•
블렌드: 알파 블렌딩, 컬러 쓰기 마스크
•
깊이-스텐실: 깊이 테스트, 스텐실 연산
•
멀티샘플: MSAA 설정
•
토폴로지: 프리미티브 타입
•
렌더 타겟 포맷: 출력 텍스처 포맷
PSO 생성 예제
D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc = {};
psoDesc.pRootSignature = mRootSignature.Get();
psoDesc.VS = { vertexShaderBlob->GetBufferPointer(), vertexShaderBlob->GetBufferSize() };
psoDesc.PS = { pixelShaderBlob->GetBufferPointer(), pixelShaderBlob->GetBufferSize() };
// 입력 레이아웃
D3D12_INPUT_ELEMENT_DESC inputLayout[] =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
{ "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
{ "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 24, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }
};
psoDesc.InputLayout = { inputLayout, _countof(inputLayout) };
// 래스터라이저 상태
psoDesc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
psoDesc.RasterizerState.CullMode = D3D12_CULL_MODE_BACK;
psoDesc.RasterizerState.FrontCounterClockwise = FALSE;
// 블렌드 상태
psoDesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
// 깊이-스텐실 상태
psoDesc.DepthStencilState = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT);
psoDesc.DepthStencilState.DepthEnable = TRUE;
psoDesc.DepthStencilState.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ALL;
psoDesc.DepthStencilState.DepthFunc = D3D12_COMPARISON_FUNC_LESS;
// 출력 포맷
psoDesc.SampleMask = UINT_MAX;
psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
psoDesc.NumRenderTargets = 1;
psoDesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM;
psoDesc.DSVFormat = DXGI_FORMAT_D32_FLOAT;
psoDesc.SampleDesc.Count = 1;
mDevice->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&mPSO));
C++
복사
PSO는 생성 비용이 높으므로 초기화 시 미리 생성하고 캐싱해야 합니다. 렌더링 중에는 SetPipelineState()로 빠르게 전환할 수 있습니다.
렌더링 루프: 전체 프로세스
프레임 시작과 Command List 준비
void RenderFrame()
{
UINT frameIndex = mSwapChain->GetCurrentBackBufferIndex();
// 이전 프레임 완료 대기
WaitForFrame(frameIndex);
// Command Allocator와 List 리셋
mFrameContext[frameIndex].CommandAllocator->Reset();
mCommandList->Reset(mFrameContext[frameIndex].CommandAllocator.Get(), mPSO.Get());
C++
복사
Allocator를 리셋하면 이전 명령의 메모리가 해제되고 새 명령을 기록할 준비가 됩니다. Command List를 리셋할 때 PSO를 지정하면 초기 파이프라인 상태가 설정됩니다.
리소스 배리어: 상태 전환
DirectX 12에서는 리소스 상태를 명시적으로 관리해야 합니다. 백버퍼를 렌더 타겟으로 사용하기 전에 Present 상태에서 RenderTarget 상태로 전환합니다.
CD3DX12_RESOURCE_BARRIER barrier = CD3DX12_RESOURCE_BARRIER::Transition(
mRenderTargets[frameIndex].Get(),
D3D12_RESOURCE_STATE_PRESENT,
D3D12_RESOURCE_STATE_RENDER_TARGET);
mCommandList->ResourceBarrier(1, &barrier);
C++
복사
리소스 배리어는 GPU에게 리소스 사용 방식이 변경됨을 알려 메모리 일관성을 보장합니다. 이를 생략하면 데이터 레이스나 미정의 동작이 발생할 수 있습니다.
렌더 타겟 설정과 화면 클리어
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(
mRTVHeap->GetCPUDescriptorHandleForHeapStart(),
frameIndex,
mRTVDescriptorSize);
mCommandList->OMSetRenderTargets(1, &rtvHandle, FALSE, nullptr);
const float clearColor[] = { 0.2f, 0.3f, 0.4f, 1.0f };
mCommandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);
C++
복사
OMSetRenderTargets()는 출력 병합(Output Merger) 단계의 렌더 타겟을 설정합니다. ClearRenderTargetView()는 백버퍼를 단색으로 클리어하여 이전 프레임의 잔상을 제거합니다.
뷰포트와 시저 영역 설정
D3D12_VIEWPORT viewport = { 0.0f, 0.0f, (float)width, (float)height, 0.0f, 1.0f };
D3D12_RECT scissorRect = { 0, 0, (LONG)width, (LONG)height };
mCommandList->RSSetViewports(1, &viewport);
mCommandList->RSSetScissorRects(1, &scissorRect);
C++
복사
뷰포트는 NDC 좌표를 화면 좌표로 변환하는 영역을 정의합니다. 시저 영역은 렌더링을 특정 사각형 영역으로 제한하여 UI나 분할 화면에 사용됩니다.
Root Signature와 리소스 바인딩
mCommandList->SetGraphicsRootSignature(mRootSignature.Get());
mCommandList->SetGraphicsRoot32BitConstants(0, 16, &worldViewProj, 0);
mCommandList->SetGraphicsRootConstantBufferView(1, materialCB->GetGPUVirtualAddress());
mCommandList->SetGraphicsRootDescriptorTable(2, textureTableHandle);
C++
복사
Root Signature를 설정한 후 각 Root Parameter에 실제 데이터를 바인딩합니다. 이 데이터는 셰이더에서 레지스터를 통해 접근할 수 있습니다.
지오메트리 설정과 드로우 호출
mCommandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
mCommandList->IASetVertexBuffers(0, 1, &vertexBufferView);
mCommandList->IASetIndexBuffer(&indexBufferView);
mCommandList->DrawIndexedInstanced(indexCount, 1, 0, 0, 0);
}
C++
복사
IASet 함수들은 입력 어셈블러(Input Assembler) 단계를 구성합니다. DrawIndexedInstanced()는 실제 렌더링 명령으로, GPU가 정점 셰이더부터 픽셀 셰이더까지 전체 파이프라인을 실행하게 합니다.
Present로 상태 전환 및 화면 출력
void PresentFrame()
{
UINT frameIndex = mSwapChain->GetCurrentBackBufferIndex();
// 렌더 타겟에서 Present 상태로 전환
CD3DX12_RESOURCE_BARRIER barrier = CD3DX12_RESOURCE_BARRIER::Transition(
mRenderTargets[frameIndex].Get(),
D3D12_RESOURCE_STATE_RENDER_TARGET,
D3D12_RESOURCE_STATE_PRESENT);
mCommandList->ResourceBarrier(1, &barrier);
// Command List 종료 및 제출
mCommandList->Close();
ID3D12CommandList* cmdLists[] = { mCommandList.Get() };
mCommandQueue->ExecuteCommandLists(1, cmdLists);
// 화면에 표시 (V-Sync 활성화)
mSwapChain->Present(1, 0);
// Fence 신호
mFrameContext[frameIndex].FenceValue = mFenceValue;
mCommandQueue->Signal(mFence.Get(), mFenceValue);
mFenceValue++;
}
C++
복사
Present()의 첫 번째 매개변수는 동기화 간격입니다.
•
1: V-Sync 활성화, 60Hz 모니터에서 60 FPS 제한
•
0: V-Sync 비활성화, 최대 프레임레이트 허용 (화면 찢어짐 가능)
•
2: 30 FPS로 제한 (모든 두 번째 V-Sync 대기)
정리 및 핵심 개념 요약
DirectX 12 초기화는 복잡하고 많은 코드를 요구하지만, 이를 통해 얻을 수 있는 성능과 제어 수준은 DirectX 11과 비교할 수 없습니다. 각 구성 요소의 역할과 상호작용을 이해하면 고성능 그래픽 엔진의 기반을 구축할 수 있습니다.
핵심 개념 정리
개념 | 역할과 중요성 |
Device | GPU와의 통신 인터페이스. 모든 DirectX 객체 생성의 팩토리 역할을 하며, GPU 기능 쿼리와 리소스 생성을 담당합니다. |
Command Queue | CPU에서 GPU로 명령을 전달하는 비동기 파이프라인. DIRECT, COMPUTE, COPY 타입으로 작업을 분류하여 병렬 처리를 가능하게 합니다. |
Swap Chain | 화면 출력을 위한 백버퍼 관리 시스템. 더블/트리플 버퍼링으로 화면 찢어짐을 방지하고 부드러운 렌더링을 제공합니다. |
Descriptor Heap | GPU 리소스 참조(Descriptor)를 저장하는 메모리 블록. RTV, DSV, SRV, UAV, CBV, Sampler 등 다양한 뷰를 관리합니다. |
Command Allocator | Command List가 명령을 기록할 메모리를 할당하는 풀. 각 프레임마다 독립적인 Allocator를 사용하여 멀티프레임 파이프라이닝을 지원합니다. |
Command List | 렌더링 명령을 기록하는 녹화 장치. 멀티스레드 환경에서 여러 List를 동시에 기록하여 CPU 병렬화를 극대화합니다. |
Fence | CPU와 GPU 간 동기화 메커니즘. GPU 작업 완료를 확인하고 리소스 재사용 타이밍을 제어하여 데이터 레이스를 방지합니다. |
Root Signature | 셰이더 리소스 바인딩 레이아웃 정의. Root Constant, Root Descriptor, Descriptor Table로 셰이더가 접근할 리소스 인터페이스를 명시합니다. |
Pipeline State Object | 렌더링 파이프라인의 전체 상태를 캡슐화한 단일 객체. 셰이더, 입력 레이아웃, 래스터라이저, 블렌드, 깊이-스텐실 상태를 포함하여 런타임 검증 오버헤드를 제거합니다. |
성능 최적화를 위한 핵심 원칙
•
명시적 동기화: Fence를 사용하여 CPU와 GPU의 작업 타이밍을 정확히 제어
•
멀티스레드 활용: 여러 Command List를 동시에 기록하여 CPU 병목 해소
•
리소스 재사용: Command Allocator와 백버퍼를 순환하며 재사용하여 메모리 할당 최소화
•
상태 변경 최소화: PSO 전환을 줄이고 동일한 상태의 객체를 배치 렌더링
•
배리어 최적화: 불필요한 리소스 배리어를 제거하고 여러 배리어를 배치로 처리
DirectX 12는 학습 곡선이 가파르지만, 이러한 구조를 통해 현대 GPU의 성능을 최대한 활용할 수 있습니다. 멀티스레드 렌더링, 명시적 메모리 관리, 비동기 컴퓨트, 효율적인 리소스 바인딩 등 고급 기법을 구현할 수 있는 기반을 제공합니다.








