How to Implement a Custom Dear ImGui Backend for Non-Standard Rendering APIs
To implement a custom Dear ImGui backend, you must initialize backend data in ImGuiIO, create shaders and buffers matching the ImDrawVert layout, then iterate ImDrawData commands in your render loop to translate ImGui's draw lists into your engine's API calls.
Dear ImGui separates input handling (platform backends) from GPU rendering (renderer backends). According to the ocornut/imgui source code, a custom renderer backend bridges this gap by consuming ImDrawData structures and outputting commands compatible with your proprietary engine, software rasterizer, or specialized graphics API.
Understanding the Dear ImGui Backend Architecture
The architecture centers on the ImGuiIO structure defined in imgui.h. This structure exposes three critical fields for renderer backends:
BackendRendererUserData– Stores a pointer to your backend-specific data struct (shaders, buffers, textures).BackendRendererName– A string identifier shown in the About window (e.g.,"MyEngine").BackendFlags– Capability flags such asImGuiBackendFlags_RendererHasVtxOffset(for 64k+ vertex meshes) orImGuiBackendFlags_RendererHasTextures.
Your backend must implement a consistent function naming convention: ImGui_Impl<Backend>_Init(), ImGui_Impl<Backend>_NewFrame(), ImGui_Impl<Backend>_RenderDrawData(), and ImGui_Impl<Backend>_Shutdown().
Core Backend Functions and API Structure
As implemented in backends/imgui_impl_opengl3.cpp, every renderer backend provides four essential functions:
| Function | Purpose | Reference Implementation |
|---|---|---|
ImGui_Impl<Backend>_Init |
Allocate BackendRendererUserData, compile shaders, create vertex/index buffers, upload font texture |
imgui_impl_opengl3.cpp (line 216) |
ImGui_Impl<Backend>_NewFrame |
Update per-frame state like viewport size and projection matrices | imgui_impl_opengl3.cpp (line 402) |
ImGui_Impl<Backend>_RenderDrawData |
Iterate ImDrawData and issue draw calls |
imgui_impl_opengl3.cpp (line 462) |
ImGui_Impl<Backend>_Shutdown |
Free GPU resources and clear BackendRendererUserData |
imgui_impl_opengl3.cpp (line 229) |
Step-by-Step Implementation Guide
Define Backend-Specific Data Structures
Create a struct to hold your engine's resources, then store it in ImGuiIO:
struct MyBackendData {
MyEngine::Shader* shader = nullptr;
MyEngine::Buffer* vtxBuf = nullptr;
MyEngine::Buffer* idxBuf = nullptr;
MyEngine::Texture* fontTex = nullptr;
// Additional engine-specific state
};
static MyBackendData* GetBackendData() {
return (MyBackendData*)ImGui::GetIO().BackendRendererUserData;
}
Initialize the Backend and Create Resources
In your Init function, perform the following steps as shown in the OpenGL3 reference:
- Allocate backend data and assign it to
io.BackendRendererUserData. - Create shaders that match ImGui's vertex format (
ImDrawVert). - Allocate dynamic buffers large enough for maximum draw-list size.
- Generate and upload the font texture using
ImGui::GetIO().Fonts->GetTexDataAsRGBA32(). - Set capability flags:
ImGuiIO& io = ImGui::GetIO();
io.BackendRendererUserData = (void*)IM_NEW(MyBackendData)();
io.BackendRendererName = "MyEngine";
io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset |
ImGuiBackendFlags_RendererHasTextures;
// Upload font texture
unsigned char* pixels; int w, h;
io.Fonts->GetTexDataAsRGBA32(&pixels, &w, &h);
// ... upload to GPU and store handle ...
io.Fonts->TexID = (ImTextureID)fontTextureHandle;
Handle Per-Frame Preparation
The NewFrame function updates transient state. For basic implementations, this may only require updating the projection matrix to map io.DisplayPos and io.DisplaySize to your engine's clip space:
void ImGui_ImplMyEngine_NewFrame() {
// Update uniform buffer with orthographic projection
// based on ImGui::GetIO().DisplaySize
}
For multi-viewport support (as demonstrated in imgui_impl_vulkan.cpp), expose render state via ImGui::GetPlatformIO().Renderer_RenderState.
Render Draw Data Commands
The RenderDrawData function in imgui_impl_opengl3.cpp demonstrates the iteration pattern:
void ImGui_ImplMyEngine_RenderDrawData(ImDrawData* draw_data) {
if (draw_data->TotalVtxCount == 0) return;
MyBackendData* bd = GetBackendData();
// Upload geometry to GPU
// Map bd->vtxBuf and bd->idxBuf, copy draw_data vertex/index data
// Setup pipeline state
MyEngine::BindShader(bd->shader);
MyEngine::SetBlendMode(BlendMode_Alpha);
// Iterate command lists
int vtxOffset = 0, idxOffset = 0;
for (int n = 0; n < draw_data->CmdListsCount; n++) {
const ImDrawList* cmd_list = draw_data->CmdLists[n];
for (int cmd_i = 0; cmd_i < cmd_list->CmdBuffer.Size; cmd_i++) {
const ImDrawCmd* pcmd = &cmd_list->CmdBuffer[cmd_i];
// Handle user callbacks (e.g., DrawCallback_ResetRenderState)
if (pcmd->UserCallback) {
pcmd->UserCallback(cmd_list, pcmd);
} else {
// Bind texture (pcmd->TextureId is your engine's texture handle)
MyEngine::BindTexture((MyEngine::Texture*)pcmd->TextureId);
// Set scissor rectangle from pcmd->ClipRect
MyEngine::SetScissor(
(int)pcmd->ClipRect.x,
(int)pcmd->ClipRect.y,
(int)(pcmd->ClipRect.z - pcmd->ClipRect.x),
(int)(pcmd->ClipRect.w - pcmd->ClipRect.y)
);
// Issue draw call with offsets
MyEngine::DrawIndexed(
pcmd->ElemCount,
idxOffset + pcmd->IdxOffset,
vtxOffset + pcmd->VtxOffset
);
}
}
vtxOffset += cmd_list->VtxBuffer.Size;
idxOffset += cmd_list->IdxBuffer.Size;
}
}
Cleanup and Shutdown
Follow the pattern in imgui_impl_opengl3.cpp lines 229-244:
void ImGui_ImplMyEngine_Shutdown() {
MyBackendData* bd = GetBackendData();
if (bd) {
MyEngine::DestroyShader(bd->shader);
MyEngine::DestroyBuffer(bd->vtxBuf);
MyEngine::DestroyBuffer(bd->idxBuf);
MyEngine::DestroyTexture(bd->fontTex);
IM_DELETE(bd);
}
ImGui::GetIO().BackendRendererUserData = nullptr;
}
Example Implementation Skeleton
Here is a complete header and implementation skeleton following the patterns in ocornut/imgui:
// my_imgui_backend.h
#pragma once
#include "imgui.h"
bool ImGui_ImplMyEngine_Init();
void ImGui_ImplMyEngine_NewFrame();
void ImGui_ImplMyEngine_RenderDrawData(ImDrawData* draw_data);
void ImGui_ImplMyEngine_Shutdown();
// my_imgui_backend.cpp
#include "my_imgui_backend.h"
#include "my_engine.h"
struct MyBackendData {
MyEngine::Shader* shader = nullptr;
MyEngine::Buffer* vtxBuf = nullptr;
MyEngine::Buffer* idxBuf = nullptr;
MyEngine::Texture* fontTex = nullptr;
};
static MyBackendData* GetBackendData() {
return (MyBackendData*)ImGui::GetIO().BackendRendererUserData;
}
bool ImGui_ImplMyEngine_Init() {
ImGuiIO& io = ImGui::GetIO();
MyBackendData* bd = IM_NEW(MyBackendData)();
io.BackendRendererUserData = (void*)bd;
io.BackendRendererName = "MyEngine";
// Create shader matching ImDrawVert layout
bd->shader = MyEngine::CreateShader(vertex_glsl, fragment_glsl);
// Create dynamic buffers (2MB initial size)
bd->vtxBuf = MyEngine::CreateBuffer(2*1024*1024, BufferUsage_DynamicVertex);
bd->idxBuf = MyEngine::CreateBuffer(2*1024*1024, BufferUsage_DynamicIndex);
// Upload font atlas
unsigned char* pixels; int w, h;
io.Fonts->GetTexDataAsRGBA32(&pixels, &w, &h);
bd->fontTex = MyEngine::CreateTexture(w, h, pixels);
io.Fonts->TexID = (ImTextureID)bd->fontTex;
io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset |
ImGuiBackendFlags_RendererHasTextures;
return true;
}
void ImGui_ImplMyEngine_NewFrame() {
// Update projection matrix based on io.DisplaySize
}
void ImGui_ImplMyEngine_RenderDrawData(ImDrawData* draw_data) {
// Implementation as shown in Step 4 above
}
void ImGui_ImplMyEngine_Shutdown() {
MyBackendData* bd = GetBackendData();
if (!bd) return;
MyEngine::DestroyShader(bd->shader);
MyEngine::DestroyBuffer(bd->vtxBuf);
MyEngine::DestroyBuffer(bd->idxBuf);
MyEngine::DestroyTexture(bd->fontTex);
IM_DELETE(bd);
ImGui::GetIO().BackendRendererUserData = nullptr;
}
Summary
- Dear ImGui backends separate into platform (input) and renderer (GPU) concerns, with the renderer translating
ImDrawDatainto your API. - Four functions form the backend contract:
Init,NewFrame,RenderDrawData, andShutdown, following the naming conventionImGui_Impl<Backend>_*. - Store state in a custom struct pointed to by
ImGuiIO::BackendRendererUserData, and declare capabilities viaBackendFlags. - The render loop iterates
ImDrawCmdstructures, binding textures and setting scissor rectangles before issuing indexed draw calls withVtxOffsetandIdxOffset. - Reference implementations in
imgui_impl_opengl3.cppandimgui_impl_vulkan.cppprovide battle-tested patterns for buffer management and multi-viewport support.
Frequently Asked Questions
What vertex format does Dear ImGui require for custom backends?
Dear ImGui uses the ImDrawVert structure defined in imgui.h, which consists of a 2D position (ImVec2 pos), texture coordinates (ImVec2 uv), and a 32-bit color (ImU32 col). Your vertex shader must accept this exact layout to correctly render ImGui draw lists.
How do I support drawing more than 64,000 vertices in a single draw call?
Set the ImGuiBackendFlags_RendererHasVtxOffset flag in ImGuiIO::BackendFlags during initialization. This tells Dear ImGui that your backend supports 32-bit index offsets via the ImDrawCmd::VtxOffset field, allowing meshes larger than 16-bit index limits.
Can I use Dear ImGui with a software rasterizer instead of a GPU?
Yes. A custom Dear ImGui backend can target any rendering API, including software rasterizers. You would implement RenderDrawData to scan-convert the triangles in ImDrawData using your CPU rasterizer, sampling from the texture handle provided in ImDrawCmd::TextureId.
Where should I handle DPI scaling and framebuffer size changes?
Handle DPI and framebuffer changes in your platform backend (e.g., imgui_impl_win32.cpp or imgui_impl_glfw.cpp), then update ImGuiIO::DisplayFramebufferScale and ImGuiIO::DisplaySize before calling NewFrame. The renderer backend consumes these values to calculate proper projection matrices and scissor rectangles.
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 →