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

Did I understand that correctly with WebAssembly you can run SDLx programs in a browser?

Yes, Emscripten will compile your C/C++ style code to run in any browser that supports using HTML5, javascript, and WebAssembly. Emscripten supports SDL 1 and 2 and the common sub-libraries (TTF, IMG, NET, MIXER).
Looks like support is being added or has been added for SDL3, but it may take a bit for the SDL3 sub-libraries to also be brought in line (I don’t know this for certain, perhaps they already are?).

… But there are some pretty severe limitations.
It is very difficult to get save files to work with Emscripten. I have found several tutorials that show different ways to create local storage, but Emscripten keeps getting updated in ways that then break those tutorials. They have a major focus on running in a sandboxed environment. To date I have not found a correct way to save game data while running up-to-date-Emscripten compiled code.

Click it: Emscripten-Generated Code to run SDL3 application in your broswer.

It is the source code of the applications above that was compiled to WebAssembly:

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

struct AppContext {
    SDL_Window* window;
    SDL_Renderer* renderer;
    SDL_bool app_quit = SDL_FALSE;
};

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_RESIZABLE);
    if (!window){
        return SDL_Fail();
    }
    
    SDL_Renderer* renderer = SDL_CreateRenderer(window, NULL, 0);
    if (!renderer){
        return SDL_Fail();
    }
    
    // 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,
       renderer,
    };
    
    SDL_Log("Application started successfully!");

    return 0;
}

int SDL_AppEvent(void *appstate, const SDL_Event* event) {
    auto* app = (AppContext*)appstate;
    
    if (event->type == SDL_EVENT_QUIT) {
        app->app_quit = SDL_TRUE;
    }

    return 0;
}

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

    // draw a color
    auto time = SDL_GetTicks() / 1000.f;
    auto red = (std::sin(time) + 1) / 2.0 * 255;
    auto green = (std::sin(time / 2) + 1) / 2.0 * 255;
    auto blue = (std::sin(time) * 2 + 1) / 2.0 * 255;
    
    SDL_SetRenderDrawColor(app->renderer, red, green, blue, SDL_ALPHA_OPAQUE);
    SDL_RenderClear(app->renderer);
    SDL_RenderPresent(app->renderer);

    return app->app_quit;
}

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

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

You can even write applications with 3D graphics in C++ using OpenGL ES and compile them to WebAssembly. You can use WebSockets to connect with Node.js server to create multiplayer games. Use the free Glitch hosting to practice in creating of game servers:

This program in pure OpenGL ES that I have ran in the browser:

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

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

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();
    }

    // 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;

    if (event->type == SDL_EVENT_QUIT)
    {
        app->app_quit = SDL_TRUE;
    }

    return 0;
}

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

    // draw a color
    auto time = SDL_GetTicks() / 1000.f;
    auto red = (std::sin(time) + 1) / 2.0;
    auto green = (std::sin(time / 2) + 1) / 2.0;
    auto blue = (std::sin(time) * 2 + 1) / 2.0;

    glClearColor(red, green, blue, 1.f);
    glClear(GL_COLOR_BUFFER_BIT);
    // Draw your stuff here
    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!");
}

Drawing a simple triangle in OpenGL ES 2.0 that I have tested in the browser and real Android phone:

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

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

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

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

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::printf("%s\n", &(errorLog[0]));
    }
    return shader;
}

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;
}

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("OpenGL Window", 352, 430,
        SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
    if (!window)
    {
        return SDL_Fail();
    }

    SDL_GLContext glcontext = SDL_GL_CreateContext(window);

    GLuint program = createShaderProgram();
    initVertexBuffers(program);

    SDL_ShowWindow(window);

    // 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;

    if (event->type == SDL_EVENT_QUIT)
    {
        app->app_quit = SDL_TRUE;
    }

    return 0;
}

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

    glClearColor(0.1, 0.3, 0.2, 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!");
}

I have made the next demo for WebAssembly, Android, and Desktop using: Qt C++, OpenGL ES 2.0, OpenAL-Soft (this is a library for music and sounds), Box2D (for jumps, collision detections, and ray casting), Hiero (this is an application to create a font with distance field from TTF), Free Texture Packer (to pack images to one texture atlas), and Tiled map editor (to position sprites and Box2D static colliders).

All resources (sprites, music and sounds) have been replaced with free ones. You can see a list of free resources here. For example, I took the sprites here: More-Bit 8-Bit Mario by webfussel

I have made a custom joystick for Android in pure OpenGL ES 2.0 (this is an animation from the real phone that I made using scrcpy):

