Let's Make an Engine: ImGui

2022-12-27

In the previous entry we converted our app to a GUI window and left our next step open. It’s time to take a big step forward, a forcing function that will set up our next couple of moves. We’re going to integrate our first external library (not counting the OS SDK).

Dear ImGui

Initially released in only 2014 Dear ImGui has taken the game development industry by storm. It is one of the most widely and rapidly adopted middleware libraries I’ve ever seen. It has a stellar reputation for integrating near seamlessly into any engine.

ImGui’s primary purpose is for quickly making integrated development tools. It is possible to build your game’s full user interface using ImGui but there are better options available in traditional retained mode GUI packages. ImGui excels as allowing programmers to build simple, functional UIs for testing, debugging and analysis.

For our purposes it will immediately give us something functional that we can use to help build and test future features. It also has requirements that will force us to implement a rendering backend which is something we want to do soon anyways.

Integration

The first thing we need to do is get the Dear ImGui package. In order to do this we’ll use a git submodule pointed directly at the project’s master github branch as that is the recommended version.1 We’re going to make an external folder in the root of our repo just so we don’t need a separate copy for every entry in the series. In a finished engine externals would be included with the engine.

Backends

ImGui needs two backends, one for the platform/OS and one for the renderer. Since we previously decided to target the Win32 platform we can simply integrate ImGui’s built-in Win32 backend into our existing Window abstraction.

We do not have a renderer yet. Building a renderer is certainly worthy of its own post, indeed it’s worth of a whole series on its own. But we need to at least decide on an underlying technology to start with now. Worthy candidates are OpenGL, DirectX 11, Vulkan and DirectX 12. I’m going to discard old non-explicit APIs out of hand. You can still build fine games with those APIs but it’s not interesting to me anymore and the future is definitely in the modern DX12/Vk explicit paradigms.

Vulkan and DX12 are mostly interchangeable. Vulkan is a bit more consistent and flexible, in my opinion largely due to being standardized later and being able to learn from DX12. However Vulkan is also considerably more verbose and that flexibility isn’t always necessary. You can’t ship Vulkan on Xbox anyways so you’re always going to need a DX12 backend so in my mind it’s perfectly reasonable to start there. We can eventually add support for a Vulkan backend but we’re going with DirectX 12 to start.

Build Scripts

We’re going to add a new external folder inside of engine to contain our cmake build scripts for our external dependencies.

engine/external/CMakeLists.txt
# IMGUI
cmake_path(SET IMGUI_ROOT NORMALIZE "${CMAKE_SOURCE_DIR}/../external/imgui")

file(GLOB IMGUI_SOURCE CONFIGURE_DEPENDS "${IMGUI_ROOT}/*.cpp")
list(APPEND IMGUI_SOURCE "${IMGUI_ROOT}/backends/imgui_impl_win32.cpp" )
list(APPEND IMGUI_SOURCE "${IMGUI_ROOT}/backends/imgui_impl_dx12.cpp" )

add_library(imgui STATIC ${IMGUI_SOURCE})
target_include_directories(imgui PUBLIC "${IMGUI_ROOT}" "${IMGUI_ROOT}/backends")

ImGui itself doesn’t contain a build system so we make a very simple one here. We create a static library target and all all of the cpp files to it along with the backends for win32 and dx12.

Then we’re going to add a simple interface library in order to use DirectX 12 in our project.

# DX12
add_library(dx12 INTERFACE)
target_link_libraries(dx12 INTERFACE d3d12.lib dxgi.lib d3dcompiler.lib)

Then we simply need to update our engine build script to depend on the imgui and dx12 libraries.

engine/CMakeLists.txt
... 

add_subdirectory(external)
target_link_libraries(engine PUBLIC imgui dx12)

And through the magic of transitive target properties we don’t need to update our application build script at all.

Win32 Backend Integration

We need to extend our event processing to allow ImGui the chance to handle events. This allows for interaction like mouse clicks and text input.

extern "C++" IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);

