Search
Duplicate

imgui DirectX 12 에서 초기화하기

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에게 현재 화면 크기 전달
→ 뷰포트 크기 맞추기 위해 필요. 안 하면 마우스 좌표 등이 이상하게 동작.
ImGui::Render();
C++
복사
ImGui 내부 DrawCommand 리스트 생성
→ 이게 호출돼야 ImGui::GetDrawData()에서 유효한 데이터가 나옴.
auto* graphicDevice = ya::graphics::GetDevice(); auto* frameCtx = graphicDevice->WaitForNextFrameResources(); UINT backBufferIdx = graphicDevice->GetSwapChain()->GetCurrentBackBufferIndex();
C++
복사
이번 프레임용 CommandAllocator / Fence 확보
현재 SwapChain 버퍼 index 가져오기
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++
복사
백버퍼를 GPU 렌더 타겟 상태로 전환 준비
DX12에선 명시적인 리소스 상태 전환이 필요하므로 ResourceBarrier로 선언해둠.
아직은 적용하지 않았고, 뒤에서 commandList->ResourceBarrier()로 실제 적용.
auto commandList = graphicDevice->GetCommandList();
C++
복사
현재 프레임용 CommandList 확보
외부에서 이미 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++
복사
ImGui 배경색 설정
→ 현재는 ClearRenderTargetView 호출은 생략되어 있어 효과는 없음
→ 커스텀 배경색 클리어 하려면 아래 주석 해제 필요
auto rtvHeap = graphicDevice->GetRTVHeap(); CD3DX12_CPU_DESCRIPTOR_HANDLE handle = graphicDevice->GetRnderTargetDescriptorHandle(backBufferIdx); auto srvHeap = graphicDevice->GetSrvHeap();
C++
복사
렌더 타겟/텍스처 관련 DescriptorHeap 핸들 준비
//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++
복사
ImGui가 사용할 텍스처 정보가 있는 SRV 힙 바인딩
ImTextureID 기반으로 ImGui 내부가 해당 힙을 참조
ImGui_ImplDX12_RenderDrawData(ImGui::GetDrawData(), commandList.Get());
C++
복사
ImGui의 draw data를 DX12 명령으로 변환하여 CommandList에 기록
barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_RENDER_TARGET; barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_PRESENT; commandList->ResourceBarrier(1, &barrier);
C++
복사
다시 화면 표시 상태로 리소스 전환
commandList->Close();
C++
복사
GPU 제출 준비 완료. ExecuteCommandLists() 가능 상태로 전환

정리 요약

파트
설명
io.DisplaySize
뷰포트/화면 크기 반영
ImGui::Render()
내부 draw list 구성
commandList 설정
외부에서 초기화되어 있다고 가정
SetDescriptorHeaps()
ImGui가 사용할 텍스처 힙 등록
RenderDrawData()
draw list → GPU 명령 변환
ResourceBarrier()
렌더 타겟 → 프레젠트 상태 전환
Close()
명령 리스트 제출 준비 완료

코드 한 줄씩 분석

UINT64 fenceValue = mFenceLastSignalValue + 1;
C++
복사
다음 프레임의 FenceValue를 하나 증가시킴
→ Fence는 시점 정보이므로 단조 증가하는 값으로 프레임 구분
mCommandQueue->Signal(mFence.Get(), fenceValue);
C++
복사
GPU 커맨드 큐에 "이 시점이 끝나면 펜스를 fenceValue로 설정하라"는 신호
→ GPU가 해당 프레임의 명령을 완료하면, 펜스값이 fenceValue로 올라감
mFenceLastSignalValue = fenceValue;
C++
복사
현재까지 GPU에 보낸 마지막 FenceValue 기록
→ 추후 동기화할 때 사용
FrameContext* frameCtx = &mFrameContext[mFrameIndex % 2]; frameCtx->FenceValue = fenceValue;
C++
복사
현재 프레임 컨텍스트(예: 프레임별 CommandAllocator 등)에 해당 FenceValue를 저장
→ 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번 배송 끝났나?"를 체크해서 리소스 재사용 여부 판단