How to Create a Custom Renderer Backend for Dear ImGui: A Complete Implementation Guide

To create a custom Dear ImGui renderer backend, implement the four-function API (Init, NewFrame, RenderDrawData, Shutdown) that translates ImDrawData into your engine's graphics API calls while storing persistent state in ImGuiIO::BackendRendererUserData.

Dear ImGui separates platform concerns (input, clipboard, OS integration) from renderer concerns (GPU draw calls). A custom rendering backend plugs into the renderer side of the ocornut/imgui repository and is responsible for converting ImGui's draw-lists into the API of your target engine, whether Vulkan, DirectX, Metal, or a custom software rasterizer.

Understanding the Renderer Backend Architecture

The core architecture defining backend behavior lives in imgui.h through the ImGuiIO structure. This structure acts as the contract between your application and Dear ImGui.

The ImGuiIO Structure and Backend Flags

Your backend must populate specific fields in ImGuiIO to register capabilities and store state:

  • BackendRendererUserData – Holds a pointer to your backend-specific data block (shaders, buffers, textures).
  • BackendRendererName – Identifies your backend in the About window and debugging tools.
  • BackendFlags – Declares supported features using bitflags like ImGuiBackendFlags_RendererHasVtxOffset for 64k+ vertex meshes or ImGuiBackendFlags_RendererHasTextures for dynamic texture updates.

The Four-Function API Contract

