ImGui DirectX 12 예제에서 사용된 멀티프레임 구조 초기화에 대해 설명드리겠습니다. 핵심은 프레임 별 리소스 분리와 GPU-CPU 동기화를 통해 프레임 파이프라인을 효율화하는 데 있습니다.
구조 핵심 요약
1. FrameContext
struct FrameContext
{
ID3D12CommandAllocator* CommandAllocator;
UINT64 FenceValue;
};
C++
복사
•
각 프레임에 독립된 CommandAllocator와 Fence 값을 보유.
•
APP_NUM_FRAMES_IN_FLIGHT 만큼 배열로 선언되어 GPU가 렌더링 중인 프레임과 CPU가 준비하는 프레임이 겹치지 않도록 구성됨.
왜 멀티프레임 구조가 필요한가?
이유 1. D3D12는 CommandAllocator 재사용 시점이 엄격
•
CommandAllocator는 GPU가 해당 프레임의 작업을 완료한 이후에만 재사용 가능합니다.
•
하나만 쓰면 CPU가 GPU를 매 프레임 기다려야 하므로 비효율적입니다.
•
→ 여러 개의 FrameContext를 순환하면서 GPU 작업이 끝난 프레임만 재사용함.
이유 2. ImGui도 내부적으로 FrameData를 유지
•
ImGui는 한 프레임에 그리기 위한 draw command 들을 모아서 RenderDrawData()에 넘기는데,
•
Viewport, Docking, FontAtlas, PlatformWindows 등도 프레임 사이에 상태를 공유하거나 유지합니다.
•
그래서 싱글프레임 상태가 아님, 프레임 파이프라인이 1개일 경우 내부 동기화 문제 발생 가능.
Begin()
ImGui_ImplDX12_NewFrame();
ImGui_ImplWin32_NewFrame();
ImGui::NewFrame();
ImGuizmo::BeginFrame();
C++
복사
•
ImGui의 새 프레임을 준비하는 함수들 호출
•
ImGuizmo도 ImGui 위에서 동작하는 위젯이므로 이 시점에 BeginFrame() 필요
// IMGUI UI 들 렌더링
End()
함수 설명 (주석 기준 코드 분석)
ImGuiIO& io = ImGui::GetIO();
io.DisplaySize = ImVec2((float)application.GetWindow().GetWidth(),
(float)application.GetWindow().GetHeight());
C++
복사
→ 뷰포트 크기 맞추기 위해 필요. 안 하면 마우스 좌표 등이 이상하게 동작.
ImGui::Render();
C++
복사
→ 이게 호출돼야 ImGui::GetDrawData()에서 유효한 데이터가 나옴.
auto* graphicDevice = ya::graphics::GetDevice();
auto* frameCtx = graphicDevice->WaitForNextFrameResources();
UINT backBufferIdx = graphicDevice->GetSwapChain()->GetCurrentBackBufferIndex();
C++
복사
WaitForNextFrameResources() 안에서:
FenceValue 동기화 + Allocator Reset + CommandList Reset 등이 진행되었을 가능성 있음
D3D12_RESOURCE_BARRIER barrier = {};
barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
barrier.Transition.pResource = graphicDevice->GetRenderTargetResource(backBufferIdx).Get();
barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_PRESENT;
barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_RENDER_TARGET;
C++
복사
DX12에선 명시적인 리소스 상태 전환이 필요하므로 ResourceBarrier로 선언해둠.
아직은 적용하지 않았고, 뒤에서 commandList->ResourceBarrier()로 실제 적용.
auto commandList = graphicDevice->GetCommandList();
C++
복사
외부에서 이미 Reset, Allocator 연결, 초기화되어 있다고 가정
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 };
C++
복사
→ 현재는 ClearRenderTargetView 호출은 생략되어 있어 효과는 없음
→ 커스텀 배경색 클리어 하려면 아래 주석 해제 필요
auto rtvHeap = graphicDevice->GetRTVHeap();
CD3DX12_CPU_DESCRIPTOR_HANDLE handle = graphicDevice->GetRnderTargetDescriptorHandle(backBufferIdx);
auto srvHeap = graphicDevice->GetSrvHeap();
C++
복사
//commandList->ClearRenderTargetView(handle, clear_color_with_alpha, 0, nullptr);
//commandList->OMSetRenderTargets(1, &handle, FALSE, nullptr);
C++
복사
❗️현재는 주석 처리
→ 배경 지우기, RT 바인딩이 이 함수에서 담당하지 않는 구조라는 뜻
→ 예: GBuffer 또는 MainPass에서 이미 처리한 경우 ImGui는 UI만 얹기
commandList->SetDescriptorHeaps(1, srvHeap.GetAddressOf());
C++
복사
→ ImTextureID 기반으로 ImGui 내부가 해당 힙을 참조
ImGui_ImplDX12_RenderDrawData(ImGui::GetDrawData(), commandList.Get());
C++
복사
barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_RENDER_TARGET;
barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_PRESENT;
commandList->ResourceBarrier(1, &barrier);
C++
복사
commandList->Close();
C++
복사
정리 요약
파트 | 설명 |
io.DisplaySize | 뷰포트/화면 크기 반영 |
ImGui::Render() | 내부 draw list 구성 |
commandList 설정 | 외부에서 초기화되어 있다고 가정 |
SetDescriptorHeaps() | ImGui가 사용할 텍스처 힙 등록 |
RenderDrawData() | draw list → GPU 명령 변환 |
ResourceBarrier() | 렌더 타겟 → 프레젠트 상태 전환 |
Close() | 명령 리스트 제출 준비 완료 |
코드 한 줄씩 분석
UINT64 fenceValue = mFenceLastSignalValue + 1;
C++
복사
→ Fence는 시점 정보이므로 단조 증가하는 값으로 프레임 구분
mCommandQueue->Signal(mFence.Get(), fenceValue);
C++
복사
→ GPU가 해당 프레임의 명령을 완료하면, 펜스값이 fenceValue로 올라감
mFenceLastSignalValue = fenceValue;
C++
복사
→ 추후 동기화할 때 사용
FrameContext* frameCtx = &mFrameContext[mFrameIndex % 2];
frameCtx->FenceValue = fenceValue;
C++
복사
→ GPU가 이 프레임을 끝냈는지 추적할 수 있게 됨
mFrameIndex = mSwapChain->GetCurrentBackBufferIndex();
C++
복사
→ 2~3프레임 분기 구조의 Ping-Pong용 인덱스 관리
왜 이렇게 해야 하나요?
이유 | 설명 |
DX12는 자동 동기화가 없다 | 프레임 단위로 수동 동기화를 해줘야 한다 |
FenceValue를 추적해야 한다 | GPU가 어떤 프레임까지 끝났는지 알아야 리소스를 재사용할 수 있음 |
FrameContext마다 FenceValue 필요 | 프레임별로 사용한 Allocator 등은 GPU 작업 완료 전엔 재사용 금지 |
비유: 택배 트래킹
•
Signal(fence, 5) → 택배 회사에 "5번째 박스 배송 끝나면 연락 줘"
•
FenceValue = 5 → 박스 번호
•
FrameContext.FenceValue = 5 → 이 프레임은 5번 박스임
→ 다음 프레임에서 GetCompletedValue()로 "5번 배송 끝났나?"를 체크해서 리소스 재사용 여부 판단