Device(그래픽 디바이스 생성)
순번 | 그림 요소 | 코드 예시 | 설명 |
1 | 응용 프로그램 | GraphicDevice_DX12::CreateDevice() | 사용자 코드에서 DX12 초기화를 시작 |
2 | D3D12 API | D3D12CreateDevice(...) | GPU 제어를 위한 DX12 디바이스를 생성 |
3 | 사용자-모드 드라이버 | 내부적으로 D3D12 호출 시 작동 | GPU 제조사의 드라이버가 DX 명령 처리 |
4 | DXGI | CreateDXGIFactory2() 등 | 어댑터 탐색, 스왑체인 관리 등 |
5 | 커널-모드 드라이버 | DXGI → GPU 연결 시 자동 개입 | OS 레벨에서 하드웨어 명령 전달 |
6 | 하드웨어 | GPU | 최종 렌더링 수행 |
① DXGI 팩토리 생성
if (FAILED(CreateDXGIFactory2(dxgiFactoryFlags, IID_PPV_ARGS(&mFactory))))
assert(NULL && "Create DXGI Factory Failed!");
C++
복사
•
그림에서: DXGI (초록색 상자)
•
역할: DXGI 팩토리를 생성하여 GPU 어댑터, 스왑체인(출력) 등을 관리할 준비를 합니다.
DXGI Factory로 얻을 수 있는 대표적인 정보들
기능 | 설명 | 관련 함수 |
어댑터 정보 (GPU 목록) | 시스템에 연결된 GPU(어댑터)의 수와 정보를 얻을 수 있습니다. | EnumAdapters1(), EnumWarpAdapter() |
출력 장치 정보 (모니터) | GPU에 연결된 디스플레이(모니터) 정보를 가져올 수 있습니다. | IDXGIAdapter::EnumOutputs() |
디스플레이 모드 정보 | 해상도, 리프레시 레이트 등 모니터의 디스플레이 모드를 나열합니다. | IDXGIOutput::GetDisplayModeList() |
어댑터 설명 | GPU 이름, 전용 비디오 메모리 용량 등 하드웨어 상세 정보를 얻습니다. | IDXGIAdapter1::GetDesc1() |
WARP 어댑터 접근 | GPU가 없어도 CPU 렌더링용 WARP 어댑터 사용 가능 | EnumWarpAdapter() |
스왑체인 생성 | 화면 출력용 스왑체인을 생성할 수 있습니다. | CreateSwapChainForHwnd() 등 |
전체 시스템 그래픽 구성 | 디스플레이와 어댑터의 연결 관계를 확인 가능 | 어댑터 → 출력장치 → 모드 확인 트리 구조 |
② 디버그 레이어 활성화 (옵션)
#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.
Microsoft::WRL::ComPtr<ID3D12Debug> debugController;
if (SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController))))
{
debugController->EnableDebugLayer();
// Enable additional debug layers.
dxgiFactoryFlags |= DXGI_CREATE_FACTORY_DEBUG;
}
#endif
C++
복사
•
응용 프로그램(Application) 내부에서 디버깅 기능을 켭니다.
•
DX12의 내부 호출 및 리소스 생성을 점검할 수 있게 해줍니다.
③ 어댑터 선택 (하드웨어 or 소프트웨어)
if (mbUseWarpDevice)
{
Microsoft::WRL::ComPtr<IDXGIAdapter> warpAdapter;
if (FAILED(mFactory->EnumWarpAdapter(IID_PPV_ARGS(&warpAdapter))))
assert(NULL && "Enum Warp Adapter Failed!");
if (FAILED(D3D12CreateDevice(
warpAdapter.Get()
, D3D_FEATURE_LEVEL_11_0
, IID_PPV_ARGS(&mDevice))))
assert(NULL && "Create Device with Warp Adapter Failed!");
}
else
{
Microsoft::WRL::ComPtr<IDXGIAdapter1> hardwareAdapter;
GetHardwareAdapter(mFactory.Get(), &hardwareAdapter);
...
}
C++
복사
•
하드웨어 어댑터 사용 시 → 사용자-모드 드라이버 및 DXGI
•
WARP 어댑터 사용 시 → CPU 기반 소프트웨어 렌더러 (GPU 없음)
•
역할: 시스템에 연결된 GPU를 찾아서 어떤 디바이스로 초기화할지 결정합니다.
④ 디바이스 생성
if (FAILED(D3D12CreateDevice(
hardwareAdapter.Get(),
D3D_FEATURE_LEVEL_11_0,
IID_PPV_ARGS(&mDevice))))
assert(NULL && "Create Device with Hardware Adapter Failed!");
C++
복사
•
그림에서:
◦
D3D12 → 사용자-모드 드라이버 → DXGI → 커널 드라이버 → 하드웨어
•
역할:
◦
DX12 디바이스를 생성하고, GPU와 통신할 수 있는 연결을 설정합니다.
DX12 초기화는 응용 프로그램 → DX12 API → 드라이버(DXGI 포함) → GPU 순서로 이어지는 구조입니다. 이 모든 과정을 한눈에 보여주는 흐름도이며, 특히 사용자-모드와 커널-모드를 분리해 구현되어 있습니다.
Command Queue, Swapchain 생성
단계 | 목적 | 대표 함수 |
① Command Queue 생성 | GPU 명령 전달용 큐 | CreateCommandQueue() |
② Swap Chain 구성 | 백버퍼 관리 구조 | CreateSwapChainForHwnd() |
③ Alt+Enter 비활성화 | 창 전환 제어 | MakeWindowAssociation() |
④ 버전 변환 | DX12 인터페이스로 캐스팅 | swapChain.As() |
⑤ 프레임 인덱스 획득 | 현재 백버퍼 인덱스 확인 | GetCurrentBackBufferIndex() |
Command Queue = 주방 주문서 대기열
•
손님이 음식(화면)을 요청하면, 주문서가 주방의 주문 대기열에 올라갑니다.
•
주방장(GPU)은 이 Command Queue를 보고 하나씩 요리(렌더링)를 합니다.
•
요리가 끝나야(렌더링 완료) 손님(사용자)이 음식을 받습니다.
Swap Chain = 음식 서빙 트레이 (여러 개의 접시)
•
요리가 완성된 후, 음식을 담을 접시(버퍼) 가 필요합니다.
•
트레이에 접시 2~3개를 돌려가며 쓰는 것이 Swap Chain입니다.
•
한 접시에 요리하는 동안, 다른 접시는 손님이 보고 있고, 나머지 하나는 준비 중일 수 있습니다.
개념 | 한 줄 설명 |
Command Queue | GPU에게 할 일을 순서대로 넘겨주는 "작업 대기열"입니다. |
Swap Chain | 그린 화면을 사용자에게 보여주는 "화면 버퍼 묶음"입니다. |
예제 순서는 윈도우 핸들에 출력할 수 있도록:
1.
GPU 명령을 처리할 Command Queue를 만들고,
2.
더블 버퍼 기반 Swap Chain을 생성하여
3.
윈도우와 연결합니다.
1. Command Queue 생성
D3D12_COMMAND_QUEUE_DESC queueDesc = {};
queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
if (FAILED(mDevice->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&mCommandQueue))))
assert(NULL && "Create Command Queue Failed!");
C++
복사
•
Command Queue란?
GPU에게 명령(Command List)을 보내는 대기열(Queue) 입니다.
•
Type = DIRECT
일반적인 그래픽 렌더링 명령용 (Compute, Copy 타입도 존재)
•
위 설정으로 GPU 명령을 실행할 Command Queue를 생성합니다.
•
생성된 큐는 mCommandQueue에 저장됩니다.
2. Swap Chain 설정 및 생성
화면 출력 문제
게임이나 그래픽 프로그램에서 하나의 프레임 버퍼만 사용하는 경우 다음과 같은 문제가 발생합니다:
•
화면 깜빡임 (Screen Flickering)
: 매 프레임마다 버퍼를 지우고 다시 그릴 경우, 지워진 찰나의 버퍼가 보여지면서 화면이 깜빡이는 현상이 생깁니다.
•
화면 찢어짐 (Screen Tearing)
: 이전 프레임의 이미지 위에 새로운 프레임을 덮어 그리는 도중, 두 프레임의 이미지가 동시에 보이는 순간이 발생해 화면이 찢어져 보입니다.
해결책: 더블 버퍼링 (Double Buffering)
이러한 문제를 방지하기 위해 두 개의 버퍼를 사용합니다:
◦
전면 버퍼 (Front Buffer)
: 현재 화면에 표시되고 있는 버퍼
◦
후면 버퍼 (Back Buffer)
: 다음 프레임을 그리는 데 사용하는 버퍼
버퍼 교환 방식
◦
후면 버퍼에 완성된 프레임을 모두 그린 후,
◦
전면 버퍼와 역할을 맞바꾸는(Flipping) 방식으로 출력
→ 사용자는 항상 완성된 프레임만을 보게 되어 깜빡임이나 찢어짐이 발생하지 않음
// Decribe and create the swap chain.
DXGI_SWAP_CHAIN_DESC1 swapChainDesc = {};
swapChainDesc.BufferCount = 2; // Double buffering maybe upgrade 3buffering later multithread rendering
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; // No MSAA
swapChainDesc.SampleDesc.Quality = 0;
swapChainDesc.Scaling = DXGI_SCALING_STRETCH;
swapChainDesc.Stereo = FALSE;
Microsoft::WRL::ComPtr<IDXGISwapChain1> swapChain;
HWND hwnd = application.GetWindow().GetHwnd(); // Get the window handle
if (FAILED(mFactory->CreateSwapChainForHwnd(
mCommandQueue.Get(),
hwnd,
&swapChainDesc,
nullptr, nullptr,
&swapChain)))
{
assert(NULL && "Create Swap Chain Failed!");
}
C++
복사
이 구조체는 스왑체인의 속성을 정의합니다.
설정 항목 | 설명 |
BufferCount = 2 | 더블 버퍼링 (후에 트리플 버퍼링으로 확장 가능) |
Width, Height | 창의 너비와 높이 |
Format | 버퍼 포맷. 일반적인 32bit RGBA |
Flags | 프레임 지연 동기화 설정 (DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT) |
BufferUsage | 렌더 타겟 용도로 사용함 |
SwapEffect | FLIP_DISCARD는 DXGI 1.3 이상에서 추천되는 효율적인 방식 |
SampleDesc | MSAA 샘플 수 (여기선 1, 즉 없음) |
Scaling, Stereo | 창 크기 자동 확장 여부, 3D 입체 모드 사용 여부 (일반적으로 FALSE) |
•
설명: 윈도우 핸들(HWND)을 대상으로 스왑체인을 생성합니다.
•
생성된 스왑체인은 swapChain (임시 변수)에 저장됩니다.
MSAA 란?
3. 전체 화면 전환 비활성화
// This sample does not support fullscreen transitions.
if (FAILED(mFactory->MakeWindowAssociation(hwnd, DXGI_MWA_NO_ALT_ENTER)))
assert(NULL && "Make Window Association Failed!");
C++
복사
•
설명: Alt+Enter 키를 눌러도 전체 화면으로 전환되지 않도록 설정합니다.
•
일반적으로 엔진에서 자체적인 전체화면 전환 제어를 원할 때 사용합니다.
4. IDXGISwapChain1 → IDXGISwapChain4 변환
if (FAILED(swapChain.As(&mSwapChain)))
assert(NULL && "Swap Chain As Failed!");
C++
복사
•
IDXGISwapChain1은 기본 버전입니다.
•
mSwapChain은 DX12용 IDXGISwapChain4이므로 As()를 통해 형 변환(Query Interface) 을 수행합니다.
5. 현재 프레임 인덱스 얻기
mFrameIndex = mSwapChain->GetCurrentBackBufferIndex();
C++
복사
•
설명: 더블 버퍼 중 현재 GPU가 사용할 수 있는 버퍼 인덱스를 가져옵니다.
•
이후 렌더 타겟 선택 등에 사용됩니다.
왜 현재 프레임 인덱스를 얻어야 하나?
DirectX 12에서는 여러 개의 백버퍼(Back Buffer) 를 번갈아 가며 사용합니다.
•
일반적으로 더블 버퍼링(2개), 트리플 버퍼링(3개) 구조입니다.
•
GPU는 버퍼 0, 1, (2) 를 돌아가며 화면을 그립니다.
•
Dx11 에서는 내부에서 자동으로 변환해줬던 것이다.
그래서 매 프레임마다 "지금 GPU가 그릴 차례인 버퍼" 가 다릅니다.
•
어떤 버퍼는 이미 화면에 표시 중이고,
•
어떤 버퍼는 GPU가 다음에 그릴 준비를 해야 합니다.
GetCurrentBackBufferIndex() 는 현재 렌더링 타겟으로 사용할 버퍼의 인덱스를 알려줍니다.
mRenderTargets[mFrameIndex]; // 예: 0번 또는 1번
C++
복사
→ 여기에만 그려야 GPU 결과가 제대로 화면에 나타납니다.
원하신다면 다음 단계인 RTV 생성, 커맨드 리스트 초기화, 렌더 루프 진입 과정도 함께 정리해드릴 수 있습니다.
Descriptor Heap과 Command Allocator 생성
이 코드가 하는 일
1. RTV Descriptor Heap 생성
**디스크립터 힙(Descriptor Heap)**은 DirectX 12에서 GPU가 리소스를 사용할 수 있도록 서술자(Descriptor)들을 연속적으로 저장해놓은 **메모리 공간(배열 구조)**입니다.
GPU는 리소스를 메모리 덩어리로만 본다 (== "RAW" 메모리)
•
예를 들어 ID3D12Resource*가 있다고 해도,
GPU는 그게 텍스처인지, 버퍼인지, SRV인지, RTV인지, 어떤 서브리소스를 쓸 건지 모릅니다.
•
CPU 쪽에서 CreateCommittedResource()로 만든 그 리소스를 GPU가 사용할 수 있으려면,
"이건 SRV니까 이렇게 읽어!" 라는 식의 명시적인 설명이 필요합니다.
D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc = {};
rtvHeapDesc.NumDescriptors = 2; // Double buffering
rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
if (FAILED(mDevice->CreateDescriptorHeap(&rtvHeapDesc, IID_PPV_ARGS(&mRtvHeap))))
assert(NULL && "Create RTV Heap Failed!");
mRtvDescriptorSize = mDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
// Create a RTV for each frame.
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(mRtvHeap->GetCPUDescriptorHandleForHeapStart());
for (int i = 0; i < 2; i++)
{
if (FAILED(mSwapChain->GetBuffer(i, IID_PPV_ARGS(&mRenderTargets[i])))) // Added parentheses around the expression
assert(NULL && "Get Swap Chain Buffer Failed!");
mRenderTragetDesciptorHandle[i] = rtvHandle;
mDevice->CreateRenderTargetView(mRenderTargets[i].Get(), nullptr, rtvHandle);
rtvHandle.Offset(1, mRtvDescriptorSize);
}
C++
복사
•
RTV는 화면에 그리는 Render Target View입니다.
•
더블 버퍼링을 위해 RTV는 2개 필요 (back buffer 2개).
•
mRenderTargets[i] 는 IDXGISwapChain::GetBuffer() 로 가져온 실제 버퍼.(스왑체인이 가지고 있는 화면버퍼)
•
각 버퍼에 대한 RTV 핸들은 mRenderTragetDesciptorHandle[i] 에 저장.
2. Command Allocator 생성
Command Allocator는 Command List가 사용할 수 있도록 GPU 명령을 기록할 "저장 공간(메모리 풀)"을 관리하는 객체입니다.
for (size_t i = 0; i < 2; i++)
{
if (FAILED(mDevice->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&mFrameContext[i].CommandAllocator))))
assert(NULL && "Create Command Allocator Failed!");
}
C++
복사
•
프레임별로 별도 Command Allocator 생성
•
이유: GPU가 프레임 A에 대한 커맨드 실행 중일 때, 프레임 B는 새로 Command를 기록해야 하기 때문.
•
같은 Allocator를 리셋하면 GPU가 작업 중이면 crash or DEVICE_REMOVED.
•
멀티 프레임 환경에서는 프레임 개수만큼 Command Allocator를 따로 만들어야 한다.
왜 여러 개가 필요할까?
1. Command Allocator는 GPU 작업이 끝나야 Reset() 가능
•
Command Allocator는 그 위에 기록된 Command List가 GPU에서 실행 완료될 때까지 Reset 금지
•
하나만 쓰면, GPU 작업이 끝날 때까지 CPU는 다음 명령을 기록 못 함 → CPU-GPU 병목
2. 프레임 단위로 작업을 분리해야 효율적
•
CPU는 매 프레임마다 새로운 명령을 기록하고 싶음
•
GPU는 이전 프레임을 아직 처리 중일 수 있음
→ 그래서 **프레임 개수만큼 별도 Command Allocator**를 둬서 순환시킴
이 구조의 핵심 포인트
항목 | 이유 | 개수 |
RenderTargets[i] | SwapChain이 제공하는 백버퍼 | 2 (더블 버퍼링) |
RTV Descriptor | 각 RenderTarget에 대응하는 뷰 | 2 |
CommandAllocator | 각 프레임에서 명령 재기록 시 충돌 방지 | 2 |
주의할 점
•
CommandAllocator->Reset() 은 GPU가 해당 allocator의 명령을 모두 실행한 후에만 해야 합니다.
•
그래서 각 프레임마다 별도의 allocator를 쓰고, Fence로 GPU 완료를 확인합니다.
SRV Descriptor Heap 생성
ImGui는 자체 텍스처(SRV)를 GPU 셰이더에서 사용할 수 있어야 하며,
이때 사용하는 디스크립터는 "Shader Visible"한 SRV Heap에 있어야 하기 때문입니다.
ImGui는 폰트 텍스처와 사용자 이미지 텍스처를 씁니다
•
ImGui는 내부적으로 폰트 텍스처를 생성하고 SRV로 GPU에 업로드합니다.
•
또한 ImGui::Image() 같은 함수에서 개발자가 지정한 **임의의 텍스처(SRV)**를 쓸 수 있어야 합니다.
•
이 모든 SRV를 GPU에서 접근 가능하게 하려면, D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE한 Descriptor Heap이 필요합니다.
// imgui
const int APP_SRV_HEAP_SIZE = 64;
D3D12_DESCRIPTOR_HEAP_DESC srvHeapDesc = {};
srvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
srvHeapDesc.NumDescriptors = APP_SRV_HEAP_SIZE;
srvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
if (FAILED(mDevice->CreateDescriptorHeap(&srvHeapDesc, IID_PPV_ARGS(&mSrvHeap))))
assert(NULL && "Create SRV Heap Failed!");
C++
복사
ImGui는 자체 셰이더를 사용
•
ImGui는 GUI 렌더링을 위해 자체적인 vertex/fragment shader를 사용하고,
•
이때 필요한 텍스처를 SRV Descriptor Heap에서 가져옵니다.
•
즉, **셰이더에서 접근 가능한 디스크립터 힙(SHADER_VISIBLE)**이어야 합니다.
구조적으로 일반 게임 자원과 분리하는 이유:
1.
서로 충돌 방지:
게임 측에서 쓰는 SRV Heap과 ImGui가 관리하는 SRV Heap이 분리되면 관리 충돌 방지
(예: 인덱스 범위 겹침 등)
2.
업데이트 유연성:
ImGui의 텍스처는 내부적으로 동적으로 갱신되기도 하므로, 독립된 힙에서 관리하는 것이 깔끔
3.
쉐이더용 바인딩을 구분:
게임 측 SRV 바인딩과 ImGui의 바인딩은 목적과 수명이 다름
루트 시그니처
루트 시그니처(Root Signature)는 GPU 렌더링 파이프라인에서 셰이더가 접근할 수 있는 자원(CBV, SRV, UAV 등)의 구조와 슬롯을 정의하는 규칙표입니다.
쉽게 말하면:
GPU는 무조건 빠르게 동작해야 하므로,
"셰이더에서 어떤 리소스를 사용할지"를 미리 알려주는 구조가 필요합니다.
루트 시그니처는 이 역할을 하며,
드라이버와 GPU가 바인딩될 리소스를 빠르게 처리할 수 있도록 도와줍니다.
루트 시그니처 구성 요소
항목 | 설명 |
Root Constant | 32비트 값 한두 개를 직접 루트 테이블에 박아넣음 (상수) |
Root Descriptor | CBV/SRV/UAV 한 개의 자원을 직접 지정 |
Descriptor Table | Descriptor Heap 안의 여러 자원을 GPU가 참조할 수 있도록 묶은 포인터 테이블 (많이 씀) |
// create root signature
CD3DX12_ROOT_SIGNATURE_DESC rootSignatureDesc = {};
rootSignatureDesc.Init(0, nullptr, 0, nullptr, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);
Microsoft::WRL::ComPtr<ID3DBlob> signature;
Microsoft::WRL::ComPtr<ID3DBlob> error;
if (FAILED(D3D12SerializeRootSignature(&rootSignatureDesc, D3D_ROOT_SIGNATURE_VERSION_1, &signature, &error)))
assert(NULL && "SerializeRootSignature");
if (FAILED(mDevice->CreateRootSignature(0, signature->GetBufferPointer(), signature->GetBufferSize(), IID_PPV_ARGS(&mRootSignature))))
assert(NULL && "CreateRootSignature");
C++
복사
•
루트 파라미터나 스태틱 샘플러 없이 비어 있는 루트 시그니처를 생성합니다.
•
대신 입력 조립기(Input Assembler) 단계의 입력 레이아웃을 허용하는 플래그를 설정합니다.
•
이는 보통 삼각형을 그리고 싶을 때 사용됩니다 (예: IASetVertexBuffers() 사용 시).
GPU는 자원을 직접 접근하지 않고,
Root Signature → 디스크립터 테이블 → 디스크립터 힙 → GPU 리소스 (리소스힙)
이런 구조로 간접 접근합니다. 이게 Direct3D 12의 핵심 디자인입니다.
루트 시그니처란? GPU 셰이더에서 사용할 리소스 구조 정의
이 코드는 리소스 없이 비어 있는 루트 시그니처를 만들고, 입력 레이아웃 사용을 허용함
삼각형 그리기 같은 간단한 예제에서 사용되는 패턴입니다
해당 코드는 Direct3D 12에서 HLSL 셰이더를 컴파일하는 예제입니다. 각 줄의 의미와 사용 목적을 해석해드리겠습니다:
전체 목적
•
HLSL 셰이더 파일을 런타임에 컴파일하여 vertexShader와 pixelShader로 각각 저장합니다.
코드 해석
// 셰이더 저장용 Blob
Microsoft::WRL::ComPtr<ID3DBlob> vertexShader;
Microsoft::WRL::ComPtr<ID3DBlob> pixelShader;
C++
복사
•
ID3DBlob은 컴파일된 셰이더 바이트코드가 담기는 버퍼입니다.
#if defined(_DEBUG)
// 디버그 빌드일 경우, 셰이더 디버깅 정보 포함 + 최적화 생략
UINT compileFlags = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
#else
UINT compileFlags = 0;
#endif
C++
복사
•
디버그 모드에서는 디버깅 도구들이 셰이더 내부 상태를 추적할 수 있도록 합니다.
•
릴리즈 빌드에서는 최적화가 적용된 상태로 컴파일됩니다.
// 정점 셰이더 컴파일
ID3DBlob* errorVSBlob = nullptr;
D3DCompileFromFile(
L"..\\Shaders_SOURCE\\TriangleVS.hlsl", // HLSL 파일 경로
nullptr, // 매크로 정의 없음
D3D_COMPILE_STANDARD_FILE_INCLUDE, // #include 지원
"main", // 진입 함수 이름
"vs_5_0", // 셰이더 모델 버전 (Vertex Shader 5.0)
compileFlags, 0,
&vertexShader, &errorVSBlob // 출력 버퍼
);
C++
복사
// 픽셀 셰이더 컴파일
ID3DBlob* errorPSBlob = nullptr;
D3DCompileFromFile(
L"..\\Shaders_SOURCE\\TrianglePS.hlsl",
nullptr,
D3D_COMPILE_STANDARD_FILE_INCLUDE,
"main",
"ps_5_0", // 픽셀 셰이더 버전
compileFlags, 0,
&pixelShader, &errorPSBlob
);
C++
복사
요약
항목 | 설명 |
D3DCompileFromFile | .hlsl 셰이더 파일을 런타임에 컴파일하는 함수 |
vs_5_0 / ps_5_0 | 셰이더 모델 버전 5.0 |
main | 셰이더 진입 함수 이름 |
vertexShader, pixelShader | 컴파일된 셰이더 바이트코드를 담는 Blob |
D3DCOMPILE_DEBUG | 디버그 정보 포함 |
D3DCOMPILE_SKIP_OPTIMIZATION | 최적화 생략 (디버깅에 유리) |
보충 설명
•
errorVSBlob이나 errorPSBlob은 컴파일 실패 시 에러 메시지를 담습니다. 실패 처리를 추가하려면 아래와 같이 검사할 수 있습니다:
if (FAILED(hr) && errorVSBlob)
{
OutputDebugStringA((char*)errorVSBlob->GetBufferPointer());
}
C++
복사
DirectX 12의 PSO (Pipeline State Object)란?
PSO란?
PSO(Pipeline State Object)는 DirectX 12에서 그래픽 또는 컴퓨트 파이프라인의 모든 상태를 묶어 놓은 객체입니다.
DirectX 11과 달리, 상태(State)를 개별적으로 설정하지 않고 한 번에 패키징해서 GPU에게 넘깁니다.
1. 정점 입력 레이아웃 정의
D3D12_INPUT_ELEMENT_DESC inputElementDescs[] =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
{ "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }
};
C++
복사
•
이 배열은 정점 구조의 포맷을 GPU에게 알려줍니다.
•
POSITION: float3 (12 bytes), 오프셋 0
•
COLOR: float4 (16 bytes), 오프셋 12
•
전부 정점마다 제공되는 데이터(PER_VERTEX_DATA)
2. PSO(Pipeline State Object) 생성 정보 설정
PSO는 DirectX 12에서 렌더링 파이프라인의 상태들을 한 번에 묶는 핵심 구조입니다.
주요 필드 구성
psoDesc.InputLayout = { inputElementDescs, _countof(inputElementDescs) };
C++
복사
•
입력 레이아웃을 지정합니다.
psoDesc.pRootSignature = mRootSignature.Get();
C++
복사
•
이 파이프라인에서 사용할 루트 시그니처 설정
psoDesc.VS = CD3DX12_SHADER_BYTECODE(vertexShader.Get());
psoDesc.PS = CD3DX12_SHADER_BYTECODE(pixelShader.Get());
C++
복사
•
정점 및 픽셀 셰이더 바이트코드 설정
psoDesc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
psoDesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
C++
복사
•
래스터라이저 설정 (예: 컬링, 채우기 모드)
•
블렌딩 설정 (기본: 알파블렌딩 없음)
psoDesc.DepthStencilState.DepthEnable = FALSE;
psoDesc.DepthStencilState.StencilEnable = FALSE;
C++
복사
•
깊이 테스트와 스텐실 기능을 사용하지 않음
psoDesc.SampleMask = UINT_MAX;
C++
복사
•
모든 샘플을 렌더링 대상으로 사용
psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
C++
복사
•
기본 도형을 삼각형으로 설정
psoDesc.NumRenderTargets = 1;
psoDesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM;
C++
복사
•
출력 렌더 타겟은 1개이고, 포맷은 8비트 RGBA입니다.
psoDesc.SampleDesc.Count = 1;
C++
복사
•
멀티 샘플링 안함 (MSAA 끔)
3. PSO 생성
if (FAILED(mDevice->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&mPipelineState))))
assert(NULL, "CreateGraphicsPipelineState");
C++
복사
•
위에서 설정한 모든 정보를 바탕으로 GPU에 PSO를 생성
•
mPipelineState는 이후 커맨드 리스트에 설정해서 사용함
요약
항목 | 설명 |
PSO | 파이프라인 전체 상태를 담는 객체 |
InputLayout | 정점 입력 구조 정의 |
RootSignature | 리소스 바인딩 정의 |
VS / PS | 정점 셰이더 / 픽셀 셰이더 |
Blend / Rasterizer / Depth | 렌더링 동작 정의 |
RTVFormats | 출력 포맷 |
SampleDesc | 안티 앨리어싱 |
필요하면 PSO 사용 예시나 이걸 적용하는 커맨드 리스트 세팅도 추가로 설명해 드릴게요.
Command List (커맨드 리스트)란?
정의
Command List는 GPU에게 보낼 그리기 명령, 상태 설정, 리소스 전환 등 일련의 작업들을 기록하는 객체입니다.
한마디로 말하면:
흐름
1.
Reset(): 커맨드 리스트를 초기화 (with Allocator)
2.
Record: Draw(), Clear(), ResourceBarrier() 등 GPU 명령들을 기록
3.
Close(): 기록 완료
4.
Execute: CommandQueue->ExecuteCommandLists()로 실행
왜 사용하는가?
이유 | 설명 |
GPU에 시킬 일을 묶어서 보내고, CPU는 다른 일 수행 가능 | |
여러 명령을 한 번에 제출해서 GPU 효율 향상 | |
여러 스레드에서 Command List를 만들어 나중에 병합 가능 (Bundle 포함) |
종류 (D3D12_COMMAND_LIST_TYPE)
종류 | 용도 |
DIRECT | 모든 명령 가능 (Draw, Copy 등) |
BUNDLE | 재사용 가능한 서브 커맨드 목록 |
COMPUTE | Compute Shader 전용 |
COPY | 복사 전용 (리소스 복사 등) |
CreateCommandList(...)
mDevice->CreateCommandList(
0, // 노드 인덱스 (싱글 GPU 환경에서는 0)
D3D12_COMMAND_LIST_TYPE_DIRECT, // DIRECT 타입 (Draw, Copy, Compute 모두 가능)
mFrameContext[0].CommandAllocator, // 커맨드 할당자 (명령 기록을 위한 메모리)
mPipelineState, // 초기 바인딩할 PSO (파이프라인 상태 객체)
IID_PPV_ARGS(&mCommandList) // 반환 포인터
);
C++
복사
•
Command Allocator: 명령을 기록할 수 있는 메모리 공간을 제공
•
PipelineState: 초기 상태로 사용할 PSO 바인딩 (선택 사항)
•
결과: 커맨드 리스트는 자동으로 Recording 상태로 시작됨
mCommandList->Close()
mCommandList->Close();
C++
복사
→ 아직 아무 명령도 기록하지 않았으므로, 우선 닫아(close)서 Idle 상태로 전환시켜야 합니다.
나중에 실제로 사용할 때는:
mCommandAllocator->Reset();
mCommandList->Reset(mCommandAllocator.Get(), mPipelineState.Get());
... // 명령 기록
mCommandList->Close();
C++
복사
요약
항목 | 설명 |
CreateCommandList | 명령 기록 객체 생성 (Allocator와 연결됨) |
Close() | 명령이 없으므로 닫아야 다음 프레임에서 다시 Reset() 가능 |
초기 구조 | D3D12은 recording 상태로 생성, 실행 전에 항상 Close() 필요 |
필요하시면 다음 단계인 명령 기록 후 제출 흐름, 또는 프레임마다 Command List를 어떻게 재사용하는지도 이어서 설명해드릴게요.
삼각형 정점 데이터 정의
Vertex triangleVertices[] =
{
{ { 0.0f, 0.25f * 1600.0f / 900.0f, 0.0f }, { 1.0f, 0.0f, 0.0f, 1.0f } },
{ { 0.25f, -0.25f * 1600.0f / 900.0f, 0.0f }, { 0.0f, 1.0f, 0.0f, 1.0f } },
{ { -0.25f, -0.25f * 1600.0f / 900.0f, 0.0f }, { 0.0f, 0.0f, 1.0f, 1.0f } }
};
C++
복사
•
삼각형 3개의 정점, 각각 위치 + 색상
•
1600 / 900 비율 고려해서 Y좌표 스케일 조정 (비율 고정)
리소스 힙 설정: Upload Heap 사용
CD3DX12_HEAP_PROPERTIES heapProps(D3D12_HEAP_TYPE_UPLOAD);
CD3DX12_RESOURCE_DESC bufferDesc = CD3DX12_RESOURCE_DESC::Buffer(vertexBufferSize);
C++
복사
•
UPLOAD 힙은 CPU가 접근 가능, GPU는 읽기 전용
•
주의: 업로드 힙은 동적 데이터용이며 정적 정점에는 적합하지 않음
실무에서는 DEFAULT 힙 + UPLOAD로 복사하는 게 성능상 더 좋음
Committed Resource 생성 (버퍼 생성)
mDevice->CreateCommittedResource(...);
C++
복사
•
하나의 자원에 Heap + Resource를 동시에 생성
•
D3D12_RESOURCE_STATE_GENERIC_READ: GPU에서 읽을 준비가 됨
버퍼에 정점 데이터 복사
mVertexBuffer->Map(...);
memcpy(...);
mVertexBuffer->Unmap(...);
C++
복사
•
Map 함수로 CPU가 접근할 수 있는 포인터를 얻음
•
memcpy로 정점 데이터를 직접 복사
•
Unmap해서 GPU가 다시 읽을 수 있도록 반환
VertexBufferView 초기화
mVertexBufferView.BufferLocation = mVertexBuffer->GetGPUVirtualAddress();
mVertexBufferView.StrideInBytes = sizeof(Vertex);
mVertexBufferView.SizeInBytes = vertexBufferSize;
C++
복사
•
GPU가 참조할 수 있는 버퍼의 정보(주소, stride, 크기)를 제공
•
이 정보를 나중에 IASetVertexBuffers()에 넘김
요약 흐름
단계 | 내용 |
정점 데이터 정의 | 삼각형 3개 정점 정의 |
힙/버퍼 정의 | Upload 힙 + 버퍼 크기 정의 |
리소스 생성 | CreateCommittedResource() |
데이터 업로드 | Map → memcpy → Unmap |
뷰 생성 | VertexBufferView 초기화 |
필요하시면 이 버퍼를 GPU에서 실제로 그리는 흐름 (PSO 설정, 커맨드 리스트 기록 등)도 이어서 설명해드릴 수 있어요.
Fecne
한 시스템에 CPU와 GPU가 병렬로 실행되다 보니 동기화 문제가 발생한다. 예를 들어 그리고자 하는 어떤 기하구조의 위치를 R이라는 자원에 담는다고 하자. 그 기하구조를 위치 p1에 그리려는 목적으로 CPU는 위치 p1을 R에 추가하고, R을 참조하는 그리기 명령 C를 명령 대기열에 추가한다.
명령 대기열에 명령을 추가하는 연산은 CPU의 실행을 차단하지 않으므로, CPU는 계속해서 다음 단계로 넘어간다. 만약 GPU가 그리기 명령 C를 실행하기 전에 CPU가 새 위치 p2를 R에 추가해서 R에 있던 기존 p1을 덮어쓰면, 기하구조는 의도했던 위치에 그려지지 않게 된다.
이런 문제의 해결책은 GPU가 명령 대기열의 명령들 중 특정 지점까지의 모든 명령을 다 처리할 때까지 CPU를 기다리게 하는 것이다. 대기열의 특정 지점까지의 명령을 처리하는 것을 가리켜 명령 대기열을 비운다 또는 방출한다(Flush)라고 말한다.
이때 필요한 것이 바로 울타리(Fence)이다. 울타리(펜스)는 ID3D12Fence 인터페이스로 대표되며, GPU와 CPU의 동기화를 위한 수단으로 쓰인다. 다음은 펜스 객체를 생성하는 메서드이다.
CreateFence() — 펜스 생성
if (FAILED(mDevice->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&mFence))))
assert(NULL, "CreateFence");
C++
복사
•
Fence는 GPU 작업의 완료 여부를 추적하는 장치입니다.
•
0: 초기 fence 값 (GPU가 아직 아무 일도 하지 않았음)
•
D3D12_FENCE_FLAG_NONE: 특별한 동작 없음
•
mFence: CPU와 GPU 간에 동기화할 때 사용할 펜스 객체
2. 현재 프레임의 FenceValue 증가
mFrameContext[mFrameIndex].FenceValue++;
C++
복사
•
GPU 작업이 제출될 때마다 Fence 값 증가 필요
•
나중에 이 값을 기반으로 GPU가 어느 시점까지 완료했는지 확인
3. CreateEvent() — 이벤트 핸들 생성
mFenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
if (mFenceEvent == nullptr)
{
HRESULT_FROM_WIN32(GetLastError());
assert(NULL, "Create Fence Event");
}
C++
복사
•
펜스의 완료 신호를 기다리기 위한 CPU 측 이벤트
•
CreateEvent는 Windows 함수로, GPU 작업이 끝날 때 OS 차원에서 알려주는 도구
•
FALSE, FALSE: auto-reset 이벤트, 초기 비신호 상태
화면에 물체 그리기
이 함수 GraphicDevice_DX12::PopulateCommandList()는 현재 프레임을 그리기 위해 GPU에 제출할 커맨드 리스트(Command List)를 구성하는 단계입니다. 한 줄씩 분석해서 어떤 흐름인지 정리해드릴게요.
함수 목적
GPU 명령 리스트를 기록(Record)하고 준비하는 함수입니다.
이 명령 리스트는 나중에 ExecuteCommandLists()에 의해 실행됩니다.
단계별 설명
1. 커맨드 할로케이터 리셋
mFrameContext[mFrameIndex].CommandAllocator->Reset();
C++
복사
•
이 프레임에서 사용할 커맨드 할로케이터를 리셋합니다.
•
이전 GPU 작업이 끝났는지 Fence로 확인하고 안전할 때만 리셋 가능함.
2. 커맨드 리스트 리셋
mCommandList->Reset(CommandAllocator, PipelineState);
mCommandList->SetGraphicsRootSignature(mRootSignature.Get());
C++
복사
•
해당 커맨드 할로케이터와 PSO를 기반으로 명령 리스트 재사용 준비
•
Reset 후 커맨드 기록(Recording)이 가능해짐
3. 뷰포트 및 시저 설정
int width = application.GetWindow().GetWidth();
int height = application.GetWindow().GetHeight();
CD3DX12_VIEWPORT viewport(0.0f, 0.0f, static_cast<float>(width), static_cast<float>(height));
CD3DX12_RECT scissorRect(0, 0, static_cast<LONG>(width), static_cast<LONG>(height));
mCommandList->RSSetViewports(1, &viewport);
mCommandList->RSSetScissorRects(1, &scissorRect);
C++
복사
•
뷰포트(Viewport): 렌더링 영역 (화면 크기만큼 설정)
•
시저(Scissor): 자를 영역
4. 리소스 상태 전환 (Present → RenderTarget)
CD3DX12_RESOURCE_BARRIER resourceBarrierPR
= CD3DX12_RESOURCE_BARRIER::Transition(mRenderTargets[mFrameIndex].Get(), D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET);
mCommandList->ResourceBarrier(1, &resourceBarrierPR);
C++
복사
•
스왑체인 버퍼는 Present 이후 PRESENT 상태이므로, 다시 렌더링하려면 RENDER_TARGET 상태로 바꿔야 함
5. 렌더 타겟 바인딩 및 클리어
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(mRtvHeap->GetCPUDescriptorHandleForHeapStart(), mFrameIndex, mRtvDescriptorSize);
mCommandList->OMSetRenderTargets(1, &rtvHandle, FALSE, nullptr);
C++
복사
•
현재 RTV를 Output-Merger 단계에 설정
•
화면을 0.0f, 0.2f, 0.4f, 1.0f 컬러로 초기화
6. 그리기 명령 기록
// Record commands.
const float clearColor[] = { 0.0f, 0.2f, 0.4f, 1.0f };
mCommandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);
mCommandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
mCommandList->IASetVertexBuffers(0, 1, &mVertexBufferView);
mCommandList->DrawInstanced(3, 1, 0, 0);
C++
복사
•
입력 조립기(IA) 설정: 삼각형 리스트, 버텍스 버퍼 바인딩
•
정점 3개로 삼각형 1개 드로우
Present로 상태 전환
CD3DX12_RESOURCE_BARRIER resourceBarrierRT
= CD3DX12_RESOURCE_BARRIER::Transition(mRenderTargets[mFrameIndex].Get(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT);
mCommandList->ResourceBarrier(1, &resourceBarrierRT);
C++
복사
•
이 코드를 현재 비활성화한 이유는:
CommandList Close
if (FAILED(mCommandList->Close()))
assert(NULL, "mCommandList->Close()");
C++
복사
•
아직 명령 기록이 끝난 것이 아니기 때문에 ImGui 이후에 닫을 예정
요약 흐름 (현재 코드 기준)
[CommandAllocator.Reset]
→ [CommandList.Reset]
→ [Viewport/Scissor 설정]
→ [리소스 상태 전환: Present → RenderTarget]
→ [RTV 바인딩, Clear]
→ [버텍스 설정 및 드로우]
→ [ImGui 렌더링 등 추가]
→ [리소스 상태 전환: RenderTarget → Present] ← (ImGui 이후 해야 함)
→ [CommandList.Close]
Plain Text
복사
GPU와 CPU 간 동기화
전체 흐름 설명
이 함수의 목적:
•
GPU가 현재 프레임의 커맨드 처리를 끝낼 때까지 기다리고,
•
다음 프레임으로 안전하게 넘어가도록 Fence 값을 갱신하는 프레임 간 동기화 루틴입니다.
1. 커맨드 큐에 Fence Signal 명령 제출
const UINT64 currentFenceValue = mFrameContext[mFrameIndex].FenceValue;
mCommandQueue->Signal(mFence.Get(), currentFenceValue);
C++
복사
•
GPU에게 "현재 프레임 작업이 끝나면 FenceValue를 여기까지 올려줘"라고 신호(Signal).
•
이 값은 GPU가 처리 완료 시점에서 Fence의 내부 카운터에 기록됨.
2. 다음 백버퍼의 인덱스를 얻어옴
mFrameIndex = mSwapChain->GetCurrentBackBufferIndex();
C++
복사
•
Present() 이후의 새로운 백버퍼 인덱스를 CPU 쪽에서 획득함.
•
이 인덱스를 기준으로 다음 사용할 FrameContext를 선택.
3. 해당 백버퍼용 FrameContext의 Fence가 GPU에서 끝났는지 확인
if (mFence->GetCompletedValue() < mFrameContext[mFrameIndex].FenceValue)
{
mFence->SetEventOnCompletion(...);
WaitForSingleObjectEx(...);
}
C++
복사
•
GPU가 FenceValue를 아직 올리지 않았다면 → 즉, GPU 작업이 끝나지 않았음
•
이벤트 등록 후, CPU는 WaitForSingleObjectEx()로 기다림 (블로킹)
4. 다음 프레임의 FenceValue 준비
mFrameContext[mFrameIndex].FenceValue = currentFenceValue + 1;
C++
복사
•
다음 프레임에서 사용할 Fence 신호값을 1 증가시켜 등록
다이어그램
전체 개념 요약
•
프레임 n에서 GPU에게 명령을 넘겨주고
•
프레임 n+1에서 CPU는 동일 자원을 다시 사용하고 싶어짐
•
하지만 GPU가 자원 사용 중일 수 있기 때문에 Fence를 통해 GPU가 끝났는지 확인 후 자원을 사용해야 함
순서별 동작 설명
① CPU는 프레임 n의 Command List에서 자원을 바인딩해서 사용함
•
예: Draw, CopyResource, OMSetRenderTargets 등
•
이 시점에 자원(Resource) 은 GPU에 제출되며 아직 사용 중 아님
② CPU는 Command List를 Command Queue에 제출 (ExecuteCommandLists)
•
GPU는 이 큐를 순서대로 실행
•
이 Command List는 FenceValue m+1과 연동됨 (아직 실행 전)
③ CPU는 Queue에 Signal 명령을 보냄
mCommandQueue->Signal(fence, m+1);
C++
복사
•
“GPU야, 이 명령 다 끝나면 fence를 m+1로 올려줘”
•
이때 GPU는 아직 작업 안 했지만, Fence는 예약됨
④ GPU가 Command List를 실행하면서 자원을 진짜 사용하기 시작함
•
여기서 GPU가 해당 리소스를 건드리기 시작
•
이때 CPU가 동일 리소스를 건드리면 동기화 충돌
⑤ GPU가 프레임 n의 명령 처리를 완료하면 FenceValue = m+1로 설정됨
•
이 시점부터 CPU는 안전하게 해당 리소스를 다시 사용 가능
⑥ CPU는 다음 프레임 n+1에서 동일 리소스를 사용하려고 함
•
Fence가 m+1이 됐는지 확인
if (fence->GetCompletedValue() < m+1)
fence->SetEventOnCompletion(m+1, event);
WaitForSingleObject(event, INFINITE);
C++
복사
•
이때 CPU는 GPU가 리소스를 다 쓰기 전이면 기다림
•
GPU가 끝났으면 → 안전하게 다음 명령 준비 가능
정리 다이어그램 순서 요약
단계 | 내용 | 주체 |
① | 자원 바인딩 (사용 예약) | CPU |
② | Command List 제출 | CPU → GPU |
③ | Fence Signal 예약 | CPU |
④ | 자원 실제 사용 | GPU |
⑤ | GPU 작업 완료 후 Fence m+1 설정 | GPU |
⑥ | CPU가 다음 프레임에서 자원 재사용 전 Fence 완료 확인 | CPU |
왜 이 구조가 중요한가?
•
DX12에서는 CPU가 GPU보다 빠르게 앞서나갈 수 있음
•
따라서 Fence로 GPU 작업 완료 여부를 추적하지 않으면,
◦
GPU가 작업 중인 리소스를 CPU가 덮어씀 → 비정상 동작 / Device 제거
필요하다면 이 흐름을 실제 코드 구조나 상태도(State Transition)로도 시각화해드릴게요.
멀티 프레임 구조에서 이 함수의 중요성
DirectX12는 자원 관리와 GPU 동기화 책임이 개발자에게 있음
→ 이 함수를 잘못 구현하면:
•
Race condition (동시 접근)
•
GPU가 잘못된 버퍼에 그리기 → D3D 오류 및 Device 제거
•
프레임 중간 덮어쓰기 → 화면 찢어짐/깨짐
더블 버퍼링 (2프레임)
•
예를 들어 FrameContext[0], FrameContext[1] 이렇게 2개를 사용하는 경우:
프레임 번호 | Fence Value | Frame Index |
프레임 0 | 0 | 0 |
프레임 1 | 1 | 1 |
프레임 2 | 2 | 0 |
프레임 3 | 3 | 1 |
프레임 4 | 4 | 0 |
... | ... | ... |