How to Create a Custom ImGui Renderer for a Proprietary Graphics API
ImGui separates core UI logic from backend rendering; you only need to implement four backend functions (Init, Shutdown, NewFrame, and RenderDrawData) to translate ImDrawData into your proprietary graphics API's command buffers.
The ocornut/imgui repository provides a modular immediate-mode GUI library where the core UI logic remains completely separate from graphics API-specific rendering code. To integrate ImGui with a proprietary or custom graphics engine, you must implement a backend that converts ImGui's draw lists into your native rendering commands. This architecture allows you to leverage ImGui's full widget library—including tables, plots, and docking—while maintaining complete control over the rendering pipeline.
Architecture Overview
ImGui's design strictly divides responsibilities between the core UI system and the backend renderer. According to the ocornut/imgui source code, the core handles state management, widget generation, and layout in files like imgui.cpp, imgui_draw.cpp, imgui_widgets.cpp, and imgui_tables.cpp. The backend, typically implemented in backends/imgui_impl_*.cpp files (such as imgui_impl_vulkan.cpp or imgui_impl_opengl3.cpp), translates ImGui's platform-agnostic draw data into native graphics API calls.
Each frame, the application calls two critical entry points that bridge these layers:
ImGui::NewFrame()– Called after the platform backend populatesImGuiIOwith input data (mouse, keyboard, clipboard).ImGui::Render()– Called after your UI code executes; this generates anImDrawDatastructure that your custom backend must render.
| Layer | Responsibility | Key Files |
|---|---|---|
| Core | UI state, widget generation, layout, draw list creation | imgui.cpp, imgui_draw.cpp, imgui_widgets.cpp |
| Backend | Translate ImDrawData to native commands, handle GPU resources |
backends/imgui_impl_*.cpp |
| Application | Window creation, device setup, per-frame orchestration | examples/example_*/main.cpp |
A custom renderer implements the same small function set exposed by existing backends:
bool MyBackend_Init(void* device, void* render_target);
void MyBackend_Shutdown();
void MyBackend_NewFrame(); // Optional, for per-frame state updates
void MyBackend_RenderDrawData(ImDrawData* draw_data);
Implementation Steps
1. Create the Backend Source File
Create a new source file in the backends/ directory, such as backends/imgui_impl_myapi.cpp. Include the core headers to access ImGui's public and internal APIs:
#include "imgui.h"
#include "imgui_internal.h"
2. Define Backend Data Structures
Define a static structure to hold your graphics API objects. As implemented in existing backends like imgui_impl_vulkan.cpp, store handles for the device, command queue, and font texture:
struct MyBackendData
{
void* Device = nullptr;
void* FontTexture = nullptr;
// Add API-specific handles here
};
static MyBackendData* GMyBackend = nullptr;
3. Initialize the Font Texture
During initialization, generate and upload ImGui's default font atlas. Retrieve the pixel data using io.Fonts->GetTexDataAsRGBA32, upload it to your GPU, and store the resulting texture identifier in io.Fonts->TexID:
bool MyBackend_Init(void* device)
{
GMyBackend = IM_NEW(MyBackendData);
GMyBackend->Device = device;
ImGuiIO& io = ImGui::GetIO();
unsigned char* pixels;
int width, height;
io.Fonts->GetTexDataAsRGBA32(&pixels, &width, &height);
// Upload to your proprietary API
GMyBackend->FontTexture = MyAPI_CreateTexture(device, pixels, width, height);
io.Fonts->TexID = (ImTextureID)GMyBackend->FontTexture;
return true;
}
4. Implement the Render Loop
The MyBackend_RenderDrawData function iterates over ImDrawData::CmdLists, where each ImDrawList contains vertex and index buffers. For each ImDrawCmd, bind the texture specified in cmd.TextureId, set the scissor rectangle using cmd.ClipRect, and issue the draw call:
void MyBackend_RenderDrawData(ImDrawData* draw_data)
{
if (draw_data->DisplaySize.x <= 0.0f || draw_data->DisplaySize.y <= 0.0f)
return;
void* cmd_buf = MyAPI_BeginCommandBuffer(GMyBackend->Device);
// Set viewport covering the full display size
MyAPI_SetViewport(cmd_buf, 0, 0,
(int)draw_data->DisplaySize.x,
(int)draw_data->DisplaySize.y);
for (int n = 0; n < draw_data->CmdListsCount; n++)
{
const ImDrawList* cmd_list = draw_data->CmdLists[n];
// Upload vertex/index data to GPU-accessible memory
MyAPI_UpdateVertexBuffer(GMyBackend->Device,
cmd_list->VtxBuffer.Data,
cmd_list->VtxBuffer.Size * sizeof(ImDrawVert));
MyAPI_UpdateIndexBuffer(GMyBackend->Device,
cmd_list->IdxBuffer.Data,
cmd_list->IdxBuffer.Size * sizeof(ImDrawIdx));
// Process each command
for (int cmd_i = 0; cmd_i < cmd_list->CmdBuffer.Size; cmd_i++)
{
const ImDrawCmd* pcmd = &cmd_list->CmdBuffer[cmd_i];
// Bind texture and set scissor rectangle
MyAPI_BindTexture(cmd_buf, (void*)pcmd->TextureId);
MyAPI_SetScissor(cmd_buf,
(int)pcmd->ClipRect.x,
(int)pcmd->ClipRect.y,
(int)(pcmd->ClipRect.z - pcmd->ClipRect.x),
(int)(pcmd->ClipRect.w - pcmd->ClipRect.y));
// Issue indexed draw call
MyAPI_DrawIndexed(cmd_buf, pcmd->ElemCount, pcmd->IdxOffset, pcmd->VtxOffset);
}
}
MyAPI_EndCommandBuffer(cmd_buf);
MyAPI_Submit(GMyBackend->Device, cmd_buf);
}
5. Handle Shutdown and Cleanup
Release all GPU resources when the application exits:
void MyBackend_Shutdown()
{
if (!GMyBackend) return;
MyAPI_DestroyTexture(GMyBackend->Device, GMyBackend->FontTexture);
IM_DELETE(GMyBackend);
GMyBackend = nullptr;
}
Integration Example
Wire your custom backend into the application's main loop. You can combine your custom renderer with existing platform backends (such as imgui_impl_win32.cpp or imgui_impl_glfw.cpp) that handle input and window events:
// Initialization
MyDevice* device = MyAPI_CreateDevice();
MyBackend_Init(device);
// Main loop
while (running)
{
// Platform backend updates ImGuiIO with input events
ImGui_ImplWin32_NewFrame(); // or your platform equivalent
MyBackend_NewFrame(); // Optional per-frame setup
ImGui::NewFrame();
// Your UI code here
ImGui::ShowDemoWindow();
ImGui::Render();
MyBackend_RenderDrawData(ImGui::GetDrawData());
}
Key Reference Files in the Repository
When implementing your backend, reference these specific files from the ocornut/imgui repository to understand the exact patterns used:
imgui.cpp– ContainsImGui::Render()which constructs the finalImDrawDatastructure.imgui_draw.cpp– Implements low-level draw list construction and vertex buffer management.backends/imgui_impl_vulkan.cpp– Reference forInit,RenderDrawDataimplementation, and command buffer submission patterns.backends/imgui_impl_opengl3.cpp– Demonstrates font texture creation viaImGui_ImplOpenGL3_CreateDeviceObjectsand texture ID handling.backends/imgui_impl_dx12.cpp– Shows command-list recording and resource barrier management.backends/imgui_impl_null.cpp– Minimal no-op implementation useful as a starting template.examples/example_win32_vulkan/main.cpp– Complete integration example showing how platform and renderer backends work together.
Summary
- ImGui's core is API-agnostic: The UI logic in
imgui.cppnever touches graphics API code; it only producesImDrawDatastructures. - Backend requirements are minimal: You only need to implement initialization, shutdown, optional per-frame setup, and the main
RenderDrawDatafunction. - Font atlas handling is mandatory: Your
Initfunction must create a texture fromio.Fonts->GetTexDataAsRGBA32and assign it toio.Fonts->TexID. - Command translation follows a fixed pattern: Iterate
CmdLists, upload vertex/index buffers, then for eachImDrawCmdbind the texture, set the scissor fromClipRect, and draw. - Platform independence: You can mix your custom renderer with existing platform backends like
imgui_impl_win32.cpporimgui_impl_glfw.cppto avoid reimplementing input handling.
Frequently Asked Questions
Do I need to modify ImGui's core files to support a custom graphics API?
No. According to the ocornut/imgui architecture, you should never modify imgui.cpp or other core files. The backend interface is designed specifically to allow external implementations. Create new files in the backends/ directory following the naming convention imgui_impl_[platform].cpp and imgui_impl_[renderer].cpp.
How do I handle the font atlas texture in a custom renderer?
Call io.Fonts->GetTexDataAsRGBA32(&pixels, &width, &height) during initialization to retrieve the raw RGBA32 pixel data. Upload this to your GPU using your proprietary API's texture creation functions, then store the resulting handle in io.Fonts->TexID. This identifier is passed back to you in ImDrawCmd::TextureId during rendering.
Can I use existing platform backends with my custom renderer?
Yes. Platform backends (such as imgui_impl_win32.cpp, imgui_impl_glfw.cpp, or imgui_impl_sdl2.cpp) only populate the ImGuiIO structure with input data and window events. They are completely separate from renderer backends. You can combine ImGui_ImplWin32_NewFrame() with your custom MyBackend_RenderDrawData() without conflicts.
What is the performance cost of the backend abstraction layer?
The backend abstraction has negligible overhead. The core ImGui library generates vertex and index buffers once per frame in ImGui::Render(). Your backend performs a straightforward translation of these buffers into native API calls. The existing backends in the repository demonstrate that this approach achieves high performance even with complex UIs, as the heavy lifting (clipping, culling, batching) happens in the core, not the backend.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →