Every renderer is based on the UImGui::GenericRenderer abstract class. It looks like this:

class GenericRenderer
{
public:
    GenericRenderer() noexcept = default;

    virtual void parseCustomConfig(YAML::Node& config) noexcept = 0;

    virtual void setupWindowIntegration() noexcept = 0;
    virtual void setupPostWindowCreation() noexcept = 0;

    virtual void init(RendererInternalMetadata& metadata) noexcept = 0;
    virtual void renderStart(double deltaTime) noexcept = 0;
    virtual void renderEnd(double deltaTime) noexcept = 0;
    virtual void destroy() noexcept = 0;

    virtual void ImGuiNewFrame() noexcept = 0;
    virtual void ImGuiShutdown() noexcept = 0;
    virtual void ImGuiInit() noexcept = 0;
    virtual void ImGuiRenderData() noexcept = 0;

    virtual void waitOnGPU() noexcept = 0;

    virtual ~GenericRenderer() noexcept = default;
};

The parseCustomConfig() function

The parseCustomConfig() function is called during the loading of Config/Core/Renderer.yaml and it gives you access to a YAML::Node& to the custom-renderer key in Renderer.yaml. In C++ you can use the bundled yaml-cpp library to parse it.

Note

This function is not available in the C API. Config parsing and saving has to be done manually with your own library there.

The setupWindowIntegration() function

This function is called before the window is created. Renderers should set up their window hints there. For example, an OpenGL renderer may call UImGui::RendererUtils::OpenGL::setHints() or another renderer may call UImGui::RendererUtils::setupManually().

You can also set up any additional raw hints here. For example, an OpenGL renderer may want to enable MSAA using a line similar to this glfwWindowHint(GLFW_SAMPLES, static_cast<int>(UImGui::Renderer::data().msaaSamples));.

Tip

This is also a good place to set your texture renderer type using a line like UImGui::Renderer::data()->textureRendererType = UIMGUI_RENDERER_TYPE_CUSTOM;

The setupPostWindowCreation function

This function is called after the window is created and is mainly used for OpenGL renderers, because in OpenGL the context needs to be created and set up as part of the window creation process. For example, an OpenGL renderer might have code similar to this there:

virtual void setupPostWindowCreation() noexcept override
{
#if !__APPLE__
    const int version = gladLoadGL(glfwGetProcAddress);
    Logger::log
    (
    #ifdef __EMSCRIPTEN__
        "Successfully loaded WebGL ",
    #else
        "Successfully loaded OpenGL ",
    #endif
        ULOG_LOG_TYPE_SUCCESS, GLAD_VERSION_MAJOR(version), ".", GLAD_VERSION_MINOR(version)
    );
#endif

    glfwSwapInterval(UImGui_Renderer_data()->bUsingVSync);
    glEnable(GL_MULTISAMPLE);
    glEnable(GL_DEPTH_TEST);

    const auto size = UImGui_Window_windowSize();

    // Set viewport and global pointer to use in callbacks
    glViewport(0, 0, CAST(int, size->x), CAST(int, size->y));
}

The init() function

The init() function is after a renderer is set up and initialised with a window. Here a renderer also has access to a UImGui::RendererInternalMetadata& structure which allows the developer to set the platform strings from the API. The structure looks like this:

struct RendererInternalMetadata
{
    FString vendorString;
    FString apiVersion;
    FString driverVersion;
    FString gpuName;
};

As you can see, the strings map 1:1 to the platform strings you get immutable access to from the Renderer interface.

In the C API, setting these strings is part of the Renderer interface. The functions are as follows:

void UImGui_RendererInternalMetadata_setVendorString(UImGui_String str);
void UImGui_RendererInternalMetadata_setApiVersion(UImGui_String str);
void UImGui_RendererInternalMetadata_setDriverVersion(UImGui_String str);
void UImGui_RendererInternalMetadata_setGPUName(UImGui_String str);

The renderStart() function

This function is called at the beginning of each render loop iteration. It receives a float deltaTime argument.

In our example code, this function is mainly used in the OpenGL renderer to set the clear colour and in the WebGPU renderer to do surface resize checks

The renderEnd() function

This function is called at the end of each render loop iteration. It receives a float deltaTime argument.

It's recommended that you do your framebuffer swapping here

The destroy() function

This function is called when the renderer instance is destroyed after the framework has exited past the render loop.

The ImGuiNewFrame() function

This function is called every frame and is used to tell dear imgui to start constructing a new frame. You need to call your dear imgui's platform backend's specific NewFrame function and then call RendererUtils::beginImGuiFrame();.

For example:

void UImGui::VulkanRenderer::ImGuiNewFrame() noexcept
{
    ImGui_ImplVulkan_NewFrame();
    RendererUtils::beginImGuiFrame();
}

Tip

For OpenGL renderers, it's important that you also call glUseProgram(0) like this:

void UImGui::OpenGLRenderer::ImGuiNewFrame() noexcept
{
    ImGui_ImplOpenGL3_NewFrame();
    RendererUtils::beginImGuiFrame();
    glUseProgram(0);
}

