삼각형 위에 ImGui 창을 함께 그려 봅시다
삼각형이 나오는 DX12 프로그램에 ImGui 초기화 코드를 붙였는데 UI가 안 보이거나, Scene 화면 안에 에디터 전체가 다시 그려질 수 있습니다. UI도 GPU의 Draw이므로 어느 Command List에, 어느 RenderTarget을 대상으로, 언제 기록하는지를 맞춰야 합니다.
이번에는 텍스트 창 하나에서 시작해 현재 엔진의 ImguiEditor::Begin/End 경로를 따라가겠습니다. Game과 Scene은 먼저 이미지를 만들고, ImGui는 그 이미지와 버튼·문자를 마지막에 백버퍼로 합성합니다. GPU 완료 대기는 엔진 프레임 시작에서 관리하고 UI 중간에 명령 메모리를 다시 Reset하지 않습니다.
ImGui DirectX 12 통합과 멀티프레임 렌더링
ImGui는 매 프레임 UI를 함수 호출로 구성합니다. 내부에서는 창·입력·도킹 상태를 유지하고 DX12 백엔드는 Vertex/Index 버퍼와 텍스처를 관리합니다. 즉시 모드라고 상태가 없거나 GPU 동기화가 필요 없는 것은 아닙니다.
1. 초기화: 플랫폼과 렌더러를 나눈다
Win32 백엔드는 창 입력과 플랫폼 정보를, DX12 백엔드는 UI의 GPU 렌더링을 담당합니다. 저장소에 포함된 ImGui는 1.92.0 WIP 표기의 버전이며, 아래 코드는 이 저장소의 ImGui_ImplDX12_InitInfo 인터페이스를 기준으로 합니다. 다른 버전의 구형 6개 인자 초기화 예제와 섞지 않습니다.
폰트도 GPU가 읽는 텍스처입니다
ImGui의 글자는 폰트 atlas 텍스처를 샘플해 그립니다. 따라서 Device만 전달해서 끝나는 것이 아니라, 그 텍스처를 가리킬 SRV 슬롯을 어떻게 얻고 돌려줄지도 알려 줘야 합니다. Scene/Game Image도 같은 shader-visible heap에 유효한 descriptor를 가지고 있어야 합니다.
ImGui_ImplDX12_InitInfo init_info = {};
init_info.Device = device->GetID3D12Device().Get();
init_info.CommandQueue = device->GetCommandQueue().Get();
init_info.NumFramesInFlight = 2; //double buuffering
init_info.RTVFormat = DXGI_FORMAT_R8G8B8A8_UNORM;
init_info.DSVFormat = DXGI_FORMAT_UNKNOWN;
// Allocating SRV descriptors (for textures) is up to the application, so we provide callbacks.
init_info.SrvDescriptorHeap = device->GetSrvHeap().Get();
init_info.SrvDescriptorAllocFn =
[] (ImGui_ImplDX12_InitInfo*,
D3D12_CPU_DESCRIPTOR_HANDLE* out_cpu_handle,
D3D12_GPU_DESCRIPTOR_HANDLE* out_gpu_handle)
{
const auto handle = ya::graphics::GetDevice()->AllocateDescriptor(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
*out_cpu_handle = handle.Cpu;
*out_gpu_handle = handle.Gpu;
};
init_info.SrvDescriptorFreeFn =
[] (ImGui_ImplDX12_InitInfo*,
D3D12_CPU_DESCRIPTOR_HANDLE cpu_handle,
D3D12_GPU_DESCRIPTOR_HANDLE gpu_handle)
{
ya::graphics::GetDevice()->RetireResource(nullptr, { cpu_handle, gpu_handle });
};
ImGui_ImplDX12_Init(&init_info);
C++
복사
Alloc callback은 한 슬롯의 CPU/GPU handle을 함께 반환합니다. CPU handle은 backend가 SRV 내용을 써 넣는 자리이고 GPU handle은 UI Draw가 그 텍스처를 읽을 때 사용할 주소입니다. Free callback은 즉시 free list에 넣는 대신 엔진의 RetireResource로 넘깁니다. 이미 기록한 UI 명령이 그 슬롯을 GPU에서 읽을 수 있기 때문입니다.
예를 들어 폰트가 슬롯 0을 사용 중인데 Scene RT를 같은 슬롯 0에 덮어쓰면 글자가 Scene 이미지의 일부를 샘플할 수 있습니다. 각 이미지의 슬롯을 분리하고, 해제 후에도 마지막 GPU 사용이 끝난 다음 재사용하는 것이 초기화 callback의 실제 목적입니다.
device는 이 함수 앞에서 얻은 GraphicDevice_DX12 포인터입니다. 컨텍스트 생성, ConfigFlags, 스타일, Win32 초기화도 앞부분에서 수행합니다. NumFramesInFlight=2로 백엔드의 순환 버퍼 수를 정하고 엔진의 같은 shader-visible SRV Heap을 전달합니다. 할당 callback은 새로운 슬롯을, 해제 callback은 Fence에 따른 지연 반환을 연결합니다. 폰트 descriptor 하나만 놓고 Scene/Game 이미지까지 같은 슬롯에 덮어쓰면 안 됩니다.
현재 초기화 함수는 backend Init의 bool 반환을 검사하지 않습니다. 견고한 실패 처리에는 각 Init 결과를 확인하고 이미 생성한 컨텍스트·백엔드를 역순으로 정리하는 경로가 필요합니다. 위는 실제 코드 발췌이며 이 검수에서 엔진 코드를 변경한 것은 아닙니다.
2. FrameContext와 백버퍼의 관계
프레임 슬롯에는 Command Allocator와 마지막 사용 Fence 값을 둡니다. 해당 GPU 작업이 끝나기 전에는 Allocator와 같은 업로드 구간을 재사용하지 않습니다. 이번 엔진은 2개 슬롯을 사용합니다.
SwapChain의 BufferCount와 CPU가 선행할 FrameContext 수는 개념적으로 다릅니다. 서로 다른 개수로도 설계할 수 있으므로 2와 3이 다르다는 이유만으로 항상 오류인 것은 아닙니다. 다만 이 엔진처럼 같은 인덱스를 사용하는 코드에서는 배열·매핑과 ImGui 순환 리소스의 재사용 조건을 함께 맞춰야 합니다. 여러 프레임이 항상 2배 성능을 만드는 것도 아닙니다.
3. Begin: 이번 프레임의 UI 구성 시작
void ImguiEditor::Begin()
{
// Start the Dear ImGui frame
ImGui_ImplDX12_NewFrame();
ImGui_ImplWin32_NewFrame();
ImGui::NewFrame();
ImGuizmo::BeginFrame();
}
C++
복사
Begin은 UI 프레임의 입력·레이아웃 계산을 시작합니다. 이 시점에는 버튼과 창을 어떤 모습으로 만들지 함수 호출로 선언할 준비가 된 것이며, GPU에 제출한 상태는 아닙니다. ImGuizmo도 같은 UI 프레임 안에 선과 도구를 추가하므로 여기서 프레임을 시작합니다.
DX12 NewFrame은 필요한 backend 객체를 준비합니다. 실제 UI Vertex/Index 데이터 업로드와 Draw 기록은 RenderDrawData에서 수행합니다. Win32 NewFrame은 플랫폼 정보를 갱신하며 키 메시지 자체는 Win32 WndProc의 backend handler로 전달합니다.
// Begin과 End 사이에 매 프레임 호출하는 UI 예시.
if (ImGui::Begin("Debug Info"))
ImGui::Text("FPS: %.1f", ImGui::GetIO().Framerate);
ImGui::End();
C++
복사
NewFrame 이후에 Begin("Debug Info")와 Text를 매 프레임 호출해야 창이 그려집니다. 이 Begin의 반환값은 내용을 그릴 수 있는 상태인지 나타내며 false여도 End는 호출합니다. 예를 들어 창을 접었을 때 내용은 생략해도 Begin/End의 구조는 유지합니다.
비싼 통계 계산은 별도로 캐시할 수 있지만 Text나 Image 선언까지 몇 프레임마다 생략하면 UI 자체가 사라질 수 있습니다. 클릭 횟수처럼 프레임 사이에 유지할 값은 멤버나 static 변수로 보관하고 매 프레임 초기화하지 않습니다.
4. End: Scene RT에서 백버퍼로 돌아와 UI 합성
UI 명령을 백버퍼에 기록하고 목록을 닫습니다
SceneWindow가 마지막으로 카메라 화면을 자기 RT에 그렸다면 현재 출력도 Scene RT입니다. End는 먼저 ImGui의 DrawData를 마무리하고, 출력을 백버퍼로 복원한 뒤 그 DrawData를 같은 열린 Command List에 추가합니다. 아래 코드는 현재 구현의 순서를 보여 줍니다.
void ImguiEditor::End()
{
ImGuiIO& io = ImGui::GetIO();
io.DisplaySize = ImVec2((float)application.GetWindow().GetWidth()
, (float)application.GetWindow().GetHeight());
ImGui::Render();
ya::graphics::GraphicDevice_DX12* graphicDevice = ya::graphics::GetDevice();
Microsoft::WRL::ComPtr<ID3D12GraphicsCommandList> commandList = graphicDevice->GetCommandList();
// SceneWindow rendered offscreen last. Restore the main RTV without
// clearing it again, and leave depth unbound for the ImGui PSO.
graphicDevice->BindFrameBuffer(false, false);
// Descriptor heap must be set before rendering ImGui
Microsoft::WRL::ComPtr<ID3D12DescriptorHeap> srvHeap = graphicDevice->GetSrvHeap();
commandList->SetDescriptorHeaps(1, srvHeap.GetAddressOf());
ImGui_ImplDX12_RenderDrawData(ImGui::GetDrawData(), commandList.Get());
// Transition RT → PRESENT.
// Application::Render() already transitioned PRESENT → RT using mFrameIndex,
// so we use the same index here to match.
UINT backBufferIdx = graphicDevice->GetFrameIndex();
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_RENDER_TARGET;
barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_PRESENT;
commandList->ResourceBarrier(1, &barrier);
commandList->Close();
}
C++
복사
BindFrameBuffer(false,false)의 첫 false는 다시 Clear하지 않겠다는 뜻이고 두 번째는 깊이 타깃을 붙이지 않겠다는 뜻입니다. ImGui PSO의 DSVFormat=UNKNOWN과 맞춥니다. SetDescriptorHeaps에는 InitInfo에 전달한 것과 같은 heap을 사용합니다. 다른 heap을 바인딩하고 이전 heap의 GPU handle을 Image에 주면 올바른 이미지를 읽지 못합니다.
마지막 RT→PRESENT Barrier와 Close는 UI Draw까지 끝난 뒤입니다. Game 렌더링이 먼저 List를 닫아 버리면 UI를 뒤에 추가할 수 없습니다. 반대로 End 안에서 Allocator를 Reset하면 앞서 기록한 Game/Scene 명령의 저장 공간까지 재사용하게 됩니다. 그래서 프레임 시작의 Reset과 UI 끝의 Close를 한 번씩만 수행합니다.
SceneWindow가 마지막으로 오프스크린 RT에 그렸으므로 BindFrameBuffer(false, false)로 백버퍼를 다시 바인딩합니다. 여기서 clear를 반복하면 앞선 결과가 사라지고, Scene RT를 그대로 두면 ImGui가 잘못된 텍스처에 그려집니다. ImGui PSO에 맞게 DSV는 연결하지 않습니다.
ImGui::Render는 UI draw data를 마무리하고 ImGui_ImplDX12_RenderDrawData가 열린 Command List에 기록합니다. End에서 다시 Allocator를 Reset하거나 WaitForNextFrameResources를 호출하지 않습니다. 마지막 RT→PRESENT barrier와 Close 뒤에는 같은 목록에 더 기록하지 않습니다. 현재 Close의 HRESULT도 검사하도록 개선할 여지가 있습니다.
5. 실제 프레임 전체 순서
flowchart TD
W[재사용할 슬롯 Fence 대기] --> A[Application Run: Game 렌더링]
A --> U[Editor Run: Begin · UI 구성 · Scene 렌더링]
U --> E[ImguiEditor End: 백버퍼 복구 · UI Draw · Close]
E --> X[메인 Command List 제출]
X --> P[추가 플랫폼 창 제출]
P --> S[메인 Present]
S --> F[최종 Fence Signal]
F --> O[EndOfFrame: Scene 이벤트 적용]Mermaid
복사
추가 플랫폼 창은 Scene/Game 탭을 OS 창 밖으로 꺼낼 때 생깁니다. 메인 창의 Draw만 끝났다고 전체 프레임의 GPU 사용이 끝나는 것은 아닙니다. 추가 창도 같은 RT의 SRV를 읽을 수 있으므로 플랫폼 창 제출 뒤에 찍은 최종 Fence까지 해당 리소스가 살아 있어야 합니다.
플랫폼 창은 도킹 탭을 메인 창 밖으로 꺼냈을 때 만들어지는 OS 창입니다. Docking과 Multi-Viewport는 관련은 있지만 서로 다른 옵션입니다. 추가 창도 엔진 Queue를 사용하므로 최종 Fence는 그 제출까지 포함해야 합니다. 메인 목록 Close와 Queue 제출을 UI 함수와 엔진 함수 양쪽에서 중복 실행하지 않습니다.
종료는 GPU 사용 완료 대기 → ImGui DX12/Win32 backend 종료 → 컨텍스트 종료 → 엔진 리소스 정리 순서입니다. ComPtr만으로 GPU 사용 완료가 보장되지는 않습니다.
6. 텍스트 창에서 Scene 이미지까지 확인합니다
먼저 Debug Info 창의 글자가 보이는지 확인합니다. 이것으로 폰트 SRV·UI PSO·DrawData 기록·백버퍼 출력 경로를 확인할 수 있습니다. 다음에 Scene RT를 Image로 넣고, 마지막으로 탭을 별도 OS 창으로 꺼내 봅니다. 처음부터 모든 기능을 동시에 붙이는 것보다 어느 연결에서 문제가 생겼는지 구별하기 쉽습니다.
결과에 따라 확인할 연결
UI가 Scene 안에 반복해서 그려지면 출력 RTV를, 이미지가 엉뚱하면 GPU handle·현재 Heap·descriptor 수명을 확인합니다. 리사이즈에서만 깨지면 이전 RT와 descriptor의 지연 해제를 확인합니다. io.DisplaySize는 플랫폼 backend가 제공하는 클라이언트 크기와 일관돼야 하며, 고 DPI 처리는 끝에서 값 하나만 덮어쓴다고 모두 해결되지 않습니다.
성능 최적화는 실제 병목을 측정한 뒤 합니다. 비싼 값의 계산은 시간 기준으로 캐시할 수 있지만 UI 선언은 매 프레임 필요하며, 60프레임마다 실행하는 것은 항상 1초마다 실행한다는 뜻이 아닙니다. 폰트 압축이나 backend 버퍼 수정은 기본 통합의 필수 과정이 아닙니다.
동기화 원리:
Dx12 Frame, imgui rendering 동기화
Dx12 Frame, imgui rendering 동기화RT·descriptor와 현재 화면:
DX12 Texture·RenderTarget 구현과 Scene / Game 뷰 연결
DX12 Texture·RenderTarget 구현과 Scene / Game 뷰 연결최종 합성·PSO·수명 관리:
DX12 렌더링 모드 복구와 ImGui 화면 합성 완성
DX12 렌더링 모드 복구와 ImGui 화면 합성 완성

