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 as ImGuiBackendFlags_RendererHasVtxOffset (for 64k+ vertex meshes) or ImGuiBackendFlags_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:

  1. Allocate backend data and assign it to io.BackendRendererUserData.
  2. Create shaders that match ImGui's vertex format (ImDrawVert).
  3. Allocate dynamic buffers large enough for maximum draw-list size.
  4. Generate and upload the font texture using ImGui::GetIO().Fonts->GetTexDataAsRGBA32().
  5. 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 ImDrawData into your API.
  • Four functions form the backend contract: Init, NewFrame, RenderDrawData, and Shutdown, following the naming convention ImGui_Impl<Backend>_*.
  • Store state in a custom struct pointed to by ImGuiIO::BackendRendererUserData, and declare capabilities via BackendFlags.
  • The render loop iterates ImDrawCmd structures, binding textures and setting scissor rectangles before issuing indexed draw calls with VtxOffset and IdxOffset.
  • Reference implementations in imgui_impl_opengl3.cpp and imgui_impl_vulkan.cpp provide 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →