Integrating Dear ImGui with SDL3 (Callbacks) and WebGPU-Native Интеграция C++-библиотеки Dear ImGui с SDL3 (Callbacks) и WebGPU-Native

A complete guide to configuring and building a native desktop application with Dear ImGui, the SDL3 Callbacks architecture (SDL_AppInit, SDL_AppIterate, SDL_AppEvent, SDL_AppQuit), and WebGPU-Native using CMake and Ninja. Полное руководство по настройке и сборке нативного десктопного приложения с Dear ImGui, архитектурой колбэков SDL3 (SDL_AppInit, SDL_AppIterate, SDL_AppEvent, SDL_AppQuit) и WebGPU-Native с использованием CMake и Ninja.

Step 1: Create Project Directory Шаг 1: Создание директории проекта

Create a workspace folder and a new directory for your project: Создайте рабочую папку для проектов и новую директорию для вашего проекта:

Windows (Command Prompt / PowerShell)

mkdir C:\projects\imgui-sdl3-wgpu-callbacks-test
cd C:\projects\imgui-sdl3-wgpu-callbacks-test

Ubuntu (Terminal)

mkdir -p ~/projects/imgui-sdl3-wgpu-callbacks-test
cd ~/projects/imgui-sdl3-wgpu-callbacks-test

Step 2: Create Source Files (main.cpp & CMakeLists.txt) Шаг 2: Создание исходных файлов (main.cpp и CMakeLists.txt)

Create main.cpp and CMakeLists.txt in the project directory: Создайте файлы main.cpp и CMakeLists.txt в директории проекта:

Windows (Terminal / Notepad)

notepad main.cpp
notepad CMakeLists.txt

Ubuntu (Terminal / Nano)

nano main.cpp
nano CMakeLists.txt

1. File Contents for main.cpp 1. Содержимое файла main.cpp

#define SDL_MAIN_USE_CALLBACKS 1
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>

#include <iostream>
#include <string>

#if defined(_WIN32)
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#elif defined(__linux__)
#include <X11/Xlib.h>
#endif

#include <webgpu/webgpu.h>
#include <webgpu/wgpu.h>

#include "imgui.h"
#include "imgui_impl_sdl3.h"
#include "imgui_impl_wgpu.h"

// -----------------------------------------------------------------------------
// Application Context
// -----------------------------------------------------------------------------
struct AppContext {
    SDL_Window*              window          = nullptr;
    WGPUInstance             instance        = nullptr;
    WGPUSurface              surface         = nullptr;
    WGPUAdapter              adapter         = nullptr;
    WGPUDevice               device          = nullptr;
    WGPUQueue                queue           = nullptr;
    WGPUTextureFormat        surface_format  = WGPUTextureFormat_Undefined;
    WGPUSurfaceConfiguration config          = {};
    bool                     need_reconfigure = false;
    int                      width           = 1280;
    int                      height          = 720;
};

// -----------------------------------------------------------------------------
// WebGPU Native Surface Creation Helper (Windows & Linux)
// -----------------------------------------------------------------------------
static WGPUSurface CreateWGPUSurface(WGPUInstance instance, SDL_Window* window) {
#if defined(_WIN32)
    SDL_PropertiesID props = SDL_GetWindowProperties(window);
    HWND hwnd = (HWND)SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL);
    HINSTANCE hinstance = (HINSTANCE)SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WIN32_INSTANCE_POINTER, NULL);

    WGPUSurfaceSourceWindowsHWND hwndSource = {};
    hwndSource.chain.sType = WGPUSType_SurfaceSourceWindowsHWND;
    hwndSource.hinstance   = hinstance;
    hwndSource.hwnd        = hwnd;

    WGPUSurfaceDescriptor desc = {};
    desc.nextInChain = (WGPUChainedStruct*)&hwndSource;
    return wgpuInstanceCreateSurface(instance, &desc);

#elif defined(__linux__)
    SDL_PropertiesID props = SDL_GetWindowProperties(window);

    // Check Wayland first
    void* wayland_display = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_DISPLAY_POINTER, NULL);
    void* wayland_surface = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_SURFACE_POINTER, NULL);

    if (wayland_display && wayland_surface) {
        WGPUSurfaceSourceWaylandSurface waylandSource = {};
        waylandSource.chain.sType = WGPUSType_SurfaceSourceWaylandSurface;
        waylandSource.display     = wayland_display;
        waylandSource.surface     = wayland_surface;

        WGPUSurfaceDescriptor desc = {};
        desc.nextInChain = (WGPUChainedStruct*)&waylandSource;
        return wgpuInstanceCreateSurface(instance, &desc);
    }

    // Fall back to X11
    void* x11_display = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_X11_DISPLAY_POINTER, NULL);
    uint64_t x11_window = SDL_GetNumberProperty(props, SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0);

    if (x11_display && x11_window) {
        WGPUSurfaceSourceXlibWindow x11Source = {};
        x11Source.chain.sType = WGPUSType_SurfaceSourceXlibWindow;
        x11Source.display     = x11_display;
        x11Source.window      = x11_window;

        WGPUSurfaceDescriptor desc = {};
        desc.nextInChain = (WGPUChainedStruct*)&x11Source;
        return wgpuInstanceCreateSurface(instance, &desc);
    }

    return nullptr;
