ImGui DirectX 12 통합과 멀티프레임 렌더링
ImGui(Immediate Mode GUI)는 게임 엔진과 그래픽 애플리케이션 개발에 널리 사용되는 경량 UI 라이브러리입니다. DirectX 12와 ImGui를 통합하는 과정은 DirectX 12의 명시적 동기화와 멀티프레임 파이프라이닝 구조를 정확히 이해해야 하는 도전적인 작업입니다. 이 문서에서는 멀티프레임 구조 초기화의 핵심 개념과 구현 방법을 상세히 다룹니다.
ImGui란 무엇인가
ImGui는 Omar Cornut이 개발한 즉시 모드(Immediate Mode) GUI 라이브러리로, 복잡한 상태 관리 없이 간단한 함수 호출만으로 UI를 구성할 수 있습니다. 기존의 유지 모드(Retained Mode) GUI 시스템과 달리, 매 프레임마다 UI를 다시 그리는 방식으로 동작합니다.
ImGui의 주요 특징
•
즉시 모드 패러다임: UI 코드가 렌더링 루프 안에서 직접 실행되어 상태 동기화 문제가 없습니다.
•
경량성: 헤더 파일 몇 개로 구성되어 프로젝트에 쉽게 통합할 수 있습니다.
•
플랫폼 독립성: DirectX, OpenGL, Vulkan, Metal 등 다양한 백엔드를 지원합니다.
•
디버깅 도구 제작에 최적: 프로파일러, 인스펙터, 콘솔 등을 빠르게 구현할 수 있습니다.
•
도킹과 멀티 뷰포트: 복잡한 편집기 레이아웃을 지원합니다.
DirectX 12에서 ImGui를 사용하는 이유
DirectX 12 애플리케이션에서 ImGui를 통합하면 다음과 같은 이점을 얻을 수 있습니다.
•
엔진 설정 UI, 디버그 오버레이, 프로파일링 정보 표시
•
씬 에디터, 머티리얼 에디터, 파티클 시스템 파라미터 조정
•
실시간 셰이더 파라미터 튜닝과 시각화
•
개발 도구와 진단 패널 구현
그러나 DirectX 12는 DirectX 11과 달리 명시적인 동기화와 리소스 관리를 요구하므로, ImGui 통합 시 멀티프레임 파이프라이닝과 동기화를 올바르게 처리해야 합니다.
멀티프레임 구조의 핵심: FrameContext
DirectX 12에서 효율적인 렌더링을 위해서는 CPU와 GPU가 서로 다른 프레임을 동시에 처리하는 멀티프레임 파이프라이닝이 필수입니다. 이를 위해 각 프레임마다 독립적인 리소스를 관리하는 FrameContext 구조체를 사용합니다.
FrameContext 구조체 정의
struct FrameContext
{
ID3D12CommandAllocator* CommandAllocator;
UINT64 FenceValue;
};
static constexpr UINT APP_NUM_FRAMES_IN_FLIGHT = 2; // 또는 3
FrameContext mFrameContext[APP_NUM_FRAMES_IN_FLIGHT];
C++
복사
이 구조체는 각 프레임에 필요한 두 가지 핵심 요소를 포함합니다.
Command Allocator: 메모리 격리
Command Allocator는 GPU 명령을 기록할 메모리를 할당합니다. 각 프레임마다 독립적인 Allocator를 사용하는 이유는 다음과 같습니다.
•
GPU는 비동기적으로 명령을 실행하므로 이전 프레임이 아직 처리 중일 수 있습니다.
•
단일 Allocator를 재사용하면 이전 프레임의 명령이 덮어써져 크래시나 렌더링 오류가 발생합니다.
•
프레임별 Allocator를 순환하면 GPU가 해당 프레임을 완료한 후에만 재사용할 수 있습니다.
Fence Value: 동기화 타임스탬프
Fence는 CPU와 GPU 간의 동기화 지점을 표시하는 64비트 정수입니다. 각 FrameContext에 고유한 FenceValue를 저장하여 해당 프레임의 GPU 작업 완료 여부를 추적합니다.
// 프레임 시작 시 동기화
UINT frameIndex = mSwapChain->GetCurrentBackBufferIndex();
FrameContext* frameCtx = &mFrameContext[frameIndex];
// 이 프레임의 이전 작업이 완료될 때까지 대기
UINT64 fenceValue = frameCtx->FenceValue;
if (mFence->GetCompletedValue() < fenceValue)
{
mFence->SetEventOnCompletion(fenceValue, mFenceEvent);
WaitForSingleObject(mFenceEvent, INFINITE);
}
C++
복사
멀티프레임 파이프라이닝의 필요성
단일 프레임 구조의 문제점을 이해하면 멀티프레임 파이프라이닝의 중요성을 알 수 있습니다.
단일 프레임 구조의 성능 병목
하나의 Command Allocator만 사용하는 경우:
1.
CPU가 Command List에 명령을 기록합니다.
2.
GPU에 제출하고 실행이 완료될 때까지 CPU가 대기합니다.
3.
GPU 작업이 끝나면 Command Allocator를 리셋하고 다음 프레임을 시작합니다.
이 방식은 CPU와 GPU가 번갈아 작동하여 둘 중 하나는 항상 유휴 상태가 되어 전체 처리량이 절반으로 감소합니다.
멀티프레임 구조의 병렬성
2개 또는 3개의 FrameContext를 순환하는 경우:
이렇게 하면 CPU와 GPU가 동시에 작업하여 전체 프레임레이트가 2배 이상 향상됩니다.
DirectX 12의 엄격한 재사용 규칙
DirectX 12는 리소스 재사용에 엄격한 규칙을 적용합니다.
•
Command Allocator는 GPU가 해당 명령을 모두 실행한 후에만 리셋 가능
•
백버퍼는 Present 호출 후 다음 프레임에서만 재사용 가능
•
Descriptor Heap과 리소스도 GPU 사용 중에는 수정 불가
이러한 규칙을 위반하면 디버그 레이어에서 오류가 발생하거나 릴리즈 빌드에서 랜덤 크래시가 발생합니다.
ImGui의 내부 프레임 상태 관리
ImGui 자체도 프레임 간 상태를 유지합니다.
•
Vertex/Index 버퍼: 각 프레임의 UI 지오메트리를 저장
•
Docking 상태: 패널 레이아웃과 크기 정보
•
Font Atlas: 텍스처 업로드와 GPU 리소스
•
Platform Windows: 멀티 뷰포트 윈도우 관리
ImGui의 DirectX 12 백엔드는 NUM_FRAMES_IN_FLIGHT만큼의 프레임 리소스를 내부적으로 관리하므로, 애플리케이션의 프레임 수와 일치시켜야 합니다.
ImGui 프레임 렌더링 흐름
ImGui 렌더링은 크게 두 단계로 나뉩니다: UI 구성(Begin)과 GPU 명령 생성(End).
Begin 단계: UI 구성
void ImGuiManager::Begin()
{
// DirectX 12 백엔드 프레임 시작
ImGui_ImplDX12_NewFrame();
// Win32 플랫폼 백엔드 프레임 시작 (입력 처리)
ImGui_ImplWin32_NewFrame();
// ImGui 코어 프레임 시작
ImGui::NewFrame();
// ImGuizmo 프레임 시작 (선택적)
ImGuizmo::BeginFrame();
}
C++
복사
각 NewFrame 함수의 역할
•
ImGui_ImplDX12_NewFrame(): DirectX 12 리소스 준비
◦
현재 프레임의 Vertex/Index 버퍼 선택
◦
Descriptor Heap 업데이트
◦
이전 프레임의 업로드 버퍼 해제
•
ImGui_ImplWin32_NewFrame(): 플랫폼 입력 처리
◦
마우스 위치와 버튼 상태 업데이트
◦
키보드 입력 처리
◦
윈도우 포커스 및 호버 상태 확인
•
ImGui::NewFrame(): ImGui 코어 초기화
◦
델타 타임 계산
◦
레이아웃 스택 초기화
◦
드로우 리스트 준비
UI 코드 작성
Begin과 End 사이에 실제 UI 코드를 작성합니다.
// Begin 이후 UI 구성
ImGui::Begin("Debug Info");
ImGui::Text("FPS: %.1f", ImGui::GetIO().Framerate);
ImGui::SliderFloat("Exposure", &exposure, 0.0f, 5.0f);
ImGui::ColorEdit3("Clear Color", clearColor);
ImGui::End();
C++
복사
End 단계: GPU 명령 생성
End 함수는 ImGui의 드로우 데이터를 DirectX 12 명령으로 변환하여 Command List에 기록합니다.
디스플레이 크기 동기화
void ImGuiManager::End()
{
ImGuiIO& io = ImGui::GetIO();
io.DisplaySize = ImVec2(
static_cast<float>(application.GetWindow().GetWidth()),
static_cast<float>(application.GetWindow().GetHeight())
);
C++
복사
디스플레이 크기를 정확히 설정하지 않으면 다음과 같은 문제가 발생합니다.
•
마우스 클릭 위치와 UI 요소가 불일치
•
윈도우 크기 조절 시 UI가 늘어나거나 잘림
•
멀티 뷰포트 도킹이 잘못된 위치에 생성
드로우 데이터 생성
ImGui::Render();
C++
복사
ImGui::Render() 함수는 이번 프레임에 작성된 모든 UI 코드를 분석하여 내부 드로우 리스트를 생성합니다. 이 함수를 호출하기 전에는 ImGui::GetDrawData()가 유효하지 않습니다.
프레임 리소스 획득
auto* graphicDevice = ya::graphics::GetDevice();
auto* frameCtx = graphicDevice->WaitForNextFrameResources();
UINT backBufferIdx = graphicDevice->GetSwapChain()->GetCurrentBackBufferIndex();
C++
복사
WaitForNextFrameResources()는 다음 작업을 수행합니다.
1.
현재 프레임 인덱스에 해당하는 FrameContext 선택
2.
해당 프레임의 FenceValue 확인
3.
GPU가 이전 작업을 완료하지 않았으면 대기
4.
Command Allocator 리셋
5.
Command List 리셋 및 초기화
리소스 배리어: Present에서 Render Target으로 전환
D3D12_RESOURCE_BARRIER barrier = {};
barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
barrier.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE;
barrier.Transition.pResource = graphicDevice->GetRenderTargetResource(backBufferIdx).Get();
barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_PRESENT;
barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_RENDER_TARGET;
C++
복사
DirectX 12에서는 리소스 상태 전환을 명시적으로 선언해야 합니다. 백버퍼는 Present 호출 후 PRESENT 상태이므로, 렌더링하기 전에 RENDER_TARGET 상태로 전환해야 합니다.
이 배리어를 생략하면:
•
디버그 레이어에서 상태 불일치 경고 발생
•
릴리즈 빌드에서 렌더링 결과가 화면에 나타나지 않거나 깨짐
•
GPU 타임아웃이나 TDR(Timeout Detection and Recovery) 발생 가능
Command List와 Descriptor Heap 설정
auto commandList = graphicDevice->GetCommandList();
// Clear color 설정 (선택적)
ImVec4 clear_color = ImVec4(0.45f, 0.55f, 0.60f, 1.00f);
const float clear_color_with_alpha[4] = {
clear_color.x * clear_color.w,
clear_color.y * clear_color.w,
clear_color.z * clear_color.w,
clear_color.w
};
// Descriptor Heap 핸들 준비
auto rtvHeap = graphicDevice->GetRTVHeap();
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle =
graphicDevice->GetRenderTargetDescriptorHandle(backBufferIdx);
auto srvHeap = graphicDevice->GetSrvHeap();
C++
복사
렌더 타겟 클리어와 바인딩
// 배경 클리어 (메인 렌더링에서 이미 처리한 경우 생략 가능)
// commandList->ClearRenderTargetView(rtvHandle, clear_color_with_alpha, 0, nullptr);
// 렌더 타겟 설정 (메인 렌더링에서 이미 설정한 경우 생략 가능)
// commandList->OMSetRenderTargets(1, &rtvHandle, FALSE, nullptr);
C++
복사
많은 경우 메인 렌더링 패스에서 이미 렌더 타겟을 클리어하고 설정했으므로, ImGui는 단순히 UI를 오버레이로 그리기만 하면 됩니다. 주석 처리된 부분은 ImGui를 독립적으로 사용할 때만 활성화합니다.
SRV Descriptor Heap 바인딩
commandList->SetDescriptorHeaps(1, srvHeap.GetAddressOf());
C++
복사
ImGui는 폰트 아틀라스나 사용자 정의 텍스처를 렌더링하기 위해 SRV(Shader Resource View) Descriptor Heap에 접근해야 합니다. ImTextureID로 참조된 텍스처는 이 힙에서 조회됩니다.
ImGui 드로우 데이터를 GPU 명령으로 변환
ImGui_ImplDX12_RenderDrawData(ImGui::GetDrawData(), commandList.Get());
C++
복사
이 함수는 ImGui의 드로우 리스트를 순회하며 다음 작업을 수행합니다.
1.
Vertex와 Index 버퍼를 업데이트하거나 확장
2.
Viewport와 Scissor Rect 설정
3.
Root Signature와 PSO 바인딩
4.
Descriptor Table 설정
5.
DrawIndexed 호출로 각 드로우 커맨드 실행
리소스 배리어: Render Target에서 Present로 전환
barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_RENDER_TARGET;
barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_PRESENT;
commandList->ResourceBarrier(1, &barrier);
C++
복사
렌더링이 완료되면 백버퍼를 다시 PRESENT 상태로 전환하여 화면에 표시할 준비를 합니다.
Command List 종료
commandList->Close();
}
C++
복사
Command List를 닫으면 더 이상 명령을 추가할 수 없으며, ExecuteCommandLists()로 Command Queue에 제출할 수 있는 상태가 됩니다.
Present 함수: 프레임 완료와 동기화
Present 함수는 현재 프레임을 화면에 표시하고, 다음 프레임을 위한 동기화를 설정합니다.
Fence 값 증가
void GraphicDevice_DX12::Present()
{
UINT64 fenceValue = mFenceLastSignalValue + 1;
C++
복사
Fence 값은 단조 증가하는 타임스탬프로, 각 프레임을 고유하게 식별합니다. 이를 통해 GPU가 특정 프레임까지 완료했는지 추적할 수 있습니다.
GPU에 Fence 신호 명령 제출
mCommandQueue->Signal(mFence.Get(), fenceValue);
C++
복사
Signal() 함수는 Command Queue에 특수한 명령을 추가하여, 이전 모든 명령이 완료된 후 Fence 값을 업데이트하도록 지시합니다. 이는 비동기적으로 실행되며, CPU는 즉시 다음 코드로 진행합니다.
마지막 신호 값 기록
mFenceLastSignalValue = fenceValue;
C++
복사
현재까지 GPU에 제출한 가장 높은 Fence 값을 저장합니다. 이는 나중에 GPU 완료 여부를 확인할 때 사용됩니다.
FrameContext에 Fence 값 저장
FrameContext* frameCtx = &mFrameContext[mFrameIndex % APP_NUM_FRAMES_IN_FLIGHT];
frameCtx->FenceValue = fenceValue;
C++
복사
현재 프레임의 FrameContext에 Fence 값을 저장하여, 다음에 이 프레임을 재사용할 때 GPU 작업 완료를 확인할 수 있습니다.
다음 백버퍼 인덱스 획득
mFrameIndex = mSwapChain->GetCurrentBackBufferIndex();
}
C++
복사
Swap Chain의 GetCurrentBackBufferIndex()는 Present 호출 후 다음에 렌더링할 백버퍼의 인덱스를 반환합니다. 더블 버퍼링에서는 0과 1을, 트리플 버퍼링에서는 0, 1, 2를 순환합니다.
동기화 메커니즘의 동작 원리
DirectX 12의 수동 동기화는 처음에는 복잡해 보이지만, 패턴을 이해하면 명확해집니다.
Fence를 통한 타임라인 추적
Fence는 시간의 흐름을 나타내는 타임라인과 같습니다.
각 프레임이 끝날 때마다 Fence 값이 증가하므로, GetCompletedValue()로 현재 Fence 값을 확인하여 GPU가 어느 프레임까지 완료했는지 알 수 있습니다.
프레임 재사용 조건 확인
프레임을 재사용하기 전에 다음을 확인해야 합니다.
FrameContext* frameCtx = &mFrameContext[frameIndex];
UINT64 requiredFence = frameCtx->FenceValue;
UINT64 completedFence = mFence->GetCompletedValue();
if (completedFence < requiredFence)
{
// GPU가 아직 이 프레임을 처리 중
// 대기 필요
}
else
{
// GPU가 이 프레임을 완료함
// 안전하게 재사용 가능
}
C++
복사
이벤트 기반 대기
Fence 값을 폴링하는 대신, 이벤트를 사용하여 효율적으로 대기할 수 있습니다.
if (mFence->GetCompletedValue() < requiredFence)
{
// Fence 값이 도달하면 이벤트 신호
mFence->SetEventOnCompletion(requiredFence, mFenceEvent);
// 이벤트가 신호될 때까지 CPU 스레드 대기
WaitForSingleObject(mFenceEvent, INFINITE);
}
C++
복사
이 방식은 CPU가 스핀 루프로 Fence를 체크하는 것보다 효율적이며, 운영체제가 스레드를 슬립 상태로 전환하여 다른 작업에 CPU 시간을 할당할 수 있습니다.
동기화 타이밍 최적화
동기화를 너무 자주 하면 성능이 저하됩니다.
•
나쁜 예: 매 프레임 GPU 완료를 기다림 → CPU와 GPU가 순차 실행
•
좋은 예: N 프레임 앞서가며 필요한 경우에만 대기 → 병렬 실행
// 나쁜 예: 매 프레임 동기화
Present();
WaitForGPU(); // 매번 대기 - 병렬성 상실
// 좋은 예: 필요할 때만 동기화
Present();
// GPU가 N 프레임 뒤처질 때만 대기
// 대부분의 경우 즉시 진행
C++
복사
실전 통합 예제
완전한 ImGui DirectX 12 통합 예제를 단계별로 구성합니다.
1단계: 초기화
void GraphicDevice_DX12::InitializeImGui(HWND hwnd)
{
// ImGui 컨텍스트 생성
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO();
// 도킹과 멀티 뷰포트 활성화
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable;
// 스타일 설정
ImGui::StyleColorsDark();
// SRV Descriptor Heap 생성 (ImGui용)
D3D12_DESCRIPTOR_HEAP_DESC desc = {};
desc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
desc.NumDescriptors = 1; // Font atlas용
desc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
mDevice->CreateDescriptorHeap(&desc, IID_PPV_ARGS(&mImGuiSrvHeap));
// Win32 백엔드 초기화
ImGui_ImplWin32_Init(hwnd);
// DirectX 12 백엔드 초기화
ImGui_ImplDX12_Init(
mDevice.Get(),
APP_NUM_FRAMES_IN_FLIGHT,
DXGI_FORMAT_R8G8B8A8_UNORM,
mImGuiSrvHeap.Get(),
mImGuiSrvHeap->GetCPUDescriptorHandleForHeapStart(),
mImGuiSrvHeap->GetGPUDescriptorHandleForHeapStart()
);
}
C++
복사
2단계: 프레임 루프 통합
void Application::Run()
{
while (!shouldClose)
{
// 입력 처리
ProcessWindowMessages();
// ImGui 프레임 시작
imguiManager->Begin();
// UI 구성
BuildUI();
// 렌더링 시작
graphicDevice->BeginFrame();
// 메인 렌더링
RenderScene();
// ImGui 렌더링
imguiManager->End();
// 프레임 제출 및 Present
graphicDevice->EndFrame();
graphicDevice->Present();
}
}
C++
복사
3단계: 종료 처리
void GraphicDevice_DX12::ShutdownImGui()
{
// GPU 작업 완료 대기
WaitForGPU();
// ImGui 백엔드 종료
ImGui_ImplDX12_Shutdown();
ImGui_ImplWin32_Shutdown();
// ImGui 컨텍스트 파괴
ImGui::DestroyContext();
}
void GraphicDevice_DX12::WaitForGPU()
{
// 현재까지 제출한 모든 작업 완료 대기
UINT64 fenceValue = mFenceLastSignalValue + 1;
mCommandQueue->Signal(mFence.Get(), fenceValue);
mFenceLastSignalValue = fenceValue;
if (mFence->GetCompletedValue() < fenceValue)
{
mFence->SetEventOnCompletion(fenceValue, mFenceEvent);
WaitForSingleObject(mFenceEvent, INFINITE);
}
}
C++
복사
주의사항과 트러블슈팅
ImGui DirectX 12 통합 시 자주 발생하는 문제와 해결 방법입니다.
프레임 수 불일치
ImGui 백엔드 초기화 시 지정한 NUM_FRAMES_IN_FLIGHT와 실제 Swap Chain의 버퍼 수가 다르면 메모리 손상이나 크래시가 발생할 수 있습니다.
// 잘못된 예
const UINT APP_NUM_FRAMES_IN_FLIGHT = 2;
swapChainDesc.BufferCount = 3; // 불일치!
// 올바른 예
const UINT APP_NUM_FRAMES_IN_FLIGHT = 2;
swapChainDesc.BufferCount = 2; // 일치
C++
복사
Descriptor Heap 부족
ImGui가 사용하는 텍스처 수가 초기화 시 지정한 Descriptor 수를 초과하면 오류가 발생합니다. 여유 있게 할당하세요.
// 최소: Font atlas만
desc.NumDescriptors = 1;
// 권장: 사용자 정의 텍스처 고려
desc.NumDescriptors = 100;
C++
복사
리소스 배리어 누락
백버퍼의 상태 전환을 누락하면 렌더링이 작동하지 않습니다. 디버그 레이어를 활성화하여 경고를 확인하세요.
동기화 오류
Fence 대기 없이 Command Allocator를 리셋하면 GPU가 사용 중인 메모리를 덮어써 크래시가 발생합니다. 항상 WaitForNextFrameResources()를 호출하세요.
멀티 뷰포트 문제
멀티 뷰포트 기능을 활성화한 경우, 각 뷰포트마다 별도의 Swap Chain이 생성됩니다. ImGui가 이를 자동으로 관리하지만, 리소스 정리 시 주의해야 합니다.
성능 최적화 팁
Vertex/Index 버퍼 크기 조정
ImGui는 필요에 따라 버퍼를 확장하지만, 초기 크기를 적절히 설정하면 재할당을 줄일 수 있습니다.
// ImGui 백엔드 내부에서 버퍼 크기 힌트 제공
// (imgui_impl_dx12.cpp 수정 필요)
const int VERTEX_BUFFER_SIZE = 50000;
const int INDEX_BUFFER_SIZE = 100000;
C++
복사
불필요한 드로우 콜 제거
복잡한 UI는 수백 개의 드로우 콜을 생성할 수 있습니다. 보이지 않는 창을 숨기거나, UI 업데이트 빈도를 줄이세요.
// 매 프레임 업데이트하지 않기
static float cachedValue = 0.0f;
static int frameCount = 0;
if (++frameCount % 60 == 0) // 1초마다 업데이트
{
cachedValue = CalculateExpensiveValue();
}
ImGui::Text("Value: %.2f", cachedValue);
C++
복사
텍스처 압축
폰트 아틀라스나 아이콘 텍스처를 압축 포맷으로 저장하여 메모리와 대역폭을 절약할 수 있습니다.
정리
ImGui를 DirectX 12에 통합하는 과정은 멀티프레임 파이프라이닝과 명시적 동기화의 훌륭한 학습 기회입니다. 핵심 개념을 정리하면 다음과 같습니다.
개념 | 핵심 내용 |
FrameContext | 각 프레임마다 독립적인 Command Allocator와 Fence Value를 유지하여 GPU와 CPU의 병렬 실행을 가능하게 합니다. |
멀티프레임 파이프라이닝 | 2-3개의 프레임을 순환하며 CPU와 GPU가 동시에 다른 프레임을 처리하여 전체 처리량을 극대화합니다. |
명시적 동기화 | Fence를 사용하여 GPU 작업 완료를 추적하고, 리소스 재사용 전에 명시적으로 대기합니다. |
리소스 배리어 | 백버퍼의 상태를 PRESENT와 RENDER_TARGET 간에 명시적으로 전환하여 GPU 메모리 일관성을 보장합니다. |
Descriptor Heap 관리 | ImGui가 사용할 SRV Descriptor Heap을 별도로 관리하고, 렌더링 전에 바인딩합니다. |
이러한 패턴을 이해하면 ImGui뿐만 아니라 다른 서드파티 라이브러리를 DirectX 12에 통합하는 작업도 훨씬 수월해집니다. 멀티프레임 구조와 명시적 동기화는 DirectX 12의 핵심 설계 철학이며, 이를 올바르게 구현하면 최대 성능을 달성할 수 있습니다.

