Procházet zdrojové kódy

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.
Ryan C. Gordon před 1 dnem
rodič
revize
dcc177faa4

+ 10 - 0
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

+ 58 - 0
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
 

+ 2 - 0
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@

+ 8 - 4
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++) {

+ 29 - 1
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