Files
QuaternionEngine/docs/ImGuiSystem.md
2025-12-17 01:43:13 +09:00

7.5 KiB

ImGui System: Immediate-Mode UI Integration

Manages Dear ImGui lifecycle, event processing, and rendering within the Vulkan engine. Provides DPI-aware font scaling and callback-based UI composition.

Components

  • ImGuiSystem (src/core/ui/imgui_system.h/.cpp)

    • Initializes ImGui context with Vulkan backend.
    • Processes SDL events for ImGui input.
    • Manages draw callback registration.
    • Handles DPI scaling and font rebuilding.
  • ImGuiPass (src/render/passes/imgui_pass.h/.cpp)

    • RenderGraph pass that records ImGui draw commands.
    • Renders to swapchain image using dynamic rendering.
  • engine_ui.cpp

    • Built-in debug UI widgets (stats, render graph, texture streaming, etc.).
    • Example of using the draw callback system.

ImGuiSystem API

Initialization:

void init(EngineContext *context);
void cleanup();

Frame Lifecycle:

void begin_frame();  // NewFrame + invoke draw callbacks
void end_frame();    // ImGui::Render()

Event Processing:

void process_event(const SDL_Event &event);

Draw Callbacks:

void add_draw_callback(DrawCallback callback);
void clear_draw_callbacks();

// DrawCallback type
using DrawCallback = std::function<void()>;

Input Capture Queries:

bool want_capture_mouse() const;
bool want_capture_keyboard() const;

Swapchain Events:

void on_swapchain_recreated();  // Update image count after resize

Usage Examples

Basic Setup (Engine Internal):

// In VulkanEngine::init()
_imgui_system.init(&_context);

// Register debug UI callback
_imgui_system.add_draw_callback([this]() {
    vk_engine_draw_debug_ui(this);
});

Event Processing:

// In event loop
for (const SDL_Event& event : events)
{
    _imgui_system.process_event(event);
}

Frame Integration:

// Start of frame (after input processing)
_imgui_system.begin_frame();

// ... game update, scene rendering ...

// End of frame (before RenderGraph execution)
_imgui_system.end_frame();

Custom UI Callback:

void Game::init()
{
    engine.imgui_system().add_draw_callback([this]() {
        draw_game_ui();
    });
}

void Game::draw_game_ui()
{
    if (ImGui::Begin("Game Stats"))
    {
        ImGui::Text("Score: %d", _score);
        ImGui::Text("Health: %.0f%%", _health * 100.0f);

        if (ImGui::Button("Pause"))
        {
            toggle_pause();
        }
    }
    ImGui::End();
}

Respecting Input Capture:

void Game::update()
{
    // Don't process game input when ImGui wants it
    if (!engine.imgui_system().want_capture_mouse())
    {
        handle_mouse_input();
    }

    if (!engine.imgui_system().want_capture_keyboard())
    {
        handle_keyboard_input();
    }
}

DPI Scaling

The system automatically handles HiDPI displays:

  1. DPI Detection: Computed from swapchain extent vs window size ratio.
  2. Font Scaling: Base font size (16px) scaled by DPI factor.
  3. Global Scale: FontGlobalScale set to 1/DPI for proper sizing.
  4. Dynamic Updates: Fonts rebuilt when DPI changes (e.g., monitor switch).

DPI scale range: 0.5x to 4.0x (clamped for stability).

Vulkan Integration

ImGui is initialized with:

  • Dynamic Rendering: No render pass objects, uses VK_KHR_dynamic_rendering.
  • Dedicated Descriptor Pool: Separate pool with generous limits for ImGui textures.
  • Swapchain Format: Renders directly to swapchain image format.
ImGui_ImplVulkan_InitInfo init_info{};
init_info.Instance = device->instance();
init_info.PhysicalDevice = device->physicalDevice();
init_info.Device = device->device();
init_info.QueueFamily = device->graphicsQueueFamily();
init_info.Queue = device->graphicsQueue();
init_info.DescriptorPool = _imgui_pool;
init_info.MinImageCount = swapchain_image_count;
init_info.ImageCount = swapchain_image_count;
init_info.UseDynamicRendering = true;
init_info.PipelineRenderingCreateInfo.colorAttachmentCount = 1;
init_info.PipelineRenderingCreateInfo.pColorAttachmentFormats = &swapchain_format;
init_info.MSAASamples = VK_SAMPLE_COUNT_1_BIT;

