From 1fc5001f77d50819c66bbe87b7e266fee247973f Mon Sep 17 00:00:00 2001 From: "Ryan C. Gordon" Date: Wed, 25 Mar 2026 09:58:41 -0400 Subject: [PATCH] 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 dcc177faa42c8c325ab5003d214fd1f3576e7a19) --- CMakeLists.txt | 10 ++++ docs/README-emscripten.md | 58 +++++++++++++++++++ include/build_config/SDL_build_config.h.cmake | 2 + src/filesystem/emscripten/SDL_sysfilesystem.c | 12 ++-- src/main/emscripten/SDL_sysmain_runapp.c | 30 +++++++++- 5 files changed, 107 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index bca6faaef5..2d40822f0d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/docs/README-emscripten.md b/docs/README-emscripten.md index b9667f1ed5..ae2af4a7fd 100644 --- a/docs/README-emscripten.md +++ b/docs/README-emscripten.md @@ -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 diff --git a/include/build_config/SDL_build_config.h.cmake b/include/build_config/SDL_build_config.h.cmake index 5d4e0717ef..520b721771 100644 --- a/include/build_config/SDL_build_config.h.cmake +++ b/include/build_config/SDL_build_config.h.cmake @@ -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@ diff --git a/src/filesystem/emscripten/SDL_sysfilesystem.c b/src/filesystem/emscripten/SDL_sysfilesystem.c index 13427fcdab..0d21cd2698 100644 --- a/src/filesystem/emscripten/SDL_sysfilesystem.c +++ b/src/filesystem/emscripten/SDL_sysfilesystem.c @@ -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++) { diff --git a/src/main/emscripten/SDL_sysmain_runapp.c b/src/main/emscripten/SDL_sysmain_runapp.c index 0564240ac8..5671c96ed9 100644 --- a/src/main/emscripten/SDL_sysmain_runapp.c +++ b/src/main/emscripten/SDL_sysmain_runapp.c @@ -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