LRESULT CALLBACK WndProc(HWND aHwnd, UINT aMsg, WPARAM aWparam, LPARAM aLparam) {
    if (ImGui_ImplWin32_WndProcHandler(aHwnd, aMsg, aWparam, aLparam)) {
        return true;
    }
    ...

The ImGui_ImplWin32_WndProcHandler function is defined in the imgui_impl_win32.cpp file but not declared in the header, so we need to explicitly forward declare it ourselves in order to call it. This is by design of the builtin Win32 backend for ImGui. Note that we need to explicitly give the forward declaration “C++” language linkage to detach it from the module otherwise on MSVC the symbol will get mangled to include our module name and fail to link. Alternatively we could move the declaration to a separate header and include that in the global module fragment.

Next we need to initialize ImGui for our window when it is created.

std::unique_ptr<Window> MakeWindow(WindowConfig aConfig) {
    ...

    ImGui_ImplWin32_Init(hwnd); // <----
    return window;
}

And correspondingly we need to shut it down when our window is destroyed.

case WM_NCDESTROY:
    PostQuitMessage(0);
    ImGui_ImplWin32_Shutdown(); // <----
    _hwnd = nullptr;
    SetWindowLongPtr(_hwnd, GWLP_USERDATA, LONG_PTR(0));
    break;
}

And finally we need to let ImGui know when we’re starting a new frame. We don’t have a convenient place to do this right now, so we will hack it into PumpEvents which is not ideal but I don’t want to get bogged down creating abstraction layers just yet.

bool PumpEvents() {
    MSG msg{};
    while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) {
        ... 
    }
    ImGui_ImplWin32_NewFrame(); // <----
    return true;
}

Using ImGui in our App

To avoid doing too much in one engine iteration we’re going to force some temporary code into the application layer. This is going to make a mess like when you have to take everything out of a closet before you can organize it properly. But doing so allows us to make rapid progress and we can refactor towards a clean design rather than having to invent one apriori.

Thus we are going to integrate the ImGui Win32/DX12 example program2 into our application nearly verbatim, only swapping out parts we already have implementations for.

DirectX 12 Setup

Directly in our main.cpp file we’re going to implement the basic DX12 framework from the ImGui example.

#include <imgui_impl_dx12.h>
#include <d3d12.h>
#include <dxgi1_4.h>

#ifdef _DEBUG
#define DX12_ENABLE_DEBUG_LAYER
#endif

#ifdef DX12_ENABLE_DEBUG_LAYER
#include <dxgidebug.h>
#pragma comment(lib, "dxguid.lib")
#endif

using namespace LetsMakeEngine;

struct FrameContext {
    ID3D12CommandAllocator* CommandAllocator;
    UINT64                  FenceValue;
};

// Data
static int const                    NUM_FRAMES_IN_FLIGHT = 3;
static FrameContext                 g_frameContext[NUM_FRAMES_IN_FLIGHT] = {};
static UINT                         g_frameIndex = 0;

static int const                    NUM_BACK_BUFFERS = 3;
static ID3D12Device*                g_pd3dDevice = nullptr;
static ID3D12DescriptorHeap*        g_pd3dRtvDescHeap = nullptr;
static ID3D12DescriptorHeap*        g_pd3dSrvDescHeap = nullptr;
static ID3D12CommandQueue*          g_pd3dCommandQueue = nullptr;
static ID3D12GraphicsCommandList*   g_pd3dCommandList = nullptr;
static ID3D12Fence*                 g_fence = nullptr;
static HANDLE                       g_fenceEvent = nullptr;
static UINT64                       g_fenceLastSignaledValue = 0;
static IDXGISwapChain3*             g_pSwapChain = nullptr;
static HANDLE                       g_hSwapChainWaitableObject = nullptr;
static ID3D12Resource*              g_mainRenderTargetResource[NUM_BACK_BUFFERS] = {};
static D3D12_CPU_DESCRIPTOR_HANDLE  g_mainRenderTargetDescriptor[NUM_BACK_BUFFERS] = {};

// Forward declarations of helper functions
bool CreateDeviceD3D(HWND hWnd);
void CleanupDeviceD3D();
void CreateRenderTarget();
void CleanupRenderTarget();
void WaitForLastSubmittedFrame();
FrameContext* WaitForNextFrameResources();

The implementations of these functions are unchanged from the example program2 and therefor I won’t include their full source in the article.

Main Updates

Initialization

We need to setup ImGui and while it is certainly appealing to make an engine or application wrapper to handle the details we’re going to avoid the extra abstraction for now and just place the required logic directly into the application layer.