The ImGuiShutdown() function

Here you can call your platform backend-specific dear imgui Shutdown function. For example:

void UImGui::OpenGLRenderer::ImGuiShutdown() noexcept
{
    ImGui_ImplOpenGL3_Shutdown();
}

Tip

For Vulkan renderers, always wait on your device before calling ImGui_ImplVulkan_Shutdown(), for example:

void UImGui::VulkanRenderer::ImGuiShutdown() noexcept
{
    device.waitIdle();
    ImGui_ImplVulkan_Shutdown();
}

The ImGuiInit() function

In this function, you need to initialise your imgui platform backend-specific function.

OpenGL example:

void UImGui::OpenGLRenderer::ImGuiInit() noexcept
{
    ImGui_ImplGlfw_InitForOpenGL(Window::getInternal(), true);

#ifdef __EMSCRIPTEN__
    ImGui_ImplGlfw_InstallEmscriptenCallbacks(Window::getInternal(), "canvas");
#endif
    ImGui_ImplOpenGL3_Init(UIMGUI_LATEST_GLSL_VERSION);
}

WebGPU example:

void UImGui::WebGPURenderer::ImGuiInit() noexcept
{
    ImGui_ImplGlfw_InitForOther(Window::getInternal(), true);
    ImGui_ImplGlfw_InstallEmscriptenCallbacks(Window::getInternal(), "canvas");
}

Vulkan example:

void UImGui::WebGPURenderer::ImGuiInit() noexcept
{
    ImGui_ImplGlfw_InitForVulkan(Window::getInternal(), true);
    ImGui_ImplVulkan_InitInfo initInfo =
    {
        .Instance = instance->data(),
        .PhysicalDevice = device->physicalDevice,
        .Device = device->device,
        .QueueFamily = CAST(uint32_t, device->indices.graphicsFamily),
        .Queue = device->queue,
        .DescriptorPool = device->descriptorPools.pool,
        .RenderPass = window.RenderPass,
        .MinImageCount = CAST(uint32_t, minimalImageCount),
        .ImageCount = window.ImageCount,
        .MSAASamples = CAST(VkSampleCountFlagBits, Renderer::data().msaaSamples),
        .PipelineCache = VK_NULL_HANDLE,
        .Subpass = 0,
        .Allocator = nullptr,
        .CheckVkResultFn = [](VkResult result) -> void
        {
            if (result != VK_SUCCESS)
            {
                Logger::log("Dear imgui Vulkan rendering failure. Error code: ", ULOG_LOG_TYPE_ERROR, result);
                std::terminate();
            }
        }
    };
    ImGui_ImplVulkan_Init(&initInfo);
}

The ImGuiRenderData() function

In this function you must call your platform backend-specific RenderData function. For example:

void UImGui::OpenGLRenderer::ImGuiRenderData() noexcept
{
    ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
}

The waitOnGPU() function

This function is only used for low level APIs like Vulkan, where one needs to do waiting on the GPU manually. This function is used when clearing textures or when destroying the renderer. Most graphics APIs will not need to write anything here.

Registering your renderer

Now that you have your renderer done, you need to register it with the framework so that you can use it.

To register it, simply create an instance of its derived class and assign your Instance's InitInfo struct's customRenderer member to its address.

C API

The C API for custom renderers supports the same events. Registration is done through the same customRenderer field of the CInitInfo struct.

Custom renderers in the C API do not have access to parsing the custom-renderer YAML field in Config/Core/Renderer.yaml.

The CGenericRenderer is implemented like this:

typedef void(*UImGui_CGenericRenderer_VoidVoidFun)(void);
typedef void(*UImGui_CGenericRenderer_TickEvent)(float);

UImGui_CGenericRenderer* UImGui_CGenericRenderer_init(
    UImGui_CGenericRenderer_VoidVoidFun setupWindowIntegration,
    UImGui_CGenericRenderer_VoidVoidFun setupPostWindowIntegration,

    UImGui_CGenericRenderer_VoidVoidFun init,
    UImGui_CGenericRenderer_TickEvent renderStart,
    UImGui_CGenericRenderer_TickEvent renderEnd,
    UImGui_CGenericRenderer_VoidVoidFun destroy,

    UImGui_CGenericRenderer_VoidVoidFun ImGuiNewFrame,
    UImGui_CGenericRenderer_VoidVoidFun ImGuiShutdown,
    UImGui_CGenericRenderer_VoidVoidFun ImGuiInit,
    UImGui_CGenericRenderer_VoidVoidFun ImGuiRenderData,

    UImGui_CGenericRenderer_VoidVoidFun waitOnGPU,
    UImGui_CGenericRenderer_VoidVoidFun destruct
);

void UImGui_CGenericRenderer_free(UImGui_CGenericRenderer* data);

As you can see the events are the same, the only difference is that you need to manage your memory yourself and your data for the renderer has to come through one of the different context pointers.