Technical Report/Graphics Tech Reports

DX11, DX12 렌더링 기본 구조 비교(작성중)

illu_yun 2024. 10. 17. 17:32
반응형

 

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 후의 펜스 등) 처리를 수동으로 해줘야 함
  • 다중 스레드에서 더 많은 제어권을 가지며, 높은 효율을 얻을 수 있는 구조

 
주요 차이점

  1. 명령 리스트 및 큐 관리: DX12는 명령 리스트와 큐를 사용하여 명령을 기록하고 제출하는 작업을 수동으로 처리하는 반면, DX11은 자동으로 처리.
  2. 동기화: DX11에서는 GPU와의 동기화가 내부적으로 처리되지만, DX12는 펜스와 같은 동기화 객체를 사용하여 명시적으로 처리.
  3. 리소스 상태 관리: DX11에서는 자동으로 리소스 상태를 관리하지만, DX12는 ResourceBarrier를 통해 개발자가 직접 리소스 상태를 전환해야 함.
  4. 멀티스레드: DX12는 멀티스레드에서 더 효율적으로 동작할 수 있게 설계되었지만, 개발자가 더 많은 관리 작업을 해야 함.

 

DX11과 DX12의 HLSL (High-Level Shading Language)에서 셰이더 코드를 작성할 때 주의해야 할 차이점

주로 API의 동작 방식에 따른 리소스 관리 및 최적화와 관련있는데, 두 API 모두 HLSL을 지원하지만, 리소스 바인딩 방식과 성능 최적화 방식에서 차이가 존재한다. 아래는 DX11과 DX12에서 셰이더 개발 시 고려해야 할 주요 차이점을 비교하고 있다.
 

1. 리소스 바인딩(Resource Binding) 차이

DX11에서는 셰이더 리소스 바인딩[각주:1]Descriptor Heap[각주: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 IntrinsicsRaytracing 기능을 사용하려면 Shader Model 6.0 이상이 필요한데, 이는 구형 디바이스에는 지원되지 않을수 있으므로 fallback 처리가 필수이다.
 
 
 

  1. DirectX 12에서 리소스(예: 텍스처, 버퍼, 샘플러 등)는 셰이더에서 직접 사용할 수 있도록 GPU에 전달되어야 한다. 이를 위해 리소스는 특정 "슬롯"에 바인딩되는데, DirectX 11에서는 바인딩 슬롯들이 자동으로 관리되었지만, DX12에서는 DX11에 비해 더 많은 제어를 할 수 있게 되었지만, 그만큼 수동으로 바인딩을 Root Signature[footnote] Root Signature : 리소스가 셰이더에 어떻게 연결될지 정의하는 '설계도'와 같은 역할 [본문으로]
  2. Descriptor Heap : 실제로 리소스들의 위치나 상태를 GPU가 참조할 수 있도록 설명하는 '메모리 구조체'로, 여러 리소스들을 묶어 관리. [본문으로]
반응형