int main() {
    // Setup Dear ImGui
    IMGUI_CHECKVERSION();      // <---- 1
    ImGui::CreateContext();
    ImGui::StyleColorsDark();

    // Initialize systems
    auto window = MakeWindow(WindowConfig{
        .title = "Let's Make an Engine",
        .width = 1366,
        .height = 768,
        .mode = WindowMode::windowed,
        .onResize = OnResize  // <---- 2
    });

    auto hwnd = window->NativeHandle(); // <---- 3

The 1st code callout simply initializes and configures ImGui. The 3rd code callout is an extension to our Window interface to allow access to the underlying native Win32 window handle. It is frequently necessary to have escape hatches like this to allow your engine clients to clear

The second code callout needs some elaboration. When using a rendering API it is no long sufficient to just allow the OS to handle it and not care ourselves. We need to update a graphics device resources in response. However our graphics code currently lives in the application layer and our event handler lives in the engine layer so we need a way to poke through. As always there are many ways to solve this. For example we could subclass window and make ProcessEvent virtual. However in this instance we’re going to just going to allow the application to bind a callback for this specific event.

Resize Event

using OnResizeHandler = std::function<bool(Window&, std::uint32_t, std::uint32_t)>;

struct WindowConfig {
    ... 
    OnResizeHandler     onResize = nullptr;
};

Then we’ll call into that user supplied function when we receive a resize message.

case WM_SIZE:
    if (!_inSizemove && aWparam != SIZE_MINIMIZED && _config.onResize) {
        if (_config.onResize(*this,
                static_cast<std::uint32_t>(LOWORD(aLparam)),
                static_cast<std::uint32_t>(HIWORD(aLparam)))) {
            return 0;
        }
    }
    break;
}

And we need to handle 2 more events in order to handle edge resizing without spamming our new resize handler.

case WM_ENTERSIZEMOVE:
    _inSizemove = true;
    break;
case WM_EXITSIZEMOVE:
    _inSizemove = false;
    if (_config.onResize) {
        RECT cr;
        GetClientRect(_hwnd, &cr);
        if (_config.onResize(*this,
                static_cast<std::uint32_t>(cr.right - cr.left),
                static_cast<std::uint32_t>(cr.bottom - cr.top))) {
            return 0;
        }
    }
    break;

And finally in our application layer we’ll handle the event.

bool OnResize([[maybe_unused]] Window& aWindow, std::uint32_t aWidth, std::uint32_t aHeight) {
    if (g_pd3dDevice != NULL) {
        WaitForLastSubmittedFrame();
        CleanupRenderTarget();
        HRESULT result = g_pSwapChain->ResizeBuffers(0, aWidth, aHeight, DXGI_FORMAT_UNKNOWN, DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT);
        assert(SUCCEEDED(result) && "Failed to resize swapchain.");
        CreateRenderTarget();
    }
    return true;
}

In the future as we move the renderer into the engine layer we can pull back on this.

Update Loop

The majority of the update loop is again directly from the ImGui example code so I won’t include all of it inline here. But I do want to show off the library API a little.

while (PumpEvents()) {
    // Start the Dear ImGui frame
    ImGui_ImplDX12_NewFrame();
    ImGui::NewFrame();

    ... 

    {
        static float f = 0.0f;
        static int counter = 0;

        ImGui::Begin("Hello, world!");                          // Create a window called "Hello, world!" and append into it.

        ImGui::Text("This is some useful text.");               // Display some text (you can use a format strings too)
        ImGui::Checkbox("Demo Window", &show_demo_window);      // Edit bools storing our window open/close state
        ImGui::Checkbox("Another Window", &show_another_window);

        ImGui::SliderFloat("float", &f, 0.0f, 1.0f);            // Edit 1 float using a slider from 0.0f to 1.0f
        ImGui::ColorEdit3("clear color", (float*)&clear_color); // Edit 3 floats representing a color

        if (ImGui::Button("Button")) {                          // Buttons return true when clicked (most widgets return true when edited/activated)
            counter++;
        }
        ImGui::SameLine();
        ImGui::Text("counter = %d", counter);

        ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / ImGui::GetIO().Framerate, ImGui::GetIO().Framerate);
        ImGui::End();
    }

    ...

The result is this rather pretty window.

ImGui

Wrapping Up

I have used the ImGui library for years but never had an occasion to try and integrate it into a codebase my self. And I must say that it has lived up to its reputation for being easy to integrate. This is one of the more pleasant and painless integrations I have ever done. The initial integration took only about an hour.

We have now a powerful development UI we can use in our engine evolution. But we have also introduced a lot of trouble for ourselves. Our ImGui integration is leaky and while we don’t need to abstract the entire library we should encapsulate the initialization and the NewFrame call would also be nice to move into the engine.

However, the much more pressing issue is the renderer that is spilled out all over our application code. And therefor we will move to address that shortly.


Series: Lets Make an Engine