SDL: Add SDL_storage

From 744227e6abee4ec84aca758b0531c179025f5842 Mon Sep 17 00:00:00 2001
From: Ethan Lee <[EMAIL REDACTED]>
Date: Fri, 8 Mar 2024 14:38:46 -0500
Subject: [PATCH] Add SDL_storage

---
 CMakeLists.txt                                |  30 ++
 VisualC-GDK/SDL/SDL.vcxproj                   |   3 +
 VisualC-WinRT/SDL-UWP.vcxproj                 |   3 +
 VisualC/SDL/SDL.vcxproj                       |   4 +
 include/SDL3/SDL.h                            |   1 +
 include/SDL3/SDL_hints.h                      |  18 ++
 include/SDL3/SDL_storage.h                    | 276 ++++++++++++++++++
 include/build_config/SDL_build_config.h.cmake |   4 +
 src/dynapi/SDL_dynapi.sym                     |   8 +
 src/dynapi/SDL_dynapi_overrides.h             |   8 +
 src/dynapi/SDL_dynapi_procs.h                 |   8 +
 src/storage/SDL_storage.c                     | 222 ++++++++++++++
 src/storage/SDL_sysstorage.h                  |  49 ++++
 src/storage/generic/SDL_genericstorage.c      | 188 ++++++++++++
 src/storage/steam/SDL_steamstorage.c          | 199 +++++++++++++
 src/storage/steam/SDL_steamstorage_proc.h     |  14 +
 16 files changed, 1035 insertions(+)
 create mode 100644 include/SDL3/SDL_storage.h
 create mode 100644 src/storage/SDL_storage.c
 create mode 100644 src/storage/SDL_sysstorage.h
 create mode 100644 src/storage/generic/SDL_genericstorage.c
 create mode 100644 src/storage/steam/SDL_steamstorage.c
 create mode 100644 src/storage/steam/SDL_steamstorage_proc.h

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 408c6e88dc0c3..fcbb93fc3d792 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -494,6 +494,7 @@ sdl_glob_sources(
   "${SDL3_SOURCE_DIR}/src/render/*/*.c"
   "${SDL3_SOURCE_DIR}/src/sensor/*.c"
   "${SDL3_SOURCE_DIR}/src/stdlib/*.c"
+  "${SDL3_SOURCE_DIR}/src/storage/*.c"
   "${SDL3_SOURCE_DIR}/src/thread/*.c"
   "${SDL3_SOURCE_DIR}/src/timer/*.c"
   "${SDL3_SOURCE_DIR}/src/video/*.c"
@@ -1753,6 +1754,14 @@ elseif(UNIX AND NOT APPLE AND NOT RISCOS AND NOT HAIKU)
   sdl_glob_sources("${SDL3_SOURCE_DIR}/src/filesystem/unix/*.c")
   set(HAVE_SDL_FILESYSTEM TRUE)
 
+  set(SDL_STORAGE_GENERIC 1)
+  sdl_glob_sources("${SDL3_SOURCE_DIR}/src/storage/generic/*.c")
+  if(LINUX)
+    set(SDL_STORAGE_STEAM 1)
+    sdl_glob_sources("${SDL3_SOURCE_DIR}/src/storage/steam/*.c")
+  endif()
+  set(HAVE_SDL_STORAGE 1)
+
   set(SDL_TIMER_UNIX 1)
   sdl_glob_sources("${SDL3_SOURCE_DIR}/src/timer/unix/*.c")
   set(HAVE_SDL_TIMERS TRUE)
@@ -1972,6 +1981,14 @@ elseif(WINDOWS)
   endif()
   set(HAVE_SDL_FILESYSTEM TRUE)
 
+  set(SDL_STORAGE_GENERIC 1)
+  sdl_glob_sources("${SDL3_SOURCE_DIR}/src/storage/generic/*.c")
+  if(NOT WINDOWS_STORE)
+    set(SDL_STORAGE_STEAM 1)
+    sdl_glob_sources("${SDL3_SOURCE_DIR}/src/storage/steam/*.c")
+  endif()
+  set(HAVE_SDL_STORAGE 1)
+
   # Libraries for Win32 native and MinGW
   if(NOT WINDOWS_STORE)
     sdl_link_dependency(base LIBS kernel32 user32 gdi32 winmm imm32 ole32 oleaut32 version uuid advapi32 setupapi shell32)
@@ -2204,6 +2221,15 @@ elseif(APPLE)
   sdl_glob_sources("${SDL3_SOURCE_DIR}/src/filesystem/cocoa/*.m")
   set(HAVE_SDL_FILESYSTEM TRUE)
 
+  # TODO: SDL_STORAGE_ICLOUD
+  set(SDL_STORAGE_GENERIC 1)
+  sdl_glob_sources("${SDL3_SOURCE_DIR}/src/storage/generic/*.c")
+  if(DARWIN OR MACOSX)
+    set(SDL_STORAGE_STEAM 1)
+    sdl_glob_sources("${SDL3_SOURCE_DIR}/src/storage/steam/*.c")
+  endif()
+  set(HAVE_SDL_STORAGE 1)
+
   if(SDL_SENSOR)
     if(IOS OR VISIONOS)
       set(SDL_SENSOR_COREMOTION 1)
@@ -2820,6 +2846,10 @@ if(NOT HAVE_SDL_FILESYSTEM)
   set(SDL_FILESYSTEM_DUMMY 1)
   sdl_glob_sources("${SDL3_SOURCE_DIR}/src/filesystem/dummy/*.c")
 endif()
+if(NOT HAVE_SDL_STORAGE)
+  set(SDL_STORAGE_GENERIC 1)
+  sdl_glob_sources("${SDL3_SOURCE_DIR}/src/storage/generic/*.c")
+endif()
 if(NOT HAVE_SDL_LOCALE)
   set(SDL_LOCALE_DUMMY 1)
   sdl_glob_sources("${SDL3_SOURCE_DIR}/src/locale/dummy/*.c")
diff --git a/VisualC-GDK/SDL/SDL.vcxproj b/VisualC-GDK/SDL/SDL.vcxproj
index 8b8fdad0b0088..e50832cb1a827 100644
--- a/VisualC-GDK/SDL/SDL.vcxproj
+++ b/VisualC-GDK/SDL/SDL.vcxproj
@@ -368,6 +368,7 @@
     <ClInclude Include="..\..\include\SDL3\SDL_scancode.h" />
     <ClInclude Include="..\..\include\SDL3\SDL_sensor.h" />
     <ClInclude Include="..\..\include\SDL3\SDL_stdinc.h" />
+    <ClInclude Include="..\..\include\SDL3\SDL_storage.h" />
     <ClInclude Include="..\..\include\SDL3\SDL_surface.h" />
     <ClInclude Include="..\..\include\SDL3\SDL_system.h" />
     <ClInclude Include="..\..\include\SDL3\SDL_test.h" />
@@ -778,6 +779,8 @@
     <ClCompile Include="..\..\src\stdlib\SDL_stdlib.c" />
     <ClCompile Include="..\..\src\stdlib\SDL_string.c" />
     <ClCompile Include="..\..\src\stdlib\SDL_strtokr.c" />
+    <ClCompile Include="..\..\src\storage\generic\SDL_genericstorage.c" />
+    <ClCompile Include="..\..\src\storage\SDL_storage.c" />
     <ClCompile Include="..\..\src\thread\generic\SDL_syscond.c" />
     <ClCompile Include="..\..\src\thread\generic\SDL_sysrwlock.c" />
     <ClCompile Include="..\..\src\thread\SDL_thread.c" />
diff --git a/VisualC-WinRT/SDL-UWP.vcxproj b/VisualC-WinRT/SDL-UWP.vcxproj
index 807697af5433a..79d7a609786ea 100644
--- a/VisualC-WinRT/SDL-UWP.vcxproj
+++ b/VisualC-WinRT/SDL-UWP.vcxproj
@@ -82,6 +82,7 @@
     <ClInclude Include="..\include\SDL3\SDL_scancode.h" />
     <ClInclude Include="..\include\SDL3\SDL_sensor.h" />
     <ClInclude Include="..\include\SDL3\SDL_stdinc.h" />
+    <ClInclude Include="..\include\SDL3\SDL_storage.h" />
     <ClInclude Include="..\include\SDL3\SDL_surface.h" />
     <ClInclude Include="..\include\SDL3\SDL_system.h" />
     <ClInclude Include="..\include\SDL3\SDL_thread.h" />
@@ -439,6 +440,8 @@
     <ClCompile Include="..\src\stdlib\SDL_stdlib.c" />
     <ClCompile Include="..\src\stdlib\SDL_string.c" />
     <ClCompile Include="..\src\stdlib\SDL_strtokr.c" />
+    <ClCompile Include="..\src\storage\generic\SDL_genericstorage.c" />
+    <ClCompile Include="..\src\storage\SDL_storage.c" />
     <ClCompile Include="..\src\thread\generic\SDL_syssem.c" />
     <ClCompile Include="..\src\thread\SDL_thread.c" />
     <ClCompile Include="..\src\thread\stdcpp\SDL_syscond.cpp">
diff --git a/VisualC/SDL/SDL.vcxproj b/VisualC/SDL/SDL.vcxproj
index f923aba8411b0..504e3f9c00f5e 100644
--- a/VisualC/SDL/SDL.vcxproj
+++ b/VisualC/SDL/SDL.vcxproj
@@ -291,6 +291,7 @@
     <ClInclude Include="..\..\include\SDL3\SDL_scancode.h" />
     <ClInclude Include="..\..\include\SDL3\SDL_sensor.h" />
     <ClInclude Include="..\..\include\SDL3\SDL_stdinc.h" />
+    <ClInclude Include="..\..\include\SDL3\SDL_storage.h" />
     <ClInclude Include="..\..\include\SDL3\SDL_surface.h" />
     <ClInclude Include="..\..\include\SDL3\SDL_system.h" />
     <ClInclude Include="..\..\include\SDL3\SDL_test.h" />
@@ -635,6 +636,9 @@
     <ClCompile Include="..\..\src\stdlib\SDL_stdlib.c" />
     <ClCompile Include="..\..\src\stdlib\SDL_string.c" />
     <ClCompile Include="..\..\src\stdlib\SDL_strtokr.c" />
+    <ClCompile Include="..\..\src\storage\generic\SDL_genericstorage.c" />
+    <ClCompile Include="..\..\src\storage\steam\SDL_steamstorage.c" />
+    <ClCompile Include="..\..\src\storage\SDL_storage.c" />
     <ClCompile Include="..\..\src\thread\generic\SDL_syscond.c" />
     <ClCompile Include="..\..\src\thread\generic\SDL_sysrwlock.c" />
     <ClCompile Include="..\..\src\thread\SDL_thread.c" />
diff --git a/include/SDL3/SDL.h b/include/SDL3/SDL.h
index 0b5ad69287b7e..4a25af8c19a69 100644
--- a/include/SDL3/SDL.h
+++ b/include/SDL3/SDL.h
@@ -71,6 +71,7 @@
 #include <SDL3/SDL_iostream.h>
 #include <SDL3/SDL_scancode.h>
 #include <SDL3/SDL_sensor.h>
+#include <SDL3/SDL_storage.h>
 #include <SDL3/SDL_surface.h>
 #include <SDL3/SDL_system.h>
 #include <SDL3/SDL_thread.h>
diff --git a/include/SDL3/SDL_hints.h b/include/SDL3/SDL_hints.h
index 5b16a34516b4d..0dbbe4e31730b 100644
--- a/include/SDL3/SDL_hints.h
+++ b/include/SDL3/SDL_hints.h
@@ -1904,6 +1904,24 @@ extern "C" {
  */
 #define SDL_HINT_SHUTDOWN_DBUS_ON_QUIT "SDL_SHUTDOWN_DBUS_ON_QUIT"
 
+/**
+ * A variable that specifies a backend to use for title storage.
+ *
+ * By default, SDL will try all available storage backends in a reasonable order until it finds one that can work, but this hint allows the app or user to force a specific target, such as "pc" if, say, you are on Steam but want to avoid SteamRemoteStorage for title data.
+ *
+ * This hint should be set before SDL is initialized.
+ */
+#define SDL_HINT_STORAGE_TITLE_DRIVER "SDL_STORAGE_TITLE_DRIVER"
+
+/**
+ * A variable that specifies a backend to use for user storage.
+ *
+ * By default, SDL will try all available storage backends in a reasonable order until it finds one that can work, but this hint allows the app or user to force a specific target, such as "pc" if, say, you are on Steam but want to avoid SteamRemoteStorage for user data.
+ *
+ * This hint should be set before SDL is initialized.
+ */
+#define SDL_HINT_STORAGE_USER_DRIVER "SDL_STORAGE_USER_DRIVER"
+
 /**
  * Specifies whether SDL_THREAD_PRIORITY_TIME_CRITICAL should be treated as realtime.
  *
diff --git a/include/SDL3/SDL_storage.h b/include/SDL3/SDL_storage.h
new file mode 100644
index 0000000000000..386964f264feb
--- /dev/null
+++ b/include/SDL3/SDL_storage.h
@@ -0,0 +1,276 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2024 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+
+/**
+ *  \file SDL_storage.h
+ *
+ *  Include file for storage container SDL API functions
+ */
+
+#ifndef SDL_storage_h_
+#define SDL_storage_h_
+
+#include <SDL3/SDL_stdinc.h>
+#include <SDL3/SDL_mutex.h>
+#include <SDL3/SDL_properties.h>
+
+#include <SDL3/SDL_begin_code.h>
+
+/* Set up for C function definitions, even when using C++ */
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* !!! FIXME: Don't let this ship without async R/W support!!! */
+
+typedef struct SDL_StorageInterface
+{
+    int (SDLCALL *close)(void *userdata);
+
+    SDL_bool (SDLCALL *ready)(void *userdata);
+
+    int (SDLCALL *fileSize)(void *userdata, const char *path, Uint64 *length);
+
+    int (SDLCALL *readFile)(void *userdata, const char *path, void *destination, Uint64 length);
+
+    int (SDLCALL *writeFile)(void *userdata, const char *path, const void *source, Uint64 length);
+
+    Uint64 (SDLCALL *spaceRemaining)(void *userdata);
+} SDL_StorageInterface;
+
+typedef struct SDL_Storage SDL_Storage;
+
+/**
+ * Opens up a read-only container for the application's filesystem.
+ *
+ * \param override a path to override the backend's default title root
+ * \param props a property list that may contain backend-specific information
+ * \returns a title storage container on success or NULL on failure; call
+ *          SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 3.0.0.
+ *
+ * \sa SDL_OpenUserStorage
+ * \sa SDL_OpenStorage
+ * \sa SDL_TitleStorageReady
+ * \sa SDL_CloseStorage
+ * \sa SDL_StorageReady
+ * \sa SDL_StorageFileSize
+ * \sa SDL_StorageReadFile
+ * \sa SDL_StorageWriteFile
+ * \sa SDL_StorageSpaceRemaining
+ */
+extern DECLSPEC SDL_Storage *SDLCALL SDL_OpenTitleStorage(const char *override, SDL_PropertiesID props);
+
+/**
+ * Opens up a container for a user's unique read/write filesystem.
+ *
+ * While title storage can generally be kept open throughout runtime, user
+ * storage should only be opened when the client is ready to read/write files.
+ * This allows the backend to properly batch R/W operations and flush them when
+ * the container has been closed; ensuring safe and optimal save I/O.
+ *
+ * \param org the name of your organization
+ * \param app the name of your application
+ * \param props a property list that may contain backend-specific information
+ * \returns a user storage container on success or NULL on failure; call
+ *          SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 3.0.0.
+ *
+ * \sa SDL_OpenTitleStorage
+ * \sa SDL_OpenStorage
+ * \sa SDL_CloseStorage
+ * \sa SDL_StorageReady
+ * \sa SDL_StorageFileSize
+ * \sa SDL_StorageReadFile
+ * \sa SDL_StorageWriteFile
+ * \sa SDL_StorageSpaceRemaining
+ */
+extern DECLSPEC SDL_Storage *SDLCALL SDL_OpenUserStorage(const char *org, const char *app, SDL_PropertiesID props);
+
+/**
+ * Opens up a container using a client-provided storage interface.
+ *
+ * Applications do not need to use this function unless they are providing
+ * their own SDL_Storage implementation. If you just need an
+ * SDL_Storage, you should use the built-in implementations in SDL,
+ * like SDL_OpenTitleStorage() or SDL_OpenUserStorage().
+ *
+ * \param iface the function table to be used by this container
+ * \param userdata the pointer that will be passed to the store interface
+ * \returns a storage container on success or NULL on failure; call
+ *          SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 3.0.0.
+ *
+ * \sa SDL_OpenTitleStorage
+ * \sa SDL_OpenUserStorage
+ * \sa SDL_CloseStorage
+ * \sa SDL_StorageReady
+ * \sa SDL_StorageFileSize
+ * \sa SDL_StorageReadFile
+ * \sa SDL_StorageWriteFile
+ * \sa SDL_StorageSpaceRemaining
+ */
+extern DECLSPEC SDL_Storage *SDLCALL SDL_OpenStorage(const SDL_StorageInterface *iface, void *userdata);
+
+/**
+ * Closes and frees a storage container.
+ *
+ * \param storage a storage container to close
+ * \returns 0 if the container was freed with no errors, a negative value
+ *          otherwise; call SDL_GetError() for more information. Even if the
+ *          function returns an error, the container data will be freed; the
+ *          error is only for informational purposes.
+ *
+ * \since This function is available since SDL 3.0.0.
+ *
+ * \sa SDL_OpenTitleStorage
+ * \sa SDL_OpenUserStorage
+ * \sa SDL_OpenStorage
+ * \sa SDL_StorageReady
+ * \sa SDL_StorageFileSize
+ * \sa SDL_StorageReadFile
+ * \sa SDL_StorageWriteFile
+ * \sa SDL_StorageSpaceRemaining
+ */
+extern DECLSPEC int SDLCALL SDL_CloseStorage(SDL_Storage *storage);
+
+/**
+ * Checks if the storage container is ready to use.
+ *
+ * This function should be called in regular intervals until it returns
+ * SDL_TRUE - however, it is not recommended to spinwait on this call, as
+ * the backend may depend on a synchronous message loop.
+ *
+ * \param storage a storage container to query
+ * \returns SDL_TRUE if the container is ready, SDL_FALSE otherwise
+ *
+ * \since This function is available since SDL 3.0.0.
+ *
+ * \sa SDL_OpenTitleStorage
+ * \sa SDL_OpenUserStorage
+ * \sa SDL_OpenStorage
+ * \sa SDL_CloseStorage
+ * \sa SDL_StorageFileSize
+ * \sa SDL_StorageReadFile
+ * \sa SDL_StorageWriteFile
+ * \sa SDL_StorageSpaceRemaining
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_StorageReady(SDL_Storage *storage);
+
+/**
+ * Query the size of a file within a storage container.
+ *
+ * \param storage a storage container to query
+ * \param path the relative path of the file to query
+ * \param length a pointer to be filled with the file's length
+ * \returns 0 if the file could be queried, a negative value otherwise; call
+ *          SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 3.0.0.
+ *
+ * \sa SDL_OpenTitleStorage
+ * \sa SDL_OpenUserStorage
+ * \sa SDL_OpenStorage
+ * \sa SDL_CloseStorage
+ * \sa SDL_StorageReady
+ * \sa SDL_StorageReadFile
+ * \sa SDL_StorageWriteFile
+ * \sa SDL_StorageSpaceRemaining
+ */
+extern DECLSPEC int SDLCALL SDL_StorageFileSize(SDL_Storage *storage, const char *path, Uint64 *length);
+
+/**
+ * Synchronously read a file from a storage container into a client-provided buffer.
+ *
+ * \param storage a storage container to read from
+ * \param path the relative path of the file to read
+ * \param destination a client-provided buffer to read the file into
+ * \param length the length of the destination buffer
+ * \returns 0 if the file was read, a negative value otherwise; call
+ *          SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 3.0.0.
+ *
+ * \sa SDL_OpenTitleStorage
+ * \sa SDL_OpenUserStorage
+ * \sa SDL_OpenStorage
+ * \sa SDL_CloseStorage
+ * \sa SDL_StorageReady
+ * \sa SDL_StorageFileSize
+ * \sa SDL_StorageWriteFile
+ * \sa SDL_StorageSpaceRemaining
+ */
+extern DECLSPEC int SDLCALL SDL_StorageReadFile(SDL_Storage *storage, const char *path, void *destination, Uint64 length);
+
+/**
+ * Synchronously write a file from client memory into a storage container.
+ *
+ * \param storage a storage container to write to
+ * \param path the relative path of the file to write
+ * \param source a client-provided buffer to write from
+ * \param length the length of the source buffer
+ * \returns 0 if the file was written, a negative value otherwise; call
+ *          SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 3.0.0.
+ *
+ * \sa SDL_OpenTitleStorage
+ * \sa SDL_OpenUserStorage
+ * \sa SDL_OpenStorage
+ * \sa SDL_CloseStorage
+ * \sa SDL_StorageReady
+ * \sa SDL_StorageFileSize
+ * \sa SDL_StorageReadFile
+ * \sa SDL_StorageSpaceRemaining
+ */
+extern DECLSPEC int SDL_StorageWriteFile(SDL_Storage *storage, const char *path, const void *source, Uint64 length);
+
+/**
+ * Queries the remaining space in a storage container.
+ *
+ * \param storage a storage container to query
+ * \returns the amount of remaining space, in bytes
+ *
+ * \since This function is available since SDL 3.0.0.
+ *
+ * \sa SDL_OpenTitleStorage
+ * \sa SDL_OpenUserStorage
+ * \sa SDL_OpenStorage
+ * \sa SDL_CloseStorage
+ * \sa SDL_StorageReady
+ * \sa SDL_StorageFileSize
+ * \sa SDL_StorageReadFile
+ * \sa SDL_StorageReadFileAsync
+ * \sa SDL_StorageWriteFile
+ * \sa SDL_StorageWriteFileAsync
+ */
+extern DECLSPEC Uint64 SDLCALL SDL_StorageSpaceRemaining(SDL_Storage *storage);
+
+/* Ends C function definitions when using C++ */
+#ifdef __cplusplus
+}
+#endif
+#include <SDL3/SDL_close_code.h>
+
+#endif /* SDL_storage_h_ */
diff --git a/include/build_config/SDL_build_config.h.cmake b/include/build_config/SDL_build_config.h.cmake
index 606dbbe7d21dc..1e650c491c878 100644
--- a/include/build_config/SDL_build_config.h.cmake
+++ b/include/build_config/SDL_build_config.h.cmake
@@ -465,6 +465,10 @@
 #cmakedefine SDL_FILESYSTEM_PS2 @SDL_FILESYSTEM_PS2@
 #cmakedefine SDL_FILESYSTEM_N3DS @SDL_FILESYSTEM_N3DS@
 
+/* Enable system storage support */
+#cmakedefine SDL_STORAGE_GENERIC @SDL_STORAGE_GENERIC@
+#cmakedefine SDL_STORAGE_STEAM @SDL_STORAGE_STEAM@
+
 /* Enable camera subsystem */
 #cmakedefine SDL_CAMERA_DRIVER_DUMMY @SDL_CAMERA_DRIVER_DUMMY@
 /* !!! FIXME: for later cmakedefine SDL_CAMERA_DRIVER_DISK @SDL_CAMERA_DRIVER_DISK@ */
diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym
index e2e209be1e6aa..b623877d81a2d 100644
--- a/src/dynapi/SDL_dynapi.sym
+++ b/src/dynapi/SDL_dynapi.sym
@@ -979,6 +979,14 @@ SDL3_0.0.0 {
     SDL_OpenIO;
     SDL_CloseIO;
     SDL_GetIOStatus;
+    SDL_OpenTitleStorage;
+    SDL_OpenUserStorage;
+    SDL_OpenStorage;
+    SDL_CloseStorage;
+    SDL_StorageReady;
+    SDL_StorageFileSize;
+    SDL_StorageReadFile;
+    SDL_StorageSpaceRemaining;
     # extra symbols go here (don't modify this line)
   local: *;
 };
diff --git a/src/dynapi/SDL_dynapi_overrides.h b/src/dynapi/SDL_dynapi_overrides.h
index 790bf10743bad..16eec6028d9c4 100644
--- a/src/dynapi/SDL_dynapi_overrides.h
+++ b/src/dynapi/SDL_dynapi_overrides.h
@@ -1004,3 +1004,11 @@
 #define SDL_OpenIO SDL_OpenIO_REAL
 #define SDL_CloseIO SDL_CloseIO_REAL
 #define SDL_GetIOStatus SDL_GetIOStatus_REAL
+#define SDL_OpenTitleStorage SDL_OpenTitleStorage_REAL
+#define SDL_OpenUserStorage SDL_OpenUserStorage_REAL
+#define SDL_OpenStorage SDL_OpenStorage_REAL
+#define SDL_CloseStorage SDL_CloseStorage_REAL
+#define SDL_StorageReady SDL_StorageReady_REAL
+#define SDL_StorageFileSize SDL_StorageFileSize_REAL
+#define SDL_StorageReadFile SDL_StorageReadFile_REAL
+#define SDL_StorageSpaceRemaining SDL_StorageSpaceRemaining_REAL
diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h
index 83cc68d907c0d..00a2718ff8b50 100644
--- a/src/dynapi/SDL_dynapi_procs.h
+++ b/src/dynapi/SDL_dynapi_procs.h
@@ -1029,3 +1029,11 @@ SDL_DYNAPI_PROC(void,SDL_ShowOpenFolderDialog,(SDL_DialogFileCallback a, void *b
 SDL_DYNAPI_PROC(SDL_IOStream*,SDL_OpenIO,(const SDL_IOStreamInterface *a, void *b),(a,b),return)
 SDL_DYNAPI_PROC(int,SDL_CloseIO,(SDL_IOStream *a),(a),return)
 SDL_DYNAPI_PROC(SDL_IOStatus,SDL_GetIOStatus,(SDL_IOStream *a),(a),return)
+SDL_DYNAPI_PROC(SDL_Storage*,SDL_OpenTitleStorage,(const char *a, SDL_PropertiesID b),(a,b),return)
+SDL_DYNAPI_PROC(SDL_Storage*,SDL_OpenUserStorage,(const char *a, const char *b, SDL_PropertiesID c),(a,b,c),return)
+SDL_DYNAPI_PROC(SDL_Storage*,SDL_OpenStorage,(const SDL_StorageInterface *a, void *b),(a,b),return)
+SDL_DYNAPI_PROC(int,SDL_CloseStorage,(SDL_Storage *a),(a),return)
+SDL_DYNAPI_PROC(SDL_bool,SDL_StorageReady,(SDL_Storage *a),(a),return)
+SDL_DYNAPI_PROC(int,SDL_StorageFileSize,(SDL_Storage *a, const char *b, Uint64 *c),(a,b,c),return)
+SDL_DYNAPI_PROC(int,SDL_StorageReadFile,(SDL_Storage *a, const char *b, void *c, Uint64 d),(a,b,c,d),return)
+SDL_DYNAPI_PROC(Uint64,SDL_StorageSpaceRemaining,(SDL_Storage *a),(a),return)
diff --git a/src/storage/SDL_storage.c b/src/storage/SDL_storage.c
new file mode 100644
index 0000000000000..877db6c7ead62
--- /dev/null
+++ b/src/storage/SDL_storage.c
@@ -0,0 +1,222 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2024 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+
+#include "SDL_internal.h"
+
+#include "SDL_sysstorage.h"
+
+/* Available title storage drivers */
+static TitleStorageBootStrap *titlebootstrap[] = {
+    &GENERIC_titlebootstrap,
+};
+
+/* Available user storage drivers */
+static UserStorageBootStrap *userbootstrap[] = {
+#ifdef SDL_STORAGE_STEAM
+    &STEAM_userbootstrap,
+#endif
+    &GENERIC_userbootstrap,
+};
+
+struct SDL_Storage
+{
+    SDL_StorageInterface iface;
+    void *userdata;
+};
+
+#define CHECK_STORAGE_MAGIC()                             \
+    if (!storage) {                                       \
+        return SDL_SetError("Invalid storage container"); \
+    }
+
+#define CHECK_STORAGE_MAGIC_RET(retval)            \
+    if (!storage) {                                \
+        SDL_SetError("Invalid storage container"); \
+        return retval;                             \
+    }
+
+SDL_Storage *SDL_OpenTitleStorage(const char *override, SDL_PropertiesID props)
+{
+    SDL_Storage *storage = NULL;
+    int i = 0;
+
+    /* Select the proper storage driver */
+    const char *driver_name = SDL_GetHint(SDL_HINT_STORAGE_TITLE_DRIVER);
+    if (driver_name && *driver_name != 0) {
+        const char *driver_attempt = driver_name;
+        while (driver_attempt && *driver_attempt != 0 && !storage) {
+            const char *driver_attempt_end = SDL_strchr(driver_attempt, ',');
+            size_t driver_attempt_len = (driver_attempt_end) ? (driver_attempt_end - driver_attempt)
+                                                                     : SDL_strlen(driver_attempt);
+
+            for (i = 0; titlebootstrap[i]; ++i) {
+                if ((driver_attempt_len == SDL_strlen(titlebootstrap[i]->name)) &&
+                    (SDL_strncasecmp(titlebootstrap[i]->name, driver_attempt, driver_attempt_len) == 0)) {
+                    storage = titlebootstrap[i]->create(override, props);
+                    break;
+                }
+            }
+
+            driver_attempt = (driver_attempt_end) ? (driver_attempt_end + 1) : NULL;
+        }
+    } else {
+        for (i = 0; titlebootstrap[i]; ++i) {
+            storage = titlebootstrap[i]->create(override, props);
+            if (storage) {
+                break;
+            }
+        }
+    }
+    if (!storage) {
+        if (driver_name) {
+            SDL_SetError("%s not available", driver_name);
+        } else {
+            SDL_SetError("No available title storage driver");
+        }
+    }
+    return storage;
+}
+
+SDL_Storage *SDL_OpenUserStorage(const char *org, const char *app, SDL_PropertiesID props)
+{
+    SDL_Storage *storage = NULL;
+    int i = 0;
+
+    /* Select the proper storage driver */
+    const char *driver_name = SDL_GetHint(SDL_HINT_STORAGE_USER_DRIVER);
+    if (driver_name && *driver_name != 0) {
+        const char *driver_attempt = driver_name;
+        while (driver_attempt && *driver_attempt != 0 && !storage) {
+            const char *driver_attempt_end = SDL_strchr(driver_attempt, ',');
+            size_t driver_attempt_len = (driver_attempt_end) ? (driver_attempt_end - driver_attempt)
+                                                                     : SDL_strlen(driver_attempt);
+
+            for (i = 0; userbootstrap[i]; ++i) {
+                if ((driver_attempt_len == SDL_strlen(userbootstrap[i]->name)) &&
+                    (SDL_strncasecmp(userbootstrap[i]->name, driver_attempt, driver_attempt_len) == 0)) {
+                    storage = userbootstrap[i]->create(org, app, props);
+                    break;
+                }
+            }
+
+            driver_attempt = (driver_attempt_end) ? (driver_attempt_end + 1) : NULL;
+        }
+    } else {
+        for (i = 0; userbootstrap[i]; ++i) {
+            storage = userbootstrap[i]->create(org, app, props);
+            if (storage) {
+                break;
+            }
+        }
+    }
+    if (!storage) {
+        if (driver_name) {
+            SDL_SetError("%s not available", driver_name);
+        } else {
+            SDL_SetError("No available user storage driver");
+        }
+    }
+    return storage;
+}
+
+SDL_Storage *SDL_OpenStorage(const SDL_StorageInterface *iface, void *userdata)
+{
+    SDL_Storage *storage;
+
+    if (iface->close == NULL || iface->ready == NULL || iface->fileSize == NULL) {
+        SDL_SetError("iface is missing required callbacks");
+        return NULL;
+    }
+
+    if ((iface->writeFile != NULL) != (iface->spaceRemaining != NULL)) {
+        SDL_SetError("Writeable containers must have both writeFile and spaceRemaining");
+        return NULL;
+    }
+
+    storage = (SDL_Storage*) SDL_malloc(sizeof(SDL_Storage));
+    if (storage == NULL) {
+        SDL_OutOfMemory();
+        return NULL;
+    }
+
+    SDL_memcpy(&storage->iface, iface, sizeof(SDL_StorageInterface));
+    storage->userdata = userdata;
+    return storage;
+}
+
+int SDL_CloseStorage(SDL_Storage *storage)
+{
+    int retval;
+
+    CHECK_STORAGE_MAGIC()
+
+    retval = storage->iface.close(storage->userdata);
+    SDL_free(storage);
+    return retval;
+}
+
+SDL_bool SDL_StorageReady(SDL_Storage *storage)
+{
+    CHECK_STORAGE_MAGIC_RET(SDL_FALSE)
+
+    return storage->iface.ready(storage->userdata);
+}
+
+int SDL_StorageFileSize(SDL_Storage *storage, const char *path, Uint64 *length)
+{
+    CHECK_STORAGE_MAGIC()
+
+    return storage->iface.fileSize(storage->userdata, path, length);
+}
+
+int SDL_StorageReadFile(SDL_Storage *storage, const char *path, void *destination, Uint64 length)
+{
+    CHECK_STORAGE_MAGIC()
+
+    if (storage->iface.readFile == NULL) {
+        return SDL_SetError("Storage container does not have read capability");
+    }
+
+    return storage->iface.readFile(storage->userdata, path, destination, length);
+}
+
+int SDL_StorageWriteFile(SDL_Storage *storage, const char *path, const void *source, Uint64 length)
+{
+    CHECK_STORAGE_MAGIC()
+
+    if (storage->iface.writeFile == NULL) {
+        return SDL_SetError("Storage container does not have write capability");
+    }
+
+    return storage->iface.writeFile(storage->userdata, path, source, length);
+}
+
+Uint64 SDL_StorageSpaceRemaining(SDL_Storage *storage)
+{
+    CHECK_STORAGE_MAGIC_RET(0)
+
+    if (storage->iface.spaceRemaining == NULL) {
+        SDL_SetError("Storage container does not have write capability");
+        return 0;
+    }
+
+    return storage->iface.spaceRemaining(storage->userdata);
+}
diff --git a/src/storage/SDL_sysstorage.h b/src/storage/SDL_sysstorage.h
new file mode 100644
index 0000000000000..c16c5cc25175c
--- /dev/null
+++ b/src/storage/SDL_sysstorage.h
@@ -0,0 +1,49 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2024 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+
+#ifndef SDL_sysstorage_h_
+#define SDL_sysstorage_h_
+
+#include "SDL_internal.h"
+
+typedef struct TitleStorageBootStrap
+{
+    const char *name;
+    const char *desc;
+    SDL_Storage *(*create)(const char*, SDL_PropertiesID);
+} TitleStorageBootStrap;
+
+typedef struct UserStorageBootStrap
+{
+    const char *name;
+    const char *desc;
+    SDL_Storage *(*create)(const char*, const char*, SDL_PropertiesID);
+} UserStorageBootStrap;
+
+/* Not all of these are available in a given build. Use #ifdefs, etc. */
+
+extern TitleStorageBootStrap GENERIC_titlebootstrap;
+/* Steam does not have title storage APIs */
+
+extern UserStorageBootStrap GENERIC_userbootstrap;
+extern UserStorageBootStrap STEAM_userbootstrap;
+
+#endif /* SDL_sysstorage_h_ */
diff --git a/src/storage/generic/SDL_genericstorage.c b/src/

(Patch may be truncated, please check the link at the top of this post.)