Integrating compiled libSDL3.a on WebAssembly (WebGL 2.0 / OpenGL ES 3.0) Интеграция собранной библиотеки libSDL3.a на WebAssembly (WebGL 2.0 / OpenGL ES 3.0)

A step-by-step guide to creating a WebAssembly project, linking your compiled static libSDL3.a, rendering a colored triangle using WebGL 2.0 (OpenGL ES 3.0) via the modern SDL3 Callbacks API, compiling with Emscripten & CMake, and running it in a web browser. Пошаговое руководство по созданию проекта WebAssembly, связыванию собранной статической библиотеки libSDL3.a, рендерингу цветного треугольника с помощью WebGL 2.0 (OpenGL ES 3.0) через современный Callbacks API в SDL3, компиляции с Emscripten и CMake и запуску в веб-браузере.

Before proceeding, make sure you have configured MinGW, CMake, Emscripten SDK, and built the SDL3 static library using these guides: Перед началом убедитесь, что вы настроили MinGW, CMake, Emscripten SDK и собрали статическую библиотеку SDL3 по этим инструкциям:

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

Create your workspace directory C:\projects\wasm-app-sdl3-opengl-cpp and organize the files as follows: Создайте рабочую папку проекта C:\projects\wasm-app-sdl3-opengl-cpp со следующей структурой:

C:\projects\wasm-app-sdl3-opengl-cpp\
├── src\
│   └── main.cpp
├── CMakeLists.txt
├── build-web.bat
└── run-web.bat

In Command Prompt (CMD): В командной строке (CMD):

mkdir C:\projects\wasm-app-sdl3-opengl-cpp\src
cd C:\projects\wasm-app-sdl3-opengl-cpp

Step 2: WebGL 2.0 Source Code (src/main.cpp) Шаг 2: Исходный код WebGL 2.0 (src/main.cpp)

Create src/main.cpp using the modern SDL3 Callbacks API (SDL_MAIN_USE_CALLBACKS). Because web browsers execute code inside a cooperative event loop, the Callbacks API integrates seamlessly with the browser's requestAnimationFrame without blocking the UI thread: Создайте файл src/main.cpp, используя современный Callbacks API SDL3 (SDL_MAIN_USE_CALLBACKS). Так как браузер работает в едином цикле событий, Callbacks API нативно связывается с браузерным requestAnimationFrame, предотвращая зависание вкладки:

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

const char* vertexShaderSource =
    "#version 300 es\n"
    "layout (location = 0) in vec2 aPos;\n"
    "layout (location = 1) in vec3 aColor;\n"
    "out vec3 vColor;\n"
    "void main() {\n"
    "    gl_Position = vec4(aPos, 0.0, 1.0);\n"
    "    vColor = aColor;\n"
    "}\0";

const char* fragmentShaderSource =
    "#version 300 es\n"
    "precision mediump float;\n"
    "in vec3 vColor;\n"
    "out vec4 FragColor;\n"
    "void main() {\n"
    "    FragColor = vec4(vColor, 1.0);\n"
    "}\0";

struct AppState {
    SDL_Window* window = nullptr;
    SDL_GLContext glContext = nullptr;
    GLuint shaderProgram = 0;
    GLuint VAO = 0;
    GLuint VBO = 0;
};

static GLuint compileShader(GLenum type, const char* source) {
    GLuint shader = glCreateShader(type);
    glShaderSource(shader, 1, &source, nullptr);
    glCompileShader(shader);

    GLint success;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
    if (!success) {
        char infoLog[512];
        glGetShaderInfoLog(shader, 512, nullptr, infoLog);
        SDL_Log("Shader compilation error: %s", infoLog);
    }
    return shader;
}