#endif
}

// -----------------------------------------------------------------------------
// WebGPU Callbacks
// -----------------------------------------------------------------------------
static void handle_device_error(
    WGPUDevice const* device,
    WGPUErrorType type,
    WGPUStringView message,
    void* userdata1,
    void* userdata2)
{
    const char* msg = (message.data && message.length > 0) ? message.data : "(No message)";
    int len = (message.data && message.length > 0) ? (int)message.length : 12;
    std::cerr << "WebGPU Error [" << (int)type << "]: " << std::string(msg, len) << std::endl;
}

static void handle_device_request(
    WGPURequestDeviceStatus status,
    WGPUDevice res,
    WGPUStringView message,
    void* userdata1,
    void* userdata2)
{
    auto* app = static_cast<AppContext*>(userdata1);
    if (status == WGPURequestDeviceStatus_Success) {
        app->device = res;
    } else {
        const char* msg = (message.data && message.length > 0) ? message.data : "";
        int len = (message.data && message.length > 0) ? (int)message.length : 0;
        std::cerr << "Device Request Failed: " << std::string(msg, len) << std::endl;
    }
}

static void handle_adapter_request(
    WGPURequestAdapterStatus status,
    WGPUAdapter res,
    WGPUStringView message,
    void* userdata1,
    void* userdata2)
{
    auto* app = static_cast<AppContext*>(userdata1);
    if (status == WGPURequestAdapterStatus_Success) {
        app->adapter = res;

        WGPUUncapturedErrorCallbackInfo errorCallbackInfo = {};
        errorCallbackInfo.callback = handle_device_error;

        WGPUDeviceDescriptor deviceDesc = {};
        deviceDesc.uncapturedErrorCallbackInfo = errorCallbackInfo;

        WGPURequestDeviceCallbackInfo deviceCallbackInfo = {};
        deviceCallbackInfo.mode      = WGPUCallbackMode_AllowSpontaneous;
        deviceCallbackInfo.callback  = handle_device_request;
        deviceCallbackInfo.userdata1 = app;

        wgpuAdapterRequestDevice(app->adapter, &deviceDesc, deviceCallbackInfo);
    } else {
        const char* msg = (message.data && message.length > 0) ? message.data : "";
        int len = (message.data && message.length > 0) ? (int)message.length : 0;
        std::cerr << "Adapter Request Failed: " << std::string(msg, len) << std::endl;
    }
}

// -----------------------------------------------------------------------------
// SDL3 Callbacks Pipeline
// -----------------------------------------------------------------------------

