How to compile SDL3 Hello, World! to WebAssembly with Emscripten?

You do not need to build SDL3_image because SDL3 has a native support for loading PNG images:

    const char *texturePath = "./assets/images/right-arrow.png";
    SDL_Surface *surface = SDL_LoadPNG(texturePath);
    if (!surface)
    {
        SDL_Log("PNG load failed: %s: %s\n", texturePath, SDL_GetError());
        return SDL_APP_FAILURE;
    }

    texture = SDL_CreateTextureFromSurface(renderer, surface);
    SDL_DestroySurface(surface);

main.c

#define SDL_MAIN_USE_CALLBACKS 1 // Use the callbacks instead of main()

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

static SDL_Window *window = NULL;
static SDL_Renderer *renderer = NULL;
static SDL_Texture *texture = NULL;

SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
{
    if (!SDL_Init(SDL_INIT_VIDEO))
    {
        SDL_Log("Couldn't initialize SDL: %s", SDL_GetError());
        return SDL_APP_FAILURE;
    }

    if (!SDL_CreateWindowAndRenderer("Example", 400, 400, 0, &window, &renderer))
    {
        SDL_Log("Couldn't create window/renderer: %s", SDL_GetError());
        return SDL_APP_FAILURE;
    }

    SDL_SetRenderVSync(renderer, 1);

    const char *texturePath = "./assets/images/right-arrow.png";
    SDL_Surface *surface = SDL_LoadPNG(texturePath);
    if (!surface)
    {
        SDL_Log("PNG load failed: %s: %s\n", texturePath, SDL_GetError());
        return SDL_APP_FAILURE;
    }

    texture = SDL_CreateTextureFromSurface(renderer, surface);
    SDL_DestroySurface(surface);

    return SDL_APP_CONTINUE;
}

SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event)
{
    if (event->type == SDL_EVENT_QUIT)
    {
        return SDL_APP_SUCCESS; // End the program, reporting success to the OS
    }

    return SDL_APP_CONTINUE;
}

SDL_AppResult SDL_AppIterate(void *appstate)
{
    // Clear the screen
    SDL_SetRenderDrawColor(renderer, 100, 100, 100, 255);
    SDL_RenderClear(renderer);

    SDL_FRect srcRect = { 0, 0, 512, 512 };
    SDL_FRect destRect = { 50, 50, 100, 100 };
    SDL_RenderTexture(renderer, texture, &srcRect, &destRect);

    // Update the screen
    SDL_RenderPresent(renderer);
    return SDL_APP_CONTINUE;
}

void SDL_AppQuit(void *appstate, SDL_AppResult result)
{
    // SDL will clean up the window/renderer for us

    SDL_DestroyTexture(texture);
}

You need to embed your image files to WASM like this:

if (EMSCRIPTEN)
    target_link_options("app" PRIVATE "SHELL:--embed-file \"${CMAKE_CURRENT_SOURCE_DIR}/assets/images/right-arrow.png@/assets/images/right-arrow.png\"")
 
    set_property(TARGET "app" APPEND PROPERTY LINK_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/assets/images/right-arrow.png")
endif()

CMakeLists.txt

cmake_minimum_required(VERSION 3.21)
project(finish-native-png-loader-wasm-sdl3-c)

# Set the C standard (must be set before add_executable)
set(CMAKE_C_STANDARD 17)
set(CMAKE_C_STANDARD_REQUIRED ON)

# Set the name of the future application (on Windows this would be app.exe, 
# while for the web it will be app.js / app.wasm)
add_executable(app)

# Specify the exact location of the SDL3 library configuration files
# The variable name must strictly follow the pattern: LibraryName_DIR
set(SDL3_DIR "C:/libs/SDL3-devel-3.4.2-wasm/lib/cmake/SDL3")

# Check for the presence of libraries in the system
# If they are not found, CMake will abort the configuration with an error
# REQUIRED indicates that the package is mandatory for the build
find_package(SDL3 REQUIRED)

# Link the libraries to our application (configures linking and include paths)
target_link_libraries(app PRIVATE SDL3::SDL3)

# Add source code to the project
target_sources(app
    PRIVATE
    src/main.c
)

# Embed the image into the app.wasm file
if (EMSCRIPTEN)
    # Use the SHELL: prefix to pass the --embed-file option to the linker
    target_link_options("app" PRIVATE "SHELL:--embed-file \"${CMAKE_CURRENT_SOURCE_DIR}/assets/images/right-arrow.png@/assets/images/right-arrow.png\"")
 
    # Ensure the target is relinked if the image file changes
    set_property(TARGET "app" APPEND PROPERTY LINK_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/assets/images/right-arrow.png")
endif()

Just for my understanding, which components / files in this large GCC 15.2.0 (with POSIX threads) + MinGW-w64 13.0.0 (UCRT) - release 6 (LATEST) archive are actually used in the emcmake build process?

Emscripten ignores almost all of C++ compiler (GCC) and Windows libraries (MinGW). It uses only one utility - mingw32-make.exe. Emscripten is its own compiler (emcc). Gemini (AI) wrote: it does need a "manager" to read the instructions generated by CMake and run the compilation steps in order. So, when you run emcmake, it finds mingw32-make in your Path and uses it to execute the build.

You need to build SDL3_mixer (for sounds) and SDL3_ttf (for text). Try to built SDL3_mixer at first because SDL3_ttf is more complicated.

Steps:

cd C:\libs\SDL3_mixer-3.2.0
  • Configure:
emcmake cmake -S . -B dist -DCMAKE_INSTALL_PREFIX="C:/libs/SDL3_mixer-devel-3.2.0-wasm" -DSDL3_DIR="C:/libs/SDL3-devel-3.4.2-wasm/lib/cmake/SDL3" -DCMAKE_BUILD_TYPE=MinSizeRel
  • Note. You should to change the SDL3_DIR to your path in the command above
  • And use these commands again to built and install:
cmake --build dist
cmake --install dist

If it will be okay we can try to build SDL3_ttf. It’s a little bit more complicated because you should build the FreeType library at first: Tags · freetype/freetype · GitHub

From emsdk\upstream\emscripten\emcmake.py, we see

  # On Windows specify MinGW Makefiles or ninja if we have them and no other
  # toolchain was specified, to keep CMake from pulling in a native Visual
  # Studio, or Unix Makefiles.
  if utils.WINDOWS and not any(arg.startswith('-G') for arg in args):
    if shutil.which('mingw32-make'):
      args += ['-G', 'MinGW Makefiles']
    elif shutil.which('ninja'):
      args += ['-G', 'Ninja']

so I downloaded ninja and put ninja.exe on my PATH. On my system, it was much faster than mingw32-make.

1 Like

I followed the same emcmake procedure for SDL3_image that you taught me for SDL3 and was able to build a simple C++ program which called IMG_LoadTexture. I agree SDL3_image is not necessary for loading PNGs but let’s say I want to load it in the GPU with IMG_LoadTexture, then I have to build SDL3_image with emcmake, right?

Here I thought -sUSE_SDL=3solved all your problems

I found that starting a few years ago it was easier to compile non windows specific projects using linux. Wasm was a bit trickier but not that bad

What are you developing? A game? Because for the web it seems like SDL is used for games (which is a good idea imo)

I also compiled TTF in the past, if you need that I can look up my notes

What emsdk version do you use? I use 4.0.15. Please, try to execute this command:

emcc --show-ports

In my case I do not see the sdl3_image in the output:

>emcc --show-ports
Available official ports:
    boost_headers - Boost headers v1.83.0 (-sUSE_BOOST_HEADERS=1 or --use-port=boost_headers; Boost license)
    bullet (-sUSE_BULLET or --use-port=bullet; zlib license)
    bzip2 (-sUSE_BZIP2 or --use-port=bzip2; BSD license)
    cocos2d (-sUSE_COCOS2D=3 or --use-port=cocos2d)
    emdawnwebgpu (--use-port=emdawnwebgpu; Some files: BSD 3-Clause License. Other files: Emscripten's license (available under both MIT License and University of Illinois/NCSA Open Source License))
    freetype (-sUSE_FREETYPE or --use-port=freetype; freetype license)
    giflib (-sUSE_GIFLIB or --use-port=giflib; MIT license)
    harfbuzz (-sUSE_HARFBUZZ=1 or --use-port=harfbuzz; MIT license)
    icu (-sUSE_ICU or --use-port=icu; Unicode License)
    libjpeg (-sUSE_LIBJPEG or --use-port=libjpeg; BSD license)
    libmodplug (-sUSE_MODPLUG or --use-port=libmodplug; public domain)
    libpng (-sUSE_LIBPNG or --use-port=libpng; zlib license)
    mpg123 (-sUSE_MPG123 or --use-port=mpg123; zlib license)
    ogg (-sUSE_OGG or --use-port=ogg; zlib license)
    regal (-sUSE_REGAL=1 or --use-port=regal; Regal license)
    sdl2 (-sUSE_SDL=2 or --use-port=sdl2; zlib license)
    sdl2_gfx (-sUSE_SDL_GFX=2 or --use-port=sdl2_gfx; zlib license)
    sdl2_image (-sUSE_SDL_IMAGE=2 or --use-port=sdl2_image; zlib license)
    sdl2_mixer (-sUSE_SDL_MIXER=2 or --use-port=sdl2_mixer; zlib license)
    sdl2_net (-sUSE_SDL_NET=2 or --use-port=sdl2_net; zlib license)
    sdl2_ttf (-sUSE_SDL_TTF=2 or --use-port=sdl2_ttf; zlib license)
    sdl2 (-sUSE_SDL=3 or --use-port=sdl3; zlib license)
    sqlite3 (-sUSE_SQLITE3=1 or --use-port=sqlite3); public domain)
    vorbis (-sUSE_VORBIS or --use-port=vorbis; zlib license)
    zlib (-sUSE_ZLIB or --use-port=zlib; zlib license)
Available contrib ports:
    contrib.glfw3 (--use-port=contrib.glfw3; Apache 2.0 license)
    contrib.lua (--use-port=contrib.lua; MIT License)

I did not know about the flag -sUSE_SDL=3 when I originally started this thread. However not all of the SDL version 3 satellite libraries are currently supported with -sUSE_SDL* flags, like SDL3_image for example. Now that I know how to build these libraries with emscripten, it’s no longer a problem. I am developing a simulated warehouse environment for training AI controlled robots using reinforcement learning. You could describe it as a game, but it’s mainly for non-human players.

I am using the latest emsdk / emcc 5.0.4. emcc --show-ports lists sdl3 and sdl3_tff but not (yet) sdl3_image

C:\Users\xxxx\dev\SDL3>emcc --show-ports
Available official ports:
boost_headers - Boost headers v1.83.0 (-sUSE_BOOST_HEADERS=1 or --use-port=boost_headers; Boost license)
bullet (-sUSE_BULLET or --use-port=bullet; zlib license)
bzip2 (-sUSE_BZIP2 or --use-port=bzip2; BSD license)
cocos2d (-sUSE_COCOS2D=3 or --use-port=cocos2d)
emdawnwebgpu (–use-port=emdawnwebgpu; Some files: BSD 3-Clause License. Other files: Emscripten’s license (available under both MIT License and University of Illinois/NCSA Open Source License))
freetype (-sUSE_FREETYPE or --use-port=freetype; freetype license)
giflib (-sUSE_GIFLIB or --use-port=giflib; MIT license)
harfbuzz (-sUSE_HARFBUZZ=1 or --use-port=harfbuzz; MIT license)
icu (-sUSE_ICU or --use-port=icu; Unicode License)
libjpeg (-sUSE_LIBJPEG or --use-port=libjpeg; BSD license)
libmodplug (-sUSE_MODPLUG or --use-port=libmodplug; public domain)
libpng (-sUSE_LIBPNG or --use-port=libpng; zlib license)
mpg123 (-sUSE_MPG123 or --use-port=mpg123; zlib license)
ogg (-sUSE_OGG or --use-port=ogg; zlib license)
regal (-sUSE_REGAL=1 or --use-port=regal; Regal license)
sdl2 (-sUSE_SDL=2 or --use-port=sdl2; zlib license)
sdl2_gfx (-sUSE_SDL_GFX=2 or --use-port=sdl2_gfx; zlib license)
sdl2_image (-sUSE_SDL_IMAGE=2 or --use-port=sdl2_image; zlib license)
sdl2_mixer (-sUSE_SDL_MIXER=2 or --use-port=sdl2_mixer; zlib license)
sdl2_net (-sUSE_SDL_NET=2 or --use-port=sdl2_net; zlib license)
sdl2_ttf (-sUSE_SDL_TTF=2 or --use-port=sdl2_ttf; zlib license)
sdl3 (-sUSE_SDL=3 or --use-port=sdl3; zlib license)
sdl3_ttf (-sUSE_SDL_TTF=3 or --use-port=sdl3_ttf; zlib license)
sqlite3 (-sUSE_SQLITE3=1 or --use-port=sqlite3); public domain)
vorbis (-sUSE_VORBIS or --use-port=vorbis; zlib license)
zlib (-sUSE_ZLIB or --use-port=zlib; zlib license)
Available contrib ports:
contrib.glfw3 (–use-port=contrib.glfw3; Apache 2.0 license)
contrib.lua (–use-port=contrib.lua; MIT License)
1 Like

clang (and I assume emcc) has only partial support for C++23. Is there a particular reason why you recommend enforcing it in the CMakeLists.txt?

1 Like

I am currently exploring Modern C++ and practicing with the latest standards, so setting it to C++23 in CMake is more of a forward-looking choice for my learning process. However, there’s no strict requirement for C++23 features in this specific code yet. If it causes any compatibility issues with the current version of emcc, it can be safely downgraded to C++20 or C++17.

SDL3_image building to Wasm:

cd C:\libs\SDL3_image-3.4.0
emcmake cmake -S . -B dist -DCMAKE_INSTALL_PREFIX="C:/libs/SDL3_image-devel-3.4.0-wasm" -DSDL3_DIR="C:/libs/SDL3-devel-3.4.2-wasm/lib/cmake/SDL3" -DCMAKE_BUILD_TYPE=MinSizeRel
cmake --build dist
cmake --install dist

As far as I understand,

emcmake cmake ...

is equivalent to

cmake -DCMAKE_TOOLCHAIN_FILE=<emsdk repo>/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake ...

The latter format could be more useful when integrating emscripten and non-emscripten targets in the same CMakeLists.txt but this is all very new for me. Does emcmake do anything else?

I have not studied this tool in depth, but I asked AI on Google Search about the differences between emcmake and using the toolchain file directly. Here is the explanation:

You’re mostly right. Using the toolchain file directly is the standard way for IDEs and complex projects. However, emcmake is a bit more than just a shortcut for -DCMAKE_TOOLCHAIN_FILE.

Here is what it does under the hood:

  1. Automatic Path Resolution: It finds the correct Emscripten.cmake within your EMSDK automatically, so you don’t have to hardcode absolute paths.
  2. Cross-compilation Emulator: It sets CMAKE_CROSSCOMPILING_EMULATOR to point to Node.js. This allows CMake to run compiled test snippets during the configuration step (like check_include_file), which would otherwise fail on a host machine.
  3. Environment Setup: It ensures that Emscripten’s versions of ar, nm, and ranlib are used instead of the system defaults. This is especially helpful on Windows to avoid conflicts with MinGW or MSVC tools.

So, if you are integrating it into a CI/CD pipeline or a professional IDE setup (like VS Code or CLion), using the toolchain file directly is indeed more flexible. But for quick command-line builds, emcmake is a safer ‘wrapper’ that handles the environment for you.