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에서는 셰이더 리소스 뷰(SRV), 상수버퍼(CBV), Sampler등이 자동으로 관리되며, 명령리스트가 따로 필요하지 않지만, DX12에서는 셰이더 리소스 바인딩 Root Signature 1와 Descriptor Heap 2을 통해 더 명확하고 수동적으로 관리해야 한다. 3
리소스 바인딩 (DX11 vs DX12)
DX11 (HLSL):
cbuffer ConstantBuffer : register(b0) { float4x4 worldMatrix; }; Texture2D myTexture : register(t0); SamplerState mySampler : register(s0); |
DX11의 자동화된 리소스 바인딩
deviceContext->PSSetShaderResources(0, 1, &textureSRV); // DX11 리소스 바인딩 |
DX12 (HLSL with Root Signature):
DX12에서 리소스 바인딩은 아래와 같은 흐름으로 진행된다.
- Root Signature 생성: Root Signature에서 셰이더가 어떤 리소스를 필요로 하는지 정의.
- Descriptor Heap 생성: 관리할 리소스들(예: 텍스처, 버퍼 등)의 정보를 Descriptor Heap에 저장.
- 디스크립터 할당: 각 리소스에 대해 디스크립터를 생성하여 Descriptor Heap에 추가.
- Root Signature에 Descriptor Table 연결: Descriptor Table은 해당 디스크립터들을 참조하여 리소스를 셰이더와 연결
- 셰이더 실행: GPU는 Root Signature를 참조하여 Descriptor Heap에서 필요한 리소스를 가져와 셰이더에서 사용하게 된다.
RootSignature { "DescriptorTable(SRV(t0), CBV(b0), Sampler(s0))" }; cbuffer ConstantBuffer : register(b0) { float4x4 worldMatrix; }; Texture2D myTexture : register(t0); SamplerState mySampler : register(s0); |
DX12 수동화된 리소스 바인딩
commandList->SetGraphicsRootSignature(rootSignature); // Root Signature 설정 commandList->SetDescriptorHeaps(1, &descriptorHeap); // Descriptor Heap 설정 commandList->SetGraphicsRootDescriptorTable(0, descriptorHeap->GetGPUDescriptorHandleForHeapStart()); |
DX12의 리소스 관리는 명확하게 상태전환과 바인딩을 관리하므로, CPU/GPU간의 불필요한 동기화를 줄일 수 있다. 특히 멀티스레드 환경에서 효율적으로 리소스를 관리할 수 있으며, Descriptor Heap을 통해 리소스 그룹을 한번에 설정하고 바인딩 할 수 있으므로, 많은 리소스를 다룰 때 유연하게 최적화가 가능하다.
2. 상수 버퍼(Constant Buffer) 관리
DX11: 상수 버퍼는 간단하게 register(b#)로 바인딩되며, 셰이더가 사용할 수 있도록 쉽게 설정된다. 하지만 매 프레임마다 많은 상수 버퍼를 업데이트할 경우 퍼포먼스 이슈가 발생할 수 있다.
DX12: 상수 버퍼는 Root Signature를 통해 바인딩되며, 특히 상수 데이터(Constant Data)를 효율적으로 업데이트하려면 Dynamic Constant Buffers를 사용하여 더 세밀하게 관리해야 한다. 상수 버퍼가 많은 데이터를 자주 업데이트하면 성능 저하가 발생할 수 있으므로 이를 적절히 캐싱하거나 효율적으로 처리하는 방법이 중요. 4 5
Dynamic Constant Buffers DX11 예제
// Constant Buffer 구조체 정의 struct ConstantBufferData { DirectX::XMMATRIX modelMatrix; DirectX::XMMATRIX viewMatrix; DirectX::XMMATRIX projectionMatrix; }; // 동적 상수 버퍼 생성 D3D11_BUFFER_DESC bufferDesc = {}; bufferDesc.Usage = D3D11_USAGE_DYNAMIC; // 동적 사용으로 설정 bufferDesc.ByteWidth = sizeof(ConstantBufferData); bufferDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; bufferDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; ID3D11Buffer* constantBuffer = nullptr; device->CreateBuffer(&bufferDesc, nullptr, &constantBuffer); // 데이터 업데이트 D3D11_MAPPED_SUBRESOURCE mappedResource = {}; context->Map(constantBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedResource); ConstantBufferData* dataPtr = (ConstantBufferData*)mappedResource.pData; // 새로운 데이터를 복사 dataPtr->modelMatrix = XMMatrixIdentity(); dataPtr->viewMatrix = XMMatrixLookAtLH(...); dataPtr->projectionMatrix = XMMatrixPerspectiveFovLH(...); context->Unmap(constantBuffer, 0); // 셰이더에 바인딩 context->VSSetConstantBuffers(0, 1, &constantBuffer); |
DX12 예제
// 상수 버퍼 데이터 구조체 struct ConstantBufferData { DirectX::XMMATRIX modelMatrix; DirectX::XMMATRIX viewMatrix; DirectX::XMMATRIX projectionMatrix; }; // 상수 버퍼 생성 D3D12_HEAP_PROPERTIES heapProps = {}; heapProps.Type = D3D12_HEAP_TYPE_UPLOAD; // 업로드 힙 사용 D3D12_RESOURCE_DESC resourceDesc = {}; resourceDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER; resourceDesc.Width = (sizeof(ConstantBufferData) + 255) & ~255; // 256바이트 정렬 resourceDesc.Height = 1; resourceDesc.DepthOrArraySize = 1; resourceDesc.MipLevels = 1; resourceDesc.SampleDesc.Count = 1; resourceDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR; ID3D12Resource* constantBuffer = nullptr; device->CreateCommittedResource(&heapProps, D3D12_HEAP_FLAG_NONE, &resourceDesc, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&constantBuffer)); // CBV 생성 및 데이터 맵핑 D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc = {}; cbvDesc.BufferLocation = constantBuffer->GetGPUVirtualAddress(); cbvDesc.SizeInBytes = (sizeof(ConstantBufferData) + 255) & ~255; device->CreateConstantBufferView(&cbvDesc, descriptorHeap->GetCPUDescriptorHandleForHeapStart()); ConstantBufferData* cbvData = nullptr; constantBuffer->Map(0, nullptr, reinterpret_cast<void**>(&cbvData)); // 데이터 업데이트 cbvData->modelMatrix = XMMatrixIdentity(); cbvData->viewMatrix = XMMatrixLookAtLH(...); cbvData->projectionMatrix = XMMatrixPerspectiveFovLH(...); // 셰이더에 바인딩 commandList->SetGraphicsRootDescriptorTable(0, descriptorHeap->GetGPUDescriptorHandleForHeapStart()); |
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에 비해 더 많은 제어를 할 수 있게 되었지만, 그만큼 명시적으로 바인딩을 관리해야 한다. [본문으로]
- 리소스가 셰이더에 어떻게 연결될지 정의하는 '설계도'와 같은 역할로 상수(Constant), Buffer, Texture, Sampler 등이 이에 해당된다. [본문으로]
- 리소스의 실제 데이터를 GPU에 참조시키는 구조체. Descriptor Heap에는 여러 리소스(예: 텍스처, 상수 버퍼 등)에 대한 디스크립터(Descriptor)들이 저장되며, 이 디스크립터는 리소스의 위치와 속성을 나타내게 된다. 셰이더에서 사용하는 리소스가 많을수록 각각을 따로 관리하기가 어려워지는데 이를 통해 효율적으로 처리할 수 있으며, Root Signature에서 설정한 Descriptor Table이 Descriptor Heap에 있는 리소스들을 참조해 셰이더에서 필요한 리소스를 전달한다. [본문으로]
- DX11에서는 D3D11_MAP_WRITE_DISCARD를 사용해 이전 데이터를 폐기하고 새 데이터를 기록하는 방식으로 가능하나 DX12에서는 적절한 리소스 배리어(Resource Barrior)를 사용해 동기화 한다 [본문으로]
-
Barrier란..?
일단 ComputeShader와는 상관없이 Barrier는 2가지 종류가 있다. 하나는 execution barrier, 나머지는 memory barrier다.
CPU든 GPU든 이런 것들이 계산과 처리를 할 때, (여기서 CPU와 GPU같은 것이 만들어진 구조를 아키텍처[그냥단어다]라고 부른다.) 특히 멀티쓰레드로 작업을 할 때, 이 멀티쓰레드 작업의 결과가 올바르게 나오기 위해, 때로는 쓰레드의 실행을 멈추었다가 다시 실행해야 되고, 때로는 메모리 쓰여진 데이터가 다른 쓰레드에서 이 쓰여진 데이터를 읽어야 하는 경우가 있다.
실행을 멈추고 다시 실행을 계속하는 과정에서 실행을 멈추는 것을 execution barrier라고 하고, 쓰여진 데이터를 다른 쓰레드가 올바르게 읽게 하기위해 cache데이터를 메모리로 전송완료 시키는 것을 memory barrier라고 한다. 이 때 보통 다음에 처리할 쓰레드들은 완료시까지 잠시 실행을 멈춘다. 여기서 꼭 다 멈추는 것이 아니다.
출처: https://jamssoft.tistory.com/240 [What should I do?:티스토리]
다른 설명은 : https://blog.naver.com/tigerjk0409/221412320578 [본문으로] - 상수 버퍼 크기가 256바이트 정렬(Alignment)을 충족해야 하며, 크기를 (sizeof(data) + 255) & ~255로 맞춘다 [본문으로]
'Technical Report > Graphics Tech Reports' 카테고리의 다른 글
Unreal Compute Shader (1) | 2024.10.23 |
---|---|
PLS(Pixel Logical Storage)와 FBF(Frame Buffer Fetch)의 차이 (0) | 2024.10.18 |
FBX Export Geometry options (6) | 2023.10.26 |
[번역]Forward vs Deferred vs Forward+ Rendering with DirectX 11(3) Forward+ (0) | 2023.08.03 |
[번역]【CEDEC2022】「Ghostwire: Tokyo」의 리얼한 애니메이션 표현을 낳는 4개의 프로시저 애니메이션 기법 (0) | 2022.12.01 |