| # Emscripten |
| |
| ## The state of things |
| |
| (As of September 2023, but things move quickly and we don't update this |
| document often.) |
| |
| In modern times, all the browsers you probably care about (Chrome, Firefox, |
| Edge, and Safari, on Windows, macOS, Linux, iOS and Android), support some |
| reasonable base configurations: |
| |
| - WebAssembly (don't bother with asm.js any more) |
| - WebGL (which will look like OpenGL ES 2 or 3 to your app). |
| - Threads (see caveats, though!) |
| - Game controllers |
| - Autoupdating (so you can assume they have a recent version of the browser) |
| |
| All this to say we're at the point where you don't have to make a lot of |
| concessions to get even a fairly complex SDL-based game up and running. |
| |
| |
| ## RTFM |
| |
| This document is a quick rundown of some high-level details. The |
| documentation at [emscripten.org](https://emscripten.org/) is vast |
| and extremely detailed for a wide variety of topics, and you should at |
| least skim through it at some point. |
| |
| |
| ## Porting your app to Emscripten |
| |
| Many many things just need some simple adjustments and they'll compile |
| like any other C/C++ code, as long as SDL was handling the platform-specific |
| work for your program. |
| |
| First, you probably need this in at least one of your source files: |
| |
| ```c |
| #ifdef __EMSCRIPTEN__ |
| #include <emscripten.h> |
| #endif |
| ``` |
| |
| Second: assembly language code has to go. Replace it with C. You can even use |
| [x86 SIMD intrinsic functions in Emscripten](https://emscripten.org/docs/porting/simd.html)! |
| |
| Third: Middleware has to go. If you have a third-party library you link |
| against, you either need an Emscripten port of it, or the source code to it |
| to compile yourself, or you need to remove it. |
| |
| Fourth: You still start in a function called main(), but you need to get out of |
| it and into a function that gets called repeatedly, and returns quickly, |
| called a mainloop. |
| |
| Somewhere in your program, you probably have something that looks like a more |
| complicated version of this: |
| |
| ```c |
| void main(void) |
| { |
| initialize_the_game(); |
| while (game_is_still_running) { |
| check_for_new_input(); |
| think_about_stuff(); |
| draw_the_next_frame(); |
| } |
| deinitialize_the_game(); |
| } |
| ``` |
| |
| This will not work on Emscripten, because the main thread needs to be free |
| to do stuff and can't sit in this loop forever. So Emscripten lets you set up |
| a [mainloop](https://emscripten.org/docs/porting/emscripten-runtime-environment.html#browser-main-loop). |
| |
| ```c |
| static void mainloop(void) /* this will run often, possibly at the monitor's refresh rate */ |
| { |
| if (!game_is_still_running) { |
| deinitialize_the_game(); |
| #ifdef __EMSCRIPTEN__ |
| emscripten_cancel_main_loop(); /* this should "kill" the app. */ |
| #else |
| exit(0); |
| #endif |
| } |
| |
| check_for_new_input(); |
| think_about_stuff(); |
| draw_the_next_frame(); |
| } |
| |
| void main(void) |
| { |
| initialize_the_game(); |
| #ifdef __EMSCRIPTEN__ |
| emscripten_set_main_loop(mainloop, 0, 1); |
| #else |
| while (1) { mainloop(); } |
| #endif |
| } |
| ``` |
| |
| Basically, `emscripten_set_main_loop(mainloop, 0, 1);` says "run |
| `mainloop` over and over until I end the program." The function will |
| run, and return, freeing the main thread for other tasks, and then |
| run again when it's time. The `1` parameter does some magic to make |
| your main() function end immediately; this is useful because you |
| don't want any shutdown code that might be sitting below this code |
| to actually run if main() were to continue on, since we're just |
| getting started. |
| |
| There's a lot of little details that are beyond the scope of this |
| document, but that's the biggest intial set of hurdles to porting |
| your app to the web. |
| |
| |
| ## Do you need threads? |
| |
| If you plan to use threads, they work on all major browsers now. HOWEVER, |
| they bring with them a lot of careful considerations. Rendering _must_ |
| be done on the main thread. This is a general guideline for many |
| platforms, but a hard requirement on the web. |
| |
| Many other things also must happen on the main thread; often times SDL |
| and Emscripten make efforts to "proxy" work to the main thread that |
| must be there, but you have to be careful (and read more detailed |
| documentation than this for the finer points). |
| |
| Even when using threads, your main thread needs to set an Emscripten |
| mainloop that runs quickly and returns, or things will fail to work |
| correctly. |
| |
| You should definitely read [Emscripten's pthreads docs](https://emscripten.org/docs/porting/pthreads.html) |
| for all the finer points. Mostly SDL's thread API will work as expected, |
| but is built on pthreads, so it shares the same little incompatibilities |
| that are documented there, such as where you can use a mutex, and when |
| a thread will start running, etc. |
| |
| |
| IMPORTANT: You have to decide to either build something that uses |
| threads or something that doesn't; you can't have one build |
| that works everywhere. This is an Emscripten (or maybe WebAssembly? |
| Or just web browsers in general?) limitation. If you aren't using |
| threads, it's easier to not enable them at all, at build time. |
| |
| If you use threads, you _have to_ run from a web server that has |
| [COOP/COEP headers set correctly](https://web.dev/why-coop-coep/) |
| or your program will fail to start at all. |
| |
| If building with threads, `__EMSCRIPTEN_PTHREADS__` will be defined |
| for checking with the C preprocessor, so you can build something |
| different depending on what sort of build you're compiling. |
| |
| |
| ## Audio |
| |
| Audio works as expected at the API level, but not exactly like other |
| platforms. |
| |
| You'll only see a single default audio device. Audio capture also works; |
| if the browser pops up a prompt to ask for permission to access the |
| microphone, the SDL_OpenAudioDevice call will succeed and start producing |
| silence at a regular interval. Once the user approves the request, real |
| audio data will flow. If the user denies it, the app is not informed and |
| will just continue to receive silence. |
| |
| Modern web browsers will not permit web pages to produce sound before the |
| user has interacted with them (clicked or tapped on them, usually); this is |
| for several reasons, not the least of which being that no one likes when a |
| random browser tab suddenly starts making noise and the user has to scramble |
| to figure out which and silence it. |
| |
| SDL will allow you to open the audio device for playback in this |
| circumstance, and your audio callback will fire, but SDL will throw the audio |
| data away until the user interacts with the page. This helps apps that depend |
| on the audio callback to make progress, and also keeps audio playback in sync |
| once the app is finally allowed to make noise. |
| |
| There are two reasonable ways to deal with the silence at the app level: |
| if you are writing some sort of media player thing, where the user expects |
| there to be a volume control when you mouseover the canvas, just default |
| that control to a muted state; if the user clicks on the control to unmute |
| it, on this first click, open the audio device. This allows the media to |
| play at start, and the user can reasonably opt-in to listening. |
| |
| Many games do not have this sort of UI, and are more rigid about starting |
| audio along with everything else at the start of the process. For these, your |
| best bet is to write a little Javascript that puts up a "Click here to play!" |
| UI, and upon the user clicking, remove that UI and then call the Emscripten |
| app's main() function. As far as the application knows, the audio device was |
| available to be opened as soon as the program started, and since this magic |
| happens in a little Javascript, you don't have to change your C/C++ code at |
| all to make it happen. |
| |
| Please see the discussion at https://github.com/libsdl-org/SDL/issues/6385 |
| for some Javascript code to steal for this approach. |
| |
| |
| ## Rendering |
| |
| If you use SDL's 2D render API, it will use GLES2 internally, which |
| Emscripten will turn into WebGL calls. You can also use OpenGL ES 2 |
| directly by creating a GL context and drawing into it. |
| |
| Calling SDL_RenderPresent (or SDL_GL_SwapWindow) will not actually |
| present anything on the screen until your return from your mainloop |
| function. |
| |
| |
| ## Building SDL/emscripten |
| |
| First: do you _really_ need to build SDL from source? |
| |
| If you aren't developing SDL itself, have a desire to mess with its source |
| code, or need something on the bleeding edge, don't build SDL. Just use |
| Emscripten's packaged version! |
| |
| Compile and link your app with `-sUSE_SDL=2` and it'll use a build of |
| SDL packaged with Emscripten. This comes from the same source code and |
| fixes the Emscripten project makes to SDL are generally merged into SDL's |
| revision control, so often this is much easier for app developers. |
| |
| `-sUSE_SDL=1` will select Emscripten's JavaScript reimplementation of SDL |
| 1.2 instead; if you need SDL 1.2, this might be fine, but we generally |
| recommend you don't use SDL 1.2 in modern times. |
| |
| |
| If you want to build SDL, though... |
| |
| SDL currently requires at least Emscripten 3.1.35 to build. Newer versions |
| are likely to work, as well. |
| |
| |
| Build: |
| |
| This works on Linux/Unix and macOS. Please send comments about Windows. |
| |
| Make sure you've [installed emsdk](https://emscripten.org/docs/getting_started/downloads.html) |
| first, and run `source emsdk_env.sh` at the command line so it finds the |
| tools. |
| |
| (These configure options might be overkill, but this has worked for me.) |
| |
| ```bash |
| cd SDL |
| mkdir build |
| cd build |
| emconfigure ../configure --host=wasm32-unknown-emscripten --disable-pthreads --disable-assembly --disable-cpuinfo CFLAGS="-sUSE_SDL=0 -O3" |
| emmake make -j4 |
| ``` |
| |
| If you want to build with thread support, something like this works: |
| |
| ```bash |
| emconfigure ../configure --host=wasm32-unknown-emscripten --enable-pthreads --disable-assembly --disable-cpuinfo CFLAGS="-sUSE_SDL=0 -O3 -pthread" LDFLAGS="-pthread" |
| ``` |
| |
| Or with cmake: |
| |
| ```bash |
| mkdir build |
| cd build |
| emcmake cmake .. |
| emmake make -j4 |
| ``` |
| |
| To build one of the tests: |
| |
| ```bash |
| cd test/ |
| emcc -O2 --js-opts 0 -g4 testdraw2.c -I../include ../build/.libs/libSDL2.a ../build/libSDL2_test.a -o a.html |
| ``` |
| |
| ## Building your app |
| |
| You need to compile with `emcc` instead of `gcc` or `clang` or whatever, but |
| mostly it uses the same command line arguments as Clang. |
| |
| Link against the SDL/build/.libs/libSDL2.a file you generated by building SDL, |
| link with `-sUSE_SDL=2` to use Emscripten's prepackaged SDL2 build. |
| |
| Usually you would produce a binary like this: |
| |
| ```bash |
| gcc -o mygame mygame.c # or whatever |
| ``` |
| |
| But for Emscripten, you want to output something else: |
| |
| ```bash |
| emcc -o index.html mygame.c |
| ``` |
| |
| This will produce several files...support Javascript and WebAssembly (.wasm) |
| files. The `-o index.html` will produce a simple HTML page that loads and |
| runs your app. You will (probably) eventually want to replace or customize |
| that file and do `-o index.js` instead to just build the code pieces. |
| |
| If you're working on a program of any serious size, you'll likely need to |
| link with `-sALLOW_MEMORY_GROWTH=1 -sMAXIMUM_MEMORY=1gb` to get access |
| to more memory. If using pthreads, you'll need the `-sMAXIMUM_MEMORY=1gb` |
| or the app will fail to start on iOS browsers, but this might be a bug that |
| goes away in the future. |
| |
| |
| ## Data files |
| |
| Your game probably has data files. Here's how to access them. |
| |
| Filesystem access works like a Unix filesystem; you have a single directory |
| tree, possibly interpolated from several mounted locations, no drive letters, |
| '/' for a path separator. You can access them with standard file APIs like |
| open() or fopen() or SDL_RWops. You can read or write from the filesystem. |
| |
| By default, you probably have a "MEMFS" filesystem (all files are stored in |
| memory, but access to them is immediate and doesn't need to block). There are |
| other options, like "IDBFS" (files are stored in a local database, so they |
| don't need to be in RAM all the time and they can persist between runs of the |
| program, but access is not synchronous). You can mix and match these file |
| systems, mounting a MEMFS filesystem at one place and idbfs elsewhere, etc, |
| but that's beyond the scope of this document. Please refer to Emscripten's |
| [page on the topic](https://emscripten.org/docs/porting/files/file_systems_overview.html) |
| for more info. |
| |
| The _easiest_ (but not the best) way to get at your data files is to embed |
| them in the app itself. Emscripten's linker has support for automating this. |
| |
| ```bash |
| emcc -o index.html loopwave.c --embed-file=../test/sample.wav@/sounds/sample.wav |
| ``` |
| |
| This will pack ../test/sample.wav in your app, and make it available at |
| "/sounds/sample.wav" at runtime. Emscripten makes sure this data is available |
| before your main() function runs, and since it's in MEMFS, you can just |
| read it like you do on other platforms. `--embed-file` can also accept a |
| directory to pack an entire tree, and you can specify the argument multiple |
| times to pack unrelated things into the final installation. |
| |
| Note that this is absolutely the best approach if you have a few small |
| files to include and shouldn't worry about the issue further. However, if you |
| have hundreds of megabytes and/or thousands of files, this is not so great, |
| since the user will download it all every time they load your page, and it |
| all has to live in memory at runtime. |
| |
| [Emscripten's documentation on the matter](https://emscripten.org/docs/porting/files/packaging_files.html) |
| gives other options and details, and is worth a read. |
| |
| |
| ## Debugging |
| |
| Debugging web apps is a mixed bag. You should compile and link with |
| `-gsource-map`, which embeds a ton of source-level debugging information into |
| the build, and make sure _the app source code is available on the web server_, |
| which is often a scary proposition for various reasons. |
| |
| When you debug from the browser's tools and hit a breakpoint, you can step |
| through the actual C/C++ source code, though, which can be nice. |
| |
| If you try debugging in Firefox and it doesn't work well for no apparent |
| reason, try Chrome, and vice-versa. These tools are still relatively new, |
| and improving all the time. |
| |
| SDL_Log() (or even plain old printf) will write to the Javascript console, |
| and honestly I find printf-style debugging to be easier than setting up a build |
| for proper debugging, so use whatever tools work best for you. |
| |
| |
| ## Questions? |
| |
| Please give us feedback on this document at [the SDL bug tracker](https://github.com/libsdl-org/SDL/issues). |
| If something is wrong or unclear, we want to know! |
| |
| |
| |