emscripten: Add support for automounting persistent storage before SDL_main.

Now apps can have persistent files available during SDL_main()/SDL_AppInit()
and don't have to mess with Emscripten-specific code to prepare the filesystem
for use.

(cherry picked from commit dcc177faa4)
This commit is contained in:
Ryan C. Gordon
2026-03-25 09:58:41 -04:00
committed by Sam Lantinga
parent c546c5d335
commit 1fc5001f77
5 changed files with 107 additions and 5 deletions

View File

@@ -388,6 +388,10 @@ set_option(SDL_ASAN "Use AddressSanitizer to detect memory errors
set_option(SDL_CCACHE "Use Ccache to speed up build" OFF)
set_option(SDL_CLANG_TIDY "Run clang-tidy static analysis" OFF)
if(EMSCRIPTEN)
option_string(SDL_EMSCRIPTEN_PERSISTENT_PATH "Path to mount Emscripten IDBFS at startup or '' to disable" "")
endif()
set(SDL_VENDOR_INFO "" CACHE STRING "Vendor name and/or version to add to SDL_REVISION")
cmake_dependent_option(SDL_SHARED "Build a shared version of the library" ${SDL_SHARED_DEFAULT} ${SDL_SHARED_AVAILABLE} OFF)
@@ -1652,6 +1656,11 @@ elseif(EMSCRIPTEN)
# project. Uncomment at will for verbose cross-compiling -I/../ path info.
sdl_compile_options(PRIVATE "-Wno-warn-absolute-paths")
if(NOT SDL_EMSCRIPTEN_PERSISTENT_PATH STREQUAL "")
set(SDL_EMSCRIPTEN_PERSISTENT_PATH_STRING "${SDL_EMSCRIPTEN_PERSISTENT_PATH}")
sdl_link_dependency(idbfs LIBS idbfs.js)
endif()
sdl_glob_sources(
"${SDL3_SOURCE_DIR}/src/main/emscripten/*.c"
"${SDL3_SOURCE_DIR}/src/main/emscripten/*.h"
@@ -3982,6 +3991,7 @@ if(SDL_SHARED)
)
endif()
endif()
target_link_libraries(SDL3-shared PRIVATE ${SDL_CMAKE_DEPENDS})
target_include_directories(SDL3-shared
PRIVATE

View File

@@ -346,6 +346,64 @@ 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

View File

@@ -573,6 +573,8 @@
#cmakedefine SDL_VIDEO_VITA_PVR 1
#cmakedefine SDL_VIDEO_VITA_PVR_OGL 1
#cmakedefine SDL_EMSCRIPTEN_PERSISTENT_PATH_STRING "@SDL_EMSCRIPTEN_PERSISTENT_PATH_STRING@"
/* xkbcommon version info */
#define SDL_XKBCOMMON_VERSION_MAJOR @SDL_XKBCOMMON_VERSION_MAJOR@
#define SDL_XKBCOMMON_VERSION_MINOR @SDL_XKBCOMMON_VERSION_MINOR@

View File

@@ -39,19 +39,23 @@ char *SDL_SYS_GetBasePath(void)
char *SDL_SYS_GetPrefPath(const char *org, const char *app)
{
const char *append = "/libsdl/";
#ifdef SDL_EMSCRIPTEN_PERSISTENT_PATH_STRING
const char *append = SDL_EMSCRIPTEN_PERSISTENT_PATH_STRING;
#else
const char *append = "/libsdl";
#endif
char *result;
char *ptr = NULL;
const size_t len = SDL_strlen(append) + SDL_strlen(org) + SDL_strlen(app) + 3;
const size_t len = SDL_strlen(append) + SDL_strlen(org) + SDL_strlen(app) + 4;
result = (char *)SDL_malloc(len);
if (!result) {
return NULL;
}
if (*org) {
SDL_snprintf(result, len, "%s%s/%s/", append, org, app);
SDL_snprintf(result, len, "%s/%s/%s/", append, org, app);
} else {
SDL_snprintf(result, len, "%s%s/", append, app);
SDL_snprintf(result, len, "%s/%s/", append, app);
}
for (ptr = result + 1; *ptr; ptr++) {

View File

@@ -28,6 +28,11 @@
EM_JS_DEPS(sdlrunapp, "$dynCall,$stringToNewUTF8");
EMSCRIPTEN_KEEPALIVE int CallSDLEmscriptenMainFunction(int argc, char *argv[], SDL_main_func mainFunction)
{
return SDL_CallMainFunction(argc, argv, mainFunction);
}
int SDL_RunApp(int argc, char *argv[], SDL_main_func mainFunction, void * reserved)
{
(void)reserved;
@@ -52,7 +57,30 @@ int SDL_RunApp(int argc, char *argv[], SDL_main_func mainFunction, void * reserv
}
}, SDL_setenv_unsafe);
return SDL_CallMainFunction(argc, argv, mainFunction);
#ifdef SDL_EMSCRIPTEN_PERSISTENT_PATH_STRING
MAIN_THREAD_EM_ASM({
const persistent_path = UTF8ToString($0);
const argc = $1;
const argv = $2;
const mainFunction = $3;
//console.log("SDL is automounting persistent storage to '" + persistent_path + "' ...please wait.");
FS.mkdirTree(persistent_path);
FS.mount(IDBFS, { autoPersist: true }, persistent_path);
FS.syncfs(true, function(err) {
if (err) {
console.error(`WARNING: Failed to populate persistent store at '${persistent_path}' (${err.name}: ${err.message}). Save games likely lost?`);
}
_CallSDLEmscriptenMainFunction(argc, argv, mainFunction); // error or not, start the actual SDL_main().
});
}, SDL_EMSCRIPTEN_PERSISTENT_PATH_STRING, argc, argv, mainFunction);
// we need to stop running code until FS.syncfs() finishes, but we need the runtime to not clean up.
// The actual SDL_main/SDL_AppInit() will be called when the sync is done and things will pick back up where they were.
emscripten_exit_with_live_runtime();
return 0;
#else
return CallSDLEmscriptenMainFunction(argc, argv, mainFunction);
#endif
}
#endif