엔진에 대한 이해도를 더 높이기 위해 AI의 힘을 빌려 실제 DirectX12를 구상하는 엔진을 만들어 보고자 한다.
CMake를 이용하여 sln의 의존성을 지우고 아래와 같이 여러 모듈들을 root로 묶어 실행하는 역할을 한다.
TP의 경우 ImGui라는 유명한 Ui 모듈을 이용하여 dx12와 win32를 연결한다.
https://github.com/ocornut/imgui
GitHub - ocornut/imgui: Dear ImGui: Bloat-free Graphical User interface for C++ with minimal dependencies
Dear ImGui: Bloat-free Graphical User interface for C++ with minimal dependencies - ocornut/imgui
github.com
BaekEngine/
├─ CMakeLists.txt 루트: C++20, /utf-8 /W4 /MP, UNICODE·NOMINMAX·WIN32_LEAN_AND_MEAN
├─ .editorconfig 모든 소스 UTF-8(BOM)
├─ ThirdParty/
│ ├─ CMakeLists.txt imgui 정적 라이브러리 타깃
│ ├─ d3dx12.h
│ └─ imgui/ (서브모듈, docking 브랜치)
├─ Engine/ (정적 lib) → d3d12, dxgi, dxguid, d3dcompiler, imgui 링크 (PUBLIC)
│ ├─ Core/ Window
│ ├─ RHI/ DxUtil, Device, SwapChain, DescriptorHeap
│ ├─ Renderer/ Renderer
│ └─ UI/ ImGuiLayer
├─ Editor/ (exe, WIN32) → Engine 링크, main.cpp
└─ Assets/ 실행 작업 디렉터리 (launch.vs.json currentDir)
위와 같이 BaekEngine이라는 최상위 디렉터리와 루트 CMakeLists를 통하여 아래 서브 디렉터리(모듈)들을 묶어준다.
의존성은 Editor -> UI -> Renderer -> RHI ->DxUtil 순으로 호출되어 시각화를 진행하며 Cor(Window)는 어디에도 의존하지 않는다. Window는 오로지 메시지만 전달하기 때문에, ImGui와도 연결되지 않는다.
Core / Window
Win32 창 생성, 메시지 처리, 리사이즈·최소화 상태를 관리합니다.
| 멤버 | 의미 |
| mHwnd | 창 핸들, WM_NCCREATE에서 바로 설정(CreateWindowEx 반환 전에도 유효) |
| mWidth, mHeight | 클라이언트 크기, WM_SIZE의 LOWORD / HIWORD |
| mReSized | 크기 변경 발생 플래그, 렌더라가 소비 |
| mMinimized | SIZE_MINIMMIZED 여부 |
| mHook | std::function<bool(HWND, UINT, WPARAM, LPARAM)>, true면 메시지 처리 완료 |
| 함수 | 동작 |
| Create(title, w, h) | 클랙스 등록 -> AdjustWindowRect -> CreateWindowExW(..., this) |
| PumpMessages() | PeekMessage 루프, WM_QUIT이면 false |
| ConsumResize() | mResized를 읽고 false로 초기화(non-const) |
| IsMinimized() | mMinimized 반환 |
| SetMessageHook() | 외부(ImGui) 입력 처리기 연결 |
RHI / DxUtil
Comptr 별칭을 둡니다.
ThrowIfFailed(x) 매크로는 실패 시 식, 파일, 줄, HRESULT를 담아 std::runtime_error를 던집니다.main의 catch문에서 MessageBox로 표시합니다.
RHI / Device
D3D12 디바이스, 커맨드 큐, 펜스, DXGI 팩토리를 관리합니다.
| 멤버 | 의미 |
| mFactory | IDXGIFactory6 (디버그 시 DXGI_CREATE_FACTORY_DEBUG) |
| mDevice | ID3D12Device, Feature Level11_0 |
| mQueue | DIRECT 커맨드 큐1개 |
| mFence, mFenceValue | 단조 증가 펜스 |
| mFenceEvent | 대기용 이벤트 (한 번 만들어서 재사용) |
| 함수 | 동작 |
| Init(debug) | 디버그 레이어 -> 팩토리 -> 고성능 GPU 우선 어댑터 선택(소프트웨어 제외, 실패 시 WARP) -> InfoQueue 설정 -> 큐-펜스 생성 |
| Signal() | ++mFenceValue를 큐에 시그널하고 그 값을 반환 |
| WaitForValue(v) | 완료 값이 v미만이면 CPU대기 |
| Flush() | WaitForValue(Signal()), GPU 완전 유휴까지 대기 |
| Shutdown() | Flush 후 이벤트 핸들 해제 |
디버그 필드에서 SetBreakOnSeverity(ERROR/CORRUPTION)을 켭니다. 잘못된 API 호출이 그 주렝서 바로 디버거를 멈추게 합니다.
RHI / SwapChain
| 값 | 설정 |
| BufferCount | 2 |
| Format | DXGI_FORMAT_R8G8B8A8_UNORM |
| SwapEffect | FLIP_DISCARD |
| Scaling | STRETCH |
| Alt+Enter | 비활성화(DXGI_MWA_NO_ALT_ENTER) |
| RTV힙 | 자체 보유, 버퍼 수만큼 |
| 함수 | 동작 |
| Init() | CreateSwapChainForHwnd -> IDXGISwapChain3로 변환 -> RTV 생성 |
| Resize() | 버퍼 참조 해제 -> ResizeBuffers -> RTV 재생성(호출 전 Flush 필수) |
| Present(vsync) | Present(vsync ? 1 : 0, 0) |
| CuffentBuffer() / CurrentRtv() | GetCurrentBackBuferIndex() 기준 |
백 버퍼 인덱스를 직접 증가시키지 않고 스왑체인에 물어봅니다. FLIP 모델에서 인덱스가 어긋날 일이 없습니다.
RHI / DescriptorHeap
| 멤버 | 의미 |
| mHeap | 디스크립터 힙 |
| mCpuStart, mGpuStart | 시작 핸들(GPU는 shader_visible일 때만) |
| mIncrement, mCapacity | 슬롯 크기, 총 개수 |
| mShaderVisible | GPU 핸들 계산 여부 |
| mFreeList | 빈 슬롯 인덱스 스택 (0번부터 나오도록 역순 초기화) |
DescriptorHandle { cpu, gpu, index }, IsValid()는 index가 UINT32_MAX가 아닌지 봅니다.
| 함수 | 동작 |
| Allocate() | free list에서 pop, 비었으면 예외 |
| Free(handle) / Free(cpu) | 인덱스를 free list에 반환(cpu 버전은 ImGui 콜백용) |
| At(index) | 인덱스를 CPU/GPU 핸들로 변환 |
Renderer가 CBV/SRV/UAV, shader-visible, 1024칸 하나를 엔진 공용으로 소유합니다. ImGui 폰트가 첫 사용자입니다.
Renderer
프레임 링, 커맨드 리스트, 공용 SRV힙을 관리하고 한 프레임의 사직과 끝을 담당합니다.
| 멤버 | 의미 |
| FrameCount | 2 (CPU가 GPU보다 앞서갈 수 있는 프레임 수) |
| mDevice, mSwapChain, mSrvHeap | 소유 |
| mAllocator[2] | 프레임별 커맨드 할당자 |
| mFrameFence[2] | 해당 슬롯이 마지막으로 제출한 펜스 값 |
| mFrameIndex | 현재 슬롯 |
| mCmd | 그래픽 커맨드 리스트 1개(재사용) |
| 함수 | 동작 |
| Init(window) | 디바이스(디버그는 _DEBUG일 때) -> 스왑체인 -> SRV힙 -> 할당자-커맨드 리스트 |
| BeginFrame(clear) | 1. 리사이즈 플래그 소비 시 Flush + 스왑체인 리사이즈 2. 슬롯 펜스 대기 3. 할당자-리스트 Reset 4. PRESENT -> RT 5. 클리어 6. RT 바인딩 |
| EndFrame() | 1. RT -> PRESENT 2. Close-Execute 3. Present(vsync) 4. Signal 값을 슬롯에 저장 5. 슬롯 전진 |
| WaitIdle() | Flush() |
| GetDevice(), SrvHeap(), CommandList() |
외부(ImGui등) 접근용 |
로직을 할당자와 펜스 값 배열로 단순화했습니다. BeginFrame과 EndFrame 사이에 에디터나 씬 드로우 끼워 넣는 구조입니다.
UI / ImGuiLayer
| 함수 | 동작 |
| Init(window, renderer) | 컨택스트 생성, NavEnableKeyboard + DockingEnable, 다크 스타일 -> Win32 백엔드 + 메시지 훅 연결 -> DX12 백엔드 초기화 |
| BeginFrame() | DX12-Win32 NewFrame -> ImGui::NewFrame |
| EndFrame() | InGui::Render -> SRV 힙 바인딩 -> RenderDrawData |
| Shutdown() | DX12 -> Win32 -> 컨택스트 순서로 해제 |
DX12 백엔드에 넘기는 값.
| 필드 | 값 |
| NumFramesInFlight | Renderer::FrameCount(2) |
| RTVFormat / DSVFormat | 스왑체인 포맷 / UNKNOWN |
| SrvDescriptorHeap + UserData | 엔진 공용 힙 |
| SrvDescriptorAllocFn / FreeFn | 엔진 DescriptorHeap의 Allocate / Free로 연결 |
ImGui 1.92+ 는 텍스처 GPU 핸들을 텍스처ID로 사용합니다.
그래서 할당자가 GPU 핸들을 올바르게 채워야 합니다.
Editor / main.cpp
Window.Create
→ Renderer.Init
→ ImGuiLayer.Init
→ 루프: PumpMessages → (최소화면 Sleep)
→ BeginFrame(clear) → imgui.BeginFrame → DrawEditorUI → imgui.EndFrame(cmd) → EndFrame
→ WaitIdle → ImGui.Shutdown → Renderer.Shutdown
- DrawEditorUI: DockSpaceOverViewport(PassthruCentralNode), Stats 패널(FPS, 프레임 ms, 데모 토글)
- 클리어 색: (0.10, 0.10, 0.15)
- 예외는 MB_TOPMOST MessageBox로 표시
종료 순서가 중요한 이유: GPU가 ImGui 리소스를 다 쓴 뒤에 해제해야 디버그 레이어 에러가 안납니다.
한 프레임의 흐름
[CPU] WaitForValue(slot) → Reset → PRESENT→RT → Clear → OMSetRT
→ ImGui 드로우 기록 → RT→PRESENT → Close → Execute → Present → Signal(slot)
[GPU] └── 실행 ──┘
결과

'DirectX12 > 엔진 만들어보기' 카테고리의 다른 글
| 라이팅에서 PBR 머티리얼까지 (0) | 2026.10.06 |
|---|---|
| 그리드, Scene/Entity, Hierarchy·Inspector, 기즈모 (0) | 2026.10.05 |
| 서브모듈 정리, 카메라 (0) | 2026.10.03 |
| Viewport 만들기 (0) | 2026.10.02 |