mario-2d-jumps-webfussel-opengles2-qt6-cpp-android

@ 8Observer8

I tried to compile the above examples under normal Linux, but there were lots of errors.
There are obvious errors here, variables declared twice.

    auto time = SDL_GetTicks() / 1000.f;
    auto red = (std::sin(time) + 1) / 2.0 * 255;
    auto green = (std::sin(time / 2) + 1) / 2.0 * 255;
    auto blue = (std::sin(time) * 2 + 1) / 2.0 * 255;
    auto red = 150 / 255.f;
    auto green = 30 / 255.f;
    auto blue = 50 / 255.f;

I tried to compile the above examples under normal Linux, but there were lots of errors.

There are obvious errors here, variables declared twice.

WebAssembly is certainly an exciting project. I assume you can certainly run programs without SDL.
When I have the time and the desire, I’ll take a closer look at it.
I briefly tried implementing your instructions on Linux, but unfortunately I didn’t get very far.

There is a similar project in the FPC/Lazarus world. Pascal programming, which is converted into JavaScript.
Here is an example from with. If you click on project1.html, it will start.

http://mathias1000.bplaced.net/WebGL/WebGL_Matrix/
http://mathias1000.bplaced.net/WebGL/WebGL_Matrix/project1.html

Thanks and sorry! I have fixed it. I just had some experiments and had not fixed it. I have the tutorial: Convert rendering using the SDL3 API to rendering using OpenGL ES 2.0

I have added a link above to the GitHub repository in the post with Super Mario demo clone above. All resources (sprites, music and sounds) have been replaced with free ones. You can see a list of free resources here. For example, I took the sprites here: More-Bit 8-Bit Mario by webfussel

Unfortunately it still doesn’t want to.
When I look at your source the main() is missing.
As with many examples and …/SDL3/SDL/test

g++ main.c -lSDL3
/usr/bin/ld: /usr/lib/gcc/x86_64-linux-gnu/11/../../../x86_64-linux-gnu/Scrt1.o: in function `_start':
(.text+0x1b): undefined reference to `main'
collect2: error: ld returned 1 exit status

I used this template for SDL3: sdl3-sample It doesn’t have the main() function in the main.cpp file. Try to build it with cmake (read README.md in the repository). Follow this:

# You need to clone with submodules, otherwise SDL will not download.
git clone https://github.com/Ravbug/sdl3-sample --depth=1 --recurse-submodules
cd sdl3-sample
mkdir build
cd build
cmake ..

I tried this this morning with success. But it must also be possible to translate the simple program with a g++ command. The SDL_Appxxx(…) functions are called by some lib.

@Mathias sorry, I don’t know how to help you. I don’t use SDL3 now. My priority platforms are Web and Android, but SDL3 is too hard for me. I prefer to program for Web and Android using Phaser, Three.js, libGDX, pure WebGL and Qt C++ OpenGL ES 2.0. I use Cordova to build web-apps for Android.

@ 8Observer8

Sorry, I thought you were an SDL3 professional because you wrote these “SDL3 to build for Android and WebAssembly” tutorials.

I’ve already done WebGL. Recently this is even possible with FPC/Lazarus and the package pas2js.

It is just step by step guides for beginners. You don’t need to be a professional in SDL3 to write them. I simply documented every step.

I’ve now managed to compile all of your examples natively.

You have to insert the following line at the beginning of everything.

#define SDL_MAIN_USE_CALLBACKS

Compiling works like this.

# without GL-ES
g++ main.c -o main -lSDL3

# with GL-ES
g++ main.c -o main -lSDL3 -lGL

I’ll see if I can do it with gcc.

The following still needs to be changed when using the latest SDL3.

// old
SDL_Renderer* renderer = SDL_CreateRenderer(window, NULL, 0);
// new
SDL_Renderer* renderer = SDL_CreateRenderer(window, NULL);

Mouse click event handler using WebAssembly

Demo in the browser (click on the canvas)

#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#include <emscripten/html5.h>
#endif // __EMSCRIPTEN__
// Mouse Button Down:
emscripten_set_mousedown_callback(
    "#canvas", nullptr, 0,
    +[](int eventType, const EmscriptenMouseEvent *e, void *userData) -> EM_BOOL
    {
        // std::cout << "Mouse down: " << e->button << "\n";
        std::cout << "x: " << e->targetX << ", y: " << e->targetY << "\n";
        return EM_FALSE;
    });

Size of the application:

image

main.cpp

#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <cmath>
#include <iostream>

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

