반응형

엔진에 대한 이해도를 더 높이기 위해 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]                                           └── 실행 ──┘

 


결과

반응형

+ Recent posts