SDL_AppResult SDL_AppInit(void** appstate, int argc, char* argv[]) {
    (void)argc;
    (void)argv;

    if (!SDL_Init(SDL_INIT_VIDEO)) {
        SDL_Log("Unable to initialize SDL: %s", SDL_GetError());
        return SDL_APP_FAILURE;
    }

    // Configure OpenGL ES 3.0 / WebGL 2.0 context attributes
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
    SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
    SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);

    auto* state = new AppState();
    *appstate = state;

    state->window = SDL_CreateWindow(
        "SDL3 WebGL 2.0 (WebAssembly)",
        800, 600,
        SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE
    );

    if (!state->window) {
        SDL_Log("Failed to create window: %s", SDL_GetError());
        return SDL_APP_FAILURE;
    }

    state->glContext = SDL_GL_CreateContext(state->window);
    if (!state->glContext) {
        SDL_Log("Failed to create OpenGL context: %s", SDL_GetError());
        return SDL_APP_FAILURE;
    }

    SDL_GL_SetSwapInterval(1);

    // Build and compile shader program
    GLuint vertexShader = compileShader(GL_VERTEX_SHADER, vertexShaderSource);
    GLuint fragmentShader = compileShader(GL_FRAGMENT_SHADER, fragmentShaderSource);

    state->shaderProgram = glCreateProgram();
    glAttachShader(state->shaderProgram, vertexShader);
    glAttachShader(state->shaderProgram, fragmentShader);
    glLinkProgram(state->shaderProgram);

    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);

    float vertices[] = {
        // positions    // colors
         0.0f,  0.5f,   1.0f, 0.0f, 0.0f, // top (red)
        -0.5f, -0.5f,   0.0f, 1.0f, 0.0f, // bottom left (green)
         0.5f, -0.5f,   0.0f, 0.0f, 1.0f  // bottom right (blue)
    };

    glGenVertexArrays(1, &state->VAO);
    glGenBuffers(1, &state->VBO);

    glBindVertexArray(state->VAO);
    glBindBuffer(GL_ARRAY_BUFFER, state->VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    // Position attribute (layout location = 0)
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);

    // Color attribute (layout location = 1)
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(2 * sizeof(float)));
    glEnableVertexAttribArray(1);

    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);

    return SDL_APP_CONTINUE;
}

SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event) {
    if (event->type == SDL_EVENT_QUIT) {
        return SDL_APP_SUCCESS;
    }
    return SDL_APP_CONTINUE;
}