// 1. AppInit: Called once at application startup
SDL_AppResult SDL_AppInit(void** appstate, int argc, char* argv[]) {
    auto* app = new AppContext();
    *appstate = app;

    if (!SDL_Init(SDL_INIT_VIDEO)) {
        std::cerr << "SDL_Init Error: " << SDL_GetError() << std::endl;
        return SDL_APP_FAILURE;
    }

    app->window = SDL_CreateWindow(
        "Dear ImGui + SDL3 (WebGPU-Native) - Callbacks",
        app->width, app->height,
        SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY
    );

    if (!app->window) {
        std::cerr << "SDL_CreateWindow Error: " << SDL_GetError() << std::endl;
        return SDL_APP_FAILURE;
    }

    // Initialize WebGPU Instance and Surface
    app->instance = wgpuCreateInstance(nullptr);
    if (!app->instance) return SDL_APP_FAILURE;

    app->surface = CreateWGPUSurface(app->instance, app->window);
    if (!app->surface) return SDL_APP_FAILURE;

    // Request Adapter
    WGPURequestAdapterOptions opt = {};
    opt.compatibleSurface = app->surface;

    WGPURequestAdapterCallbackInfo adapterCallbackInfo = {};
    adapterCallbackInfo.mode      = WGPUCallbackMode_AllowSpontaneous;
    adapterCallbackInfo.callback  = handle_adapter_request;
    adapterCallbackInfo.userdata1 = app;

    wgpuInstanceRequestAdapter(app->instance, &opt, adapterCallbackInfo);

#ifndef __EMSCRIPTEN__
    while (app->adapter == nullptr || app->device == nullptr) {
        SDL_Delay(1);
    }
#endif

    app->queue = wgpuDeviceGetQueue(app->device);

    // Retrieve surface capabilities & choose format
    WGPUSurfaceCapabilities caps = {};
    wgpuSurfaceGetCapabilities(app->surface, app->adapter, &caps);

    app->surface_format = WGPUTextureFormat_RGBA8Unorm;
    if (caps.formatCount > 0) {
        app->surface_format = caps.formats[0];
        for (size_t i = 0; i < caps.formatCount; ++i) {
            if (caps.formats[i] == WGPUTextureFormat_RGBA8Unorm ||
                caps.formats[i] == WGPUTextureFormat_BGRA8Unorm) {
                app->surface_format = caps.formats[i];
                break;
            }
        }
    }

    WGPUPresentMode present_mode = (caps.presentModeCount > 0) ? caps.presentModes[0] : WGPUPresentMode_Fifo;
    wgpuSurfaceCapabilitiesFreeMembers(caps);

    SDL_GetWindowSizeInPixels(app->window, &app->width, &app->height);

    app->config.device      = app->device;
    app->config.format      = app->surface_format;
    app->config.usage       = WGPUTextureUsage_RenderAttachment;
    app->config.width       = (uint32_t)app->width;
    app->config.height      = (uint32_t)app->height;
    app->config.presentMode = present_mode;
    wgpuSurfaceConfigure(app->surface, &app->config);

    // Initialize Dear ImGui
    IMGUI_CHECKVERSION();
    ImGui::CreateContext();
    ImGuiIO& io = ImGui::GetIO(); (void)io;
    io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
    io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;

    ImGui::StyleColorsDark();

    ImGui_ImplSDL3_InitForOther(app->window);

    ImGui_ImplWGPU_InitInfo init_info = {};
    init_info.Device             = app->device;
    init_info.NumFramesInFlight  = 3;
    init_info.RenderTargetFormat = app->surface_format;
    init_info.DepthStencilFormat = WGPUTextureFormat_Undefined;
    ImGui_ImplWGPU_Init(&init_info);

    std::cout << "Dear ImGui version: " << IMGUI_VERSION << std::endl;
    std::cout << "SDL3 Callbacks + WebGPU-Native initialized successfully!" << std::endl;

    return SDL_APP_CONTINUE;
}

// 2. AppEvent: Called on incoming SDL events
SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event) {
    auto* app = static_cast<AppContext*>(appstate);

    ImGui_ImplSDL3_ProcessEvent(event);

    if (event->type == SDL_EVENT_QUIT) {
        return SDL_APP_SUCCESS;
    }

    if (event->type == SDL_EVENT_WINDOW_CLOSE_REQUESTED && event->window.windowID == SDL_GetWindowID(app->window)) {
        return SDL_APP_SUCCESS;
    }

    if (event->type == SDL_EVENT_WINDOW_RESIZED) {
        app->need_reconfigure = true;
    }

    return SDL_APP_CONTINUE;
}