RenderGraph Integration

ImGui rendering is handled by ImGuiPass:

// In RenderGraph build
_imgui_pass.register_graph(graph, swapchain_image_handle);

// Pass executes after all other rendering
// Renders ImGui draw data to swapchain image

The pass:

  1. Begins dynamic rendering on swapchain image.
  2. Calls ImGui_ImplVulkan_RenderDrawData().
  3. Ends rendering.

Built-in Debug UI

The engine provides comprehensive debug widgets in engine_ui.cpp:

Window Tab:

  • Monitor selection and fullscreen modes.
  • HiDPI status and size information.
  • GPU information display.

Stats Tab:

  • Frame time and FPS.
  • Draw call and triangle counts.
  • Memory usage statistics.

Scene Tab:

  • GLTF instance spawning.
  • Primitive mesh spawning.
  • Point light editor.
  • Object transform manipulation (ImGuizmo).

Render Graph Tab:

  • Pass list with toggle controls.
  • Resource tracking visualization.
  • Barrier inspection.

Texture Streaming Tab:

  • VRAM budget and usage.
  • Texture load queue status.
  • Cache statistics.

Shadows Tab:

  • Shadow mode selection.
  • Cascade visualization.
  • Ray-tracing hybrid controls.

Post Processing Tab:

  • Tonemapping settings.
  • Bloom controls.
  • FXAA parameters.
  • SSR configuration.

Draw Callback Order

Callbacks are invoked in registration order during begin_frame():

void ImGuiSystem::begin_frame()
{
    ImGui_ImplVulkan_NewFrame();
    ImGui_ImplSDL2_NewFrame();
    ImGui::NewFrame();

    // Invoke all registered callbacks
    for (auto& cb : _draw_callbacks)
    {
        if (cb) cb();
    }
}

Register order-dependent callbacks carefully:

// Engine debug UI first
imgui.add_draw_callback([]{ draw_engine_ui(); });

// Game UI on top
imgui.add_draw_callback([]{ draw_game_ui(); });

// Editor overlays last
imgui.add_draw_callback([]{ draw_editor_overlays(); });

ImGuizmo Integration

The engine integrates ImGuizmo for 3D gizmo manipulation:

#include "ImGuizmo.h"

void draw_object_gizmo(const glm::mat4& view, const glm::mat4& proj,
                       glm::mat4& object_transform)
{
    ImGuizmo::SetOrthographic(false);
    ImGuizmo::SetDrawlist();

    ImGuiIO& io = ImGui::GetIO();
    ImGuizmo::SetRect(0, 0, io.DisplaySize.x, io.DisplaySize.y);

    static ImGuizmo::OPERATION op = ImGuizmo::TRANSLATE;
    static ImGuizmo::MODE mode = ImGuizmo::WORLD;

    ImGuizmo::Manipulate(
        glm::value_ptr(view),
        glm::value_ptr(proj),
        op,
        mode,
        glm::value_ptr(object_transform));
}

Tips

  • Always check want_capture_mouse() before processing game mouse input.
  • Use want_capture_keyboard() before processing game keyboard input.
  • Register draw callbacks during initialization, not every frame.
  • Call on_swapchain_recreated() after window resize/mode change.
  • The descriptor pool is sized for 1000 sets of each type — sufficient for most debug UIs.
  • For production games, consider conditionally compiling out debug UI.
  • ImGui windows are persistent between frames — state is preserved automatically.

Frame Flow

  1. Event Processing: process_event() for each SDL event.
  2. Begin Frame: begin_frame() starts new ImGui frame and invokes callbacks.
  3. UI Building: All ImGui::* calls happen inside draw callbacks.
  4. End Frame: end_frame() calls ImGui::Render() to finalize draw data.
  5. RenderGraph: ImGuiPass executes, recording draw commands to GPU.
  6. Present: Swapchain presents the final image with ImGui overlay.