Every renderer backend implements a consistent public API prefixed with ImGui_Impl<Backend>:

  1. ImGui_Impl<Backend>_Init – Allocate backend data, create shaders, vertex/index buffers, and set BackendRendererUserData. See the reference implementation in [imgui_impl_opengl3.cpp](https://github.com/ocornut/imgui/blob/master/backends/imgui_impl_opengl3.cpp#L216).

  2. ImGui_Impl<Backend>_NewFrame – Update per-frame state such as viewport size and projection matrix. Reference: ImGui_ImplOpenGL3_NewFrame.

  3. ImGui_Impl<Backend>_RenderDrawData – Iterate over ImDrawData and issue rendering commands to your GPU API. Reference: ImGui_ImplOpenGL3_RenderDrawData.

  4. ImGui_Impl<Backend>_Shutdown – Free all GPU resources and clear BackendRendererUserData. Reference: ImGui_ImplOpenGL3_Shutdown.

Step-by-Step Implementation

Follow this structured approach to build your backend, mirroring patterns from the official reference implementations.

Step 1: Define Your Backend Data Structure

Create a struct to hold all renderer-specific state. Store a pointer to this struct in io.BackendRendererUserData, following the pattern used by ImGui_ImplOpenGL3_GetBackendData() in the OpenGL3 backend.

struct MyBackendData {
    MyEngine::Shader*  shader = nullptr;
    MyEngine::Buffer*  vtxBuf = nullptr;
    MyEngine::Buffer*  idxBuf = nullptr;
    MyEngine::Texture* fontTex = nullptr;
    // Add any other engine-specific state
};

Step 2: Initialize the Backend and Upload Resources

Your Init function must create GPU resources and configure ImGui's expectations:

  • Create shaders matching the ImDrawVert vertex format (position, UV, color).
  • Allocate dynamic buffers large enough for maximum draw-list size, or implement growth logic.
  • Generate the font texture using ImGui::GetIO().Fonts->GetTexDataAsRGBA32() and upload to GPU.
  • Set backend flags to declare capabilities:
    io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset |
                       ImGuiBackendFlags_RendererHasTextures;
    io.BackendRendererName = "MyEngine";

Step 3: Prepare Per-Frame State in NewFrame

In ImGui_Impl<Backend>_NewFrame, update the projection matrix to map ImGui's DisplayPos and DisplaySize to your engine's clip space. If supporting multi-viewport, expose render state via ImGui::GetPlatformIO().Renderer_RenderState as demonstrated in the Vulkan backend (imgui_impl_vulkan.cpp).

Step 4: Render Draw Lists and Commands

Call ImGui::GetDrawData() to obtain ImDrawData*, then iterate through command lists:

  1. Upload geometry – Copy ImDrawVert and ImDrawIdx data to your GPU buffers.
  2. Bind resources – Set shader, vertex buffer, and index buffer.
  3. Process commands – For each ImDrawCmd:
    • Bind texture using pcmd->TextureId (cast to your engine's handle type).
    • Set scissor rectangle using pcmd->ClipRect.
    • Issue draw call with pcmd->ElemCount, respecting pcmd->VtxOffset and pcmd->IdxOffset.
    • Handle pcmd->UserCallback for custom render-state resets (see DrawCallback_ResetRenderState handling in the OpenGL3 backend).

Step 5: Shutdown and Resource Cleanup

Your Shutdown function must:

  • Delete shaders, buffers, and textures.
  • Clear io.BackendRendererUserData to null.

Complete Code Example for a Custom Engine

This skeleton implements the four-function API for a hypothetical graphics engine, following patterns from imgui_impl_opengl3.cpp:

// my_imgui_backend.h
#pragma once
#include "imgui.h"

struct MyBackendData;

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"

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({
        .vertSource = vertex_glsl,
        .fragSource = fragment_glsl
    });

    // Create dynamic buffers (2MB each)
    bd->vtxBuf = MyEngine::CreateBuffer({
        .size = 2*1024*1024,
        .usage = DynamicVertex
    });
    bd->idxBuf = MyEngine::CreateBuffer({
        .size = 2*1024*1024,
        .usage = DynamicIndex
    });

    // Upload font texture
    unsigned char* texPixels;
    int texW, texH;
    io.Fonts->GetTexDataAsRGBA32(&texPixels, &texW, &texH);
    bd->fontTex = MyEngine::CreateTexture({
        .width = texW,
        .height = texH,
        .data = texPixels
    });
    io.Fonts->TexID = (ImTextureID)bd->fontTex;

    // Declare supported features
    io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset |
                       ImGuiBackendFlags_RendererHasTextures;
    return true;
}

void ImGui_ImplMyEngine_NewFrame() {
    // Update projection matrix based on io.DisplaySize if needed
}

void ImGui_ImplMyEngine_RenderDrawData(ImDrawData* draw_data) {
    if (draw_data->TotalVtxCount == 0) return;
    
    MyBackendData* bd = GetBackendData();
    
    // Upload geometry data to GPU
    // ... map and copy ImDrawVert/ImDrawIdx data ...
    
    // Setup render state
    MyEngine::BindShader(bd->shader);
    MyEngine::BindVertexBuffer(bd->vtxBuf);
    MyEngine::BindIndexBuffer(bd->idxBuf);
    
    // Iterate draw lists
    int vtxOffset = 0, idxOffset = 0;
    for (int n = 0; n < draw_data->CmdListsCount; n++) {
        const ImDrawList* cmdList = draw_data->CmdLists[n];
        for (int cmd_i = 0; cmd_i < cmdList->CmdBuffer.Size; cmd_i++) {
            const ImDrawCmd* pcmd = &cmdList->CmdBuffer[cmd_i];
            
            // Bind texture and set scissor
            MyEngine::BindTexture((MyEngine::Texture*)pcmd->TextureId);
            ImVec4 clip = pcmd->ClipRect;
            MyEngine::SetScissor(
                (int)clip.x, (int)clip.y,
                (int)(clip.z - clip.x), (int)(clip.w - clip.y)
            );
            
            // Draw
            MyEngine::DrawIndexed(
                pcmd->ElemCount,
                idxOffset + pcmd->IdxOffset,
                vtxOffset + pcmd->VtxOffset
            );
        }
        vtxOffset += cmdList->VtxBuffer.Size;
        idxOffset += cmdList->IdxBuffer.Size;
    }
}

void ImGui_ImplMyEngine_Shutdown() {
    MyBackendData* bd = GetBackendData();
    MyEngine::DestroyShader(bd->shader);
    MyEngine::DestroyBuffer(bd->vtxBuf);
    MyEngine::DestroyBuffer(bd->idxBuf);
    MyEngine::DestroyTexture(bd->fontTex);
    IM_DELETE(bd);
    ImGui::GetIO().BackendRendererUserData = nullptr;
}

Key Implementation Details from Reference Backends

Study these official backend files in the ocornut/imgui repository to handle edge cases:

  • backends/imgui_impl_opengl3.cpp – Shows complete initialization, vertex layout handling, and DrawCallback_ResetRenderState processing.
  • backends/imgui_impl_vulkan.cpp – Demonstrates Renderer_RenderState management for multi-viewport support and complex resource binding.
  • imgui.h – Contains ImGuiIO structure definitions, backend flags, and ImGuiPlatformIO for advanced viewport handling.

Summary

  • Backend State – Store all renderer data in a custom struct pointed to by ImGuiIO::BackendRendererUserData to maintain isolation between ImGui and your engine.
  • Four Functions – Implement Init, NewFrame, RenderDrawData, and Shutdown to satisfy the backend contract.
  • Flag Declaration – Set ImGuiBackendFlags_RendererHasVtxOffset and ImGuiBackendFlags_RendererHasTextures in ImGuiIO::BackendFlags to enable advanced features.
  • Draw Data Iteration – Loop through ImDrawData → ImDrawList → ImDrawCmd, binding textures and setting scissor rectangles before each draw call.
  • Resource Management – Create shaders and buffers during Init, upload geometry each frame in RenderDrawData, and destroy all resources in Shutdown.

Frequently Asked Questions

What is the difference between a platform backend and a renderer backend?

A platform backend handles windowing, input events, clipboard access, and DPI scaling (e.g., imgui_impl_win32.cpp or imgui_impl_sdl.cpp). A renderer backend handles GPU draw calls by translating ImDrawData into your graphics API commands. You need both to run Dear ImGui, but they are completely separate concerns that communicate only through the ImGuiIO structure.

How do I handle large meshes with more than 64k vertices?

Set the ImGuiBackendFlags_RendererHasVtxOffset flag in ImGuiIO::BackendFlags during initialization. This tells Dear ImGui that your backend supports 32-bit indices and vertex offsetting. When rendering, use pcmd->VtxOffset to offset into your vertex buffer, allowing meshes to exceed the 16-bit index limit.

Can I support multiple viewports with a custom renderer?

Yes. When ImGuiConfigFlags_ViewportsEnable is set, Dear ImGui creates multiple ImDrawData instances (one per viewport). Your RenderDrawData function will be called for each viewport's draw data. For shared per-frame render state across viewports, store a Renderer_RenderState pointer in ImGuiPlatformIO (see imgui_impl_vulkan.cpp for implementation details).

How do I update the font texture dynamically?

If you set ImGuiBackendFlags_RendererHasTextures, Dear ImGui will use ImTextureID to reference textures. To update the font atlas at runtime, call ImGui::GetIO().Fonts->Build() to regenerate the atlas data, then retrieve the new pixels with GetTexDataAsRGBA32() and re-upload to your GPU texture. Update the TexID field with your new texture handle.

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 →