// 3. AppIterate: Called every frame to render
SDL_AppResult SDL_AppIterate(void* appstate) {
    auto* app = static_cast<AppContext*>(appstate);

    // Reconfigure surface on resize
    if (app->need_reconfigure) {
        int w = 0, h = 0;
        SDL_GetWindowSizeInPixels(app->window, &w, &h);
        if (w > 0 && h > 0) {
            app->width         = w;
            app->height        = h;
            app->config.width  = (uint32_t)w;
            app->config.height = (uint32_t)h;
            wgpuSurfaceConfigure(app->surface, &app->config);
            app->need_reconfigure = false;
        }
    }

    // Acquire next surface texture
    WGPUSurfaceTexture surface_texture = {};
    wgpuSurfaceGetCurrentTexture(app->surface, &surface_texture);
    if (!surface_texture.texture) {
        return SDL_APP_CONTINUE;
    }

    WGPUTextureView target_view = wgpuTextureCreateView(surface_texture.texture, nullptr);

    // Start ImGui frame
    ImGui_ImplWGPU_NewFrame();
    ImGui_ImplSDL3_NewFrame();
    ImGui::NewFrame();

    // Show Demo Window
    ImGui::ShowDemoWindow();

    // Render ImGui
    ImGui::Render();

    // WebGPU Render Pass
    WGPUCommandEncoderDescriptor enc_desc = {};
    WGPUCommandEncoder encoder = wgpuDeviceCreateCommandEncoder(app->device, &enc_desc);

    WGPURenderPassColorAttachment color_attachment = {};
    color_attachment.view       = target_view;
    color_attachment.loadOp     = WGPULoadOp_Clear;
    color_attachment.storeOp    = WGPUStoreOp_Store;
    color_attachment.clearValue = WGPUColor{ 0.1, 0.12, 0.15, 1.0 };
#ifndef __EMSCRIPTEN__
    color_attachment.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED;
#endif

    WGPURenderPassDescriptor pass_desc = {};
    pass_desc.colorAttachmentCount = 1;
    pass_desc.colorAttachments     = &color_attachment;

    WGPURenderPassEncoder pass = wgpuCommandEncoderBeginRenderPass(encoder, &pass_desc);
    ImGui_ImplWGPU_RenderDrawData(ImGui::GetDrawData(), pass);
    wgpuRenderPassEncoderEnd(pass);
    wgpuRenderPassEncoderRelease(pass);

    WGPUCommandBufferDescriptor cmd_desc = {};
    WGPUCommandBuffer cmd = wgpuCommandEncoderFinish(encoder, &cmd_desc);
    wgpuCommandEncoderRelease(encoder);

    wgpuQueueSubmit(app->queue, 1, &cmd);
    wgpuCommandBufferRelease(cmd);

    wgpuSurfacePresent(app->surface);

    wgpuTextureViewRelease(target_view);
    wgpuTextureRelease(surface_texture.texture);

    return SDL_APP_CONTINUE;
}

// 4. AppQuit: Called once at exit for cleanup
void SDL_AppQuit(void* appstate, SDL_AppResult result) {
    if (appstate) {
        auto* app = static_cast<AppContext*>(appstate);

        ImGui_ImplWGPU_Shutdown();
        ImGui_ImplSDL3_Shutdown();
        ImGui::DestroyContext();

        if (app->surface)
            wgpuSurfaceUnconfigure(app->surface);
        if (app->queue)
            wgpuQueueRelease(app->queue);
        if (app->device)
            wgpuDeviceRelease(app->device);
        if (app->adapter)
            wgpuAdapterRelease(app->adapter);
        if (app->surface)
            wgpuSurfaceRelease(app->surface);
        if (app->instance)
            wgpuInstanceRelease(app->instance);

        SDL_DestroyWindow(app->window);
        delete app;
    }

    SDL_Quit();
}

2. File Contents for CMakeLists.txt 2. Содержимое файла CMakeLists.txt

cmake_minimum_required(VERSION 3.21)

if(NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE "Debug")
endif()

