diff options
author | 3gg <3gg@shellblade.net> | 2025-08-30 16:53:58 -0700 |
---|---|---|
committer | 3gg <3gg@shellblade.net> | 2025-08-30 16:53:58 -0700 |
commit | 6aaedb813fa11ba0679c3051bc2eb28646b9506c (patch) | |
tree | 34acbfc9840e02cb4753e6306ea7ce978bf8b58e /src/contrib/SDL-3.2.20/docs/README-emscripten.md | |
parent | 8f228ade99dd3d4c8da9b78ade1815c9adf85c8f (diff) |
Update to SDL3
Diffstat (limited to 'src/contrib/SDL-3.2.20/docs/README-emscripten.md')
-rw-r--r-- | src/contrib/SDL-3.2.20/docs/README-emscripten.md | 350 |
1 files changed, 350 insertions, 0 deletions
diff --git a/src/contrib/SDL-3.2.20/docs/README-emscripten.md b/src/contrib/SDL-3.2.20/docs/README-emscripten.md new file mode 100644 index 0000000..2b81468 --- /dev/null +++ b/src/contrib/SDL-3.2.20/docs/README-emscripten.md | |||
@@ -0,0 +1,350 @@ | |||
1 | # Emscripten | ||
2 | |||
3 | ## The state of things | ||
4 | |||
5 | (As of October 2024, but things move quickly and we don't update this | ||
6 | document often.) | ||
7 | |||
8 | In modern times, all the browsers you probably care about (Chrome, Firefox, | ||
9 | Edge, and Safari, on Windows, macOS, Linux, iOS and Android), support some | ||
10 | reasonable base configurations: | ||
11 | |||
12 | - WebAssembly (don't bother with asm.js any more) | ||
13 | - WebGL (which will look like OpenGL ES 2 or 3 to your app). | ||
14 | - Threads (see caveats, though!) | ||
15 | - Game controllers | ||
16 | - Autoupdating (so you can assume they have a recent version of the browser) | ||
17 | |||
18 | All this to say we're at the point where you don't have to make a lot of | ||
19 | concessions to get even a fairly complex SDL-based game up and running. | ||
20 | |||
21 | |||
22 | ## RTFM | ||
23 | |||
24 | This document is a quick rundown of some high-level details. The | ||
25 | documentation at [emscripten.org](https://emscripten.org/) is vast | ||
26 | and extremely detailed for a wide variety of topics, and you should at | ||
27 | least skim through it at some point. | ||
28 | |||
29 | |||
30 | ## Porting your app to Emscripten | ||
31 | |||
32 | Many many things just need some simple adjustments and they'll compile | ||
33 | like any other C/C++ code, as long as SDL was handling the platform-specific | ||
34 | work for your program. | ||
35 | |||
36 | First: assembly language code has to go. Replace it with C. You can even use | ||
37 | [x86 SIMD intrinsic functions in Emscripten](https://emscripten.org/docs/porting/simd.html)! | ||
38 | |||
39 | Second: Middleware has to go. If you have a third-party library you link | ||
40 | against, you either need an Emscripten port of it, or the source code to it | ||
41 | to compile yourself, or you need to remove it. | ||
42 | |||
43 | Third: If your program starts in a function called main(), you need to get | ||
44 | out of it and into a function that gets called repeatedly, and returns quickly, | ||
45 | called a mainloop. | ||
46 | |||
47 | Somewhere in your program, you probably have something that looks like a more | ||
48 | complicated version of this: | ||
49 | |||
50 | ```c | ||
51 | void main(void) | ||
52 | { | ||
53 | initialize_the_game(); | ||
54 | while (game_is_still_running) { | ||
55 | check_for_new_input(); | ||
56 | think_about_stuff(); | ||
57 | draw_the_next_frame(); | ||
58 | } | ||
59 | deinitialize_the_game(); | ||
60 | } | ||
61 | ``` | ||
62 | |||
63 | This will not work on Emscripten, because the main thread needs to be free | ||
64 | to do stuff and can't sit in this loop forever. So Emscripten lets you set up | ||
65 | a [mainloop](https://emscripten.org/docs/porting/emscripten-runtime-environment.html#browser-main-loop). | ||
66 | |||
67 | ```c | ||
68 | static void mainloop(void) /* this will run often, possibly at the monitor's refresh rate */ | ||
69 | { | ||
70 | if (!game_is_still_running) { | ||
71 | deinitialize_the_game(); | ||
72 | #ifdef __EMSCRIPTEN__ | ||
73 | emscripten_cancel_main_loop(); /* this should "kill" the app. */ | ||
74 | #else | ||
75 | exit(0); | ||
76 | #endif | ||
77 | } | ||
78 | |||
79 | check_for_new_input(); | ||
80 | think_about_stuff(); | ||
81 | draw_the_next_frame(); | ||
82 | } | ||
83 | |||
84 | void main(void) | ||
85 | { | ||
86 | initialize_the_game(); | ||
87 | #ifdef __EMSCRIPTEN__ | ||
88 | emscripten_set_main_loop(mainloop, 0, 1); | ||
89 | #else | ||
90 | while (1) { mainloop(); } | ||
91 | #endif | ||
92 | } | ||
93 | ``` | ||
94 | |||
95 | Basically, `emscripten_set_main_loop(mainloop, 0, 1);` says "run | ||
96 | `mainloop` over and over until I end the program." The function will | ||
97 | run, and return, freeing the main thread for other tasks, and then | ||
98 | run again when it's time. The `1` parameter does some magic to make | ||
99 | your main() function end immediately; this is useful because you | ||
100 | don't want any shutdown code that might be sitting below this code | ||
101 | to actually run if main() were to continue on, since we're just | ||
102 | getting started. | ||
103 | |||
104 | Another option is to use SDL' main callbacks, which handle this for you | ||
105 | without platform-specific code in your app. Please refer to | ||
106 | [the wiki](https://wiki.libsdl.org/SDL3/README/main-functions#main-callbacks-in-sdl3) | ||
107 | or `docs/README-main-functions.md` in the SDL source code. | ||
108 | |||
109 | |||
110 | |||
111 | There's a lot of little details that are beyond the scope of this | ||
112 | document, but that's the biggest initial set of hurdles to porting | ||
113 | your app to the web. | ||
114 | |||
115 | |||
116 | ## Do you need threads? | ||
117 | |||
118 | If you plan to use threads, they work on all major browsers now. HOWEVER, | ||
119 | they bring with them a lot of careful considerations. Rendering _must_ | ||
120 | be done on the main thread. This is a general guideline for many | ||
121 | platforms, but a hard requirement on the web. | ||
122 | |||
123 | Many other things also must happen on the main thread; often times SDL | ||
124 | and Emscripten make efforts to "proxy" work to the main thread that | ||
125 | must be there, but you have to be careful (and read more detailed | ||
126 | documentation than this for the finer points). | ||
127 | |||
128 | Even when using threads, your main thread needs to set an Emscripten | ||
129 | mainloop (or use SDL's main callbacks) that runs quickly and returns, or | ||
130 | things will fail to work correctly. | ||
131 | |||
132 | You should definitely read [Emscripten's pthreads docs](https://emscripten.org/docs/porting/pthreads.html) | ||
133 | for all the finer points. Mostly SDL's thread API will work as expected, | ||
134 | but is built on pthreads, so it shares the same little incompatibilities | ||
135 | that are documented there, such as where you can use a mutex, and when | ||
136 | a thread will start running, etc. | ||
137 | |||
138 | |||
139 | IMPORTANT: You have to decide to either build something that uses | ||
140 | threads or something that doesn't; you can't have one build | ||
141 | that works everywhere. This is an Emscripten (or maybe WebAssembly? | ||
142 | Or just web browsers in general?) limitation. If you aren't using | ||
143 | threads, it's easier to not enable them at all, at build time. | ||
144 | |||
145 | If you use threads, you _have to_ run from a web server that has | ||
146 | [COOP/COEP headers set correctly](https://web.dev/why-coop-coep/) | ||
147 | or your program will fail to start at all. | ||
148 | |||
149 | If building with threads, `__EMSCRIPTEN_PTHREADS__` will be defined | ||
150 | for checking with the C preprocessor, so you can build something | ||
151 | different depending on what sort of build you're compiling. | ||
152 | |||
153 | |||
154 | ## Audio | ||
155 | |||
156 | Audio works as expected at the API level, but not exactly like other | ||
157 | platforms. | ||
158 | |||
159 | You'll only see a single default audio device. Audio recording also works; | ||
160 | if the browser pops up a prompt to ask for permission to access the | ||
161 | microphone, the SDL_OpenAudioDevice call will succeed and start producing | ||
162 | silence at a regular interval. Once the user approves the request, real | ||
163 | audio data will flow. If the user denies it, the app is not informed and | ||
164 | will just continue to receive silence. | ||
165 | |||
166 | Modern web browsers will not permit web pages to produce sound before the | ||
167 | user has interacted with them (clicked or tapped on them, usually); this is | ||
168 | for several reasons, not the least of which being that no one likes when a | ||
169 | random browser tab suddenly starts making noise and the user has to scramble | ||
170 | to figure out which and silence it. | ||
171 | |||
172 | SDL will allow you to open the audio device for playback in this | ||
173 | circumstance, and your audio callback will fire, but SDL will throw the audio | ||
174 | data away until the user interacts with the page. This helps apps that depend | ||
175 | on the audio callback to make progress, and also keeps audio playback in sync | ||
176 | once the app is finally allowed to make noise. | ||
177 | |||
178 | There are two reasonable ways to deal with the silence at the app level: | ||
179 | if you are writing some sort of media player thing, where the user expects | ||
180 | there to be a volume control when you mouseover the canvas, just default | ||
181 | that control to a muted state; if the user clicks on the control to unmute | ||
182 | it, on this first click, open the audio device. This allows the media to | ||
183 | play at start, and the user can reasonably opt-in to listening. | ||
184 | |||
185 | Many games do not have this sort of UI, and are more rigid about starting | ||
186 | audio along with everything else at the start of the process. For these, your | ||
187 | best bet is to write a little Javascript that puts up a "Click here to play!" | ||
188 | UI, and upon the user clicking, remove that UI and then call the Emscripten | ||
189 | app's main() function. As far as the application knows, the audio device was | ||
190 | available to be opened as soon as the program started, and since this magic | ||
191 | happens in a little Javascript, you don't have to change your C/C++ code at | ||
192 | all to make it happen. | ||
193 | |||
194 | Please see the discussion at https://github.com/libsdl-org/SDL/issues/6385 | ||
195 | for some Javascript code to steal for this approach. | ||
196 | |||
197 | |||
198 | ## Rendering | ||
199 | |||
200 | If you use SDL's 2D render API, it will use GLES2 internally, which | ||
201 | Emscripten will turn into WebGL calls. You can also use OpenGL ES 2 | ||
202 | directly by creating a GL context and drawing into it. | ||
203 | |||
204 | If the browser (and hardware) support WebGL 2, you can create an OpenGL ES 3 | ||
205 | context. | ||
206 | |||
207 | Calling SDL_RenderPresent (or SDL_GL_SwapWindow) will not actually | ||
208 | present anything on the screen until your return from your mainloop | ||
209 | function. | ||
210 | |||
211 | |||
212 | ## Building SDL/emscripten | ||
213 | |||
214 | |||
215 | SDL currently requires at least Emscripten 3.16.0 to build. Newer versions | ||
216 | are likely to work, as well. | ||
217 | |||
218 | |||
219 | Build: | ||
220 | |||
221 | This works on Linux/Unix and macOS. Please send comments about Windows. | ||
222 | |||
223 | Make sure you've [installed emsdk](https://emscripten.org/docs/getting_started/downloads.html) | ||
224 | first, and run `source emsdk_env.sh` at the command line so it finds the | ||
225 | tools. | ||
226 | |||
227 | (These cmake options might be overkill, but this has worked for me.) | ||
228 | |||
229 | ```bash | ||
230 | mkdir build | ||
231 | cd build | ||
232 | emcmake cmake .. | ||
233 | # you can also do `emcmake cmake -G Ninja ..` and then use `ninja` instead of this command. | ||
234 | emmake make -j4 | ||
235 | ``` | ||
236 | |||
237 | If you want to build with thread support, something like this works: | ||
238 | |||
239 | ```bash | ||
240 | mkdir build | ||
241 | cd build | ||
242 | emcmake cmake -DSDL_THREADS=ON .. | ||
243 | # you can also do `emcmake cmake -G Ninja ..` and then use `ninja` instead of this command. | ||
244 | emmake make -j4 | ||
245 | ``` | ||
246 | |||
247 | To build the tests, add `-DSDL_TESTS=ON` to the `emcmake cmake` command line. | ||
248 | To build the examples, add `-DSDL_EXAMPLES=ON` to the `emcmake cmake` command line. | ||
249 | |||
250 | |||
251 | ## Building your app | ||
252 | |||
253 | You need to compile with `emcc` instead of `gcc` or `clang` or whatever, but | ||
254 | mostly it uses the same command line arguments as Clang. | ||
255 | |||
256 | Link against the libSDL3.a file you generated by building SDL. | ||
257 | |||
258 | Usually you would produce a binary like this: | ||
259 | |||
260 | ```bash | ||
261 | gcc -o mygame mygame.c # or whatever | ||
262 | ``` | ||
263 | |||
264 | But for Emscripten, you want to output something else: | ||
265 | |||
266 | ```bash | ||
267 | emcc -o index.html mygame.c | ||
268 | ``` | ||
269 | |||
270 | This will produce several files...support Javascript and WebAssembly (.wasm) | ||
271 | files. The `-o index.html` will produce a simple HTML page that loads and | ||
272 | runs your app. You will (probably) eventually want to replace or customize | ||
273 | that file and do `-o index.js` instead to just build the code pieces. | ||
274 | |||
275 | If you're working on a program of any serious size, you'll likely need to | ||
276 | link with `-s ALLOW_MEMORY_GROWTH=1 -s MAXIMUM_MEMORY=1gb` to get access | ||
277 | to more memory. If using pthreads, you'll need the `-s MAXIMUM_MEMORY=1gb` | ||
278 | or the app will fail to start on iOS browsers, but this might be a bug that | ||
279 | goes away in the future. | ||
280 | |||
281 | |||
282 | ## Data files | ||
283 | |||
284 | Your game probably has data files. Here's how to access them. | ||
285 | |||
286 | Filesystem access works like a Unix filesystem; you have a single directory | ||
287 | tree, possibly interpolated from several mounted locations, no drive letters, | ||
288 | '/' for a path separator. You can access them with standard file APIs like | ||
289 | open() or fopen() or SDL_IOStream. You can read or write from the filesystem. | ||
290 | |||
291 | By default, you probably have a "MEMFS" filesystem (all files are stored in | ||
292 | memory, but access to them is immediate and doesn't need to block). There are | ||
293 | other options, like "IDBFS" (files are stored in a local database, so they | ||
294 | don't need to be in RAM all the time and they can persist between runs of the | ||
295 | program, but access is not synchronous). You can mix and match these file | ||
296 | systems, mounting a MEMFS filesystem at one place and idbfs elsewhere, etc, | ||
297 | but that's beyond the scope of this document. Please refer to Emscripten's | ||
298 | [page on the topic](https://emscripten.org/docs/porting/files/file_systems_overview.html) | ||
299 | for more info. | ||
300 | |||
301 | The _easiest_ (but not the best) way to get at your data files is to embed | ||
302 | them in the app itself. Emscripten's linker has support for automating this. | ||
303 | |||
304 | ```bash | ||
305 | emcc -o index.html loopwave.c --embed-file ../test/sample.wav@/sounds/sample.wav | ||
306 | ``` | ||
307 | |||
308 | This will pack ../test/sample.wav in your app, and make it available at | ||
309 | "/sounds/sample.wav" at runtime. Emscripten makes sure this data is available | ||
310 | before your main() function runs, and since it's in MEMFS, you can just | ||
311 | read it like you do on other platforms. `--embed-file` can also accept a | ||
312 | directory to pack an entire tree, and you can specify the argument multiple | ||
313 | times to pack unrelated things into the final installation. | ||
314 | |||
315 | Note that this is absolutely the best approach if you have a few small | ||
316 | files to include and shouldn't worry about the issue further. However, if you | ||
317 | have hundreds of megabytes and/or thousands of files, this is not so great, | ||
318 | since the user will download it all every time they load your page, and it | ||
319 | all has to live in memory at runtime. | ||
320 | |||
321 | [Emscripten's documentation on the matter](https://emscripten.org/docs/porting/files/packaging_files.html) | ||
322 | gives other options and details, and is worth a read. | ||
323 | |||
324 | |||
325 | ## Debugging | ||
326 | |||
327 | Debugging web apps is a mixed bag. You should compile and link with | ||
328 | `-gsource-map`, which embeds a ton of source-level debugging information into | ||
329 | the build, and make sure _the app source code is available on the web server_, | ||
330 | which is often a scary proposition for various reasons. | ||
331 | |||
332 | When you debug from the browser's tools and hit a breakpoint, you can step | ||
333 | through the actual C/C++ source code, though, which can be nice. | ||
334 | |||
335 | If you try debugging in Firefox and it doesn't work well for no apparent | ||
336 | reason, try Chrome, and vice-versa. These tools are still relatively new, | ||
337 | and improving all the time. | ||
338 | |||
339 | SDL_Log() (or even plain old printf) will write to the Javascript console, | ||
340 | and honestly I find printf-style debugging to be easier than setting up a build | ||
341 | for proper debugging, so use whatever tools work best for you. | ||
342 | |||
343 | |||
344 | ## Questions? | ||
345 | |||
346 | Please give us feedback on this document at [the SDL bug tracker](https://github.com/libsdl-org/SDL/issues). | ||
347 | If something is wrong or unclear, we want to know! | ||
348 | |||
349 | |||
350 | |||