From dcc177faa42c8c325ab5003d214fd1f3576e7a19 Mon Sep 17 00:00:00 2001
From: "Ryan C. Gordon" <[EMAIL REDACTED]>
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.
---
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 0a0aec008383a..d8afc5d4a4c06 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -393,6 +393,10 @@ set_option(SDL_CCACHE "Use Ccache to speed up build" OFF)
set_option(SDL_CLANG_TIDY "Run clang-tidy static analysis" OFF)
dep_option(SDL_GPU_OPENXR "Build SDL_GPU with OpenXR support" ON "SDL_GPU;NOT RISCOS" 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")
if(DEFINED CACHE{SDL_SHARED} OR DEFINED CACHE{SDL_STATIC})
@@ -1668,6 +1672,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"
@@ -4001,6 +4010,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 2663cdaebb807..6a373b21bac5c 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 3350723aa2eb7..267e941c4f28f 100644
--- a/include/build_config/SDL_build_config.h.cmake
+++ b/include/build_config/SDL_build_config.h.cmake
@@ -574,6 +574,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 13427fcdabeb1..0d21cd2698f50 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 0564240ac860c..5671c96ed9d59 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