project(imgui_sdl3_wgpu_callbacks_test CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# -----------------------------------------------------------------------------
# Paths
# -----------------------------------------------------------------------------
if(WIN32)
    set(IMGUI_DIR "C:/libs/imgui-1.93.0")
    set(SDL3_DIR  "C:/libs/sdl3-3.4.18-mingw/lib/cmake/SDL3")
    set(WGPU_DIR  "C:/libs/wgpu-29.0.1.1-mingw")
else()
    set(IMGUI_DIR "$ENV{HOME}/libs/imgui-1.93.0")
    set(SDL3_DIR  "$ENV{HOME}/libs/sdl3-3.4.18-ubuntu/lib/cmake/SDL3")
    set(WGPU_DIR  "$ENV{HOME}/libs/wgpu-native-29.0.1.1-ubuntu")
endif()

# -----------------------------------------------------------------------------
# Locate Official SDL3 CMake Package
# -----------------------------------------------------------------------------
find_package(SDL3 REQUIRED CONFIG)

# -----------------------------------------------------------------------------
# ImGui Sources (including Demo Window and SDL3/WGPU Backends)
# -----------------------------------------------------------------------------
set(IMGUI_SOURCES
    ${IMGUI_DIR}/imgui.cpp
    ${IMGUI_DIR}/imgui_demo.cpp
    ${IMGUI_DIR}/imgui_draw.cpp
    ${IMGUI_DIR}/imgui_tables.cpp
    ${IMGUI_DIR}/imgui_widgets.cpp
    ${IMGUI_DIR}/backends/imgui_impl_sdl3.cpp
    ${IMGUI_DIR}/backends/imgui_impl_wgpu.cpp
)

# -----------------------------------------------------------------------------
# Executable Target
# -----------------------------------------------------------------------------
add_executable(app main.cpp ${IMGUI_SOURCES})

# -----------------------------------------------------------------------------
# Include Directories
# -----------------------------------------------------------------------------
target_include_directories(app PRIVATE
    ${IMGUI_DIR}
    ${IMGUI_DIR}/backends
    ${WGPU_DIR}/include
)

# -----------------------------------------------------------------------------
# Backend Selection Definition
# -----------------------------------------------------------------------------
target_compile_definitions(app PRIVATE IMGUI_IMPL_WEBGPU_BACKEND_WGPU)

# -----------------------------------------------------------------------------
# Static C++ Runtime (Applied to BOTH Windows & Ubuntu)
# -----------------------------------------------------------------------------
target_link_options(app PRIVATE -static-libgcc -static-libstdc++)

# -----------------------------------------------------------------------------
# Optimization & Symbol Stripping (Release Builds Only)
# -----------------------------------------------------------------------------
if(CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "MinSizeRel")
    target_compile_options(app PRIVATE -Os)
    target_link_options(app PRIVATE -s)
endif()

# -----------------------------------------------------------------------------
# Platform Linking & Dependencies
# -----------------------------------------------------------------------------
if(WIN32)
    add_library(wgpu_native STATIC IMPORTED)
    set_target_properties(wgpu_native PROPERTIES
        IMPORTED_LOCATION "${WGPU_DIR}/lib/libwgpu_native.a"
        INTERFACE_INCLUDE_DIRECTORIES "${WGPU_DIR}/include"
        INTERFACE_LINK_LIBRARIES "ws2_32;userenv;ntdll;bcrypt;d3d12;dxgi"
    )

    target_link_libraries(app PRIVATE
        wgpu_native
        SDL3::SDL3-static
    )

    # Windows static linking & subsystem selection
    target_link_options(app PRIVATE -static)

    if(CMAKE_BUILD_TYPE STREQUAL "Debug")
        target_link_options(app PRIVATE -mconsole)
    else()
        target_link_options(app PRIVATE -mwindows)
    endif()
else()
    add_library(wgpu_native STATIC IMPORTED)
    set_target_properties(wgpu_native PROPERTIES
        IMPORTED_LOCATION "${WGPU_DIR}/lib/libwgpu_native.a"
        INTERFACE_INCLUDE_DIRECTORIES "${WGPU_DIR}/include"
        INTERFACE_LINK_LIBRARIES "dl;pthread;m"
    )

    target_link_libraries(app PRIVATE
        wgpu_native
        SDL3::SDL3-static
        X11
    )
endif()

Step 3: Create Build & Execution Scripts Шаг 3: Создание скриптов сборки и запуска

Windows (MinGW / Batch Scripts)

config-win.bat

cmake -G Ninja -S . -B dist/win

build-win.bat

cmake --build dist/win

run-win.bat

@echo off
.\dist\win\app.exe

Run the scripts sequentially in Command Prompt: Последовательно запустите скрипты в командной строке:

config-win.bat
build-win.bat
run-win.bat

Ubuntu (Bash Scripts)

config-linux.sh

#!/usr/bin/env bash
set -e
cmake -G Ninja -S . -B dist/linux

build-linux.sh

#!/usr/bin/env bash
set -e
cmake --build dist/linux

run-linux.sh

#!/usr/bin/env bash
set -e
./dist/linux/app

Grant execute permissions and launch in Ubuntu terminal: Предоставьте права на выполнение и запустите в терминале Ubuntu:

chmod +x config-linux.sh build-linux.sh run-linux.sh
./config-linux.sh
./build-linux.sh
./run-linux.sh

Step 4: Expected Output Шаг 4: Ожидаемый результат

Once executed, the application boots using the SDL3 callbacks pipeline, initializes the WebGPU surface, and renders Dear ImGui Demo Window. In the console, you will see: После запуска приложение стартует через конвейер колбэков SDL3, инициализирует поверхность WebGPU и выполняет рендеринг окна Dear ImGui Demo. В консоли появится сообщение:

Dear ImGui version: 1.93.0
SDL3 Callbacks + WebGPU-Native initialized successfully!

Support My Work Поддержать проект

If these tutorials helped you, consider buying me a coffee! Если эти туториалы вам помогли, вы можете поддержать автора.

Sberbank (Russia only) Сбербанк (только для РФ)

Sberbank SBP QR Code

Direct transfer via phone number (Russia only) Перевод по номеру телефона (только для РФ)

+7 (917) 212-29-59

USDT TRC20

USDT TRC20 QR Code

Support via Cryptocurrency Поддержка криптовалютой

TMtY1YifNf6FKvgeFmqKGQR4NStKr3csGp