DX11, DX12 렌더링 기본 구조 비교(작성중)
DX11 기본 렌더링 코드
DX11에서는 대부분의 자원이 자동으로 관리되며, 코드가 상대적으로 간단. API가 많은 부분을 추상화하고 있어, 디바이스 상태 관리나 커맨드 버퍼 처리를 자동으로 처리해 준다.
// DX11 초기화 및 렌더링 예제 void InitD3D(HWND hWnd) { // 디바이스와 디바이스 컨텍스트 생성 D3D11CreateDeviceAndSwapChain(...); // 렌더 타겟 생성 ID3D11Texture2D * pBackBuffer; swapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), (LPVOID*)&pBackBuffer); device->CreateRenderTargetView(pBackBuffer, nullptr, &renderTargetView); // 깊이/스텐실 버퍼 생성 및 초기화 D3D11_DEPTH_STENCIL_VIEW_DESC depthStencilDesc = {}; device->CreateDepthStencilView(..., &depthStencilView); // 뷰포트 설정 D3D11_VIEWPORT viewport = {}; viewport.Width = static_cast<float>(width); viewport.Height = static_cast<float>(height); viewport.MinDepth = 0.0f; viewport.MaxDepth = 1.0f; context->RSSetViewports(1, &viewport); } void RenderFrame() { // 백버퍼 지우기 float clearColor[4] = {0.0f, 0.2f, 0.4f, 1.0f}; context->ClearRenderTargetView(renderTargetView, clearColor); context->ClearDepthStencilView(depthStencilView, D3D11_CLEAR_DEPTH, 1.0f, 0); // 렌더링 호출 context->DrawIndexed(...); // 프레임을 화면에 출력 swapChain->Present(1, 0); } |
- DX11은 상태 관리를 내부에서 처리해 주므로 개발자가 그다지 신경 쓸 필요가 없음
- 렌더링 루프에서 여러 개의 드로우 콜이 간단하게 처리
- GPU와의 동기화, 명령 큐 처리가 자동으로 처리
DX12 기본 렌더링 코드
DX12는 저수준 API로 더 많은 수동 관리가 필요하며, 이에 따라 복잡한 초기화와 자원 관리가 필요하다. 그러나 더 많은 최적화가 가능.
// DX12 초기화 및 렌더링 예제 void InitD3D12(HWND hWnd) { // 디바이스 생성 D3D12CreateDevice(...); // 커맨드 큐, 커맨드 할당자, 커맨드 리스트 생성 device->CreateCommandQueue(...); device->CreateCommandAllocator(...); device->CreateCommandList(...); // 스왑체인 생성 DXGI_SWAP_CHAIN_DESC swapChainDesc = {}; swapChain->CreateSwapChain(...); // 렌더 타겟 및 깊이 스텐실 버퍼 설정 for (UINT i = 0; i < frameBufferCount; i++) { swapChain->GetBuffer(i, IID_PPV_ARGS(&renderTargets[i])); device->CreateRenderTargetView(renderTargets[i], nullptr, rtvHeap->GetCPUDescriptorHandleForHeapStart()); } // 동기화 객체 생성 (펜스, 이벤트 등) device->CreateFence(...); } void RenderFrame() { // 커맨드 리스트 기록 시작 commandAllocator->Reset(); commandList->Reset(commandAllocator, nullptr); // 렌더 타겟 클리어 commandList->ResourceBarrier(...); // 리소스 상태 변경 float clearColor[4] = {0.0f, 0.2f, 0.4f, 1.0f}; commandList->ClearRenderTargetView(..., clearColor, 0, nullptr); commandList->ClearDepthStencilView(..., D3D12_CLEAR_FLAG_DEPTH, 1.0f, 0); // 그리기 명령 commandList->DrawIndexedInstanced(...); // 명령 리스트 끝내기 및 제출 commandList->Close(); ID3D12CommandList* ppCommandLists[] = { commandList }; commandQueue->ExecuteCommandLists(_countof(ppCommandLists), ppCommandLists); // 스왑체인 프레젠트 및 동기화 처리 swapChain->Present(1, 0); WaitForPreviousFrame(); } |
- DX12는 명령 리스트와 커맨드 큐를 통해 명령을 명시적으로 관리
- 리소스 상태 변경(ResourceBarrier)과 같은 작업을 명시적으로 선언해야 함
- 동기화(present 후의 펜스 등) 처리를 수동으로 해줘야 함
- 다중 스레드에서 더 많은 제어권을 가지며, 높은 효율을 얻을 수 있는 구조
주요 차이점
- 명령 리스트 및 큐 관리: DX12는 명령 리스트와 큐를 사용하여 명령을 기록하고 제출하는 작업을 수동으로 처리하는 반면, DX11은 자동으로 처리.
- 동기화: DX11에서는 GPU와의 동기화가 내부적으로 처리되지만, DX12는 펜스와 같은 동기화 객체를 사용하여 명시적으로 처리.
- 리소스 상태 관리: DX11에서는 자동으로 리소스 상태를 관리하지만, DX12는 ResourceBarrier를 통해 개발자가 직접 리소스 상태를 전환해야 함.
- 멀티스레드: DX12는 멀티스레드에서 더 효율적으로 동작할 수 있게 설계되었지만, 개발자가 더 많은 관리 작업을 해야 함.
DX11과 DX12의 HLSL (High-Level Shading Language)에서 셰이더 코드를 작성할 때 주의해야 할 차이점
주로 API의 동작 방식에 따른 리소스 관리 및 최적화와 관련있는데, 두 API 모두 HLSL을 지원하지만, 리소스 바인딩 방식과 성능 최적화 방식에서 차이가 존재한다. 아래는 DX11과 DX12에서 셰이더 개발 시 고려해야 할 주요 차이점을 비교하고 있다.
1. 리소스 바인딩(Resource Binding) 차이
DX11에서는 셰이더 리소스 바인딩와 Descriptor Heap 1을 통해 더 명확하고 수동적으로 관리해야 한다. 2
리소스 바인딩 (DX11 vs DX12)
DX11 (HLSL):
cbuffer ConstantBuffer : register(b0) { float4x4 worldMatrix; }; Texture2D myTexture : register(t0); SamplerState mySampler : register(s0); |
DX12 (HLSL with Root Signature):
RootSignature { "DescriptorTable(SRV(t0), CBV(b0), Sampler(s0))" }; cbuffer ConstantBuffer : register(b0) { float4x4 worldMatrix; }; Texture2D myTexture : register(t0); SamplerState mySampler : register(s0); |
주의점
- Root Signature : DX12에서는 Root Signature를 명시적으로 정의해야 하며, 이를 통해 리소스의 바인딩 레이아웃을 관리.
- Descriptor Heap : DX12에서는 Descriptor Heap을 통해 다수의 리소스를 바인딩하며, 이를 통해 효율적인 리소스 접근을 처리할 수 있다.
따라서, DX12에서는 Root Signature를 설계할 때 최적화를 신경 써야 하며, 너무 많은 리소스를 한 번에 바인딩하지 않도록 주의해야 한다.
2. 상수 버퍼(Constant Buffer) 관리
DX11: 상수 버퍼는 간단하게 register(b#)로 바인딩되며, 셰이더가 사용할 수 있도록 쉽게 설정된다. 하지만 매 프레임마다 많은 상수 버퍼를 업데이트할 경우 퍼포먼스 이슈가 발생할 수 있다.
DX12: 상수 버퍼는 Root Signature를 통해 바인딩되며, 특히 상수 데이터(Constant Data)를 효율적으로 업데이트하려면 Dynamic Constant Buffers를 사용하여 더 세밀하게 관리해야 한다. 상수 버퍼가 많은 데이터를 자주 업데이트하면 성능 저하가 발생할 수 있으므로 이를 적절히 캐싱하거나 효율적으로 처리하는 방법이 중요.
- DX12에서 상수 버퍼를 업데이트할 때는 업로드 힙 또는 리소스 배리어를 적절히 사용해야 한다.
- DX12는 상수 버퍼를 위한 더 복잡한 관리가 필요하며, 적절한 메모리 관리와 동기화를 고려해야 한다.
주의점
- DX12에서 상수 버퍼를 업데이트할 때는 업로드 힙 또는 리소스 배리어를 적절히 사용해야 합니다.
- DX12는 상수 버퍼를 위한 더 복잡한 관리가 필요하며, 적절한 메모리 관리와 동기화를 고려해야 합니다.
3. 멀티스레드 최적화
DX11 : 기본적으로 멀티스레드 성능이 제한적이며, 셰이더 코드 자체는 주로 단일 스레드에서 실행된다.
DX12 : 멀티스레드 환경에서 리소스 접근이 빈번하게 발생하므로, 이를 위해 셰이더 코드에서 스레드 간 동기화 문제를 방지하고, 효율적으로 자원을 공유하도록 설계하고 병목이 생기지 않도록 관리하는것이 중요하다.
메모리 액세스 패턴의 비효율성
비동일한 메모리 접근 (Uncoalesced Memory Access) : 셰이더에서 여러 스레드가 비연속적인 메모리 주소를 읽거나 쓰는 경우, GPU가 한 번에 메모리를 패칭하는 데 비효율이 발생한다. 이를 메모리 코얼리싱(Memory Coalescing)이 안 된다고 하며, 메모리 접근이 일렬로 이루어져야 GPU가 최대 성능을 발휘할 수 있다.
4. 동기화(Synchronization)
DX11 : 기본적으로 동기화 처리가 자동으로 이루어지기 때문에 셰이더 코드에서 동기화에 크게 신경 쓸 필요가 없다.
DX12 : 명령 큐와 명령 리스트가 비동기적으로 처리되기 때문에, 리소스 상태 전환이나 동기화에 대한 관리가 필요하다. 이는 특히 셰이더에서 사용되는 자원이 여러 명령 리스트에서 사용될 경우 발생한다. 리소스 상태 전환(ResourceBarrier)이 필요할 때 셰이더 코드에서 이를 고려해야 하며, 자원의 상태가 올바르게 설정되어 있지 않으면 충돌이나 성능 저하가 발생할 수 있다.
5. 타겟 하드웨어 및 최적화
- DX11 : DX11은 이전 세대의 하드웨어에서도 잘 작동하며, 개발자는 대부분의 하드웨어에서 균일한 성능을 기대할 수 있다.
- DX12 : 최신 GPU 아키텍처에 최적화되어 있다. 이는 셰이더 코드에서도 하드웨어 특성을 고려한 최적화가 필요하며, 각 하드웨어의 지원하는 기능 수준을 파악하는 것이 중요하다. 특히, 구형 GPU에서는 오히려 성능이 떨어질 수 있으므로 타겟 하드웨어에 맞춰 최적화하는 작업이 필요하다.
6. 셰이더 모델(Shader Model)
DX11: DX11은 주로 Shader Model 5.0을 사용. 대부분의 현대적인 GPU가 Shader Model 5.0을 지원하므로 호환성에 큰 문제가 없다.
DX12: DX12는 Shader Model 6.0 이상을 지원. 특히, 새로운 Wave Intrinsics나 Raytracing 기능을 사용하려면 Shader Model 6.0 이상이 필요한데, 이는 구형 디바이스에는 지원되지 않을수 있으므로 fallback 처리가 필수이다.
- DirectX 12에서 리소스(예: 텍스처, 버퍼, 샘플러 등)는 셰이더에서 직접 사용할 수 있도록 GPU에 전달되어야 한다. 이를 위해 리소스는 특정 "슬롯"에 바인딩되는데, DirectX 11에서는 바인딩 슬롯들이 자동으로 관리되었지만, DX12에서는 DX11에 비해 더 많은 제어를 할 수 있게 되었지만, 그만큼 수동으로 바인딩을 Root Signature[footnote] Root Signature : 리소스가 셰이더에 어떻게 연결될지 정의하는 '설계도'와 같은 역할 [본문으로]
- Descriptor Heap : 실제로 리소스들의 위치나 상태를 GPU가 참조할 수 있도록 설명하는 '메모리 구조체'로, 여러 리소스들을 묶어 관리. [본문으로]