00cc9309cb
de6e324bdseparate emu thread10d3daf86Roms List improvements95d202f37Let's make the rom list process on a separate thread so the emulator doesnt take ages to load.fc306967fWow the ROM Header was just completely busted. Game list view works nowbad1691eefuck this shit2b59e5f46game list in progressd26417b83remappable inputs in progressac4af8106inpute72abc240update readme430139dc9Qt6 frontend3080d4d45Fix this small bug too08cd13b85Cop0 unused functions do not actually pose a threat (as per manual). They don't do anything, so shall we.61bb4fb44make idle loop detection a little more specific with where the load goesb037de4c3SAZDFsdff12e81e73eneed to figure out why n64-systemtest loops indefinitely at some address that appears to be valid (i think it's me not invalidating the cache properly)204f0e13bidle skipping seems to work!cb8bb634asdkfjlasdf58e5c89c1Fix compilation issue on my machine (no idea)24fb2898eattempting more serious idle skipping214719577Place rsp.Step inside cached interpreter. Gains about 3 more fpsbb97dcc23mmmmm920b77d38wjkhasdfjhkasdf430ccdab4it's a start...4f42a673aCached interpreter plays Mario 64. Start looking into RSP as wellc9a030787idle skipping works!5fbda03cenew idea366637abaIdle skipping... maybe?609fa2fb0Cache instructions implemented but broken lmao. Commented out for nowe140a6d12- Stop using inheritance for CPU, instead use composition. - Introduce KAIZEN_JIT_ENABLED optional define instead of relying on __aarch64__ and the like. - More cache work68e613057prep cache impl811b4d809fix clang formatfda755f7didkd5024ebbfsmall MI refactor in preparation of (eventually) implementing the RDRAM interface properly694b45341Merge commit '206dcdedf195fb320913584180edb12c7731e396' as 'external/SDL'206dcdedfSquashed 'external/SDL/' content from commit 4d17b99d0a4d16e1cb4need to update sdl848b19920Fix compilation errordb61b5299Merge commit 'e94a94559f28e49678fbcf72199a5258137b0fe9' as 'external/imgui'e94a94559Squashed 'external/imgui/' content from commit 02e9b8cac52edb3757need to update imguic1a705e86Emulate weird JALR behaviour4b4c32f4bFix exception for "unusable COP1" in 4 instructions i missed accidentally (again)df5828142Bug putting 0s in the log everywheref8b580048Make isviewer a sink to file8241e9735Fix exception for "unusable COP1" in 4 instructions i missed accidentallyb29715f20small changesd9a620bc1make use of my new small utility library0d1aa938eAdd 'external/ircolib/' from commit 'ce3cd726c8df8388d554abf8bb55d55020eb4450'e64eb40b3Fuck git git-subtree-dir: external/ircolib git-subtree-split:de6e324bde
450 lines
18 KiB
Markdown
450 lines
18 KiB
Markdown
# Emscripten
|
|
|
|
## The state of things
|
|
|
|
(As of October 2024, 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: 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)!
|
|
|
|
Second: 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.
|
|
|
|
Third: If your program starts in a function called main(), 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.
|
|
|
|
Another option is to use SDL's main callbacks, which handle this for you
|
|
without platform-specific code in your app. Please refer to
|
|
[the wiki](https://wiki.libsdl.org/SDL3/README-main-functions#main-callbacks-in-sdl3)
|
|
or `docs/README-main-functions.md` in the SDL source code.
|
|
|
|
|
|
|
|
There's a lot of little details that are beyond the scope of this
|
|
document, but that's the biggest initial 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 (or use SDL's main callbacks) 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 recording 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 streams will consume data, 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, adjust your app's volume appropriately, and SDL will
|
|
also start actually feeding the data to the browser. 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. For these, your best bet might be 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, audio was able to play 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.
|
|
|
|
But if a game can just do without audio until the user clicks on the page,
|
|
it will still operate correctly, as if the page was merely muted before then.
|
|
|
|
|
|
## 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.
|
|
|
|
If the browser (and hardware) support WebGL 2, you can create an OpenGL ES 3
|
|
context.
|
|
|
|
Calling SDL_RenderPresent (or SDL_GL_SwapWindow) will not actually
|
|
present anything on the screen until your return from your mainloop
|
|
function.
|
|
|
|
Note that SDL attempts to default to vsync _off_ on all platforms. You almost
|
|
certainly do _not_ want this in Emscripten, however, as it will affect the
|
|
efficiency of the mainloop. If using OpenGL directly, you should call
|
|
SDL_GL_SetSwapInterval(1) sometime near startup; if using the 2D render API,
|
|
either create the renderer with with the property
|
|
SDL_PROP_RENDERER_CREATE_PRESENT_VSYNC_NUMBER set to 1, or call
|
|
SDL_SetRenderVSync(renderer, 1). If you don't explicitly set vsync, you'll get
|
|
a higher (but perhaps unstable) framerate, and use more power, but it will
|
|
still work. Choosing a vsync of 1 will use requestAnimationFrame if possible.
|
|
|
|
If you're using the SDL main callbacks, the mainloop defaults to using
|
|
requestAnimationFrame (effectively vsync), because it calls
|
|
emscripten_set_main_loop() with a zero fps. This is almost certainly what you
|
|
want to do! Do this even if you aren't using the main callbacks!
|
|
SDL will attempt to accommodate the app if it messes with vsync settings, or
|
|
doesn't use requestAnimationFrame, but modern thinking is that this is the
|
|
most efficient, consistent, and correct way to run a game in a web browser.
|
|
|
|
|
|
## Building SDL/emscripten
|
|
|
|
Use the latest stable Emscripten release!
|
|
|
|
It's possible to build SDL with older Emscripten releases, such as 3.x, but
|
|
several things will be silently broken, as bugs got fixed and web standards
|
|
solidified over time. At the time of this writing, Emscripten 4.0.x is the
|
|
current stable release. You're encouraged to install the latest stable release
|
|
(`emsdk install latest ; emsdk activate latest` if using Emscripten's setup
|
|
script), and make sure you're reasonably up to date as time goes on.
|
|
|
|
|
|
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 cmake options might be overkill, but this has worked for me.)
|
|
|
|
```bash
|
|
mkdir build
|
|
cd build
|
|
emcmake cmake ..
|
|
# you can also try `emcmake cmake -G Ninja ..` and then use `ninja` instead of this command.
|
|
emmake make -j4
|
|
```
|
|
|
|
If you want to build with thread support, something like this works:
|
|
|
|
```bash
|
|
mkdir build
|
|
cd build
|
|
emcmake cmake -DSDL_PTHREADS=ON ..
|
|
# you can also do `emcmake cmake -G Ninja ..` and then use `ninja` instead of this command.
|
|
emmake make -j4
|
|
```
|
|
|
|
To build the tests, add `-DSDL_TESTS=ON` to the `emcmake cmake` command line.
|
|
To build the examples, add `-DSDL_EXAMPLES=ON` to the `emcmake cmake` command line.
|
|
|
|
|
|
## 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 libSDL3.a file you generated by building SDL.
|
|
|
|
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 `-s ALLOW_MEMORY_GROWTH=1 -s MAXIMUM_MEMORY=1gb` to get access
|
|
to more memory. If using pthreads, you'll need the `-s MAXIMUM_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_IOStream. 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.
|
|
|
|
Please also read the next section on persistent storage, for a little help
|
|
from SDL.
|
|
|
|
|
|
## Automount persistent storage
|
|
|
|
The file tree in Emscripten is provided by MEMFS by default, which stores all
|
|
files in RAM. This is often what you want, because it's fast and can be
|
|
accessed with the usual synchronous i/o functions like fopen or SDL_IOFromFile.
|
|
You can also write files to MEMFS, but when the browser tab goes away, so do
|
|
the files. But we want things like high scores, save games, etc, to still
|
|
exist if we reload the game later.
|
|
|
|
For this, Emscripten offers IDBFS, which backs files with the browser's
|
|
[IndexedDB](https://en.wikipedia.org/wiki/IndexedDB) functionality.
|
|
|
|
To use this, the app has to mount the IDBFS filesystem somewhere in the
|
|
virtual file tree, and then wait for it to sync up. This needs to be done in
|
|
Javascript code. The sync will not complete until at least one (but possibly
|
|
several) iterations of the mainloop have passed, which means you can not
|
|
access any saved files during main() or SDL_AppInit() by default.
|
|
|
|
SDL can solve this problem for you: it can be built to automatically mount the
|
|
persistent files from IDBFS to a specific place in the file tree and wait
|
|
until the sync has completed before calling main() or SDL_AppInit(), so to
|
|
your C code, it looks like the files were always available.
|
|
|
|
To use this functionality, set the CMake variable
|
|
`SDL_EMSCRIPTEN_PERSISTENT_PATH` to a path in the filetree where persistent
|
|
storage should be mounted:
|
|
|
|
```bash
|
|
mkdir build
|
|
cd build
|
|
emcmake cmake -DSDL_EMSCRIPTEN_PERSISTENT_PATH=/storage ..
|
|
```
|
|
|
|
You should also link your app with `-lidbfs.js`. If your project links to SDL
|
|
using CMake's find_package(SDL3), or uses `pkg-config sdl3 --libs`, this will
|
|
be handled for you when used with an SDL built with
|
|
`-DSDL_EMSCRIPTEN_PERSISTENT_PATH`.
|
|
|
|
Now `/storage` will be prepared when your program runs, and SDL_GetPrefPath()
|
|
will return a directory under that path. The storage is mounted with the
|
|
`autoPersist: true` option, so when you write to that tree, whether with
|
|
SDL APIs or other functions like fopen(), Emscripten will know it needs to
|
|
sync that data back to the persistent database, and will do so automatically
|
|
within the next few iterations of the mainloop.
|
|
|
|
It's best to assume the sync will take a few frames to complete, and the
|
|
data is not safe until it does.
|
|
|
|
To summarize how to automate this:
|
|
|
|
- Build with `emcmake cmake -DSDL_EMSCRIPTEN_PERSISTENT_PATH=/storage`
|
|
- Link your app with `-lidbfs.js` if not handled automatically.
|
|
- Write under `/storage`, or use SDL_GetPrefPath()
|
|
|
|
|
|
## Customizing index.html
|
|
|
|
You don't have to use the HTML that Emscripten produces; the above examples
|
|
use `emcc -o index.html`, but you can `-o index.js` instead to just output
|
|
code without an HTML page, and then provide your own. This is desirable for
|
|
shipping products, even though the Emscripten-provided HTML is fine for
|
|
prototyping. Certain things _must_ be in the HTML file or your program will
|
|
not function correctly (or function at all). The specifics are beyond the
|
|
scope of this document, but it's likely best to start with the Emscripten HTML
|
|
and customize it, instead of starting from scratch.
|
|
|
|
The `<canvas>` element in the HTML _must not_ have a border or padding, or
|
|
things will break in unexpected ways. This can be surprising when customizing
|
|
the page's look. Plan accordingly.
|
|
|
|
|
|
## 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 printf) will write to the Javascript console,
|
|
so printf-style debugging can be easier than setting up a build
|
|
for proper debugging. 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!
|
|
|
|
|
|
|