struct AppContext
{
    SDL_Window *window;
    SDL_Renderer *renderer;
    SDL_bool app_quit = SDL_FALSE;
};

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_RESIZABLE);
    if (!window)
    {
        return SDL_Fail();
    }

    SDL_Renderer *renderer = SDL_CreateRenderer(window, NULL);
    if (!renderer)
    {
        return SDL_Fail();
    }

    // Mouse Button Down:
    emscripten_set_mousedown_callback(
        "#canvas", nullptr, 0,
        +[](int eventType, const EmscriptenMouseEvent *e, void *userData) -> EM_BOOL {
            // std::cout << "Mouse down: " << e->button << "\n";
            std::cout << "x: " << e->targetX << ", y: " << e->targetY << "\n";
            return EM_FALSE;
        });

    // 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,
        renderer,
    };

    SDL_Log("Application started successfully!");

    return 0;
}

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

    if (event->type == SDL_EVENT_QUIT)
    {
        app->app_quit = SDL_TRUE;
    }

    return 0;
}

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

    // draw a color
    auto time = SDL_GetTicks() / 1000.f;
    auto red = (std::sin(time) + 1) / 2.0 * 255;
    auto green = (std::sin(time / 2) + 1) / 2.0 * 255;
    auto blue = (std::sin(time) * 2 + 1) / 2.0 * 255;

    SDL_SetRenderDrawColor(app->renderer, red, green, blue, SDL_ALPHA_OPAQUE);
    SDL_RenderClear(app->renderer);
    SDL_RenderPresent(app->renderer);

    return app->app_quit;
}

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

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

Mouse click event handler using SDL3

Demo in the browser (click on the canvas)

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

    switch (event->type)
    {
        case SDL_EVENT_MOUSE_BUTTON_DOWN:
        {
            if (event->button.button == SDL_BUTTON_LEFT)
            {
                int x = event->button.x;
                int y = event->button.y;
                std::cout << "x: " << x << ", y = " << y << std::endl;
            }
            break;
        }
        case SDL_EVENT_QUIT:
        {
            app->app_quit = SDL_TRUE;
            break;
        }
        default:
        {
            break;
        }
    }

    return 0;
}

main.cpp

#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <cmath>
#include <iostream>

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

struct AppContext
{
    SDL_Window *window;
    SDL_Renderer *renderer;
    SDL_bool app_quit = SDL_FALSE;
};

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_RESIZABLE);
    if (!window)
    {
        return SDL_Fail();
    }

    SDL_Renderer *renderer = SDL_CreateRenderer(window, NULL);
    if (!renderer)
    {
        return SDL_Fail();
    }

    // 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,
        renderer,
    };

    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_MOUSE_BUTTON_DOWN:
        {
            if (event->button.button == SDL_BUTTON_LEFT)
            {
                int x = event->button.x;
                int y = event->button.y;
                std::cout << "x: " << x << ", y = " << y << std::endl;
            }
            break;
        }
        case SDL_EVENT_QUIT:
        {
            app->app_quit = SDL_TRUE;
            break;
        }
        default:
        {
            break;
        }
    }

    return 0;
}

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

    // draw a color
    auto time = SDL_GetTicks() / 1000.f;
    auto red = (std::sin(time) + 1) / 2.0 * 255;
    auto green = (std::sin(time / 2) + 1) / 2.0 * 255;
    auto blue = (std::sin(time) * 2 + 1) / 2.0 * 255;

    SDL_SetRenderDrawColor(app->renderer, red, green, blue, SDL_ALPHA_OPAQUE);
    SDL_RenderClear(app->renderer);
    SDL_RenderPresent(app->renderer);

    return app->app_quit;
}

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

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

Loading textures for WASM

Add it to the CMakeLists.txt file:

if(CMAKE_SYSTEM_NAME MATCHES Emscripten)
	set(CMAKE_EXECUTABLE_SUFFIX ".html" CACHE INTERNAL "")
    target_link_options("${EXECUTABLE_NAME}" PRIVATE "SHELL:--embed-file ${CMAKE_CURRENT_SOURCE_DIR}/src/box.jpg@/box.jpg")
    set_property(TARGET "${EXECUTABLE_NAME}" APPEND PROPERTY LINK_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/box.jpg")
endif()

Header only library to load textures: stb_image.h Place it in the src folder.

// i.e. it should look like this:
#include ...
#include ...
#include ...
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
int h_image, w_image, cnt;
const char *texturePath = "./box.jpg";
unsigned char *data = stbi_load(texturePath, &w_image, &h_image, &cnt, 0);
std::cout << "w_image: " << w_image << std::endl;

Useful links from maarten: Discord