SDL_AppResult SDL_AppIterate(void* appstate) {
    auto* state = static_cast<AppState*>(appstate);

    int w, h;
    SDL_GetWindowSizeInPixels(state->window, &w, &h);
    glViewport(0, 0, w, h);

    glClearColor(0.12f, 0.12f, 0.15f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    glUseProgram(state->shaderProgram);
    glBindVertexArray(state->VAO);
    glDrawArrays(GL_TRIANGLES, 0, 3);

    SDL_GL_SwapWindow(state->window);

    return SDL_APP_CONTINUE;
}

void SDL_AppQuit(void* appstate, SDL_AppResult result) {
    if (appstate) {
        auto* state = static_cast<AppState*>(appstate);

        glDeleteVertexArrays(1, &state->VAO);
        glDeleteBuffers(1, &state->VBO);
        glDeleteProgram(state->shaderProgram);

        if (state->glContext) {
            SDL_GL_DestroyContext(state->glContext);
        }
        if (state->window) {
            SDL_DestroyWindow(state->window);
        }
        delete state;
    }
    SDL_Quit();
}

Step 3: Configure CMakeLists.txt Шаг 3: Настройка CMakeLists.txt

Create CMakeLists.txt in the project root directory. It imports the compiled SDL3 WebAssembly library, compiles your target into app.html, app.js, and app.wasm, and creates a copy named index.html so your local server serves the app directly at the root URL: Создайте файл CMakeLists.txt в корне проекта. Он импортирует собранную библиотеку SDL3 для WebAssembly, компилирует таргет в app.html, app.js и app.wasm, а также создает копию index.html для открытия приложения сразу по корневому адресу сервера:

cmake_minimum_required(VERSION 3.21)

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

project(wasm_app_sdl3_opengl_cpp CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# -----------------------------------------------------------------------------
# Paths (points directly to the folder containing SDL3Config.cmake)
# -----------------------------------------------------------------------------
set(SDL3_DIR "C:/libs/sdl3-3.4.18-wasm/lib/cmake/SDL3")

# -----------------------------------------------------------------------------
# Locate Official SDL3 CMake Package
# -----------------------------------------------------------------------------
find_package(SDL3 REQUIRED CONFIG PATHS "${SDL3_DIR}" NO_CMAKE_FIND_ROOT_PATH)

# -----------------------------------------------------------------------------
# WebAssembly Executable Target
# -----------------------------------------------------------------------------
add_executable(app
    src/main.cpp
)

set_target_properties(app PROPERTIES
    SUFFIX ".html"
)

# Automatically generate index.html by copying app.html
# This allows http://localhost:8000/ to open directly without a directory listing
add_custom_command(TARGET app POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E copy
        "${CMAKE_CURRENT_BINARY_DIR}/app.html"
        "${CMAKE_CURRENT_BINARY_DIR}/index.html"
    COMMENT "Generating index.html from app.html for default root server loading"
)

# -----------------------------------------------------------------------------
# Link SDL3 Static Library
# -----------------------------------------------------------------------------
target_link_libraries(app PRIVATE
    SDL3::SDL3-static
)

# -----------------------------------------------------------------------------
# Emscripten Link Flags (WebGL 2.0 & Dynamic Memory)
# -----------------------------------------------------------------------------
target_link_options(app PRIVATE
    -sMAX_WEBGL_VERSION=2
    -sMIN_WEBGL_VERSION=2
    -sALLOW_MEMORY_GROWTH=1
)

Step 4: Create Build and Run Scripts Шаг 4: Создание скриптов сборки и запуска

Create build-web.bat in the project root. Because CMake reads the build configuration directly from CMakeLists.txt, there is no need to duplicate -DCMAKE_BUILD_TYPE in the script: Создайте файл build-web.bat в корне проекта. Так как CMake берёт тип сборки напрямую из CMakeLists.txt, дублировать флаг -DCMAKE_BUILD_TYPE в скрипте не требуется:

@echo off
set BUILD_DIR=dist\web

call emcmake cmake -G "MinGW Makefiles" -S . -B %BUILD_DIR%
call emmake cmake --build %BUILD_DIR%

Create run-web.bat to launch your default browser and serve the application with Python's HTTP server. Using the -d flag serves dist\web while keeping your terminal in the root folder, so stopping with Ctrl + C leaves you at the root to rebuild and run immediately: Создайте файл run-web.bat для автоматического открытия браузера и запуска локального сервера через Python. Флаг -d раздаёт папку dist\web без смены рабочего каталога в терминале, поэтому после остановки через Ctrl + C вы останетесь в корне проекта:

@echo off
echo [*] Starting local HTTP server at http://localhost:8000
echo [*] Press Ctrl+C to stop the server

start http://localhost:8000

python -m http.server 8000 -d "%~dp0dist\web"

Step 5: Build and Run Шаг 5: Сборка и запуск

Run the build script from your project root: Запустите скрипт сборки из корня проекта:

build-web.bat

Now start the local server: Теперь запустите локальный сервер:

run-web.bat

Your default browser will launch directly to http://localhost:8000, displaying your application running inside the HTML5 canvas and smoothly rendering a colored triangle powered by SDL3 and WebGL 2.0. Ваш браузер откроется сразу по адресу http://localhost:8000, отображая приложение в элементе HTML5 canvas с цветным треугольником, аппаратно ускоренным через SDL3 и WebGL 2.0.

Complete source code for this project is available on GitHub: Полный исходный код проекта доступен на GitHub:

View on GitHub Смотреть на GitHub

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