diff --git a/VisualC/SDL.sln b/VisualC/SDL.sln index a942d5fca9..794e57b9b9 100644 --- a/VisualC/SDL.sln +++ b/VisualC/SDL.sln @@ -149,6 +149,10 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "02-clipboard", "examples\mi EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "03-locale", "examples\misc\03-locale\03-locale.vcxproj", "{6381F9D3-BA5F-4E5C-80FF-9013964777EE}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "storage", "storage", "{4B337225-9433-40B9-920D-D25B6D9BFE93}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "01-user", "examples\storage\01-user\01-user.vcxproj", "{2C576550-6430-4978-9669-6FC8349D3C3A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|ARM64 = Debug|ARM64 @@ -915,6 +919,18 @@ Global {6381F9D3-BA5F-4E5C-80FF-9013964777EE}.Release|Win32.Build.0 = Release|Win32 {6381F9D3-BA5F-4E5C-80FF-9013964777EE}.Release|x64.ActiveCfg = Release|x64 {6381F9D3-BA5F-4E5C-80FF-9013964777EE}.Release|x64.Build.0 = Release|x64 + {2C576550-6430-4978-9669-6FC8349D3C3A}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {2C576550-6430-4978-9669-6FC8349D3C3A}.Debug|ARM64.Build.0 = Debug|ARM64 + {2C576550-6430-4978-9669-6FC8349D3C3A}.Debug|Win32.ActiveCfg = Debug|Win32 + {2C576550-6430-4978-9669-6FC8349D3C3A}.Debug|Win32.Build.0 = Debug|Win32 + {2C576550-6430-4978-9669-6FC8349D3C3A}.Debug|x64.ActiveCfg = Debug|x64 + {2C576550-6430-4978-9669-6FC8349D3C3A}.Debug|x64.Build.0 = Debug|x64 + {2C576550-6430-4978-9669-6FC8349D3C3A}.Release|ARM64.ActiveCfg = Release|ARM64 + {2C576550-6430-4978-9669-6FC8349D3C3A}.Release|ARM64.Build.0 = Release|ARM64 + {2C576550-6430-4978-9669-6FC8349D3C3A}.Release|Win32.ActiveCfg = Release|Win32 + {2C576550-6430-4978-9669-6FC8349D3C3A}.Release|Win32.Build.0 = Release|Win32 + {2C576550-6430-4978-9669-6FC8349D3C3A}.Release|x64.ActiveCfg = Release|x64 + {2C576550-6430-4978-9669-6FC8349D3C3A}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -989,6 +1005,8 @@ Global {6975730D-AFA0-4687-9B89-EC1FE0BDA8CB} = {FA68A4F2-2DB8-4C90-8809-6B9764C92E77} {40F19482-512F-4123-A57B-509679EC8F26} = {FA68A4F2-2DB8-4C90-8809-6B9764C92E77} {6381F9D3-BA5F-4E5C-80FF-9013964777EE} = {FA68A4F2-2DB8-4C90-8809-6B9764C92E77} + {4B337225-9433-40B9-920D-D25B6D9BFE93} = {1498F0CD-F4DA-4847-9CB2-FB18D48061D5} + {2C576550-6430-4978-9669-6FC8349D3C3A} = {4B337225-9433-40B9-920D-D25B6D9BFE93} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C320C9F2-1A8F-41D7-B02B-6338F872BCAD} diff --git a/VisualC/examples/storage/01-user/01-user.vcxproj b/VisualC/examples/storage/01-user/01-user.vcxproj new file mode 100644 index 0000000000..130a33c832 --- /dev/null +++ b/VisualC/examples/storage/01-user/01-user.vcxproj @@ -0,0 +1,12 @@ + + + + {2C576550-6430-4978-9669-6FC8349D3C3A} + + + + + + + + \ No newline at end of file diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 1e6b086d7e..b6de25f126 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -156,6 +156,7 @@ add_sdl_example_executable(input-gamepad-rumble SOURCES input/05-gamepad-rumble/ add_sdl_example_executable(camera-read-and-draw SOURCES camera/01-read-and-draw/read-and-draw.c) add_sdl_example_executable(pen-drawing-lines SOURCES pen/01-drawing-lines/drawing-lines.c) add_sdl_example_executable(asyncio-load-bitmaps SOURCES asyncio/01-load-bitmaps/load-bitmaps.c DATAFILES ${CMAKE_CURRENT_SOURCE_DIR}/../test/sample.png ${CMAKE_CURRENT_SOURCE_DIR}/../test/gamepad_front.png ${CMAKE_CURRENT_SOURCE_DIR}/../test/speaker.png ${CMAKE_CURRENT_SOURCE_DIR}/../test/icon2x.png) +add_sdl_example_executable(storage-user SOURCES storage/01-user/user.c) add_sdl_example_executable(misc-power SOURCES misc/01-power/power.c) add_sdl_example_executable(misc-clipboard SOURCES misc/02-clipboard/clipboard.c) add_sdl_example_executable(misc-locale SOURCES misc/03-locale/locale.c) diff --git a/examples/storage/01-user/user.c b/examples/storage/01-user/user.c new file mode 100644 index 0000000000..8ab815116b --- /dev/null +++ b/examples/storage/01-user/user.c @@ -0,0 +1,271 @@ +/* + * This example code creates an SDL window and renderer, and waits for the user + * to click on the window. By default, the window color is blue; When storage + * succeeds, the window turns green, if it fails the window turns red and the + * error message is logged. Left clicking will save a game, all other clicks + * will load a game. + * + * The primary goal is to show how to handle save data without blocking the main + * thread, while also making sure to keep the storage handle open for as little + * as possible; many platforms do not allow keeping user storage open for long + * periods of time so you _must_ be sure to only have user storage open when you + * are absolutely 100% ready to interact with the storage handle. + * + * This code is public domain. Feel free to use it for any purpose! + */ + +#define SDL_MAIN_USE_CALLBACKS 1 /* use the callbacks instead of main() */ +#include +#include + +/* This is the list of steps that will occur as part of saving or loading a game */ +typedef enum savestate_t +{ + SAVE_STATE_UNSTARTED, /* blue */ + SAVE_STATE_PROCESSING_GAME_WORLD, /* yellow */ + SAVE_STATE_PREPARING_STORAGE, /* cyan */ + SAVE_STATE_PROCESSING_STORAGE_FILE, /* magenta */ + SAVE_STATE_FINAL_CHECK /* green if success, red if failed */ +} savestate_t; + +SDL_AtomicInt current_save_state; + +/* During the final check, this indicates success or failure */ +static int save_result = -1; + +/* This is the thread that handles the majority of save operations */ +static SDL_Thread *save_thread = NULL; + +/* Opening storage is itself an async operation, so the thread will have some waiting to do */ +static SDL_Semaphore *storage_ready = NULL; + +/* This is the handle for the user's filesystem */ +static SDL_Storage *save_storage = NULL; + +#define SAVE_FILE_NAME "save.sav" + +/* This function pretends to serialize a fictional game world, then starts + * opening the filesystem to write the serialized data + */ +static int SDLCALL WriteSaveData(void *data) +{ + Uint64 game_world; /* to keep things simple, let's just pretend that an entire game fits in 64-bits */ + bool write_result; + + SDL_SetAtomicInt(¤t_save_state, SAVE_STATE_PROCESSING_GAME_WORLD); + + /* again, let's just pretend that an entire game fits in 64-bits */ + game_world = SDL_GetPerformanceCounter(); + + /* now that save data is ready to go, we can start opening the filesystem */ + save_storage = SDL_OpenUserStorage("libsdl", "User Storage Example", 0); + if (save_storage == NULL) { + SDL_SetAtomicInt(¤t_save_state, SAVE_STATE_FINAL_CHECK); + return -1; + } + SDL_SetAtomicInt(¤t_save_state, SAVE_STATE_PREPARING_STORAGE); + + /* the main thread will eventually signal to us that storage is ready */ + SDL_WaitSemaphore(storage_ready); + + /* the save data can now be written to the storage device */ + write_result = SDL_WriteStorageFile(save_storage, SAVE_FILE_NAME, &game_world, sizeof(game_world)); + + /* regardless of what happened above, we've reached the end of the routine */ + SDL_CloseStorage(save_storage); + SDL_SetAtomicInt(¤t_save_state, SAVE_STATE_FINAL_CHECK); + + if (!write_result) { + return -1; + } + return 0; +} + +/* This function opens the filesystem to read a save file, then deserializes the + * data into fictional game world data + */ +static int SDLCALL ReadSaveData(void *data) +{ + Uint64 game_world; /* to keep things simple, let's just pretend that an entire game fits in 64-bits */ + Uint64 save_len; + bool read_result; + + /* start by preparing the filesystem for reading */ + save_storage = SDL_OpenUserStorage("libsdl", "User Storage Example", 0); + if (save_storage == NULL) { + SDL_SetAtomicInt(¤t_save_state, SAVE_STATE_FINAL_CHECK); + return -1; + } + SDL_SetAtomicInt(¤t_save_state, SAVE_STATE_PREPARING_STORAGE); + + /* the main thread will eventually signal to us that storage is ready */ + SDL_WaitSemaphore(storage_ready); + + read_result = SDL_GetStorageFileSize(save_storage, SAVE_FILE_NAME, &save_len); + if (!read_result) { + SDL_CloseStorage(save_storage); + SDL_SetAtomicInt(¤t_save_state, SAVE_STATE_FINAL_CHECK); + SDL_Log("Save data was not found"); + return -1; + } else if (save_len != sizeof(game_world)) { + SDL_CloseStorage(save_storage); + SDL_SetAtomicInt(¤t_save_state, SAVE_STATE_FINAL_CHECK); + SDL_Log("Save data size is incorrect, was the file corrupted?"); + return -1; + } + + /* once we've read the file in, the storage handle is no longer needed */ + read_result = SDL_ReadStorageFile(save_storage, SAVE_FILE_NAME, &game_world, sizeof(game_world)); + SDL_CloseStorage(save_storage); + + SDL_SetAtomicInt(¤t_save_state, SAVE_STATE_PROCESSING_GAME_WORLD); + + if (read_result) { + /* again, let's just pretend that an entire game fits in 64-bits */ + SDL_Log("Game World loaded, value was %" SDL_PRIu64, game_world); + } + + /* regardless of what happened above, we've reached the end of the routine */ + SDL_SetAtomicInt(¤t_save_state, SAVE_STATE_FINAL_CHECK); + + if (!read_result) { + return -1; + } + return 0; +} + +/* We will use this renderer to draw into this window every frame. */ +static SDL_Window *window = NULL; +static SDL_Renderer *renderer = NULL; + +/* This function runs once at startup. */ +SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[]) +{ + SDL_SetAppMetadata("User Storage Example", "1.0", "com.example.storage-user"); + + if (!SDL_Init(SDL_INIT_VIDEO)) { + SDL_Log("Couldn't initialize SDL: %s", SDL_GetError()); + return SDL_APP_FAILURE; + } + + if (!SDL_CreateWindowAndRenderer("examples/storage/user", 640, 480, 0, &window, &renderer)) { + SDL_Log("Couldn't create window/renderer: %s", SDL_GetError()); + return SDL_APP_FAILURE; + } + + /* initialize the default save state */ + SDL_SetAtomicInt(¤t_save_state, SAVE_STATE_UNSTARTED); + storage_ready = SDL_CreateSemaphore(0); + + return SDL_APP_CONTINUE; /* carry on with the program! */ +} + +/* This function runs when a new event (mouse input, keypresses, etc) occurs. */ +SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event) +{ + if (event->type == SDL_EVENT_QUIT) { + return SDL_APP_SUCCESS; /* end the program, reporting success to the OS. */ + } else if (event->type == SDL_EVENT_MOUSE_BUTTON_DOWN) { + if (save_thread != NULL) { + SDL_Log("Ignoring interaction, save/load is in progress"); + } else { + /* once the thread starts, it will update this to the first "real" state */ + SDL_SetAtomicInt(¤t_save_state, SAVE_STATE_UNSTARTED); + if (event->button.button == 1) { + save_thread = SDL_CreateThread(WriteSaveData, "Save Write Thread", NULL); + } else { + save_thread = SDL_CreateThread(ReadSaveData, "Save Read Thread", NULL); + } + } + } + return SDL_APP_CONTINUE; /* carry on with the program! */ +} + +/* This function runs once per frame, and is the heart of the program. */ +SDL_AppResult SDL_AppIterate(void *appstate) +{ + float red, green, blue; + int save_state = SDL_GetAtomicInt(¤t_save_state); + + /* the main thread does not have to do much other than help the thread wait + * for storage to be ready and read the result when the thread is finished + */ + if (save_state == SAVE_STATE_PREPARING_STORAGE) { + if (SDL_StorageReady(save_storage)) { + SDL_SetAtomicInt(¤t_save_state, SAVE_STATE_PROCESSING_STORAGE_FILE); + SDL_SignalSemaphore(storage_ready); + } + } else if (save_state == SAVE_STATE_FINAL_CHECK) { + if (save_thread != NULL) { + SDL_WaitThread(save_thread, &save_result); + save_thread = NULL; + if (save_result == 0) { + SDL_Log("Save/Load complete!"); + } else { + SDL_Log("Save/Load failed: %s", SDL_GetError()); + } + } + } + + /* set the draw color based on the state of the save system */ + switch (save_state) + { + case SAVE_STATE_UNSTARTED: + red = 0.0f; + green = 0.0f; + blue = 1.0f; + break; + case SAVE_STATE_PROCESSING_GAME_WORLD: + red = 1.0f; + green = 1.0f; + blue = 0.0f; + break; + case SAVE_STATE_PREPARING_STORAGE: + red = 0.0f; + green = 1.0f; + blue = 1.0f; + break; + case SAVE_STATE_PROCESSING_STORAGE_FILE: + red = 1.0f; + green = 0.0f; + blue = 1.0f; + break; + case SAVE_STATE_FINAL_CHECK: + if (save_result == 0) { + red = 0.0f; + green = 1.0f; + } else { + red = 1.0f; + green = 0.0f; + } + blue = 0.0f; + break; + default: + red = 0.0f; + green = 0.0f; + blue = 0.0f; + SDL_assert(!"Unrecognized save state"); + break; + } + SDL_SetRenderDrawColorFloat(renderer, red, green, blue, SDL_ALPHA_OPAQUE_FLOAT); /* new color, full alpha. */ + + /* clear the window to the draw color. */ + SDL_RenderClear(renderer); + + /* put the newly-cleared rendering on the screen. */ + SDL_RenderPresent(renderer); + + return SDL_APP_CONTINUE; /* carry on with the program! */ +} + +/* This function runs once at shutdown. */ +void SDL_AppQuit(void *appstate, SDL_AppResult result) +{ + /* If saving/loading is still in progress, force the thread not to wait */ + SDL_SignalSemaphore(storage_ready); + SDL_WaitThread(save_thread, NULL); + SDL_DestroySemaphore(storage_ready); + + /* SDL will clean up the window/renderer for us. */ +} +