My step by step guides for SDL3 to build for Android and WebAssembly

And how do you compile the whole thing?

The following doesn’t work:
g++ test.cpp -o test -lSDL3

$ g++ test.cpp -o test -lSDL3
test.cpp: In function ‘int SDL_AppInit(void**, int, char**)’:
test.cpp:51:34: error: ‘EmscriptenMouseEvent’ does not name a type
   51 |         +[](int eventType, const EmscriptenMouseEvent *e, void *userData) -> EM_BOOL {
      |                                  ^~~~~~~~~~~~~~~~~~~~
test.cpp: In lambda function:
test.cpp:55:11: error: expected ‘{’ before ‘;’ token
   55 |         });
      |           ^
test.cpp: In function ‘int SDL_AppInit(void**, int, char**)’:
test.cpp:55:11: error: expected ‘)’ before ‘;’ token
   55 |         });
      |           ^
      |           )
test.cpp:49:38: note: to match this ‘(’
   49 |     emscripten_set_mousedown_callback(
      |                                      ^

I use the following template: GitHub - Ravbug/sdl3-sample: Minimal HowTo for building and using SDL3 on a variety of platforms, including mobile and web The templet uses CMake and Emscripten to build to WASM. It has the config-web-win.bat script:

@echo OFF
cd ..
mkdir build
mkdir build\web
cd build\web
emcmake cmake ..\..

After calling of this script I call this command: mingw32-make and the following three files are generated inside of the sdl3-sample\build\web folder:

image

I didn’t try to build to WASM without CMake. I think you should use something like emcc -o index.html mygame.c instead of gcc -o mygame mygame.c. The gcc/g++ commands are for creating a binary for desktop but emcc/em++/emcmake for creating the WASM build. You can read about it here: SDL3/README/emscripten - SDL Wiki

Simple triangle in OpenGL

WASM demo

image

main.cpp

#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <SDL3/SDL_opengles2.h>

#include <iostream>

#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#include <emscripten/html5.h>
#endif // __EMSCRIPTEN__

struct AppContext
{
    SDL_Window *window;
    SDL_GLContext glcontext;
    SDL_bool app_quit = SDL_FALSE;
};

const char *vertexShaderSource =
    "attribute vec2 aPosition;\n"
    "void main()\n"
    "{\n"
    "    gl_Position = vec4(aPosition, 0.0, 1.0);\n"
    "}\n";

const char *fragmentShaderSource =
    "void main()\n"
    "{\n"
    "    gl_FragColor = vec4(0.2, 0.7, 0.3, 1.0);\n"
    "}\n";

// Helper function for creating shaders
GLuint createShader(const char *shaderSource, int shaderType)
{
    GLuint shader = glCreateShader(shaderType);
    glShaderSource(shader, 1, &shaderSource, NULL);
    glCompileShader(shader);
    GLint status;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
    if (status == GL_FALSE)
    {
        GLint maxLength = 0;
        glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &maxLength);
        std::vector<GLchar> errorLog(maxLength);
        glGetShaderInfoLog(shader, maxLength, &maxLength, &errorLog[0]);
        glDeleteShader(shader); // Don't leak the shader
        std::cout << &(errorLog[0]) << std::endl;
        std::cout << shaderSource << std::endl;
    }
    return shader;
}

// Helper function for creating a shader program
GLuint createShaderProgram()
{
    GLuint program = glCreateProgram();
    GLuint vShader = createShader(vertexShaderSource, GL_VERTEX_SHADER);
    GLuint fShader = createShader(fragmentShaderSource, GL_FRAGMENT_SHADER);

    glAttachShader(program, vShader);
    glAttachShader(program, fShader);
    glLinkProgram(program);
    glUseProgram(program);

    return program;
}

// Load a triangle to the video card
void initVertexBuffers(GLuint program)
{
    float vertPositions[] = {
        -0.5f, -0.5f,
        0.5f, -0.5f,
        0.f, 0.5f
    };
    GLuint vertPosBuffer;
    glGenBuffers(1, &vertPosBuffer);
    glBindBuffer(GL_ARRAY_BUFFER, vertPosBuffer);
    int amount = sizeof(vertPositions) / sizeof(vertPositions[0]);
    glBufferData(GL_ARRAY_BUFFER, amount * sizeof(GLfloat),
        vertPositions, GL_STATIC_DRAW);
    GLint aPositionLocation = glGetAttribLocation(program, "aPosition");
    glVertexAttribPointer(aPositionLocation, 2, GL_FLOAT, GL_FALSE, 0, 0);
    glEnableVertexAttribArray(aPositionLocation);
}

int SDL_Fail()
{
    SDL_LogError(SDL_LOG_CATEGORY_CUSTOM, "Error %s", SDL_GetError());
    return -1;
}

int SDL_AppInit(void **appstate, int argc, char *argv[])
{
    // init the library, here we make a window so we only need the Video capabilities.
    if (SDL_Init(SDL_INIT_VIDEO))
    {
        return SDL_Fail();
    }

    // create a window
    SDL_Window *window = SDL_CreateWindow("Window", 352, 430,
        SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
    if (!window)
    {
        return SDL_Fail();
    }

    SDL_GLContext glcontext = SDL_GL_CreateContext(window);
    if (!glcontext)
    {
        return SDL_Fail();
    }

    // Create a shader program and load a triangle to the video card
    GLuint program = createShaderProgram();
    initVertexBuffers(program);

    // print some information about the window
    SDL_ShowWindow(window);
    {
        int width, height, bbwidth, bbheight;
        SDL_GetWindowSize(window, &width, &height);
        SDL_GetWindowSizeInPixels(window, &bbwidth, &bbheight);
        SDL_Log("Window size: %ix%i", width, height);
        SDL_Log("Backbuffer size: %ix%i", bbwidth, bbheight);
        if (width != bbwidth)
        {
            SDL_Log("This is a highdpi environment.");
        }
    }

    // set up the application data
    *appstate = new AppContext {
        window,
        glcontext,
    };

    SDL_Log("Application started successfully!");

    return 0;
}

int SDL_AppEvent(void *appstate, const SDL_Event *event)
{
    auto *app = (AppContext *)appstate;

    switch (event->type)
    {
        case SDL_EVENT_QUIT:
        {
            app->app_quit = SDL_TRUE;
            break;
        }
        default:
        {
            break;
        }
    }

    return 0;
}

int SDL_AppIterate(void *appstate)
{
    auto *app = (AppContext *)appstate;

    glClearColor(0.188f, 0.22f, 0.255f, 1.f);
    glClear(GL_COLOR_BUFFER_BIT);
    glDrawArrays(GL_TRIANGLES, 0, 3);
    SDL_GL_SwapWindow(app->window);

    return app->app_quit;
}

void SDL_AppQuit(void *appstate)
{
    auto *app = (AppContext *)appstate;
    if (app)
    {
        SDL_GL_DeleteContext(app->glcontext);
        SDL_DestroyWindow(app->window);
        delete app;
    }

    SDL_Quit();
    SDL_Log("Application quit successfully!");
}

Simple texture in OpenGL

WASM demo

Note. If you draw a PNG texture you should use GL_RGBA in the glTexImage2D function but if you draw a JPG texture you should use GL_RGB

image

main.cpp

#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <SDL3/SDL_opengles2.h>

#include <iostream>

#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#include <emscripten/html5.h>
#endif // __EMSCRIPTEN__

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

struct AppContext
{
    SDL_Window *window;
    SDL_GLContext glcontext;
    SDL_bool app_quit = SDL_FALSE;
};

const char *vertexShaderSource =
    "attribute vec2 aPosition;\n"
    "attribute vec2 aTexCoord;\n"
    "varying vec2 vTexCoord;\n"
    "void main()\n"
    "{\n"
    "    gl_Position = vec4(aPosition, 0.0, 1.0);\n"
    "    vTexCoord = aTexCoord;\n"
    "}\n";

const char *fragmentShaderSource =
    "#ifdef GL_ES\n"
    "precision mediump float;\n"
    "#endif\n"
    "uniform sampler2D uSampler;\n"
    "varying vec2 vTexCoord;\n"
    "void main()\n"
    "{\n"
    "    gl_FragColor = texture2D(uSampler, vTexCoord);\n"
    "}\n";

// Helper function for creating shaders
GLuint createShader(const char *shaderSource, int shaderType)
{
    GLuint shader = glCreateShader(shaderType);
    glShaderSource(shader, 1, &shaderSource, NULL);
    glCompileShader(shader);
    GLint status;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
    if (status == GL_FALSE)
    {
        GLint maxLength = 0;
        glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &maxLength);
        std::vector<GLchar> errorLog(maxLength);
        glGetShaderInfoLog(shader, maxLength, &maxLength, &errorLog[0]);
        glDeleteShader(shader); // Don't leak the shader
        std::cout << &(errorLog[0]) << std::endl;
        std::cout << shaderSource << std::endl;
    }
    return shader;
}

// Helper function for creating a shader program
GLuint createShaderProgram()
{
    GLuint program = glCreateProgram();
    GLuint vShader = createShader(vertexShaderSource, GL_VERTEX_SHADER);
    GLuint fShader = createShader(fragmentShaderSource, GL_FRAGMENT_SHADER);

    glAttachShader(program, vShader);
    glAttachShader(program, fShader);
    glLinkProgram(program);
    glUseProgram(program);

    return program;
}

// Load a triangle to the video card
void initVertexBuffers(GLuint program)
{
    float vertPositions[] = {
        // First triangle
        -0.5f, -0.5f,
        0.5f, -0.5f,
        -0.5f, 0.5f,
        // Second triangle
        -0.5f, 0.5f,
        0.5f, -0.5f,
        0.5f, 0.5f
    };
    GLuint vertPosBuffer;
    glGenBuffers(1, &vertPosBuffer);
    glBindBuffer(GL_ARRAY_BUFFER, vertPosBuffer);
    int amount = sizeof(vertPositions) / sizeof(vertPositions[0]);
    glBufferData(GL_ARRAY_BUFFER, amount * sizeof(GLfloat),
        vertPositions, GL_STATIC_DRAW);
    GLint aPositionLocation = glGetAttribLocation(program, "aPosition");
    glVertexAttribPointer(aPositionLocation, 2, GL_FLOAT, GL_FALSE, 0, 0);
    glEnableVertexAttribArray(aPositionLocation);

    float texCoords[] = {
        // First triangle
        0.f, 1.f,
        1.f, 1.f,
        0.f, 0.f,
        // Second triangle
        0.f, 0.f,
        1.f, 1.f,
        1.f, 0.f
    };
    GLuint texCoordBuffer;
    glGenBuffers(1, &texCoordBuffer);
    glBindBuffer(GL_ARRAY_BUFFER, texCoordBuffer);
    amount = sizeof(texCoords) / sizeof(texCoords[0]);
    glBufferData(GL_ARRAY_BUFFER, amount * sizeof(GLfloat),
        texCoords, GL_STATIC_DRAW);
    GLint aTexCoordLocation = glGetAttribLocation(program, "aTexCoord");
    glVertexAttribPointer(aTexCoordLocation, 2, GL_FLOAT, GL_FALSE, 0, 0);
    glEnableVertexAttribArray(aTexCoordLocation);
}

GLuint createTexture(const char *path)
{
    int h_image, w_image, cnt;
    unsigned char *data = stbi_load(path, &w_image, &h_image, &cnt, 0);
    if (data == NULL)
    {
        std::cout << "Failed to load the image: " << path << std::endl;
        return 0;
    }

    glEnable(GL_TEXTURE_2D);

    GLuint texture;
    glGenTextures(1, &texture);
    glBindTexture(GL_TEXTURE_2D, texture);
    {
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
        // GL_NEAREST - for pixel graphics
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w_image, h_image, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
    }

    stbi_image_free(data);

    return texture;
}

int SDL_Fail()
{
    SDL_LogError(SDL_LOG_CATEGORY_CUSTOM, "Error %s", SDL_GetError());
    return -1;
}

int SDL_AppInit(void **appstate, int argc, char *argv[])
{
    // init the library, here we make a window so we only need the Video capabilities.
    if (SDL_Init(SDL_INIT_VIDEO))
    {
        return SDL_Fail();
    }

    // create a window
    SDL_Window *window = SDL_CreateWindow("Window", 352, 430,
        SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
    if (!window)
    {
        return SDL_Fail();
    }

    SDL_GLContext glcontext = SDL_GL_CreateContext(window);
    if (!glcontext)
    {
        return SDL_Fail();
    }

    // Create a shader program and load a triangle to the video card
    GLuint program = createShaderProgram();
    initVertexBuffers(program);

    const char *texturePath = "./box.png";
    GLuint texture = createTexture(texturePath);

    // print some information about the window
    SDL_ShowWindow(window);
    {
        int width, height, bbwidth, bbheight;
        SDL_GetWindowSize(window, &width, &height);
        SDL_GetWindowSizeInPixels(window, &bbwidth, &bbheight);
        SDL_Log("Window size: %ix%i", width, height);
        SDL_Log("Backbuffer size: %ix%i", bbwidth, bbheight);
        if (width != bbwidth)
        {
            SDL_Log("This is a highdpi environment.");
        }
    }

    // set up the application data
    *appstate = new AppContext {
        window,
        glcontext,
    };

    SDL_Log("Application started successfully!");

    return 0;
}

int SDL_AppEvent(void *appstate, const SDL_Event *event)
{
    auto *app = (AppContext *)appstate;

    switch (event->type)
    {
        case SDL_EVENT_QUIT:
        {
            app->app_quit = SDL_TRUE;
            break;
        }
        default:
        {
            break;
        }
    }

    return 0;
}

int SDL_AppIterate(void *appstate)
{
    auto *app = (AppContext *)appstate;

    glClearColor(0.188f, 0.22f, 0.255f, 1.f);
    glClear(GL_COLOR_BUFFER_BIT);
    glDrawArrays(GL_TRIANGLES, 0, 6);
    SDL_GL_SwapWindow(app->window);

    return app->app_quit;
}

void SDL_AppQuit(void *appstate)
{
    auto *app = (AppContext *)appstate;
    if (app)
    {
        SDL_GL_DeleteContext(app->glcontext);
        SDL_DestroyWindow(app->window);
        delete app;
    }

    SDL_Quit();
    SDL_Log("Application quit successfully!");
}

GLM in the browser

GLM is a header only library for linear algebra. You can copy source code of this library to your project directory (where main.cpp is placed). Copy and paste the glm folder.

#define GLM_FORCE_PURE
#include "glm/glm.hpp"
// First vector
glm::vec3 v1 = glm::vec3(1.f, 2.f, 3.f);
std::cout << "v1 = (" << v1.x << ", " << v1.y << ", " << v1.z << ")" << std::endl;
// Second vector
glm::vec3 v2 = glm::vec3(5.f, 6.f, 7.f);
std::cout << "v2 = (" << v2.x << ", " << v2.y << ", " << v2.z << ")" << std::endl;
// Sum of the vectors
glm::vec3 result = v1 + v2;
float x = result.x;
float y = result.y;
float z = result.z;
std::cout << "v1 + v2 = (" << x << ", " << y << ", " << z << ")" << std::endl;

Output:

v1 = (1, 2, 3)
v2 = (5, 6, 7)
v1 + v2 = (6, 8, 10)

I tried Emscripten.
The only thing I managed to do was the HelloWorld with node a.out.js
It doesn’t work with HTML, my browser doesn’t work with it. I have to teach the localhost somehow.

I have to be able to start localhost locally somehow, otherwise it wouldn’t work for me with Netbeans and JavaScribt.

@Mathias try this:

  • Run emcc -g main.cpp -o public/index.html
  • Install npm i http-server -g
  • Run the following command inside of your project folder http-server -c-1 (This key -c-1 means - do not use caching)
  • Open the link in the browser: http://localhost:8080/index.html

How to copy a configured project for WebAssembly

I had a problem with sdl3-sample. Every time when I start a new project I make:

  • git clone https://github.com/Ravbug/sdl3-sample --depth=1 --recurse-submodules
  • running config-web-win from emcmdprompt.bat

It requires 13 minutes. I cannot just copy paste the old project because when I make mingw32-make in the copied project it build the previous one because it keeps the absolute paths of the previous project.

I don’t think that it is the best solution but it works. I have found the previous absolute project path in two files: build/web/SDL/Makefile and build/web/CMakeCache.txt. In the Makefile file I have replaced CMAKE_SOURCE_DIR and CMAKE_BINARY_DIR with a new project name. In the CMakeCache.txt I have found 10 lines to rename.

@Mathias try this:

  • Run emcc -g main.cpp -o public/index.html
  • Install npm i http-server -g
  • Run the following command inside of your project folder http-server -c-1 (This key -c-1 means - do not use caching)
  • Open the link in the browser: http://localhost:8080/index.html
npm i http-server -g

just spits out errors for me.
But I came across the following, which works.

python3 -m http.server

or

php -S localhost:8000

or

busybox httpd -f -p 8000

This makes it run locally in the browser.

But I still have a puzzle.
I used the following command

emcc -g main.cpp -o public/index.html

to get an SDL1.x application to run, which was included in the emscripten tutorial.
Why don’t you have to specify -lSDL with emcc, as is usual with gcc.

Install Node.js: Node.js — Download Node.js®

Just for testing to run “Hello, World” in the browser. Why don’t you use CMake and this template: sdl3-sample Did you try my tutorial? It works? How to run SDL3 app on Web with WebAssembly

1 Like

In addition to HelloWorld, I also tried SDL1.x which is on the same tutorial page.
https://emscripten.org/docs/getting_started/Tutorial.html
And there, although it is an SDL program, it doesn’t need -lSDL
I want to experiment a bit with emscripten first before I try SDL3.

Some information about SDL 1.x, SDL2, and Emscripten: Building Projects — Emscripten 3.1.61-git (dev) documentation It says how to add SDL_image and sdl2_net. You can use Web Audio API for sounds and WebSockets for the network. You can save data on Node.js server by sending it to Node.js server and save data to MongoDB, MySQL, PostegreSQL and so on. Free hosting for Node.js: 1) https://glitch.com/ 2) https://render.com/ Free database hosting for MongoDB, MySQL and so on: https://filess.io/ But I don’t want to try SDL 1.x and SDL2. I want to focus on SDL3, OpenGL ES 2.0, and WebAssembly.

1 Like

Universal key code that doesn’t depend on the system keyboard layout

scancode - SDL physical key code. Docs: SDL3/SDL_Keysym - SDL Wiki

    switch (event->type)
    {
        case SDL_EVENT_KEY_DOWN:
        {
            if (event->key.keysym.scancode == SDL_SCANCODE_T) // The 't' key
            {
                std::cout << "You pressed the 't' key" << std::endl;
            }
            break;
        }
        case SDL_EVENT_QUIT:
        {
            app->app_quit = SDL_TRUE;
            break;
        }
        default:
        {
            break;
        }
    }

If you want to stay with SDL3, you have to correct the following code, the scancode query has been changed.

// old
if (event->key.keysym.scancode == 23) // The 't' key
// new
if (event->key.scancode == SDL_SCANCODE_T) {
    SDL_Log("press T");
}
1 Like

I’d rather try something new. Apparently GTKx should work too.
Since I’m new to this area, I’d rather try something classic.
The WebAssembly looks pretty exciting.

error: no member named
‘scancode’ in ‘SDL_KeyboardEvent’
if (event->key.scancode == SDL_SCANCODE_T)
~~~~~~~~~~ ^

This works:

if (event->key.keysym.scancode == SDL_SCANCODE_T)
{
    // std::cout << "You pressed the 't' key" << std::endl;
    SDL_Log("press T");
}

This is an SDL3 change. keysym no longer exists, so Mathias’ code is for SDL3 and yours is for SDL2 or an older build of SDL3.

2 Likes

That’s exactly how it is. This was changed in SDL3 about 1-2 weeks ago.

1 Like

I see. The sdl3-sample template uses a specific SDL3 commit: GitHub - libsdl-org/SDL at b6d7adfec11c355cdbb5ebde26ed5a5c958ce489 (it was June 6)

image

Handling resizing of the canvas

  1. SDL3 - recommended. It works for Web, Desktop, Android and so on:
int SDL_AppEvent(void *appstate, const SDL_Event *event)
{
    auto *app = (AppContext *)appstate;

    switch (event->type)
    {
        case SDL_EVENT_WINDOW_RESIZED:
        {
            int w = SDL_GetWindowSurface(app->window)->w;
            int h = SDL_GetWindowSurface(app->window)->h;
            std::cout << w << " " << h << std::endl;
            break;
        }
        case SDL_EVENT_QUIT:
        {
            app->app_quit = SDL_TRUE;
            break;
        }
        default:
        {
            break;
        }
    }

    return 0;
}
  1. Emscripten - not recommended because it works in the browser only:
#include <emscripten.h>
#include <emscripten/html5.h>

int SDL_AppInit(void **appstate, int argc, char *argv[])
{
    // ...

    emscripten_set_resize_callback(
        EMSCRIPTEN_EVENT_TARGET_WINDOW, nullptr, 0,
        +[](int eventType, const EmscriptenUiEvent *e, void *userData) -> EM_BOOL
        {
            int w = e->windowInnerWidth;
            int h = e->windowInnerHeight;
            std::cout << w << " " << h << std::endl;
            return EM_FALSE;
        });

    // ...
}

If you need a size of the canvas at the first resize:

  1. SDL3 - recommended:
bool isFirstResize = true;

int SDL_AppEvent(void *appstate, const SDL_Event *event)
{
    auto *app = (AppContext *)appstate;

    if (isFirstResize)
    {
        int w = SDL_GetWindowSurface(app->window)->w;
        int h = SDL_GetWindowSurface(app->window)->h;
        std::cout << w << " " << h << std::endl;
        isFirstResize = false;
    }

    // ...
}
  1. Emscripten:
#include <emscripten.h>
#include <emscripten/html5.h>

bool isFirstResize = true;

int SDL_AppEvent(void *appstate, const SDL_Event *event)
{
    auto *app = (AppContext *)appstate;

    if (isFirstResize)
    {
        int w, h;
        emscripten_get_canvas_element_size("#canvas", &w, &h);
        std::cout << w << " " << h << std::endl;
        isFirstResize = false;
    }

    // ...
}