From 3744c5b66fdda351d0f3d88687835fdd54377279 Mon Sep 17 00:00:00 2001 From: "Ryan C. Gordon" Date: Tue, 27 Aug 2024 21:57:27 -0400 Subject: [PATCH] asyncio: Added async i/o APIs. Fixes #1374. --- Android.mk | 1 + CMakeLists.txt | 3 +- VisualC-GDK/SDL/SDL.vcxproj | 5 + VisualC-GDK/SDL/SDL.vcxproj.filters | 15 + VisualC/SDL/SDL.vcxproj | 5 + VisualC/SDL/SDL.vcxproj.filters | 18 + Xcode/SDL/SDL.xcodeproj/project.pbxproj | 29 + examples/CMakeLists.txt | 2 +- examples/asyncio/01-load-bitmaps/README.txt | 6 + .../asyncio/01-load-bitmaps/load-bitmaps.c | 125 +++++ include/SDL3/SDL.h | 1 + include/SDL3/SDL_asyncio.h | 506 ++++++++++++++++++ src/SDL.c | 3 +- src/dynapi/SDL_dynapi.sym | 11 + src/dynapi/SDL_dynapi_overrides.h | 11 + src/dynapi/SDL_dynapi_procs.h | 11 + src/file/SDL_asyncio.c | 335 ++++++++++++ src/file/SDL_asyncio_c.h | 30 ++ src/file/SDL_sysasyncio.h | 135 +++++ src/file/generic/SDL_asyncio_generic.c | 461 ++++++++++++++++ test/CMakeLists.txt | 1 + test/testasyncio.c | 176 ++++++ 22 files changed, 1886 insertions(+), 4 deletions(-) create mode 100644 examples/asyncio/01-load-bitmaps/README.txt create mode 100644 examples/asyncio/01-load-bitmaps/load-bitmaps.c create mode 100644 include/SDL3/SDL_asyncio.h create mode 100644 src/file/SDL_asyncio.c create mode 100644 src/file/SDL_asyncio_c.h create mode 100644 src/file/SDL_sysasyncio.h create mode 100644 src/file/generic/SDL_asyncio_generic.c create mode 100644 test/testasyncio.c diff --git a/Android.mk b/Android.mk index b352cb318..6735c877a 100644 --- a/Android.mk +++ b/Android.mk @@ -35,6 +35,7 @@ LOCAL_SRC_FILES := \ $(wildcard $(LOCAL_PATH)/src/dynapi/*.c) \ $(wildcard $(LOCAL_PATH)/src/events/*.c) \ $(wildcard $(LOCAL_PATH)/src/file/*.c) \ + $(wildcard $(LOCAL_PATH)/src/file/generic/*.c) \ $(wildcard $(LOCAL_PATH)/src/gpu/*.c) \ $(wildcard $(LOCAL_PATH)/src/gpu/vulkan/*.c) \ $(wildcard $(LOCAL_PATH)/src/haptic/*.c) \ diff --git a/CMakeLists.txt b/CMakeLists.txt index 01cc97466..7ba55beb4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1120,6 +1120,7 @@ sdl_glob_sources( "${SDL3_SOURCE_DIR}/src/dynapi/*.c" "${SDL3_SOURCE_DIR}/src/events/*.c" "${SDL3_SOURCE_DIR}/src/file/*.c" + "${SDL3_SOURCE_DIR}/src/file/generic/*.c" "${SDL3_SOURCE_DIR}/src/filesystem/*.c" "${SDL3_SOURCE_DIR}/src/gpu/*.c" "${SDL3_SOURCE_DIR}/src/joystick/*.c" @@ -2105,8 +2106,6 @@ elseif(APPLE) set(HAVE_SDL_MAIN_CALLBACKS TRUE) endif() - sdl_glob_sources("${SDL3_SOURCE_DIR}/src/file/cocoa/*.m") - if(SDL_CAMERA) if(MACOS OR IOS) set(SDL_CAMERA_DRIVER_COREMEDIA 1) diff --git a/VisualC-GDK/SDL/SDL.vcxproj b/VisualC-GDK/SDL/SDL.vcxproj index c6ab14168..2aef94e24 100644 --- a/VisualC-GDK/SDL/SDL.vcxproj +++ b/VisualC-GDK/SDL/SDL.vcxproj @@ -339,6 +339,7 @@ + @@ -432,6 +433,8 @@ + + @@ -517,6 +520,8 @@ + + diff --git a/VisualC-GDK/SDL/SDL.vcxproj.filters b/VisualC-GDK/SDL/SDL.vcxproj.filters index 017b9bafc..f143f8c1d 100644 --- a/VisualC-GDK/SDL/SDL.vcxproj.filters +++ b/VisualC-GDK/SDL/SDL.vcxproj.filters @@ -13,6 +13,12 @@ filesystem\windows + + file\generic + + + file + @@ -261,6 +267,9 @@ + + API Headers + @@ -352,6 +361,12 @@ filesystem + + file + + + file + diff --git a/VisualC/SDL/SDL.vcxproj b/VisualC/SDL/SDL.vcxproj index 27883a0c4..32c299e89 100644 --- a/VisualC/SDL/SDL.vcxproj +++ b/VisualC/SDL/SDL.vcxproj @@ -259,6 +259,7 @@ + @@ -352,6 +353,8 @@ + + @@ -416,6 +419,8 @@ + + diff --git a/VisualC/SDL/SDL.vcxproj.filters b/VisualC/SDL/SDL.vcxproj.filters index 34f05bb44..b959bc19c 100644 --- a/VisualC/SDL/SDL.vcxproj.filters +++ b/VisualC/SDL/SDL.vcxproj.filters @@ -211,6 +211,9 @@ {00009d5ded166cc6c6680ec771a30000} + + {00004d6806b6238cae0ed62db5440000} + @@ -279,6 +282,9 @@ API Headers + + API Headers + API Headers @@ -438,6 +444,12 @@ filesystem + + file + + + file + main @@ -944,6 +956,12 @@ filesystem\windows + + file\generic + + + file + main\generic diff --git a/Xcode/SDL/SDL.xcodeproj/project.pbxproj b/Xcode/SDL/SDL.xcodeproj/project.pbxproj index 23cf854b4..18b8c19da 100644 --- a/Xcode/SDL/SDL.xcodeproj/project.pbxproj +++ b/Xcode/SDL/SDL.xcodeproj/project.pbxproj @@ -545,6 +545,13 @@ F3FA5A242B59ACE000FEAD97 /* yuv_rgb_lsx.h in Headers */ = {isa = PBXBuildFile; fileRef = F3FA5A1B2B59ACE000FEAD97 /* yuv_rgb_lsx.h */; }; F3FA5A252B59ACE000FEAD97 /* yuv_rgb_common.h in Headers */ = {isa = PBXBuildFile; fileRef = F3FA5A1C2B59ACE000FEAD97 /* yuv_rgb_common.h */; }; FA73671D19A540EF004122E4 /* CoreVideo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FA73671C19A540EF004122E4 /* CoreVideo.framework */; platformFilters = (ios, maccatalyst, macos, tvos, watchos, ); }; + 0000E5D7110DFF81FF660000 /* SDL_cocoapen.h in Headers */ = {isa = PBXBuildFile; fileRef = 00002F2F5496FA184A0F0000 /* SDL_cocoapen.h */; }; + 0000D5B526B85DE7AB1C0000 /* SDL_cocoapen.m in Sources */ = {isa = PBXBuildFile; fileRef = 0000CCA310B73A7B59910000 /* SDL_cocoapen.m */; }; + 0000AEB9AE90228CA2D60000 /* SDL_asyncio.c in Sources */ = {isa = PBXBuildFile; fileRef = 00003928A612EC33D42C0000 /* SDL_asyncio.c */; }; + 000062F9C843687F50F70000 /* SDL_asyncio_c.h in Headers */ = {isa = PBXBuildFile; fileRef = 0000919399B1A908267F0000 /* SDL_asyncio_c.h */; }; + 00005081394CCF8322BE0000 /* SDL_sysasyncio.h in Headers */ = {isa = PBXBuildFile; fileRef = 0000585B2CAB450B40540000 /* SDL_sysasyncio.h */; }; + 000018AF97C08F2DAFFD0000 /* SDL_asyncio.h in Headers */ = {isa = PBXBuildFile; fileRef = 00004945A946DF5B1AED0000 /* SDL_asyncio.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 00004D0B73767647AD550000 /* SDL_asyncio_generic.c in Sources */ = {isa = PBXBuildFile; fileRef = 0000FB02CDE4BE34A87E0000 /* SDL_asyncio_generic.c */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -1119,6 +1126,13 @@ F59C710600D5CB5801000001 /* SDL.info */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = text; path = SDL.info; sourceTree = ""; }; F5A2EF3900C6A39A01000001 /* BUGS.txt */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = text; name = BUGS.txt; path = ../../BUGS.txt; sourceTree = SOURCE_ROOT; }; FA73671C19A540EF004122E4 /* CoreVideo.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreVideo.framework; path = System/Library/Frameworks/CoreVideo.framework; sourceTree = SDKROOT; }; + 00002F2F5496FA184A0F0000 /* SDL_cocoapen.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SDL_cocoapen.h; path = SDL_cocoapen.h; sourceTree = ""; }; + 0000CCA310B73A7B59910000 /* SDL_cocoapen.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SDL_cocoapen.m; path = SDL_cocoapen.m; sourceTree = ""; }; + 00003928A612EC33D42C0000 /* SDL_asyncio.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = SDL_asyncio.c; path = SDL_asyncio.c; sourceTree = ""; }; + 0000919399B1A908267F0000 /* SDL_asyncio_c.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SDL_asyncio_c.h; path = SDL_asyncio_c.h; sourceTree = ""; }; + 0000585B2CAB450B40540000 /* SDL_sysasyncio.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SDL_sysasyncio.h; path = SDL_sysasyncio.h; sourceTree = ""; }; + 00004945A946DF5B1AED0000 /* SDL_asyncio.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SDL_asyncio.h; path = SDL3/SDL_asyncio.h; sourceTree = ""; }; + 0000FB02CDE4BE34A87E0000 /* SDL_asyncio_generic.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = SDL_asyncio_generic.c; path = SDL_asyncio_generic.c; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -1294,6 +1308,7 @@ F3F7D8C52933074B00816151 /* SDL_video.h */, F3F7D8D42933074C00816151 /* SDL_vulkan.h */, F3F7D8CF2933074C00816151 /* SDL.h */, + 00004945A946DF5B1AED0000 /* SDL_asyncio.h */, ); name = "Public Headers"; path = ../../include; @@ -1934,6 +1949,10 @@ isa = PBXGroup; children = ( A7D8A7DB23E2513F00DCD162 /* SDL_iostream.c */, + 00003928A612EC33D42C0000 /* SDL_asyncio.c */, + 0000919399B1A908267F0000 /* SDL_asyncio_c.h */, + 0000585B2CAB450B40540000 /* SDL_sysasyncio.h */, + 000013C0F2EADC24ADC10000 /* generic */, ); path = file; sourceTree = ""; @@ -2426,6 +2445,14 @@ path = resources; sourceTree = ""; }; + 000013C0F2EADC24ADC10000 /* generic */ = { + isa = PBXGroup; + children = ( + 0000FB02CDE4BE34A87E0000 /* SDL_asyncio_generic.c */, + ); + path = generic; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -3050,6 +3077,8 @@ 000095FA1BDE436CF3AF0000 /* SDL_time.c in Sources */, 0000140640E77F73F1DF0000 /* SDL_dialog_utils.c in Sources */, 0000D5B526B85DE7AB1C0000 /* SDL_cocoapen.m in Sources */, + 0000AEB9AE90228CA2D60000 /* SDL_asyncio.c in Sources */, + 00004D0B73767647AD550000 /* SDL_asyncio_generic.c in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index e3527d087..0111abed8 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -128,7 +128,7 @@ add_sdl_example_executable(audio-load-wav SOURCES audio/03-load-wav/load-wav.c D 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(game-snake SOURCES game/01-snake/snake.c) - +add_sdl_example_executable(asyncio-load-bitmaps SOURCES asyncio/01-load-bitmaps/load-bitmaps.c DATAFILES ${CMAKE_CURRENT_SOURCE_DIR}/../test/sample.bmp ${CMAKE_CURRENT_SOURCE_DIR}/../test/gamepad_front.bmp ${CMAKE_CURRENT_SOURCE_DIR}/../test/speaker.bmp ${CMAKE_CURRENT_SOURCE_DIR}/../test/icon2x.bmp) if(PSP) # Build EBOOT files if building for PSP diff --git a/examples/asyncio/01-load-bitmaps/README.txt b/examples/asyncio/01-load-bitmaps/README.txt new file mode 100644 index 000000000..e4283d46a --- /dev/null +++ b/examples/asyncio/01-load-bitmaps/README.txt @@ -0,0 +1,6 @@ +This example code loads a few bitmap files from disk using the asynchronous +i/o, and then draws it to the window. It uses a task group to watch multiple +reads and deal with them in whatever order they finish. + +Note that for a single tiny file like this, you'd probably not want to bother +with async i/o in real life, but this is just an example of how to do it. diff --git a/examples/asyncio/01-load-bitmaps/load-bitmaps.c b/examples/asyncio/01-load-bitmaps/load-bitmaps.c new file mode 100644 index 000000000..4839491b2 --- /dev/null +++ b/examples/asyncio/01-load-bitmaps/load-bitmaps.c @@ -0,0 +1,125 @@ +/* + * This example code loads a bitmap with asynchronous i/o and renders it. + * + * 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 + +/* We will use this renderer to draw into this window every frame. */ +static SDL_Window *window = NULL; +static SDL_Renderer *renderer = NULL; +static SDL_AsyncIOQueue *queue = NULL; + +#define TOTAL_TEXTURES 4 +static const char * const bmps[TOTAL_TEXTURES] = { "sample.bmp", "gamepad_front.bmp", "speaker.bmp", "icon2x.bmp" }; +static SDL_Texture *textures[TOTAL_TEXTURES]; +static const SDL_FRect texture_rects[TOTAL_TEXTURES] = { + { 116, 156, 408, 167 }, + { 20, 200, 96, 60 }, + { 525, 180, 96, 96 }, + { 288, 375, 64, 64 } +}; + +/* This function runs once at startup. */ +SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[]) +{ + int i; + + if (!SDL_Init(SDL_INIT_VIDEO)) { + SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Couldn't initialize SDL!", SDL_GetError(), NULL); + return SDL_APP_FAILURE; + } + + if (!SDL_CreateWindowAndRenderer("examples/asyncio/load-bitmaps", 640, 480, 0, &window, &renderer)) { + SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Couldn't create window/renderer!", SDL_GetError(), NULL); + return SDL_APP_FAILURE; + } + + queue = SDL_CreateAsyncIOQueue(); + if (!queue) { + SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Couldn't create async i/o queue!", SDL_GetError(), NULL); + return SDL_APP_FAILURE; + } + + /* Load some .bmp files asynchronously from wherever the app is being run from, put them in the same queue. */ + for (i = 0; i < SDL_arraysize(bmps); i++) { + char *path = NULL; + SDL_asprintf(&path, "%s%s", SDL_GetBasePath(), bmps[i]); /* allocate a string of the full file path */ + /* you _should) check for failure, but we'll just go on without files here. */ + SDL_LoadFileAsync(path, queue, (void *) bmps[i]); /* attach the filename as app-specific data, so we can see it later. */ + SDL_free(path); + } + + 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. */ + } + + 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) +{ + SDL_AsyncIOOutcome outcome; + int i; + + if (SDL_GetAsyncIOResult(queue, &outcome)) { /* a .bmp file load has finished? */ + if (outcome.result == SDL_ASYNCIO_COMPLETE) { + /* this might be _any_ of the bmps; they might finish loading in any order. */ + for (i = 0; i < SDL_arraysize(bmps); i++) { + /* this doesn't need a strcmp because we gave the pointer from this array to SDL_LoadFileAsync */ + if (outcome.userdata == bmps[i]) { + break; + } + } + + if (i < SDL_arraysize(bmps)) { /* (just in case.) */ + SDL_Surface *surface = SDL_LoadBMP_IO(SDL_IOFromConstMem(outcome.buffer, (size_t) outcome.bytes_transferred), SDL_TRUE); + if (surface) { /* the renderer is not multithreaded, so create the texture here once the data loads. */ + textures[i] = SDL_CreateTextureFromSurface(renderer, surface); + if (!textures[i]) { + SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Couldn't create texture!", SDL_GetError(), NULL); + return SDL_APP_FAILURE; + } + SDL_DestroySurface(surface); + } + } + } + SDL_free(outcome.buffer); + } + + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); + SDL_RenderClear(renderer); + + for (i = 0; i < SDL_arraysize(textures); i++) { + SDL_RenderTexture(renderer, textures[i], NULL, &texture_rects[i]); + } + + SDL_RenderPresent(renderer); + + return SDL_APP_CONTINUE; /* carry on with the program! */ +} + +/* This function runs once at shutdown. */ +void SDL_AppQuit(void *appstate) +{ + int i; + + SDL_DestroyAsyncIOQueue(queue); + + for (i = 0; i < SDL_arraysize(textures); i++) { + SDL_DestroyTexture(textures[i]); + } + + /* SDL will clean up the window/renderer for us. */ +} + diff --git a/include/SDL3/SDL.h b/include/SDL3/SDL.h index 3698f6362..8eef1c627 100644 --- a/include/SDL3/SDL.h +++ b/include/SDL3/SDL.h @@ -30,6 +30,7 @@ #include #include +#include #include #include #include diff --git a/include/SDL3/SDL_asyncio.h b/include/SDL3/SDL_asyncio.h new file mode 100644 index 000000000..af4604161 --- /dev/null +++ b/include/SDL3/SDL_asyncio.h @@ -0,0 +1,506 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2024 Sam Lantinga + + 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. +*/ + +/* WIKI CATEGORY: AsyncIO */ + +/** + * # CategoryAsyncIO + * + * SDL offers a way to perform I/O asynchronously. This allows an app to + * read or write files without waiting for data to actually transfer; the + * functions that request I/O never block while the request is fulfilled. + * + * Instead, the data moves in the background and the app can check for + * results at their leisure. + * + * This is more complicated that just reading and writing files in a + * synchronous way, but it can allow for more efficiency, and never having + * framerate drops as the hard drive catches up, etc. + * + * The general usage pattern for async I/O is: + * + * - Create one or more SDL_AsyncIOQueue objects. + * - Open files with SDL_AsyncIOFromFile. + * - Start I/O tasks to the files with SDL_ReadAsyncIO or SDL_WriteAsyncIO, + * putting those tasks into one of the queues. + * - Later on, use SDL_GetAsyncIOResult on a queue to see if any task + * is finished without blocking. Tasks might finish in any order with + * success or failure. + * - When all your tasks are done, close the file with SDL_CloseAsyncIO. + * This also generates a task, since it might flush data to disk! + * + * This all works, without blocking, in a single thread, but one can also + * wait on a queue in a background thread, sleeping until new results + * have arrived: + * + * - Call SDL_WaitAsyncIOResult from one or more threads to efficiently block + * until new tasks complete. + * - When shutting down, call SDL_SignalAsyncIOQueue to unblock any sleeping + * threads despite there being no new tasks completed. + * + * And, of course, to match the synchronous SDL_LoadFile, we offer + * SDL_LoadFileAsync as a convenience function. This will handle allocating + * a buffer, slurping in the file data, and null-terminating it; you still + * get a task handle to check later. + */ + +#ifndef SDL_asyncio_h_ +#define SDL_asyncio_h_ + +#include + +#include +/* Set up for C function definitions, even when using C++ */ +#ifdef __cplusplus +extern "C" { +#endif + +/** + * The asynchronous I/O operation structure. + * + * This operates as an opaque handle. One can then request read or write + * operations on it. + * + * \since This struct is available since SDL 3.0.0. + * + * \sa SDL_AsyncIOFromFile + */ +typedef struct SDL_AsyncIO SDL_AsyncIO; + +/** + * Types of asynchronous I/O tasks. + * + * \since This enum is available since SDL 3.0.0. + */ +typedef enum SDL_AsyncIOTaskType +{ + SDL_ASYNCIO_TASK_READ, /**< A read operation. */ + SDL_ASYNCIO_TASK_WRITE, /**< A write operation. */ + SDL_ASYNCIO_TASK_CLOSE /**< A close operation. */ +} SDL_AsyncIOTaskType; + +/** + * Possible outcomes of an asynchronous I/O task. + * + * \since This enum is available since SDL 3.0.0. + */ +typedef enum SDL_AsyncIOResult +{ + SDL_ASYNCIO_COMPLETE, /**< request was completed without error */ + SDL_ASYNCIO_FAILURE, /**< request failed for some reason; check SDL_GetError()! */ + SDL_ASYNCIO_CANCELLED /**< request was cancelled before completing. */ +} SDL_AsyncIOResult; + +/** + * Information about a completed asynchronous I/O request. + * + * \since This struct is available since SDL 3.0.0. + */ +typedef struct SDL_AsyncIOOutcome +{ + SDL_AsyncIO *asyncio; /**< what generated this task. This pointer will be invalid if it was closed! */ + SDL_AsyncIOTaskType type; /**< What sort of task was this? Read, write, etc? */ + SDL_AsyncIOResult result; /**< the result of the work (success, failure, cancellation). */ + void *buffer; /**< buffer where data was read/written. */ + Uint64 offset; /**< offset in the SDL_AsyncIO where data was read/written. */ + Uint64 bytes_requested; /**< number of bytes the task was to read/write. */ + Uint64 bytes_transferred; /**< actual number of bytes that were read/written. */ + void *userdata; /**< pointer provided by the app when starting the task */ +} SDL_AsyncIOOutcome; + +/** + * An opaque handle for asynchronous I/O tasks. + * + * Each asynchronous read or write operation generates a task, which will + * complete at some time in the future. This handle is used to track the + * progress of that task. + * + * Tasks are added to an SDL_AsyncIOQueue, where they can be queried for + * completion later. + * + * \since This struct is available since SDL 3.0.0. + * + * \sa SDL_ReadAsyncIO + * \sa SDL_WriteAsyncIO + */ +typedef struct SDL_AsyncIOTask SDL_AsyncIOTask; + +/** + * A queue of completed asynchronous I/O tasks. + * + * When starting an asynchronous operation, you specify a queue for the new + * task. A queue can be asked later if any tasks in it have completed, + * allowing an app to manage multiple pending tasks in one place, in + * whatever order they complete. + * + * \since This struct is available since SDL 3.0.0. + * + * \sa SDL_CreateAsyncIOQueue + * \sa SDL_ReadAsyncIO + * \sa SDL_WriteAsyncIO + * \sa SDL_GetAsyncIOResult + * \sa SDL_WaitAsyncIOResult + */ +typedef struct SDL_AsyncIOQueue SDL_AsyncIOQueue; + +/** + * Use this function to create a new SDL_AsyncIO object for reading from + * and/or writing to a named file. + * + * The `mode` string understands the following values: + * + * - "r": Open a file for reading only. It must exist. + * - "w": Open a file for writing only. It will create missing files or truncate existing ones. + * - "r+": Open a file for update both reading and writing. The file must + * exist. + * - "w+": Create an empty file for both reading and writing. If a file with + * the same name already exists its content is erased and the file is + * treated as a new empty file. + * + * There is no "b" mode, as there is only "binary" style I/O, and no "a" mode + * for appending, since you specify the position when starting a task. + * + * This function supports Unicode filenames, but they must be encoded in UTF-8 + * format, regardless of the underlying operating system. + * + * This call is _not_ asynchronous; it will open the file before returning, + * under the assumption that doing so is generally a fast operation. Future + * reads and writes to the opened file will be async, however. + * + * \param file a UTF-8 string representing the filename to open. + * \param mode an ASCII string representing the mode to be used for opening + * the file. + * \returns a pointer to the SDL_AsyncIO structure that is created or NULL on + * failure; call SDL_GetError() for more information. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_CloseAsyncIO + * \sa SDL_ReadAsyncIO + * \sa SDL_WriteAsyncIO + */ +extern SDL_DECLSPEC SDL_AsyncIO * SDLCALL SDL_AsyncIOFromFile(const char *file, const char *mode); + +/** + * Use this function to get the size of the data stream in an SDL_AsyncIO. + * + * This call is _not_ asynchronous; it assumes that obtaining this info + * is a non-blocking operation in most reasonable cases. + * + * \param asyncio the SDL_AsyncIO to get the size of the data stream from. + * \returns the size of the data stream in the SDL_IOStream on success or a + * negative error code on failure; call SDL_GetError() for more + * information. + * + * \threadsafety It is safe to call this function from any thread. + * + * \since This function is available since SDL 3.0.0. + */ +extern SDL_DECLSPEC Sint64 SDLCALL SDL_GetAsyncIOSize(SDL_AsyncIO *asyncio); + +/** + * Start an async read. + * + * This function reads up to `size` bytes from `offset` position in the data + * source to the area pointed at by `ptr`. This function may read less bytes + * than requested. + * + * This function returns as quickly as possible; it does not wait for the + * read to complete. On a successful return, this work will continue in the + * background. If the work begins, even failure is asynchronous: a failing + * return value from this function only means the work couldn't start at all. + * + * `ptr` must remain available until the work is done, and may be accessed by + * the system at any time until then. Do not allocate it on the stack, as this + * might take longer than the life of the calling function to complete! + * + * An SDL_AsyncIOQueue must be specified. The newly-created SDL_AsyncIOTask + * will be added to it when it completes its work. + * + * \param asyncio a pointer to an SDL_AsyncIO structure. + * \param ptr a pointer to a buffer to read data into. + * \param offset the position to start reading in the data source. + * \param size the number of bytes to read from the data source. + * \param queue a queue to add the new SDL_AsyncIO to. + * \param userdata an app-defined pointer that will be provided with the task results. + * \returns A new task handle if a task was started, NULL on complete failure; + * call SDL_GetError() for more information. + * + * \threadsafety It is safe to call this function from any thread. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_WriteAsyncIO + * \sa SDL_CreateAsyncIOQueue + * \sa SDL_GetAsyncIOTaskResult + */ +extern SDL_DECLSPEC SDL_AsyncIOTask * SDLCALL SDL_ReadAsyncIO(SDL_AsyncIO *asyncio, void *ptr, Uint64 offset, Uint64 size, SDL_AsyncIOQueue *queue, void *userdata); + +/** + * Start an async write. + * + * This function writes `size` bytes from `offset` position in the data + * source to the area pointed at by `ptr`. + * + * This function returns as quickly as possible; it does not wait for the + * write to complete. On a successful return, this work will continue in the + * background. If the work begins, even failure is asynchronous: a failing + * return value from this function only means the work couldn't start at all. + * + * `ptr` must remain available until the work is done, and may be accessed by + * the system at any time until then. Do not allocate it on the stack, as this + * might take longer than the life of the calling function to complete! + * + * An SDL_AsyncIOQueue must be specified. The newly-created SDL_AsyncIOTask + * will be added to it when it completes its work. + * + * \param asyncio a pointer to an SDL_AsyncIO structure. + * \param ptr a pointer to a buffer to write data from. + * \param offset the position to start writing to the data source. + * \param size the number of bytes to write to the data source. + * \param queue a queue to add the new SDL_AsyncIO to. + * \param userdata an app-defined pointer that will be provided with the task results. + * \returns A new task handle if a task was started, NULL on complete failure; + * call SDL_GetError() for more information. + * + * \threadsafety It is safe to call this function from any thread. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_ReadAsyncIO + * \sa SDL_CreateAsyncIOQueue + * \sa SDL_GetAsyncIOTaskResult + + */ +extern SDL_DECLSPEC SDL_AsyncIOTask * SDLCALL SDL_WriteAsyncIO(SDL_AsyncIO *asyncio, void *ptr, Uint64 offset, Uint64 size, SDL_AsyncIOQueue *queue, void *userdata); + +/** + * Close and free any allocated resources for an async I/O object. + * + * Closing a file is _also_ an asynchronous task! If a write failure + * were to happen during the closing process, for example, the + * task results will report it as usual. + * + * This function guarantees that the close will happen after any other + * pending tasks to `asyncio`, so it's safe to open a file, start + * several operations, close the file immediately, then check for all + * results later. This function will not block until the tasks have + * completed. + * + * Once this function returns non-NULL, `asyncio` is no longer valid, + * regardless of any future outcomes. Any completed tasks might still + * contain this pointer in their SDL_AsyncIOOutcome data, in case the + * app was using this value to track information, but it should not + * be used again. + * + * If this function returns NULL, the close wasn't started at all, and + * it's safe to attempt to close again later. + * + * \param asyncio a pointer to an SDL_AsyncIO structure to close. + * \param queue a queue to add the new SDL_AsyncIO to. + * \param userdata an app-defined pointer that will be provided with the task results. + * \returns A new task handle if a task was started, NULL on complete failure; + * call SDL_GetError() for more information. + * + * \threadsafety It is safe to call this function from any thread, but + * two threads should not attempt to close the same object. + * + * \since This function is available since SDL 3.0.0. + */ +extern SDL_DECLSPEC SDL_AsyncIOTask * SDLCALL SDL_CloseAsyncIO(SDL_AsyncIO *asyncio, SDL_AsyncIOQueue *queue, void *userdata); + +/** + * Create a task queue for tracking multiple I/O operations. + * + * Async I/O operations are assigned to a queue when started. The + * queue can be checked for completed tasks thereafter. + * + * \returns a new task queue object or NULL if there was an error; call + * SDL_GetError() for more information. + * + * \threadsafety It is safe to call this function from any thread. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_DestroyAsyncIOQueue + * \sa SDL_GetAsyncIOResult + * \sa SDL_WaitAsyncIOResult + */ +extern SDL_DECLSPEC SDL_AsyncIOQueue * SDLCALL SDL_CreateAsyncIOQueue(void); + +/** + * Destroy a previously-created async I/O task queue. + * + * If there are still tasks pending for this queue, this call will block until + * those tasks are finished. All those tasks will be deallocated. Their results + * will be lost to the app. + * + * Any pending reads from SDL_LoadFileAsync() that are still in this queue + * will have their buffers deallocated by this function, to prevent a memory + * leak. + * + * Once this function is called, the queue is no longer valid and should not + * be used, including by other threads that might access it while destruction + * is blocking on pending tasks. + * + * Do not destroy a queue that still has threads waiting on it through + * SDL_WaitAsyncIOResult(). You can call SDL_SignalAsyncIOQueue() first to + * unblock those threads, and take measures (such as SDL_WaitThread()) to make sure + * they have finished their wait and won't wait on the queue again. + * + * \param queue the task queue to destroy. + * + * \threadsafety It is safe to call this function from any thread, so long as + * no other thread is waiting on the queue with SDL_WaitAsyncIOResult. + * + * \since This function is available since SDL 3.0.0. + */ +extern SDL_DECLSPEC void SDLCALL SDL_DestroyAsyncIOQueue(SDL_AsyncIOQueue *queue); + +/** + * Query an async I/O task queue for completed tasks. + * + * If a task assigned to this queue has finished, this will return SDL_TRUE and fill in + * `outcome` with the details of the task. If no task in the queue has finished, + * this function will return SDL_FALSE. This function does not block. + * + * If a task has completed, this function will free its resources and the task + * pointer will no longer be valid. The task will be removed from the queue. + * + * It is safe for multiple threads to call this function on the same queue at + * once; a completed task will only go to one of the threads. + * + * \param queue the async I/O task queue to query. + * \param outcome details of a finished task will be written here. May not be NULL. + * \returns SDL_TRUE if task has completed, SDL_FALSE otherwise. + * + * \threadsafety It is safe to call this function from any thread. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_WaitAsyncIOResult + */ +extern SDL_DECLSPEC SDL_bool SDLCALL SDL_GetAsyncIOResult(SDL_AsyncIOQueue *queue, SDL_AsyncIOOutcome *outcome); + +/** + * Block until an async I/O task queue has a completed task. + * + * This function puts the calling thread to sleep until there a task assigned to + * the queue that has finished. + * + * If a task assigned to the queue has finished, this will return SDL_TRUE and + * fill in `outcome` with the details of the task. If no task in the queue has + * finished, this function will return SDL_FALSE. + * + * If a task has completed, this function will free its resources and the task + * pointer will no longer be valid. The task will be removed from the queue. + * + * It is safe for multiple threads to call this function on the same queue at + * once; a completed task will only go to one of the threads. + * + * Note that by the nature of various platforms, more than one waiting + * thread may wake to handle a single task, but only one will obtain it, + * so `timeoutMS` is a _maximum_ wait time, and this function may return + * SDL_FALSE sooner. + * + * This function may return SDL_FALSE if there was a system error, the OS + * inadvertently awoke multiple threads, or if SDL_SignalAsyncIOQueue() was + * called to wake up all waiting threads without a finished task. + * + * A timeout can be used to specify a maximum wait time, but rather than polling, + * it is possible to have a timeout of -1 to wait forever, and use + * SDL_SignalAsyncIOQueue() to wake up the waiting threads later. + * + * \param queue the async I/O task queue to wait on. + * \param outcome details of a finished task will be written here. May not be NULL. + * \param timeoutMS the maximum time to wait, in milliseconds, or -1 to wait + * indefinitely. + * \returns SDL_TRUE if task has completed, SDL_FALSE otherwise. + * + * \threadsafety It is safe to call this function from any thread. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_SignalAsyncIOQueue + */ +extern SDL_DECLSPEC SDL_bool SDLCALL SDL_WaitAsyncIOResult(SDL_AsyncIOQueue *queue, SDL_AsyncIOOutcome *outcome, Sint32 timeoutMS); + +/** + * Wake up any threads that are blocking in SDL_WaitAsyncIOResult(). + * + * This will unblock any threads that are sleeping in a call to + * SDL_WaitAsyncIOResult for the specified queue, and cause them to + * return from that function. + * + * This can be useful when destroying a queue to make sure nothing is + * touching it indefinitely. In this case, once this call completes, the + * caller should take measures to make sure any previously-blocked threads + * have returned from their wait and will not touch the queue again (perhaps + * by setting a flag to tell the threads to terminate and then using + * SDL_WaitThread() to make sure they've done so). + * + * \param queue the async I/O task queue to signal. + * + * \threadsafety It is safe to call this function from any thread. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_WaitAsyncIOResult + */ +extern SDL_DECLSPEC void SDLCALL SDL_SignalAsyncIOQueue(SDL_AsyncIOQueue *queue); + +/** + * Load all the data from a file path, asynchronously. + * + * This function returns as quickly as possible; it does not wait for the + * read to complete. On a successful return, this work will continue in the + * background. If the work begins, even failure is asynchronous: a failing + * return value from this function only means the work couldn't start at all. + * + * The data is allocated with a zero byte at the end (null terminated) for + * convenience. This extra byte is not included in SDL_AsyncIOOutcome's + * bytes_transferred value. + * + * This function will allocate the buffer to contain the file. It must be + * deallocated by calling SDL_free() on SDL_AsyncIOOutcome's buffer field + * after completion. + * + * An SDL_AsyncIOQueue must be specified. The newly-created SDL_AsyncIOTask + * will be added to it when it completes its work. + * + * \param file the path to read all available data from. + * \param queue a queue to add the new SDL_AsyncIO to. + * \param userdata an app-defined pointer that will be provided with the task results. + * \returns an async task, to be queried for results later. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_LoadFile_IO + */ +extern SDL_DECLSPEC SDL_AsyncIOTask * SDLCALL SDL_LoadFileAsync(const char *file, SDL_AsyncIOQueue *queue, void *userdata); + +/* Ends C function definitions when using C++ */ +#ifdef __cplusplus +} +#endif +#include + +#endif /* SDL_asyncio_h_ */ diff --git a/src/SDL.c b/src/SDL.c index 0116ba9c3..00954b72d 100644 --- a/src/SDL.c +++ b/src/SDL.c @@ -54,6 +54,7 @@ #include "video/SDL_pixels_c.h" #include "video/SDL_video_c.h" #include "filesystem/SDL_filesystem_c.h" +#include "file/SDL_asyncio_c.h" #define SDL_INIT_EVERYTHING ~0U @@ -604,7 +605,7 @@ void SDL_Quit(void) SDL_DBus_Quit(); #endif - SDL_QuitTimers(); + SDL_QuitAsyncIO(); SDL_SetObjectsInvalid(); SDL_AssertionsQuit(); diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym index 1b22f0544..bbe430ebe 100644 --- a/src/dynapi/SDL_dynapi.sym +++ b/src/dynapi/SDL_dynapi.sym @@ -15,6 +15,7 @@ SDL3_0.0.0 { SDL_AddTimer; SDL_AddTimerNS; SDL_AddVulkanRenderSemaphores; + SDL_AsyncIOFromFile; SDL_AttachVirtualJoystick; SDL_AudioDevicePaused; SDL_BeginGPUComputePass; @@ -47,6 +48,7 @@ SDL3_0.0.0 { SDL_CaptureMouse; SDL_ClaimWindowForGPUDevice; SDL_CleanupTLS; + SDL_CloseAsyncIO; SDL_ClearAudioStream; SDL_ClearClipboardData; SDL_ClearComposition; @@ -76,6 +78,7 @@ SDL3_0.0.0 { SDL_CopyGPUTextureToTexture; SDL_CopyProperties; SDL_CopyStorageFile; + SDL_CreateAsyncIOQueue; SDL_CreateAudioStream; SDL_CreateColorCursor; SDL_CreateCondition; @@ -120,6 +123,7 @@ SDL3_0.0.0 { SDL_DateTimeToTime; SDL_Delay; SDL_DelayNS; + SDL_DestroyAsyncIOQueue; SDL_DestroyAudioStream; SDL_DestroyCondition; SDL_DestroyCursor; @@ -214,6 +218,8 @@ SDL3_0.0.0 { SDL_GetAppMetadataProperty; SDL_GetAssertionHandler; SDL_GetAssertionReport; + SDL_GetAsyncIOResult; + SDL_GetAsyncIOSize; SDL_GetAtomicInt; SDL_GetAtomicPointer; SDL_GetAtomicU32; @@ -607,6 +613,7 @@ SDL3_0.0.0 { SDL_LoadBMP; SDL_LoadBMP_IO; SDL_LoadFile; + SDL_LoadFileAsync; SDL_LoadFile_IO; SDL_LoadFunction; SDL_LoadObject; @@ -688,6 +695,7 @@ SDL3_0.0.0 { SDL_Quit; SDL_QuitSubSystem; SDL_RaiseWindow; + SDL_ReadAsyncIO; SDL_ReadIO; SDL_ReadProcess; SDL_ReadS16BE; @@ -904,6 +912,7 @@ SDL3_0.0.0 { SDL_ShowSimpleMessageBox; SDL_ShowWindow; SDL_ShowWindowSystemMenu; + SDL_SignalAsyncIOQueue; SDL_SignalCondition; SDL_SignalSemaphore; SDL_StartTextInput; @@ -964,6 +973,7 @@ SDL3_0.0.0 { SDL_Vulkan_GetVkGetInstanceProcAddr; SDL_Vulkan_LoadLibrary; SDL_Vulkan_UnloadLibrary; + SDL_WaitAsyncIOResult; SDL_WaitCondition; SDL_WaitConditionTimeout; SDL_WaitEvent; @@ -980,6 +990,7 @@ SDL3_0.0.0 { SDL_WindowHasSurface; SDL_WindowSupportsGPUPresentMode; SDL_WindowSupportsGPUSwapchainComposition; + SDL_WriteAsyncIO; SDL_WriteIO; SDL_WriteS16BE; SDL_WriteS16LE; diff --git a/src/dynapi/SDL_dynapi_overrides.h b/src/dynapi/SDL_dynapi_overrides.h index 4b59a825a..9b5c312be 100644 --- a/src/dynapi/SDL_dynapi_overrides.h +++ b/src/dynapi/SDL_dynapi_overrides.h @@ -40,6 +40,7 @@ #define SDL_AddTimer SDL_AddTimer_REAL #define SDL_AddTimerNS SDL_AddTimerNS_REAL #define SDL_AddVulkanRenderSemaphores SDL_AddVulkanRenderSemaphores_REAL +#define SDL_AsyncIOFromFile SDL_AsyncIOFromFile_REAL #define SDL_AttachVirtualJoystick SDL_AttachVirtualJoystick_REAL #define SDL_AudioDevicePaused SDL_AudioDevicePaused_REAL #define SDL_BeginGPUComputePass SDL_BeginGPUComputePass_REAL @@ -78,6 +79,7 @@ #define SDL_ClearError SDL_ClearError_REAL #define SDL_ClearProperty SDL_ClearProperty_REAL #define SDL_ClearSurface SDL_ClearSurface_REAL +#define SDL_CloseAsyncIO SDL_CloseAsyncIO_REAL #define SDL_CloseAudioDevice SDL_CloseAudioDevice_REAL #define SDL_CloseCamera SDL_CloseCamera_REAL #define SDL_CloseGamepad SDL_CloseGamepad_REAL @@ -101,6 +103,7 @@ #define SDL_CopyGPUTextureToTexture SDL_CopyGPUTextureToTexture_REAL #define SDL_CopyProperties SDL_CopyProperties_REAL #define SDL_CopyStorageFile SDL_CopyStorageFile_REAL +#define SDL_CreateAsyncIOQueue SDL_CreateAsyncIOQueue_REAL #define SDL_CreateAudioStream SDL_CreateAudioStream_REAL #define SDL_CreateColorCursor SDL_CreateColorCursor_REAL #define SDL_CreateCondition SDL_CreateCondition_REAL @@ -145,6 +148,7 @@ #define SDL_DateTimeToTime SDL_DateTimeToTime_REAL #define SDL_Delay SDL_Delay_REAL #define SDL_DelayNS SDL_DelayNS_REAL +#define SDL_DestroyAsyncIOQueue SDL_DestroyAsyncIOQueue_REAL #define SDL_DestroyAudioStream SDL_DestroyAudioStream_REAL #define SDL_DestroyCondition SDL_DestroyCondition_REAL #define SDL_DestroyCursor SDL_DestroyCursor_REAL @@ -239,6 +243,8 @@ #define SDL_GetAppMetadataProperty SDL_GetAppMetadataProperty_REAL #define SDL_GetAssertionHandler SDL_GetAssertionHandler_REAL #define SDL_GetAssertionReport SDL_GetAssertionReport_REAL +#define SDL_GetAsyncIOResult SDL_GetAsyncIOResult_REAL +#define SDL_GetAsyncIOSize SDL_GetAsyncIOSize_REAL #define SDL_GetAtomicInt SDL_GetAtomicInt_REAL #define SDL_GetAtomicPointer SDL_GetAtomicPointer_REAL #define SDL_GetAtomicU32 SDL_GetAtomicU32_REAL @@ -632,6 +638,7 @@ #define SDL_LoadBMP SDL_LoadBMP_REAL #define SDL_LoadBMP_IO SDL_LoadBMP_IO_REAL #define SDL_LoadFile SDL_LoadFile_REAL +#define SDL_LoadFileAsync SDL_LoadFileAsync_REAL #define SDL_LoadFile_IO SDL_LoadFile_IO_REAL #define SDL_LoadFunction SDL_LoadFunction_REAL #define SDL_LoadObject SDL_LoadObject_REAL @@ -713,6 +720,7 @@ #define SDL_Quit SDL_Quit_REAL #define SDL_QuitSubSystem SDL_QuitSubSystem_REAL #define SDL_RaiseWindow SDL_RaiseWindow_REAL +#define SDL_ReadAsyncIO SDL_ReadAsyncIO_REAL #define SDL_ReadIO SDL_ReadIO_REAL #define SDL_ReadProcess SDL_ReadProcess_REAL #define SDL_ReadS16BE SDL_ReadS16BE_REAL @@ -929,6 +937,7 @@ #define SDL_ShowSimpleMessageBox SDL_ShowSimpleMessageBox_REAL #define SDL_ShowWindow SDL_ShowWindow_REAL #define SDL_ShowWindowSystemMenu SDL_ShowWindowSystemMenu_REAL +#define SDL_SignalAsyncIOQueue SDL_SignalAsyncIOQueue_REAL #define SDL_SignalCondition SDL_SignalCondition_REAL #define SDL_SignalSemaphore SDL_SignalSemaphore_REAL #define SDL_StartTextInput SDL_StartTextInput_REAL @@ -989,6 +998,7 @@ #define SDL_Vulkan_GetVkGetInstanceProcAddr SDL_Vulkan_GetVkGetInstanceProcAddr_REAL #define SDL_Vulkan_LoadLibrary SDL_Vulkan_LoadLibrary_REAL #define SDL_Vulkan_UnloadLibrary SDL_Vulkan_UnloadLibrary_REAL +#define SDL_WaitAsyncIOResult SDL_WaitAsyncIOResult_REAL #define SDL_WaitCondition SDL_WaitCondition_REAL #define SDL_WaitConditionTimeout SDL_WaitConditionTimeout_REAL #define SDL_WaitEvent SDL_WaitEvent_REAL @@ -1005,6 +1015,7 @@ #define SDL_WindowHasSurface SDL_WindowHasSurface_REAL #define SDL_WindowSupportsGPUPresentMode SDL_WindowSupportsGPUPresentMode_REAL #define SDL_WindowSupportsGPUSwapchainComposition SDL_WindowSupportsGPUSwapchainComposition_REAL +#define SDL_WriteAsyncIO SDL_WriteAsyncIO_REAL #define SDL_WriteIO SDL_WriteIO_REAL #define SDL_WriteS16BE SDL_WriteS16BE_REAL #define SDL_WriteS16LE SDL_WriteS16LE_REAL diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h index 6649326c6..7af2b10ad 100644 --- a/src/dynapi/SDL_dynapi_procs.h +++ b/src/dynapi/SDL_dynapi_procs.h @@ -61,6 +61,7 @@ SDL_DYNAPI_PROC(SDL_bool,SDL_AddSurfaceAlternateImage,(SDL_Surface *a, SDL_Surfa SDL_DYNAPI_PROC(SDL_TimerID,SDL_AddTimer,(Uint32 a, SDL_TimerCallback b, void *c),(a,b,c),return) SDL_DYNAPI_PROC(SDL_TimerID,SDL_AddTimerNS,(Uint64 a, SDL_NSTimerCallback b, void *c),(a,b,c),return) SDL_DYNAPI_PROC(SDL_bool,SDL_AddVulkanRenderSemaphores,(SDL_Renderer *a, Uint32 b, Sint64 c, Sint64 d),(a,b,c,d),return) +SDL_DYNAPI_PROC(SDL_AsyncIO*,SDL_AsyncIOFromFile,(const char *a, const char *b),(a,b),return) SDL_DYNAPI_PROC(SDL_JoystickID,SDL_AttachVirtualJoystick,(const SDL_VirtualJoystickDesc *a),(a),return) SDL_DYNAPI_PROC(SDL_bool,SDL_AudioDevicePaused,(SDL_AudioDeviceID a),(a),return) SDL_DYNAPI_PROC(SDL_GPUComputePass*,SDL_BeginGPUComputePass,(SDL_GPUCommandBuffer *a, const SDL_GPUStorageTextureWriteOnlyBinding *b, Uint32 c, const SDL_GPUStorageBufferWriteOnlyBinding *d, Uint32 e),(a,b,c,d,e),return) @@ -99,6 +100,7 @@ SDL_DYNAPI_PROC(SDL_bool,SDL_ClearComposition,(SDL_Window *a),(a),return) SDL_DYNAPI_PROC(SDL_bool,SDL_ClearError,(void),(),return) SDL_DYNAPI_PROC(SDL_bool,SDL_ClearProperty,(SDL_PropertiesID a, const char *b),(a,b),return) SDL_DYNAPI_PROC(SDL_bool,SDL_ClearSurface,(SDL_Surface *a, float b, float c, float d, float e),(a,b,c,d,e),return) +SDL_DYNAPI_PROC(SDL_AsyncIOTask*,SDL_CloseAsyncIO,(SDL_AsyncIO *a, SDL_AsyncIOQueue *b, void *c),(a,b,c),return) SDL_DYNAPI_PROC(void,SDL_CloseAudioDevice,(SDL_AudioDeviceID a),(a),) SDL_DYNAPI_PROC(void,SDL_CloseCamera,(SDL_Camera *a),(a),) SDL_DYNAPI_PROC(void,SDL_CloseGamepad,(SDL_Gamepad *a),(a),) @@ -122,6 +124,7 @@ SDL_DYNAPI_PROC(void,SDL_CopyGPUBufferToBuffer,(SDL_GPUCopyPass *a, const SDL_GP SDL_DYNAPI_PROC(void,SDL_CopyGPUTextureToTexture,(SDL_GPUCopyPass *a, const SDL_GPUTextureLocation *b, const SDL_GPUTextureLocation *c, Uint32 d, Uint32 e, Uint32 f, SDL_bool g),(a,b,c,d,e,f,g),) SDL_DYNAPI_PROC(SDL_bool,SDL_CopyProperties,(SDL_PropertiesID a, SDL_PropertiesID b),(a,b),return) SDL_DYNAPI_PROC(SDL_bool,SDL_CopyStorageFile,(SDL_Storage *a, const char *b, const char *c),(a,b,c),return) +SDL_DYNAPI_PROC(SDL_AsyncIOQueue*,SDL_CreateAsyncIOQueue,(void),(),return) SDL_DYNAPI_PROC(SDL_AudioStream*,SDL_CreateAudioStream,(const SDL_AudioSpec *a, const SDL_AudioSpec *b),(a,b),return) SDL_DYNAPI_PROC(SDL_Cursor*,SDL_CreateColorCursor,(SDL_Surface *a, int b, int c),(a,b,c),return) SDL_DYNAPI_PROC(SDL_Condition*,SDL_CreateCondition,(void),(),return) @@ -166,6 +169,7 @@ SDL_DYNAPI_PROC(SDL_bool,SDL_CursorVisible,(void),(),return) SDL_DYNAPI_PROC(SDL_bool,SDL_DateTimeToTime,(const SDL_DateTime *a, SDL_Time *b),(a,b),return) SDL_DYNAPI_PROC(void,SDL_Delay,(Uint32 a),(a),) SDL_DYNAPI_PROC(void,SDL_DelayNS,(Uint64 a),(a),) +SDL_DYNAPI_PROC(void,SDL_DestroyAsyncIOQueue,(SDL_AsyncIOQueue *a),(a),) SDL_DYNAPI_PROC(void,SDL_DestroyAudioStream,(SDL_AudioStream *a),(a),) SDL_DYNAPI_PROC(void,SDL_DestroyCondition,(SDL_Condition *a),(a),) SDL_DYNAPI_PROC(void,SDL_DestroyCursor,(SDL_Cursor *a),(a),) @@ -260,6 +264,8 @@ SDL_DYNAPI_PROC(int,SDL_GetAndroidSDKVersion,(void),(),return) SDL_DYNAPI_PROC(const char*,SDL_GetAppMetadataProperty,(const char *a),(a),return) SDL_DYNAPI_PROC(SDL_AssertionHandler,SDL_GetAssertionHandler,(void **a),(a),return) SDL_DYNAPI_PROC(const SDL_AssertData*,SDL_GetAssertionReport,(void),(),return) +SDL_DYNAPI_PROC(SDL_bool,SDL_GetAsyncIOResult,(SDL_AsyncIOQueue *a, SDL_AsyncIOOutcome *b),(a,b),return) +SDL_DYNAPI_PROC(Sint64,SDL_GetAsyncIOSize,(SDL_AsyncIO *a),(a),return) SDL_DYNAPI_PROC(int,SDL_GetAtomicInt,(SDL_AtomicInt *a),(a),return) SDL_DYNAPI_PROC(void*,SDL_GetAtomicPointer,(void **a),(a),return) SDL_DYNAPI_PROC(Uint32,SDL_GetAtomicU32,(SDL_AtomicU32 *a),(a),return) @@ -652,6 +658,7 @@ SDL_DYNAPI_PROC(SDL_bool,SDL_KillProcess,(SDL_Process *a, SDL_bool b),(a,b),retu SDL_DYNAPI_PROC(SDL_Surface*,SDL_LoadBMP,(const char *a),(a),return) SDL_DYNAPI_PROC(SDL_Surface*,SDL_LoadBMP_IO,(SDL_IOStream *a, SDL_bool b),(a,b),return) SDL_DYNAPI_PROC(void*,SDL_LoadFile,(const char *a, size_t *b),(a,b),return) +SDL_DYNAPI_PROC(SDL_AsyncIOTask*,SDL_LoadFileAsync,(const char *a, SDL_AsyncIOQueue *b, void *c),(a,b,c),return) SDL_DYNAPI_PROC(void*,SDL_LoadFile_IO,(SDL_IOStream *a, size_t *b, SDL_bool c),(a,b,c),return) SDL_DYNAPI_PROC(SDL_FunctionPointer,SDL_LoadFunction,(void *a, const char *b),(a,b),return) SDL_DYNAPI_PROC(void*,SDL_LoadObject,(const char *a),(a),return) @@ -724,6 +731,7 @@ SDL_DYNAPI_PROC(SDL_bool,SDL_QueryGPUFence,(SDL_GPUDevice *a, SDL_GPUFence *b),( SDL_DYNAPI_PROC(void,SDL_Quit,(void),(),) SDL_DYNAPI_PROC(void,SDL_QuitSubSystem,(SDL_InitFlags a),(a),) SDL_DYNAPI_PROC(SDL_bool,SDL_RaiseWindow,(SDL_Window *a),(a),return) +SDL_DYNAPI_PROC(SDL_AsyncIOTask*,SDL_ReadAsyncIO,(SDL_AsyncIO *a, void *b, Uint64 c, Uint64 d, SDL_AsyncIOQueue *e, void *f),(a,b,c,d,e,f),return) SDL_DYNAPI_PROC(size_t,SDL_ReadIO,(SDL_IOStream *a, void *b, size_t c),(a,b,c),return) SDL_DYNAPI_PROC(void*,SDL_ReadProcess,(SDL_Process *a, size_t *b, int *c),(a,b,c),return) SDL_DYNAPI_PROC(SDL_bool,SDL_ReadS16BE,(SDL_IOStream *a, Sint16 *b),(a,b),return) @@ -939,6 +947,7 @@ SDL_DYNAPI_PROC(void,SDL_ShowSaveFileDialog,(SDL_DialogFileCallback a, void *b, SDL_DYNAPI_PROC(SDL_bool,SDL_ShowSimpleMessageBox,(SDL_MessageBoxFlags a, const char *b, const char *c, SDL_Window *d),(a,b,c,d),return) SDL_DYNAPI_PROC(SDL_bool,SDL_ShowWindow,(SDL_Window *a),(a),return) SDL_DYNAPI_PROC(SDL_bool,SDL_ShowWindowSystemMenu,(SDL_Window *a, int b, int c),(a,b,c),return) +SDL_DYNAPI_PROC(void,SDL_SignalAsyncIOQueue,(SDL_AsyncIOQueue *a),(a),) SDL_DYNAPI_PROC(void,SDL_SignalCondition,(SDL_Condition *a),(a),) SDL_DYNAPI_PROC(void,SDL_SignalSemaphore,(SDL_Semaphore *a),(a),) SDL_DYNAPI_PROC(SDL_bool,SDL_StartTextInput,(SDL_Window *a),(a),return) @@ -999,6 +1008,7 @@ SDL_DYNAPI_PROC(SDL_bool,SDL_Vulkan_GetPresentationSupport,(VkInstance a, VkPhys SDL_DYNAPI_PROC(SDL_FunctionPointer,SDL_Vulkan_GetVkGetInstanceProcAddr,(void),(),return) SDL_DYNAPI_PROC(SDL_bool,SDL_Vulkan_LoadLibrary,(const char *a),(a),return) SDL_DYNAPI_PROC(void,SDL_Vulkan_UnloadLibrary,(void),(),) +SDL_DYNAPI_PROC(SDL_bool,SDL_WaitAsyncIOResult,(SDL_AsyncIOQueue *a, SDL_AsyncIOOutcome *b, Sint32 c),(a,b,c),return) SDL_DYNAPI_PROC(void,SDL_WaitCondition,(SDL_Condition *a, SDL_Mutex *b),(a,b),) SDL_DYNAPI_PROC(SDL_bool,SDL_WaitConditionTimeout,(SDL_Condition *a, SDL_Mutex *b, Sint32 c),(a,b,c),return) SDL_DYNAPI_PROC(SDL_bool,SDL_WaitEvent,(SDL_Event *a),(a),return) @@ -1015,6 +1025,7 @@ SDL_DYNAPI_PROC(SDL_InitFlags,SDL_WasInit,(SDL_InitFlags a),(a),return) SDL_DYNAPI_PROC(SDL_bool,SDL_WindowHasSurface,(SDL_Window *a),(a),return) SDL_DYNAPI_PROC(SDL_bool,SDL_WindowSupportsGPUPresentMode,(SDL_GPUDevice *a, SDL_Window *b, SDL_GPUPresentMode c),(a,b,c),return) SDL_DYNAPI_PROC(SDL_bool,SDL_WindowSupportsGPUSwapchainComposition,(SDL_GPUDevice *a, SDL_Window *b, SDL_GPUSwapchainComposition c),(a,b,c),return) +SDL_DYNAPI_PROC(SDL_AsyncIOTask*,SDL_WriteAsyncIO,(SDL_AsyncIO *a, void *b, Uint64 c, Uint64 d, SDL_AsyncIOQueue *e, void *f),(a,b,c,d,e,f),return) SDL_DYNAPI_PROC(size_t,SDL_WriteIO,(SDL_IOStream *a, const void *b, size_t c),(a,b,c),return) SDL_DYNAPI_PROC(SDL_bool,SDL_WriteS16BE,(SDL_IOStream *a, Sint16 b),(a,b),return) SDL_DYNAPI_PROC(SDL_bool,SDL_WriteS16LE,(SDL_IOStream *a, Sint16 b),(a,b),return) diff --git a/src/file/SDL_asyncio.c b/src/file/SDL_asyncio.c new file mode 100644 index 000000000..488bfb4fc --- /dev/null +++ b/src/file/SDL_asyncio.c @@ -0,0 +1,335 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2024 Sam Lantinga + + 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_sysasyncio.h" +#include "SDL_asyncio_c.h" + +static const char *AsyncFileModeValid(const char *mode) +{ + static const struct { const char *valid; const char *with_binary; } mode_map[] = { + { "r", "rb" }, + { "w", "wb" }, + { "r+","r+b" }, + { "w+", "w+b" } + }; + + for (int i = 0; i < SDL_arraysize(mode_map); i++) { + if (SDL_strcmp(mode, mode_map[i].valid) == 0) { + return mode_map[i].with_binary; + } + } + return NULL; +} + +SDL_AsyncIO *SDL_AsyncIOFromFile(const char *file, const char *mode) +{ + if (!file) { + SDL_InvalidParamError("file"); + return NULL; + } else if (!mode) { + SDL_InvalidParamError("mode"); + return NULL; + } + + const char *binary_mode = AsyncFileModeValid(mode); + if (!binary_mode) { + SDL_SetError("Unsupported file mode"); + return NULL; + } + + SDL_AsyncIO *asyncio = (SDL_AsyncIO *)SDL_calloc(1, sizeof(*asyncio)); + if (asyncio) { + asyncio->lock = SDL_CreateMutex(); + if (!asyncio->lock) { + SDL_free(asyncio); + return NULL; + } + } + + if (!SDL_SYS_AsyncIOFromFile(file, binary_mode, asyncio)) { + SDL_DestroyMutex(asyncio->lock); + SDL_free(asyncio); + return NULL; + } + + return asyncio; +} + +Sint64 SDL_GetAsyncIOSize(SDL_AsyncIO *asyncio) +{ + if (!asyncio) { + SDL_InvalidParamError("asyncio"); + return -1; + } + return asyncio->iface.size(asyncio->userdata); +} + +static SDL_AsyncIOTask *RequestAsyncIO(bool reading, SDL_AsyncIO *asyncio, void *ptr, Uint64 offset, Uint64 size, SDL_AsyncIOQueue *queue, void *userdata) +{ + if (!asyncio) { + SDL_InvalidParamError("asyncio"); + return NULL; + } else if (!ptr) { + SDL_InvalidParamError("ptr"); + return NULL; + } else if (!queue) { + SDL_InvalidParamError("queue"); + return NULL; + } + + SDL_AsyncIOTask *task = (SDL_AsyncIOTask *) SDL_calloc(1, sizeof (*task)); + if (!task) { + return NULL; + } + + task->asyncio = asyncio; + task->type = reading ? SDL_ASYNCIO_TASK_READ : SDL_ASYNCIO_TASK_WRITE; + task->offset = offset; + task->buffer = ptr; + task->requested_size = size; + task->app_userdata = userdata; + task->queue = queue; + + SDL_LockMutex(asyncio->lock); + if (asyncio->closing) { + SDL_free(task); + SDL_UnlockMutex(asyncio->lock); + SDL_SetError("SDL_AsyncIO is closing, can't start new tasks"); + return NULL; + } + LINKED_LIST_PREPEND(task, asyncio->tasks, asyncio); + SDL_AddAtomicInt(&queue->tasks_inflight, 1); + SDL_UnlockMutex(asyncio->lock); + + const bool queued = reading ? asyncio->iface.read(asyncio->userdata, task) : asyncio->iface.write(asyncio->userdata, task); + if (!queued) { + SDL_AddAtomicInt(&queue->tasks_inflight, -1); + SDL_LockMutex(asyncio->lock); + LINKED_LIST_UNLINK(task, asyncio); + SDL_UnlockMutex(asyncio->lock); + SDL_free(task); + task = NULL; + } + + return task; +} + +SDL_AsyncIOTask *SDL_ReadAsyncIO(SDL_AsyncIO *asyncio, void *ptr, Uint64 offset, Uint64 size, SDL_AsyncIOQueue *queue, void *userdata) +{ + return RequestAsyncIO(true, asyncio, ptr, offset, size, queue, userdata); +} + +SDL_AsyncIOTask *SDL_WriteAsyncIO(SDL_AsyncIO *asyncio, void *ptr, Uint64 offset, Uint64 size, SDL_AsyncIOQueue *queue, void *userdata) +{ + return RequestAsyncIO(false, asyncio, ptr, offset, size, queue, userdata); +} + +SDL_AsyncIOTask *SDL_CloseAsyncIO(SDL_AsyncIO *asyncio, SDL_AsyncIOQueue *queue, void *userdata) +{ + if (!asyncio) { + SDL_InvalidParamError("asyncio"); + return NULL; + } else if (!queue) { + SDL_InvalidParamError("queue"); + return NULL; + } + + SDL_LockMutex(asyncio->lock); + if (asyncio->closing) { + SDL_UnlockMutex(asyncio->lock); + SDL_SetError("Already closing"); + return NULL; + } + + SDL_AsyncIOTask *task = (SDL_AsyncIOTask *) SDL_calloc(1, sizeof (*task)); + if (task) { + task->asyncio = asyncio; + task->type = SDL_ASYNCIO_TASK_CLOSE; + task->app_userdata = userdata; + task->queue = queue; + + asyncio->closing = task; + + if (LINKED_LIST_START(asyncio->tasks, asyncio) == NULL) { // no tasks? Queue the close task now. + LINKED_LIST_PREPEND(task, asyncio->tasks, asyncio); + SDL_AddAtomicInt(&queue->tasks_inflight, 1); + if (!asyncio->iface.close(asyncio->userdata, task)) { + // uhoh, maybe they can try again later...? + SDL_AddAtomicInt(&queue->tasks_inflight, -1); + LINKED_LIST_UNLINK(task, asyncio); + SDL_free(task); + task = asyncio->closing = NULL; + } + } + } + + SDL_UnlockMutex(asyncio->lock); + + return task; +} + +SDL_AsyncIOQueue *SDL_CreateAsyncIOQueue(void) +{ + SDL_AsyncIOQueue *queue = SDL_calloc(1, sizeof (*queue)); + if (queue) { + SDL_SetAtomicInt(&queue->tasks_inflight, 0); + if (!SDL_SYS_CreateAsyncIOQueue(queue)) { + SDL_free(queue); + return NULL; + } + } + return queue; +} + +static bool GetAsyncIOTaskOutcome(SDL_AsyncIOTask *task, SDL_AsyncIOOutcome *outcome) +{ + if (!task || !outcome) { + return false; + } + + SDL_AsyncIO *asyncio = task->asyncio; + + SDL_zerop(outcome); + outcome->asyncio = asyncio->oneshot ? NULL : asyncio; + outcome->result = task->result; + outcome->buffer = task->buffer; + outcome->offset = task->offset; + outcome->bytes_requested = task->requested_size; + outcome->bytes_transferred = task->result_size; + outcome->userdata = task->app_userdata; + + // Take the completed task out of the SDL_AsyncIO that created it. + SDL_LockMutex(asyncio->lock); + LINKED_LIST_UNLINK(task, asyncio); + // see if it's time to queue a pending close request (close requested and no other pending tasks) + SDL_AsyncIOTask *closing = asyncio->closing; + if (closing && (task != closing) && (LINKED_LIST_START(asyncio->tasks, asyncio) == NULL)) { + LINKED_LIST_PREPEND(closing, asyncio->tasks, asyncio); + SDL_AddAtomicInt(&closing->queue->tasks_inflight, 1); + const bool async_close_task_was_queued = asyncio->iface.close(asyncio->userdata, closing); + SDL_assert(async_close_task_was_queued); // !!! FIXME: if this fails to queue the task, we're leaking resources! + if (!async_close_task_was_queued) { + SDL_AddAtomicInt(&closing->queue->tasks_inflight, -1); + } + } + SDL_UnlockMutex(task->asyncio->lock); + + // was this the result of a closing task? Finally destroy the asyncio. + bool retval = true; + if (closing && (task == closing)) { + if (asyncio->oneshot) { + retval = false; // don't send the close task results on to the app, just the read task for these. + } + asyncio->iface.destroy(asyncio->userdata); + SDL_DestroyMutex(asyncio->lock); + SDL_free(asyncio); + } + + SDL_AddAtomicInt(&task->queue->tasks_inflight, -1); + SDL_free(task); + + return retval; +} + +SDL_bool SDL_GetAsyncIOResult(SDL_AsyncIOQueue *queue, SDL_AsyncIOOutcome *outcome) +{ + if (!queue || !outcome) { + return SDL_FALSE; + } + return GetAsyncIOTaskOutcome(queue->iface.get_results(queue->userdata), outcome); +} + +SDL_bool SDL_WaitAsyncIOResult(SDL_AsyncIOQueue *queue, SDL_AsyncIOOutcome *outcome, Sint32 timeoutMS) +{ + if (!queue || !outcome) { + return SDL_FALSE; + } + return GetAsyncIOTaskOutcome(queue->iface.wait_results(queue->userdata, timeoutMS), outcome); +} + +void SDL_SignalAsyncIOQueue(SDL_AsyncIOQueue *queue) +{ + if (queue) { + queue->iface.signal(queue->userdata); + } +} + +void SDL_DestroyAsyncIOQueue(SDL_AsyncIOQueue *queue) +{ + if (queue) { + // block until any pending tasks complete. + while (SDL_GetAtomicInt(&queue->tasks_inflight) > 0) { + SDL_AsyncIOTask *task = queue->iface.wait_results(queue->userdata, -1); + if (task) { + if (task->asyncio->oneshot) { + SDL_free(task->buffer); // throw away the buffer from SDL_LoadFileAsync that will never be consumed/freed by app. + task->buffer = NULL; + } + SDL_AsyncIOOutcome outcome; + GetAsyncIOTaskOutcome(task, &outcome); // this frees the task, and does other upkeep. + } + } + + queue->iface.destroy(queue->userdata); + SDL_free(queue); + } +} + +void SDL_QuitAsyncIO(void) +{ + SDL_SYS_QuitAsyncIO(); +} + +SDL_AsyncIOTask *SDL_LoadFileAsync(const char *file, SDL_AsyncIOQueue *queue, void *userdata) +{ + if (!file) { + SDL_InvalidParamError("file"); + return NULL; + } else if (!queue) { + SDL_InvalidParamError("queue"); + return NULL; + } + + SDL_AsyncIOTask *task = NULL; + SDL_AsyncIO *asyncio = SDL_AsyncIOFromFile(file, "r"); + if (asyncio) { + asyncio->oneshot = true; + + void *ptr = NULL; + const Sint64 flen = SDL_GetAsyncIOSize(asyncio); + if (flen >= 0) { + // !!! FIXME: check if flen > address space, since it'll truncate and we'll just end up with an incomplete buffer or a crash. + ptr = SDL_malloc((size_t) (flen + 1)); // over-allocate by one so we can add a null-terminator. + if (ptr) { + task = SDL_ReadAsyncIO(asyncio, ptr, 0, (Uint64) flen, queue, userdata); + } + } + + if (!task) { + SDL_free(ptr); + } + + SDL_CloseAsyncIO(asyncio, queue, userdata); // if this fails, we'll have a resource leak, but this would already be a dramatic system failure. + } + + return task; +} diff --git a/src/file/SDL_asyncio_c.h b/src/file/SDL_asyncio_c.h new file mode 100644 index 000000000..85b443a9a --- /dev/null +++ b/src/file/SDL_asyncio_c.h @@ -0,0 +1,30 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2024 Sam Lantinga + + 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" + +#ifndef SDL_asyncio_c_h_ +#define SDL_asyncio_c_h_ + +// Shutdown any still-existing Async I/O. Note that there is no Init function, as it inits on-demand! +extern void SDL_QuitAsyncIO(void); + +#endif // SDL_asyncio_c_h_ + diff --git a/src/file/SDL_sysasyncio.h b/src/file/SDL_sysasyncio.h new file mode 100644 index 000000000..3ab12482a --- /dev/null +++ b/src/file/SDL_sysasyncio.h @@ -0,0 +1,135 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2024 Sam Lantinga + + 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" + +#ifndef SDL_sysasyncio_h_ +#define SDL_sysasyncio_h_ + +// If your platform has an option other than the "generic" code, make sure this +// is #defined to 0 instead and implement the SDL_SYS_* functions below in your +// backend (having them maybe call into the SDL_SYS_*_Generic versions as a +// fallback if the platform has functionality that isn't always available). +#define SDL_ASYNCIO_ONLY_HAVE_GENERIC 1 + +// this entire thing is just juggling doubly-linked lists, so make some helper macros. +#define LINKED_LIST_DECLARE_FIELDS(type, prefix) \ + type *prefix##prev; \ + type *prefix##next + +#define LINKED_LIST_PREPEND(item, list, prefix) do { \ + item->prefix##prev = &list; \ + item->prefix##next = list.prefix##next; \ + if (item->prefix##next) { \ + item->prefix##next->prefix##prev = item; \ + } \ + list.prefix##next = item; \ +} while (false) + +#define LINKED_LIST_UNLINK(item, prefix) do { \ + if (item->prefix##next) { \ + item->prefix##next->prefix##prev = item->prefix##prev; \ + } \ + item->prefix##prev->prefix##next = task->prefix##next; \ + item->prefix##prev = item->prefix##next = NULL; \ +} while (false) + +#define LINKED_LIST_START(list, prefix) (list.prefix##next) +#define LINKED_LIST_NEXT(item, prefix) (item->prefix##next) +#define LINKED_LIST_PREV(item, prefix) (item->prefix##prev) + +typedef struct SDL_AsyncIOTask SDL_AsyncIOTask; + +struct SDL_AsyncIOTask +{ + SDL_AsyncIO *asyncio; + SDL_AsyncIOTaskType type; + SDL_AsyncIOQueue *queue; + bool reading; + Uint64 offset; + void *buffer; + char *error; + SDL_AsyncIOResult result; + Uint64 requested_size; + Uint64 result_size; + void *app_userdata; + LINKED_LIST_DECLARE_FIELDS(struct SDL_AsyncIOTask, asyncio); + LINKED_LIST_DECLARE_FIELDS(struct SDL_AsyncIOTask, queue); // the generic backend uses this, so I've added it here to avoid the extra allocation. + LINKED_LIST_DECLARE_FIELDS(struct SDL_AsyncIOTask, threadpool); // the generic backend uses this, so I've added it here to avoid the extra allocation. +}; + +typedef struct SDL_AsyncIOQueueInterface +{ + bool (*queue_task)(void *userdata, SDL_AsyncIOTask *task); + void (*cancel_task)(void *userdata, SDL_AsyncIOTask *task); + SDL_AsyncIOTask * (*get_results)(void *userdata); + SDL_AsyncIOTask * (*wait_results)(void *userdata, Sint32 timeoutMS); + void (*signal)(void *userdata); + void (*destroy)(void *userdata); +} SDL_AsyncIOQueueInterface; + +struct SDL_AsyncIOQueue +{ + SDL_AsyncIOQueueInterface iface; + void *userdata; + SDL_AtomicInt tasks_inflight; +}; + +// this interface is kept per-object, even though generally it's going to decide +// on a single interface that is the same for the entire process, but I've kept +// the abstraction in case we start exposing more types of async i/o, like +// sockets, in the future. +typedef struct SDL_AsyncIOInterface +{ + Sint64 (*size)(void *userdata); + bool (*read)(void *userdata, SDL_AsyncIOTask *task); + bool (*write)(void *userdata, SDL_AsyncIOTask *task); + bool (*close)(void *userdata, SDL_AsyncIOTask *task); + void (*destroy)(void *userdata); +} SDL_AsyncIOInterface; + +struct SDL_AsyncIO +{ + SDL_AsyncIOInterface iface; + void *userdata; + SDL_Mutex *lock; + SDL_Mutex *iolock; // the generic backend uses this, so I've added it here for laziness's sake. + SDL_AsyncIOTask tasks; + SDL_AsyncIOTask *closing; // The close task, which isn't queued until all pending work for this file is done. + bool oneshot; // true if this is a SDL_LoadFileAsync open. +}; + +// This is implemented for various platforms; param validation is done before calling this. Open file, fill in iface and userdata. +extern bool SDL_SYS_AsyncIOFromFile(const char *file, const char *mode, SDL_AsyncIO *asyncio); + +// This is implemented for various platforms. Call SDL_OpenAsyncIOQueue from in here. +extern bool SDL_SYS_CreateAsyncIOQueue(SDL_AsyncIOQueue *queue); + +// This is called during SDL_QuitAsyncIO, after all tasks have completed and all files are closed, to let the platform clean up global backend details. +extern void SDL_SYS_QuitAsyncIO(void); + +// the "generic" version is always available, since it is almost always needed as a fallback even on platforms that might offer something better. +extern bool SDL_SYS_AsyncIOFromFile_Generic(const char *file, const char *mode, SDL_AsyncIO *asyncio); +extern bool SDL_SYS_CreateAsyncIOQueue_Generic(SDL_AsyncIOQueue *queue); +extern void SDL_SYS_QuitAsyncIO_Generic(void); + +#endif + diff --git a/src/file/generic/SDL_asyncio_generic.c b/src/file/generic/SDL_asyncio_generic.c new file mode 100644 index 000000000..020c34aab --- /dev/null +++ b/src/file/generic/SDL_asyncio_generic.c @@ -0,0 +1,461 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2024 Sam Lantinga + + 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. +*/ + +// The generic backend uses a threadpool to block on synchronous i/o. +// This is not ideal, it's meant to be used if there isn't a platform-specific +// backend that can do something more efficient! + +#include "SDL_internal.h" +#include "../SDL_sysasyncio.h" + +// on Emscripten without threads, async i/o is synchronous. Sorry. Almost +// everything is MEMFS, so it's just a memcpy anyhow, and the Emscripten +// filesystem APIs don't offer async. In theory, directly accessing +// persistent storage _does_ offer async APIs at the browser level, but +// that's not exposed in Emscripten's filesystem abstraction. +#if defined(SDL_PLATFORM_EMSCRIPTEN) && !defined(__EMSCRIPTEN_PTHREADS__) +#define SDL_ASYNCIO_USE_THREADPOOL 0 +#else +#define SDL_ASYNCIO_USE_THREADPOOL 1 +#endif + +typedef struct GenericAsyncIOQueueData +{ + SDL_Mutex *lock; + SDL_Condition *condition; + SDL_AsyncIOTask completed_tasks; +} GenericAsyncIOQueueData; + +typedef struct GenericAsyncIOData +{ + SDL_Mutex *lock; // !!! FIXME: we can skip this lock if we have an equivalent of pread/pwrite + SDL_IOStream *io; +} GenericAsyncIOData; + +static void AsyncIOTaskComplete(SDL_AsyncIOTask *task) +{ + SDL_assert(task->queue); + GenericAsyncIOQueueData *data = (GenericAsyncIOQueueData *) task->queue->userdata; + SDL_LockMutex(data->lock); + LINKED_LIST_PREPEND(task, data->completed_tasks, queue); + SDL_SignalCondition(data->condition); // wake a thread waiting on the queue. + SDL_UnlockMutex(data->lock); +} + +// synchronous i/o is offloaded onto the threadpool. This function does the threaded work. +// This is called directly, without a threadpool, if !SDL_ASYNCIO_USE_THREADPOOL. +static void SynchronousIO(SDL_AsyncIOTask *task) +{ + SDL_assert(task->result != SDL_ASYNCIO_CANCELLED); // shouldn't have gotten in here if cancelled! + + GenericAsyncIOData *data = (GenericAsyncIOData *) task->asyncio->userdata; + SDL_IOStream *io = data->io; + const size_t size = (size_t) task->requested_size; + void *ptr = task->buffer; + + // this seek won't work if two tasks are reading from the same file at the same time, + // so we lock here. This makes multiple reads from a single file serialize, but different + // files will still run in parallel. An app can also open the same file twice to avoid this. + SDL_LockMutex(data->lock); + if (task->type == SDL_ASYNCIO_TASK_CLOSE) { + task->result = SDL_CloseIO(data->io) ? SDL_ASYNCIO_COMPLETE : SDL_ASYNCIO_FAILURE; + } else if (SDL_SeekIO(io, (Sint64) task->offset, SDL_IO_SEEK_SET) < 0) { + task->result = SDL_ASYNCIO_FAILURE; + } else { + const bool writing = (task->type == SDL_ASYNCIO_TASK_WRITE); + task->result_size = (Uint64) (writing ? SDL_WriteIO(io, ptr, size) : SDL_ReadIO(io, ptr, size)); + if (task->result_size == task->requested_size) { + task->result = SDL_ASYNCIO_COMPLETE; + } else { + if (writing) { + task->result = SDL_ASYNCIO_FAILURE; // it's always a failure on short writes. + } else { + const SDL_IOStatus status = SDL_GetIOStatus(io); + SDL_assert(status != SDL_IO_STATUS_READY); // this should have either failed or been EOF. + SDL_assert(status != SDL_IO_STATUS_NOT_READY); // these should not be non-blocking reads! + task->result = (status == SDL_IO_STATUS_EOF) ? SDL_ASYNCIO_COMPLETE : SDL_ASYNCIO_FAILURE; + } + } + } + SDL_UnlockMutex(data->lock); + + AsyncIOTaskComplete(task); +} + +#if SDL_ASYNCIO_USE_THREADPOOL +static SDL_InitState threadpool_init; +static SDL_Mutex *threadpool_lock = NULL; +static bool stop_threadpool = false; +static SDL_AsyncIOTask threadpool_tasks; +static SDL_Condition *threadpool_condition = NULL; +static int max_threadpool_threads = 0; +static int running_threadpool_threads = 0; +static int idle_threadpool_threads = 0; +static int threadpool_threads_spun = 0; + +static int SDLCALL AsyncIOThreadpoolWorker(void *data) +{ + SDL_LockMutex(threadpool_lock); + + while (!stop_threadpool) { + SDL_AsyncIOTask *task = LINKED_LIST_START(threadpool_tasks, threadpool); + if (!task) { + // if we go 30 seconds without a new task, terminate unless we're the only thread left. + idle_threadpool_threads++; + const SDL_bool rc = SDL_WaitConditionTimeout(threadpool_condition, threadpool_lock, 30000); + idle_threadpool_threads--; + + if (!rc) { + // decide if we have too many idle threads, and if so, quit to let thread pool shrink when not busy. + if (idle_threadpool_threads) { + break; + } + } + + continue; + } + + LINKED_LIST_UNLINK(task, threadpool); + + SDL_UnlockMutex(threadpool_lock); + + // bookkeeping is done, so we drop the mutex and fire the work. + SynchronousIO(task); + + SDL_LockMutex(threadpool_lock); // take the lock again and see if there's another task (if not, we'll wait on the Condition). + } + + running_threadpool_threads--; + + // this is kind of a hack, but this lets us reuse threadpool_condition to block on shutdown until all threads have exited. + if (stop_threadpool) { + SDL_BroadcastCondition(threadpool_condition); + } + + SDL_UnlockMutex(threadpool_lock); + + return 0; +} + +static bool MaybeSpinNewWorkerThread(void) +{ + // if all existing threads are busy and the pool of threads isn't maxed out, make a new one. + if ((idle_threadpool_threads == 0) && (running_threadpool_threads < max_threadpool_threads)) { + char threadname[32]; + SDL_snprintf(threadname, sizeof (threadname), "SDLasyncio%d", threadpool_threads_spun); + SDL_Thread *thread = SDL_CreateThread(AsyncIOThreadpoolWorker, threadname, NULL); + if (thread == NULL) { + return false; + } + SDL_DetachThread(thread); // these terminate themselves when idle too long, so we never WaitThread. + running_threadpool_threads++; + threadpool_threads_spun++; + } + return true; +} + +static void QueueAsyncIOTask(SDL_AsyncIOTask *task) +{ + SDL_assert(task != NULL); + + SDL_LockMutex(threadpool_lock); + + if (stop_threadpool) { // just in case. + task->result = SDL_ASYNCIO_CANCELLED; + AsyncIOTaskComplete(task); + } else { + LINKED_LIST_PREPEND(task, threadpool_tasks, threadpool); + MaybeSpinNewWorkerThread(); // okay if this fails or the thread pool is maxed out. Something will get there eventually. + + // tell idle threads to get to work. + // This is a broadcast because we want someone from the thread pool to wake up, but + // also shutdown might also be blocking on this. One of the threads will grab + // it, the others will go back to sleep. + SDL_BroadcastCondition(threadpool_condition); + } + + SDL_UnlockMutex(threadpool_lock); +} + +// We don't initialize async i/o at all until it's used, so +// JUST IN CASE two things try to start at the same time, +// this will make sure everything gets the same mutex. +static bool PrepareThreadpool(void) +{ + bool okay = true; + if (SDL_ShouldInit(&threadpool_init)) { + max_threadpool_threads = (SDL_GetNumLogicalCPUCores() * 2) + 1; // !!! FIXME: this should probably have a hint to override. + max_threadpool_threads = SDL_clamp(max_threadpool_threads, 1, 8); // 8 is probably more than enough. + + okay = (okay && ((threadpool_lock = SDL_CreateMutex()) != NULL)); + okay = (okay && ((threadpool_condition = SDL_CreateCondition()) != NULL)); + okay = (okay && MaybeSpinNewWorkerThread()); // make sure at least one thread is going, since we'll need it. + + if (!okay) { + if (threadpool_condition) { + SDL_DestroyCondition(threadpool_condition); + threadpool_condition = NULL; + } + if (threadpool_lock) { + SDL_DestroyMutex(threadpool_lock); + threadpool_lock = NULL; + } + } + + SDL_SetInitialized(&threadpool_init, okay); + } + return okay; +} + +static void ShutdownThreadpool(void) +{ + if (SDL_ShouldQuit(&threadpool_init)) { + SDL_LockMutex(threadpool_lock); + + // cancel anything that's still pending. + SDL_AsyncIOTask *task; + while ((task = LINKED_LIST_START(threadpool_tasks, threadpool)) != NULL) { + LINKED_LIST_UNLINK(task, threadpool); + task->result = SDL_ASYNCIO_CANCELLED; + AsyncIOTaskComplete(task); + } + + stop_threadpool = true; + SDL_BroadcastCondition(threadpool_condition); // tell the whole threadpool to wake up and quit. + + while (running_threadpool_threads > 0) { + // each threadpool thread will broadcast this condition before it terminates if stop_threadpool is set. + // we can't just join the threads because they are detached, so the thread pool can automatically shrink as necessary. + SDL_WaitCondition(threadpool_condition, threadpool_lock); + } + + SDL_UnlockMutex(threadpool_lock); + + SDL_DestroyMutex(threadpool_lock); + threadpool_lock = NULL; + SDL_DestroyCondition(threadpool_condition); + threadpool_condition = NULL; + + max_threadpool_threads = running_threadpool_threads = idle_threadpool_threads = threadpool_threads_spun = 0; + + stop_threadpool = false; + SDL_SetInitialized(&threadpool_init, false); + } +} +#endif + + +static Sint64 generic_asyncio_size(void *userdata) +{ + GenericAsyncIOData *data = (GenericAsyncIOData *) userdata; + return SDL_GetIOSize(data->io); +} + +static bool generic_asyncio_io(void *userdata, SDL_AsyncIOTask *task) +{ + return task->queue->iface.queue_task(task->queue->userdata, task); +} + +static void generic_asyncio_destroy(void *userdata) +{ + GenericAsyncIOData *data = (GenericAsyncIOData *) userdata; + SDL_DestroyMutex(data->lock); + SDL_free(data); +} + + +static bool generic_asyncioqueue_queue_task(void *userdata, SDL_AsyncIOTask *task) +{ + #if SDL_ASYNCIO_USE_THREADPOOL + QueueAsyncIOTask(task); + #else + SynchronousIO(task); // oh well. Get a better platform. + #endif + return true; +} + +static void generic_asyncioqueue_cancel_task(void *userdata, SDL_AsyncIOTask *task) +{ + #if !SDL_ASYNCIO_USE_THREADPOOL // in theory, this was all synchronous and should never call this, but just in case. + task->result = SDL_ASYNCIO_CANCELLED; + AsyncIOTaskComplete(task); + #else + // we can't stop i/o that's in-flight, but we _can_ just refuse to start it if the threadpool hadn't picked it up yet. + SDL_LockMutex(threadpool_lock); + if (LINKED_LIST_PREV(task, threadpool) != NULL) { // still in the queue waiting to be run? Take it out. + LINKED_LIST_UNLINK(task, threadpool); + task->result = SDL_ASYNCIO_CANCELLED; + AsyncIOTaskComplete(task); + } + SDL_UnlockMutex(threadpool_lock); + #endif +} + +static SDL_AsyncIOTask *generic_asyncioqueue_get_results(void *userdata) +{ + GenericAsyncIOQueueData *data = (GenericAsyncIOQueueData *) userdata; + SDL_LockMutex(data->lock); + SDL_AsyncIOTask *task = LINKED_LIST_START(data->completed_tasks, queue); + if (task) { + LINKED_LIST_UNLINK(task, queue); + } + SDL_UnlockMutex(data->lock); + return task; +} + +static SDL_AsyncIOTask *generic_asyncioqueue_wait_results(void *userdata, Sint32 timeoutMS) +{ + GenericAsyncIOQueueData *data = (GenericAsyncIOQueueData *) userdata; + SDL_LockMutex(data->lock); + SDL_AsyncIOTask *task = LINKED_LIST_START(data->completed_tasks, queue); + if (!task) { + SDL_WaitConditionTimeout(data->condition, data->lock, timeoutMS); + task = LINKED_LIST_START(data->completed_tasks, queue); + } + if (task) { + LINKED_LIST_UNLINK(task, queue); + } + SDL_UnlockMutex(data->lock); + return task; +} + +static void generic_asyncioqueue_signal(void *userdata) +{ + GenericAsyncIOQueueData *data = (GenericAsyncIOQueueData *) userdata; + SDL_LockMutex(data->lock); + SDL_BroadcastCondition(data->condition); + SDL_UnlockMutex(data->lock); +} + +static void generic_asyncioqueue_destroy(void *userdata) +{ + // !!! FIXME: remove/free completed tasks? Refuse to destroy a queue that has remaining tasks? + GenericAsyncIOQueueData *data = (GenericAsyncIOQueueData *) userdata; + SDL_DestroyMutex(data->lock); + SDL_DestroyCondition(data->condition); + SDL_free(data); +} + +bool SDL_SYS_CreateAsyncIOQueue_Generic(SDL_AsyncIOQueue *queue) +{ + #if SDL_ASYNCIO_USE_THREADPOOL + if (!PrepareThreadpool()) { + return false; + } + #endif + + GenericAsyncIOQueueData *data = (GenericAsyncIOQueueData *) SDL_calloc(1, sizeof (*data)); + if (!data) { + return false; + } + + data->lock = SDL_CreateMutex(); + if (!data->lock) { + SDL_free(data); + return false; + } + + data->condition = SDL_CreateCondition(); + if (!data->condition) { + SDL_DestroyMutex(data->lock); + SDL_free(data); + return false; + } + + static const SDL_AsyncIOQueueInterface SDL_AsyncIOQueue_Generic = { + generic_asyncioqueue_queue_task, + generic_asyncioqueue_cancel_task, + generic_asyncioqueue_get_results, + generic_asyncioqueue_wait_results, + generic_asyncioqueue_signal, + generic_asyncioqueue_destroy + }; + + SDL_copyp(&queue->iface, &SDL_AsyncIOQueue_Generic); + queue->userdata = data; + return true; +} + + +bool SDL_SYS_AsyncIOFromFile_Generic(const char *file, const char *mode, SDL_AsyncIO *asyncio) +{ + #if SDL_ASYNCIO_USE_THREADPOOL + if (!PrepareThreadpool()) { + return false; + } + #endif + + GenericAsyncIOData *data = (GenericAsyncIOData *) SDL_calloc(1, sizeof (*data)); + if (!data) { + return false; + } + + data->lock = SDL_CreateMutex(); + if (!data->lock) { + SDL_free(data); + return false; + } + + data->io = SDL_IOFromFile(file, mode); + if (!data->io) { + SDL_DestroyMutex(data->lock); + SDL_free(data); + return false; + } + + static const SDL_AsyncIOInterface SDL_AsyncIOFile_Generic = { + generic_asyncio_size, + generic_asyncio_io, + generic_asyncio_io, + generic_asyncio_io, + generic_asyncio_destroy + }; + + SDL_copyp(&asyncio->iface, &SDL_AsyncIOFile_Generic); + asyncio->userdata = data; + return true; +} + +void SDL_SYS_QuitAsyncIO_Generic(void) +{ + #if SDL_ASYNCIO_USE_THREADPOOL + ShutdownThreadpool(); + #endif +} + + +#if SDL_ASYNCIO_ONLY_HAVE_GENERIC +bool SDL_SYS_AsyncIOFromFile(const char *file, const char *mode, SDL_AsyncIO *asyncio) +{ + return SDL_SYS_AsyncIOFromFile_Generic(file, mode, asyncio); +} + +bool SDL_SYS_CreateAsyncIOQueue(SDL_AsyncIOQueue *queue) +{ + return SDL_SYS_CreateAsyncIOQueue_Generic(queue); +} + +void SDL_SYS_QuitAsyncIO(void) +{ + SDL_SYS_QuitAsyncIO_Generic(); +} +#endif + diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 034a297e1..7f3a60569 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -326,6 +326,7 @@ files2headers(gamepad_image_headers files2headers(icon_bmp_header icon.bmp) files2headers(glass_bmp_header glass.bmp) +add_sdl_test_executable(testasyncio MAIN_CALLBACKS NEEDS_RESOURCES TESTUTILS SOURCES testasyncio.c) add_sdl_test_executable(testaudio MAIN_CALLBACKS NEEDS_RESOURCES TESTUTILS SOURCES testaudio.c) add_sdl_test_executable(testcolorspace SOURCES testcolorspace.c) add_sdl_test_executable(testfile NONINTERACTIVE SOURCES testfile.c) diff --git a/test/testasyncio.c b/test/testasyncio.c new file mode 100644 index 000000000..47616597e --- /dev/null +++ b/test/testasyncio.c @@ -0,0 +1,176 @@ +/* + Copyright (C) 1997-2024 Sam Lantinga + + 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. +*/ + +#define SDL_MAIN_USE_CALLBACKS 1 +#include +#include +#include + +static SDL_Renderer *renderer = NULL; +static SDL_Texture *texture = NULL; +static SDL_AsyncIOQueue *queue = NULL; +static SDLTest_CommonState *state = NULL; + +SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[]) +{ + const char *base = NULL; + char **bmps = NULL; + int bmpcount = 0; + int i; + + SDL_srand(0); + + /* Initialize test framework */ + state = SDLTest_CommonCreateState(argv, SDL_INIT_VIDEO); + if (!state) { + return SDL_APP_FAILURE; + } + + /* Enable standard application logging */ + SDL_SetLogPriority(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_INFO); + + /* Parse commandline */ + for (i = 1; i < argc;) { + int consumed = SDLTest_CommonArg(state, i); + if (consumed <= 0) { + static const char *options[] = { + NULL, + }; + SDLTest_CommonLogUsage(state, argv[0], options); + SDL_Quit(); + SDLTest_CommonDestroyState(state); + return 1; + } + i += consumed; + } + + state->num_windows = 1; + + /* Load the SDL library */ + if (!SDLTest_CommonInit(state)) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't initialize SDL: %s", SDL_GetError()); + return SDL_APP_FAILURE; + } + + SDL_SetLogPriorities(SDL_LOG_PRIORITY_VERBOSE); + + renderer = state->renderers[0]; + if (!renderer) { + /* SDL_Log("Couldn't create renderer: %s", SDL_GetError()); */ + return SDL_APP_FAILURE; + } + + texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_STATIC, 512, 512); + if (!texture) { + SDL_Log("Couldn't create texture: %s", SDL_GetError()); + return SDL_APP_FAILURE; + } else { + static const Uint32 blank[512 * 512]; + const SDL_Rect rect = { 0, 0, 512, 512 }; + SDL_UpdateTexture(texture, &rect, blank, 512 * sizeof (Uint32)); + } + + queue = SDL_CreateAsyncIOQueue(); + if (!queue) { + SDL_Log("Couldn't create async i/o queue: %s", SDL_GetError()); + return SDL_APP_FAILURE; + } + + base = SDL_GetBasePath(); + bmps = SDL_GlobDirectory(base, "*.bmp", SDL_GLOB_CASEINSENSITIVE, &bmpcount); + if (!bmps || (bmpcount == 0)) { + SDL_Log("No BMP files found."); + return SDL_APP_FAILURE; + } + + for (i = 0; i < bmpcount; i++) { + char *path = NULL; + if (SDL_asprintf(&path, "%s%s", base, bmps[i]) < 0) { + SDL_free(path); + } else { + SDL_Log("Loading %s...", path); + SDL_LoadFileAsync(path, queue, path); + } + } + + SDL_free(bmps); + + return SDL_APP_CONTINUE; +} + +SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event) +{ + switch (event->type) { + case SDL_EVENT_QUIT: + return SDL_APP_SUCCESS; + + default: + break; + } + + return SDLTest_CommonEventMainCallbacks(state, event); +} + +static void async_io_task_complete(const SDL_AsyncIOOutcome *outcome) +{ + const char *fname = (const char *) outcome->userdata; + const char *resultstr = "[unknown result]"; + + switch (outcome->result) { + #define RESCASE(x) case x: resultstr = #x; break + RESCASE(SDL_ASYNCIO_COMPLETE); + RESCASE(SDL_ASYNCIO_FAILURE); + RESCASE(SDL_ASYNCIO_CANCELLED); + #undef RESCASE + } + + SDL_Log("File '%s' async results: %s", fname, resultstr); + + if (outcome->result == SDL_ASYNCIO_COMPLETE) { + SDL_Surface *surface = SDL_LoadBMP_IO(SDL_IOFromConstMem(outcome->buffer, (size_t) outcome->bytes_transferred), SDL_TRUE); + if (surface) { + SDL_Surface *converted = SDL_ConvertSurface(surface, SDL_PIXELFORMAT_RGBA8888); + SDL_DestroySurface(surface); + if (converted) { + const SDL_Rect rect = { 50 + SDL_rand(512 - 100), 50 + SDL_rand(512 - 100), converted->w, converted->h }; + SDL_UpdateTexture(texture, &rect, converted->pixels, converted->pitch); + SDL_DestroySurface(converted); + } + } + } + + SDL_free(outcome->userdata); + SDL_free(outcome->buffer); +} + +SDL_AppResult SDL_AppIterate(void *appstate) +{ + SDL_AsyncIOOutcome outcome; + if (SDL_GetAsyncIOResult(queue, &outcome)) { + async_io_task_complete(&outcome); + } + + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); + SDL_RenderClear(renderer); + SDL_RenderTexture(renderer, texture, NULL, NULL); + SDL_RenderPresent(renderer); + + return SDL_APP_CONTINUE; +} + +void SDL_AppQuit(void *appstate) +{ + SDL_DestroyAsyncIOQueue(queue); + SDL_DestroyTexture(texture); + SDLTest_CommonQuit(state); +} +