328
src/io/SDL_asyncio.c
Normal file
328
src/io/SDL_asyncio.c
Normal file
@@ -0,0 +1,328 @@
|
||||
/*
|
||||
Simple DirectMedia Layer
|
||||
Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
*/
|
||||
|
||||
#include "SDL_internal.h"
|
||||
#include "SDL_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 bool RequestAsyncIO(bool reading, SDL_AsyncIO *asyncio, void *ptr, Uint64 offset, Uint64 size, SDL_AsyncIOQueue *queue, void *userdata)
|
||||
{
|
||||
if (!asyncio) {
|
||||
return SDL_InvalidParamError("asyncio");
|
||||
} else if (!ptr) {
|
||||
return SDL_InvalidParamError("ptr");
|
||||
} else if (!queue) {
|
||||
return SDL_InvalidParamError("queue");
|
||||
}
|
||||
|
||||
SDL_AsyncIOTask *task = (SDL_AsyncIOTask *) SDL_calloc(1, sizeof (*task));
|
||||
if (!task) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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);
|
||||
return SDL_SetError("SDL_AsyncIO is closing, can't start new tasks");
|
||||
}
|
||||
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 != NULL);
|
||||
}
|
||||
|
||||
bool 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);
|
||||
}
|
||||
|
||||
bool 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);
|
||||
}
|
||||
|
||||
bool SDL_CloseAsyncIO(SDL_AsyncIO *asyncio, bool flush, SDL_AsyncIOQueue *queue, void *userdata)
|
||||
{
|
||||
if (!asyncio) {
|
||||
return SDL_InvalidParamError("asyncio");
|
||||
} else if (!queue) {
|
||||
return SDL_InvalidParamError("queue");
|
||||
}
|
||||
|
||||
SDL_LockMutex(asyncio->lock);
|
||||
if (asyncio->closing) {
|
||||
SDL_UnlockMutex(asyncio->lock);
|
||||
return SDL_SetError("Already closing");
|
||||
}
|
||||
|
||||
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;
|
||||
task->flush = flush;
|
||||
|
||||
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 != NULL);
|
||||
}
|
||||
|
||||
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->type = task->type;
|
||||
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;
|
||||
}
|
||||
|
||||
bool SDL_GetAsyncIOResult(SDL_AsyncIOQueue *queue, SDL_AsyncIOOutcome *outcome)
|
||||
{
|
||||
if (!queue || !outcome) {
|
||||
return false;
|
||||
}
|
||||
return GetAsyncIOTaskOutcome(queue->iface.get_results(queue->userdata), outcome);
|
||||
}
|
||||
|
||||
bool SDL_WaitAsyncIOResult(SDL_AsyncIOQueue *queue, SDL_AsyncIOOutcome *outcome, Sint32 timeoutMS)
|
||||
{
|
||||
if (!queue || !outcome) {
|
||||
return 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();
|
||||
}
|
||||
|
||||
bool SDL_LoadFileAsync(const char *file, SDL_AsyncIOQueue *queue, void *userdata)
|
||||
{
|
||||
if (!file) {
|
||||
return SDL_InvalidParamError("file");
|
||||
} else if (!queue) {
|
||||
return SDL_InvalidParamError("queue");
|
||||
}
|
||||
|
||||
bool retval = false;
|
||||
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) {
|
||||
retval = SDL_ReadAsyncIO(asyncio, ptr, 0, (Uint64) flen, queue, userdata);
|
||||
if (!retval) {
|
||||
SDL_free(ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SDL_CloseAsyncIO(asyncio, false, queue, userdata); // if this fails, we'll have a resource leak, but this would already be a dramatic system failure.
|
||||
}
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
30
src/io/SDL_asyncio_c.h
Normal file
30
src/io/SDL_asyncio_c.h
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
Simple DirectMedia Layer
|
||||
Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
*/
|
||||
#include "../SDL_internal.h"
|
||||
|
||||
#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_
|
||||
|
||||
1654
src/io/SDL_iostream.c
Normal file
1654
src/io/SDL_iostream.c
Normal file
File diff suppressed because it is too large
Load Diff
35
src/io/SDL_iostream_c.h
Normal file
35
src/io/SDL_iostream_c.h
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
Simple DirectMedia Layer
|
||||
Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
*/
|
||||
#include "SDL_internal.h"
|
||||
|
||||
#ifndef SDL_iostream_c_h_
|
||||
#define SDL_iostream_c_h_
|
||||
|
||||
#if defined(SDL_PLATFORM_WINDOWS)
|
||||
SDL_IOStream *SDL_IOFromHandle(HANDLE handle, const char *mode, bool autoclose);
|
||||
#else
|
||||
#if defined(HAVE_STDIO_H)
|
||||
extern SDL_IOStream *SDL_IOFromFP(FILE *fp, bool autoclose);
|
||||
#endif
|
||||
extern SDL_IOStream *SDL_IOFromFD(int fd, bool autoclose);
|
||||
#endif
|
||||
|
||||
#endif // SDL_iostream_c_h_
|
||||
144
src/io/SDL_sysasyncio.h
Normal file
144
src/io/SDL_sysasyncio.h
Normal file
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
Simple DirectMedia Layer
|
||||
Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
*/
|
||||
|
||||
#include "SDL_internal.h"
|
||||
|
||||
#ifndef SDL_sysasyncio_h_
|
||||
#define SDL_sysasyncio_h_
|
||||
|
||||
#if defined(SDL_PLATFORM_WINDOWS) && defined(NTDDI_WIN10_NI)
|
||||
#if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_APP) && NTDDI_VERSION >= NTDDI_WIN10_NI
|
||||
#define HAVE_IORINGAPI_H
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// 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).
|
||||
#if defined(HAVE_LIBURING_H) || defined(HAVE_IORINGAPI_H)
|
||||
#define SDL_ASYNCIO_ONLY_HAVE_GENERIC 0
|
||||
#else
|
||||
#define SDL_ASYNCIO_ONLY_HAVE_GENERIC 1
|
||||
#endif
|
||||
|
||||
// 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;
|
||||
Uint64 offset;
|
||||
bool flush;
|
||||
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_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
|
||||
|
||||
465
src/io/generic/SDL_asyncio_generic.c
Normal file
465
src/io/generic/SDL_asyncio_generic.c
Normal file
@@ -0,0 +1,465 @@
|
||||
/*
|
||||
Simple DirectMedia Layer
|
||||
Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
*/
|
||||
|
||||
// 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_CANCELED); // shouldn't have gotten in here if canceled!
|
||||
|
||||
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) {
|
||||
bool okay = true;
|
||||
if (task->flush) {
|
||||
okay = SDL_FlushIO(data->io);
|
||||
}
|
||||
okay = SDL_CloseIO(data->io) && okay;
|
||||
task->result = okay ? 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 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_CANCELED;
|
||||
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_CANCELED;
|
||||
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_CANCELED;
|
||||
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_CANCELED;
|
||||
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)
|
||||
{
|
||||
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
|
||||
|
||||
551
src/io/io_uring/SDL_asyncio_liburing.c
Normal file
551
src/io/io_uring/SDL_asyncio_liburing.c
Normal file
@@ -0,0 +1,551 @@
|
||||
/*
|
||||
Simple DirectMedia Layer
|
||||
Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
*/
|
||||
|
||||
// The Linux backend uses io_uring for asynchronous i/o, and falls back to
|
||||
// the "generic" threadpool implementation if liburing isn't available or
|
||||
// fails for some other reason.
|
||||
|
||||
#include "SDL_internal.h"
|
||||
|
||||
#ifdef HAVE_LIBURING_H
|
||||
|
||||
#include "../SDL_sysasyncio.h"
|
||||
|
||||
#include <liburing.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <string.h> // for strerror()
|
||||
|
||||
static SDL_InitState liburing_init;
|
||||
|
||||
// We could add a whole bootstrap thing like the audio/video/etc subsystems use, but let's keep this simple for now.
|
||||
static bool (*CreateAsyncIOQueue)(SDL_AsyncIOQueue *queue);
|
||||
static void (*QuitAsyncIO)(void);
|
||||
static bool (*AsyncIOFromFile)(const char *file, const char *mode, SDL_AsyncIO *asyncio);
|
||||
|
||||
// we never link directly to liburing.
|
||||
// (this says "-ffi" which sounds like a scripting language binding thing, but the non-ffi version
|
||||
// is static-inline code we can't lookup with dlsym. This is by design.)
|
||||
static const char *liburing_library = "liburing-ffi.so.2";
|
||||
static void *liburing_handle = NULL;
|
||||
|
||||
#define SDL_LIBURING_FUNCS \
|
||||
SDL_LIBURING_FUNC(int, io_uring_queue_init, (unsigned entries, struct io_uring *ring, unsigned flags)) \
|
||||
SDL_LIBURING_FUNC(struct io_uring_probe *,io_uring_get_probe,(void)) \
|
||||
SDL_LIBURING_FUNC(void, io_uring_free_probe, (struct io_uring_probe *probe)) \
|
||||
SDL_LIBURING_FUNC(int, io_uring_opcode_supported, (const struct io_uring_probe *p, int op)) \
|
||||
SDL_LIBURING_FUNC(struct io_uring_sqe *, io_uring_get_sqe, (struct io_uring *ring)) \
|
||||
SDL_LIBURING_FUNC(void, io_uring_prep_read,(struct io_uring_sqe *sqe, int fd, void *buf, unsigned nbytes, __u64 offset)) \
|
||||
SDL_LIBURING_FUNC(void, io_uring_prep_write,(struct io_uring_sqe *sqe, int fd, const void *buf, unsigned nbytes, __u64 offset)) \
|
||||
SDL_LIBURING_FUNC(void, io_uring_prep_close, (struct io_uring_sqe *sqe, int fd)) \
|
||||
SDL_LIBURING_FUNC(void, io_uring_prep_fsync, (struct io_uring_sqe *sqe, int fd, unsigned fsync_flags)) \
|
||||
SDL_LIBURING_FUNC(void, io_uring_prep_cancel, (struct io_uring_sqe *sqe, void *user_data, int flags)) \
|
||||
SDL_LIBURING_FUNC(void, io_uring_prep_timeout, (struct io_uring_sqe *sqe, struct __kernel_timespec *ts, unsigned count, unsigned flags)) \
|
||||
SDL_LIBURING_FUNC(void, io_uring_prep_nop, (struct io_uring_sqe *sqe)) \
|
||||
SDL_LIBURING_FUNC(void, io_uring_sqe_set_data, (struct io_uring_sqe *sqe, void *data)) \
|
||||
SDL_LIBURING_FUNC(void, io_uring_sqe_set_flags, (struct io_uring_sqe *sqe, unsigned flags)) \
|
||||
SDL_LIBURING_FUNC(int, io_uring_submit, (struct io_uring *ring)) \
|
||||
SDL_LIBURING_FUNC(int, io_uring_peek_cqe, (struct io_uring *ring, struct io_uring_cqe **cqe_ptr)) \
|
||||
SDL_LIBURING_FUNC(int, io_uring_wait_cqe, (struct io_uring *ring, struct io_uring_cqe **cqe_ptr)) \
|
||||
SDL_LIBURING_FUNC(int, io_uring_wait_cqe_timeout, (struct io_uring *ring, struct io_uring_cqe **cqe_ptr, struct __kernel_timespec *ts)) \
|
||||
SDL_LIBURING_FUNC(void, io_uring_cqe_seen, (struct io_uring *ring, struct io_uring_cqe *cqe)) \
|
||||
SDL_LIBURING_FUNC(void, io_uring_queue_exit, (struct io_uring *ring)) \
|
||||
|
||||
|
||||
#define SDL_LIBURING_FUNC(ret, fn, args) typedef ret (*SDL_fntype_##fn) args;
|
||||
SDL_LIBURING_FUNCS
|
||||
#undef SDL_LIBURING_FUNC
|
||||
|
||||
typedef struct SDL_LibUringFunctions
|
||||
{
|
||||
#define SDL_LIBURING_FUNC(ret, fn, args) SDL_fntype_##fn fn;
|
||||
SDL_LIBURING_FUNCS
|
||||
#undef SDL_LIBURING_FUNC
|
||||
} SDL_LibUringFunctions;
|
||||
|
||||
static SDL_LibUringFunctions liburing;
|
||||
|
||||
|
||||
typedef struct LibUringAsyncIOQueueData
|
||||
{
|
||||
SDL_Mutex *sqe_lock;
|
||||
SDL_Mutex *cqe_lock;
|
||||
struct io_uring ring;
|
||||
SDL_AtomicInt num_waiting;
|
||||
} LibUringAsyncIOQueueData;
|
||||
|
||||
|
||||
static void UnloadLibUringLibrary(void)
|
||||
{
|
||||
if (liburing_library) {
|
||||
SDL_UnloadObject(liburing_handle);
|
||||
liburing_library = NULL;
|
||||
}
|
||||
SDL_zero(liburing);
|
||||
}
|
||||
|
||||
static bool LoadLibUringSyms(void)
|
||||
{
|
||||
#define SDL_LIBURING_FUNC(ret, fn, args) { \
|
||||
liburing.fn = (SDL_fntype_##fn) SDL_LoadFunction(liburing_handle, #fn); \
|
||||
if (!liburing.fn) { \
|
||||
return false; \
|
||||
} \
|
||||
}
|
||||
SDL_LIBURING_FUNCS
|
||||
#undef SDL_LIBURING_FUNC
|
||||
return true;
|
||||
}
|
||||
|
||||
// we rely on the presence of liburing to handle io_uring for us. The alternative is making
|
||||
// direct syscalls into the kernel, which is undesirable. liburing both shields us from this,
|
||||
// but also smooths over some kernel version differences, etc.
|
||||
static bool LoadLibUring(void)
|
||||
{
|
||||
bool result = true;
|
||||
|
||||
if (!liburing_handle) {
|
||||
liburing_handle = SDL_LoadObject(liburing_library);
|
||||
if (!liburing_handle) {
|
||||
result = false;
|
||||
// Don't call SDL_SetError(): SDL_LoadObject already did.
|
||||
} else {
|
||||
result = LoadLibUringSyms();
|
||||
if (result) {
|
||||
static const int needed_ops[] = {
|
||||
IORING_OP_NOP,
|
||||
IORING_OP_FSYNC,
|
||||
IORING_OP_TIMEOUT,
|
||||
IORING_OP_CLOSE,
|
||||
IORING_OP_READ,
|
||||
IORING_OP_WRITE,
|
||||
IORING_OP_ASYNC_CANCEL
|
||||
};
|
||||
|
||||
struct io_uring_probe *probe = liburing.io_uring_get_probe();
|
||||
if (!probe) {
|
||||
result = false;
|
||||
} else {
|
||||
for (int i = 0; i < SDL_arraysize(needed_ops); i++) {
|
||||
if (!io_uring_opcode_supported(probe, needed_ops[i])) {
|
||||
result = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
liburing.io_uring_free_probe(probe);
|
||||
}
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
UnloadLibUringLibrary();
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool liburing_SetError(const char *what, int err)
|
||||
{
|
||||
SDL_assert(err <= 0);
|
||||
return SDL_SetError("%s failed: %s", what, strerror(-err));
|
||||
}
|
||||
|
||||
static Sint64 liburing_asyncio_size(void *userdata)
|
||||
{
|
||||
const int fd = (int) (intptr_t) userdata;
|
||||
struct stat statbuf;
|
||||
if (fstat(fd, &statbuf) < 0) {
|
||||
SDL_SetError("fstat failed: %s", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
return ((Sint64) statbuf.st_size);
|
||||
}
|
||||
|
||||
// you must hold sqe_lock when calling this!
|
||||
static bool liburing_asyncioqueue_queue_task(void *userdata, SDL_AsyncIOTask *task)
|
||||
{
|
||||
LibUringAsyncIOQueueData *queuedata = (LibUringAsyncIOQueueData *) userdata;
|
||||
const int rc = liburing.io_uring_submit(&queuedata->ring);
|
||||
return (rc < 0) ? liburing_SetError("io_uring_submit", rc) : true;
|
||||
}
|
||||
|
||||
static void liburing_asyncioqueue_cancel_task(void *userdata, SDL_AsyncIOTask *task)
|
||||
{
|
||||
SDL_AsyncIOTask *cancel_task = (SDL_AsyncIOTask *) SDL_calloc(1, sizeof (*cancel_task));
|
||||
if (!cancel_task) {
|
||||
return; // oh well, the task can just finish on its own.
|
||||
}
|
||||
|
||||
LibUringAsyncIOQueueData *queuedata = (LibUringAsyncIOQueueData *) userdata;
|
||||
|
||||
// have to hold a lock because otherwise two threads could get_sqe and submit while one request isn't fully set up.
|
||||
SDL_LockMutex(queuedata->sqe_lock);
|
||||
struct io_uring_sqe *sqe = liburing.io_uring_get_sqe(&queuedata->ring);
|
||||
if (!sqe) {
|
||||
SDL_UnlockMutex(queuedata->sqe_lock);
|
||||
SDL_free(cancel_task); // oh well, the task can just finish on its own.
|
||||
return;
|
||||
}
|
||||
|
||||
cancel_task->app_userdata = task;
|
||||
liburing.io_uring_prep_cancel(sqe, task, 0);
|
||||
liburing.io_uring_sqe_set_data(sqe, cancel_task);
|
||||
liburing_asyncioqueue_queue_task(userdata, task);
|
||||
SDL_UnlockMutex(queuedata->sqe_lock);
|
||||
}
|
||||
|
||||
static SDL_AsyncIOTask *ProcessCQE(LibUringAsyncIOQueueData *queuedata, struct io_uring_cqe *cqe)
|
||||
{
|
||||
if (!cqe) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
SDL_AsyncIOTask *task = (SDL_AsyncIOTask *) io_uring_cqe_get_data(cqe);
|
||||
if (task) { // can be NULL if this was just a wakeup message, a NOP, etc.
|
||||
if (!task->queue) { // We leave `queue` blank to signify this was a task cancellation.
|
||||
SDL_AsyncIOTask *cancel_task = task;
|
||||
task = (SDL_AsyncIOTask *) cancel_task->app_userdata;
|
||||
SDL_free(cancel_task);
|
||||
if (cqe->res >= 0) { // cancel was successful?
|
||||
task->result = SDL_ASYNCIO_CANCELED;
|
||||
} else {
|
||||
task = NULL; // it already finished or was too far along to cancel, so we'll pick up the actual results later.
|
||||
}
|
||||
} else if (cqe->res < 0) {
|
||||
task->result = SDL_ASYNCIO_FAILURE;
|
||||
// !!! FIXME: fill in task->error.
|
||||
} else {
|
||||
if ((task->type == SDL_ASYNCIO_TASK_WRITE) && (((Uint64) cqe->res) < task->requested_size)) {
|
||||
task->result = SDL_ASYNCIO_FAILURE; // it's always a failure on short writes.
|
||||
}
|
||||
|
||||
// don't explicitly mark it as COMPLETE; that's the default value and a linked task might have failed in an earlier operation and this would overwrite it.
|
||||
|
||||
if ((task->type == SDL_ASYNCIO_TASK_READ) || (task->type == SDL_ASYNCIO_TASK_WRITE)) {
|
||||
task->result_size = (Uint64) cqe->res;
|
||||
}
|
||||
}
|
||||
|
||||
if ((task->type == SDL_ASYNCIO_TASK_CLOSE) && task->flush) {
|
||||
task->flush = false;
|
||||
task = NULL; // don't return this one, it's a linked task, so it'll arrive in a later CQE.
|
||||
}
|
||||
}
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
static SDL_AsyncIOTask *liburing_asyncioqueue_get_results(void *userdata)
|
||||
{
|
||||
LibUringAsyncIOQueueData *queuedata = (LibUringAsyncIOQueueData *) userdata;
|
||||
|
||||
// have to hold a lock because otherwise two threads will get the same cqe until we mark it "seen". Copy and mark it right away, then process further.
|
||||
SDL_LockMutex(queuedata->cqe_lock);
|
||||
struct io_uring_cqe *cqe = NULL;
|
||||
const int rc = liburing.io_uring_peek_cqe(&queuedata->ring, &cqe);
|
||||
if (rc != 0) {
|
||||
SDL_assert(rc == -EAGAIN); // should only fail because nothing is available at the moment.
|
||||
SDL_UnlockMutex(queuedata->cqe_lock);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
struct io_uring_cqe cqe_copy;
|
||||
SDL_copyp(&cqe_copy, cqe); // this is only a few bytes.
|
||||
liburing.io_uring_cqe_seen(&queuedata->ring, cqe); // let io_uring use this slot again.
|
||||
SDL_UnlockMutex(queuedata->cqe_lock);
|
||||
|
||||
return ProcessCQE(queuedata, &cqe_copy);
|
||||
}
|
||||
|
||||
static SDL_AsyncIOTask *liburing_asyncioqueue_wait_results(void *userdata, Sint32 timeoutMS)
|
||||
{
|
||||
LibUringAsyncIOQueueData *queuedata = (LibUringAsyncIOQueueData *) userdata;
|
||||
struct io_uring_cqe *cqe = NULL;
|
||||
|
||||
SDL_AddAtomicInt(&queuedata->num_waiting, 1);
|
||||
if (timeoutMS < 0) {
|
||||
liburing.io_uring_wait_cqe(&queuedata->ring, &cqe);
|
||||
} else {
|
||||
struct __kernel_timespec ts = { (Sint64) timeoutMS / SDL_MS_PER_SECOND, (Sint64) SDL_MS_TO_NS(timeoutMS % SDL_MS_PER_SECOND) };
|
||||
liburing.io_uring_wait_cqe_timeout(&queuedata->ring, &cqe, &ts);
|
||||
}
|
||||
SDL_AddAtomicInt(&queuedata->num_waiting, -1);
|
||||
|
||||
// (we don't care if the wait failed for any reason, as the upcoming peek_cqe will report valid information. We just wanted the wait operation to block.)
|
||||
|
||||
// each thing that peeks or waits for a completion _gets the same cqe_ until we mark it as seen. So when we wake up from the wait, lock the mutex and
|
||||
// then use peek to make sure we have a unique cqe, and other competing threads either get their own or nothing.
|
||||
return liburing_asyncioqueue_get_results(userdata); // this just happens to do all those things.
|
||||
}
|
||||
|
||||
static void liburing_asyncioqueue_signal(void *userdata)
|
||||
{
|
||||
LibUringAsyncIOQueueData *queuedata = (LibUringAsyncIOQueueData *) userdata;
|
||||
const int num_waiting = SDL_GetAtomicInt(&queuedata->num_waiting);
|
||||
|
||||
SDL_LockMutex(queuedata->sqe_lock);
|
||||
for (int i = 0; i < num_waiting; i++) { // !!! FIXME: is there a better way to do this than pushing a zero-timeout request for everything waiting?
|
||||
struct io_uring_sqe *sqe = liburing.io_uring_get_sqe(&queuedata->ring);
|
||||
if (sqe) {
|
||||
static struct __kernel_timespec ts; // no wait, just wake a thread as fast as this can land in the completion queue.
|
||||
liburing.io_uring_prep_timeout(sqe, &ts, 0, 0);
|
||||
liburing.io_uring_sqe_set_data(sqe, NULL);
|
||||
}
|
||||
}
|
||||
liburing.io_uring_submit(&queuedata->ring);
|
||||
|
||||
SDL_UnlockMutex(queuedata->sqe_lock);
|
||||
}
|
||||
|
||||
static void liburing_asyncioqueue_destroy(void *userdata)
|
||||
{
|
||||
LibUringAsyncIOQueueData *queuedata = (LibUringAsyncIOQueueData *) userdata;
|
||||
liburing.io_uring_queue_exit(&queuedata->ring);
|
||||
SDL_DestroyMutex(queuedata->sqe_lock);
|
||||
SDL_DestroyMutex(queuedata->cqe_lock);
|
||||
SDL_free(queuedata);
|
||||
}
|
||||
|
||||
static bool SDL_SYS_CreateAsyncIOQueue_liburing(SDL_AsyncIOQueue *queue)
|
||||
{
|
||||
LibUringAsyncIOQueueData *queuedata = (LibUringAsyncIOQueueData *) SDL_calloc(1, sizeof (*queuedata));
|
||||
if (!queuedata) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_SetAtomicInt(&queuedata->num_waiting, 0);
|
||||
|
||||
queuedata->sqe_lock = SDL_CreateMutex();
|
||||
if (!queuedata->sqe_lock) {
|
||||
SDL_free(queuedata);
|
||||
return false;
|
||||
}
|
||||
|
||||
queuedata->cqe_lock = SDL_CreateMutex();
|
||||
if (!queuedata->cqe_lock) {
|
||||
SDL_DestroyMutex(queuedata->sqe_lock);
|
||||
SDL_free(queuedata);
|
||||
return false;
|
||||
}
|
||||
|
||||
// !!! FIXME: no idea how large the queue should be. Is 128 overkill or too small?
|
||||
const int rc = liburing.io_uring_queue_init(128, &queuedata->ring, 0);
|
||||
if (rc != 0) {
|
||||
SDL_DestroyMutex(queuedata->sqe_lock);
|
||||
SDL_DestroyMutex(queuedata->cqe_lock);
|
||||
SDL_free(queuedata);
|
||||
return liburing_SetError("io_uring_queue_init", rc);
|
||||
}
|
||||
|
||||
static const SDL_AsyncIOQueueInterface SDL_AsyncIOQueue_liburing = {
|
||||
liburing_asyncioqueue_queue_task,
|
||||
liburing_asyncioqueue_cancel_task,
|
||||
liburing_asyncioqueue_get_results,
|
||||
liburing_asyncioqueue_wait_results,
|
||||
liburing_asyncioqueue_signal,
|
||||
liburing_asyncioqueue_destroy
|
||||
};
|
||||
|
||||
SDL_copyp(&queue->iface, &SDL_AsyncIOQueue_liburing);
|
||||
queue->userdata = queuedata;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
static bool liburing_asyncio_read(void *userdata, SDL_AsyncIOTask *task)
|
||||
{
|
||||
LibUringAsyncIOQueueData *queuedata = (LibUringAsyncIOQueueData *) task->queue->userdata;
|
||||
const int fd = (int) (intptr_t) userdata;
|
||||
|
||||
// !!! FIXME: `unsigned` is likely smaller than requested_size's Uint64. If we overflow it, we could try submitting multiple SQEs
|
||||
// !!! FIXME: and make a note in the task that there are several in sequence.
|
||||
if (task->requested_size > ((Uint64) ~((unsigned) 0))) {
|
||||
return SDL_SetError("io_uring: i/o task is too large");
|
||||
}
|
||||
|
||||
// have to hold a lock because otherwise two threads could get_sqe and submit while one request isn't fully set up.
|
||||
SDL_LockMutex(queuedata->sqe_lock);
|
||||
bool retval;
|
||||
struct io_uring_sqe *sqe = liburing.io_uring_get_sqe(&queuedata->ring);
|
||||
if (!sqe) {
|
||||
retval = SDL_SetError("io_uring: submission queue is full");
|
||||
} else {
|
||||
liburing.io_uring_prep_read(sqe, fd, task->buffer, (unsigned) task->requested_size, task->offset);
|
||||
liburing.io_uring_sqe_set_data(sqe, task);
|
||||
retval = task->queue->iface.queue_task(task->queue->userdata, task);
|
||||
}
|
||||
SDL_UnlockMutex(queuedata->sqe_lock);
|
||||
return retval;
|
||||
}
|
||||
|
||||
static bool liburing_asyncio_write(void *userdata, SDL_AsyncIOTask *task)
|
||||
{
|
||||
LibUringAsyncIOQueueData *queuedata = (LibUringAsyncIOQueueData *) task->queue->userdata;
|
||||
const int fd = (int) (intptr_t) userdata;
|
||||
|
||||
// !!! FIXME: `unsigned` is likely smaller than requested_size's Uint64. If we overflow it, we could try submitting multiple SQEs
|
||||
// !!! FIXME: and make a note in the task that there are several in sequence.
|
||||
if (task->requested_size > ((Uint64) ~((unsigned) 0))) {
|
||||
return SDL_SetError("io_uring: i/o task is too large");
|
||||
}
|
||||
|
||||
// have to hold a lock because otherwise two threads could get_sqe and submit while one request isn't fully set up.
|
||||
SDL_LockMutex(queuedata->sqe_lock);
|
||||
bool retval;
|
||||
struct io_uring_sqe *sqe = liburing.io_uring_get_sqe(&queuedata->ring);
|
||||
if (!sqe) {
|
||||
retval = SDL_SetError("io_uring: submission queue is full");
|
||||
} else {
|
||||
liburing.io_uring_prep_write(sqe, fd, task->buffer, (unsigned) task->requested_size, task->offset);
|
||||
liburing.io_uring_sqe_set_data(sqe, task);
|
||||
retval = task->queue->iface.queue_task(task->queue->userdata, task);
|
||||
}
|
||||
SDL_UnlockMutex(queuedata->sqe_lock);
|
||||
return retval;
|
||||
}
|
||||
|
||||
static bool liburing_asyncio_close(void *userdata, SDL_AsyncIOTask *task)
|
||||
{
|
||||
LibUringAsyncIOQueueData *queuedata = (LibUringAsyncIOQueueData *) task->queue->userdata;
|
||||
const int fd = (int) (intptr_t) userdata;
|
||||
|
||||
// have to hold a lock because otherwise two threads could get_sqe and submit while one request isn't fully set up.
|
||||
SDL_LockMutex(queuedata->sqe_lock);
|
||||
bool retval;
|
||||
struct io_uring_sqe *sqe = liburing.io_uring_get_sqe(&queuedata->ring);
|
||||
if (!sqe) {
|
||||
retval = SDL_SetError("io_uring: submission queue is full");
|
||||
} else {
|
||||
if (task->flush) {
|
||||
struct io_uring_sqe *flush_sqe = sqe;
|
||||
sqe = liburing.io_uring_get_sqe(&queuedata->ring); // this will be our actual close task.
|
||||
if (!sqe) {
|
||||
liburing.io_uring_prep_nop(flush_sqe); // we already have the first sqe, just make it a NOP.
|
||||
liburing.io_uring_sqe_set_data(flush_sqe, NULL);
|
||||
task->queue->iface.queue_task(task->queue->userdata, task);
|
||||
SDL_UnlockMutex(queuedata->sqe_lock);
|
||||
return SDL_SetError("io_uring: submission queue is full");
|
||||
}
|
||||
liburing.io_uring_prep_fsync(flush_sqe, fd, IORING_FSYNC_DATASYNC);
|
||||
liburing.io_uring_sqe_set_data(flush_sqe, task);
|
||||
liburing.io_uring_sqe_set_flags(flush_sqe, IOSQE_IO_HARDLINK); // must complete before next sqe starts, and next sqe should run even if this fails.
|
||||
}
|
||||
|
||||
liburing.io_uring_prep_close(sqe, fd);
|
||||
liburing.io_uring_sqe_set_data(sqe, task);
|
||||
|
||||
retval = task->queue->iface.queue_task(task->queue->userdata, task);
|
||||
}
|
||||
SDL_UnlockMutex(queuedata->sqe_lock);
|
||||
return retval;
|
||||
}
|
||||
|
||||
static void liburing_asyncio_destroy(void *userdata)
|
||||
{
|
||||
// this is only a Unix file descriptor, should have been closed elsewhere.
|
||||
}
|
||||
|
||||
static int PosixOpenModeFromString(const char *mode)
|
||||
{
|
||||
// this is exactly the set of strings that SDL_AsyncIOFromFile promises will work.
|
||||
static const struct { const char *str; int flags; } mappings[] = {
|
||||
{ "rb", O_RDONLY },
|
||||
{ "wb", O_WRONLY | O_CREAT | O_TRUNC },
|
||||
{ "r+b", O_RDWR },
|
||||
{ "w+b", O_RDWR | O_CREAT | O_TRUNC }
|
||||
};
|
||||
|
||||
for (int i = 0; i < SDL_arraysize(mappings); i++) {
|
||||
if (SDL_strcmp(mappings[i].str, mode) == 0) {
|
||||
return mappings[i].flags;
|
||||
}
|
||||
}
|
||||
|
||||
SDL_assert(!"Shouldn't have reached this code");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool SDL_SYS_AsyncIOFromFile_liburing(const char *file, const char *mode, SDL_AsyncIO *asyncio)
|
||||
{
|
||||
const int fd = open(file, PosixOpenModeFromString(mode), 0644);
|
||||
if (fd == -1) {
|
||||
return SDL_SetError("open failed: %s", strerror(errno));
|
||||
}
|
||||
|
||||
static const SDL_AsyncIOInterface SDL_AsyncIOFile_liburing = {
|
||||
liburing_asyncio_size,
|
||||
liburing_asyncio_read,
|
||||
liburing_asyncio_write,
|
||||
liburing_asyncio_close,
|
||||
liburing_asyncio_destroy
|
||||
};
|
||||
|
||||
SDL_copyp(&asyncio->iface, &SDL_AsyncIOFile_liburing);
|
||||
asyncio->userdata = (void *) (intptr_t) fd;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void SDL_SYS_QuitAsyncIO_liburing(void)
|
||||
{
|
||||
UnloadLibUringLibrary();
|
||||
}
|
||||
|
||||
static void MaybeInitializeLibUring(void)
|
||||
{
|
||||
if (SDL_ShouldInit(&liburing_init)) {
|
||||
if (LoadLibUring()) {
|
||||
CreateAsyncIOQueue = SDL_SYS_CreateAsyncIOQueue_liburing;
|
||||
QuitAsyncIO = SDL_SYS_QuitAsyncIO_liburing;
|
||||
AsyncIOFromFile = SDL_SYS_AsyncIOFromFile_liburing;
|
||||
} else { // can't use liburing? Use the "generic" threadpool implementation instead.
|
||||
CreateAsyncIOQueue = SDL_SYS_CreateAsyncIOQueue_Generic;
|
||||
QuitAsyncIO = SDL_SYS_QuitAsyncIO_Generic;
|
||||
AsyncIOFromFile = SDL_SYS_AsyncIOFromFile_Generic;
|
||||
}
|
||||
SDL_SetInitialized(&liburing_init, true);
|
||||
}
|
||||
}
|
||||
|
||||
bool SDL_SYS_CreateAsyncIOQueue(SDL_AsyncIOQueue *queue)
|
||||
{
|
||||
MaybeInitializeLibUring();
|
||||
return CreateAsyncIOQueue(queue);
|
||||
}
|
||||
|
||||
bool SDL_SYS_AsyncIOFromFile(const char *file, const char *mode, SDL_AsyncIO *asyncio)
|
||||
{
|
||||
MaybeInitializeLibUring();
|
||||
return AsyncIOFromFile(file, mode, asyncio);
|
||||
}
|
||||
|
||||
void SDL_SYS_QuitAsyncIO(void)
|
||||
{
|
||||
if (SDL_ShouldQuit(&liburing_init)) {
|
||||
QuitAsyncIO();
|
||||
CreateAsyncIOQueue = NULL;
|
||||
QuitAsyncIO = NULL;
|
||||
AsyncIOFromFile = NULL;
|
||||
SDL_SetInitialized(&liburing_init, false);
|
||||
}
|
||||
}
|
||||
|
||||
#endif // defined HAVE_LIBURING_H
|
||||
|
||||
88
src/io/n3ds/SDL_iostreamromfs.c
Normal file
88
src/io/n3ds/SDL_iostreamromfs.c
Normal file
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
Simple DirectMedia Layer
|
||||
Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
*/
|
||||
|
||||
#include "SDL_iostreamromfs.h"
|
||||
|
||||
// Checks if the mode is a kind of reading
|
||||
static bool IsReadMode(const char *mode);
|
||||
|
||||
// Checks if the file starts with the given prefix
|
||||
static bool HasPrefix(const char *file, const char *prefix);
|
||||
|
||||
static FILE *TryOpenFile(const char *file, const char *mode);
|
||||
static FILE *TryOpenInRomfs(const char *file, const char *mode);
|
||||
|
||||
/* Nintendo 3DS applications may embed resources in the executable. The
|
||||
resources are stored in a special read-only partition prefixed with
|
||||
'romfs:/'. As such, when opening a file, we should first try the romfs
|
||||
unless sdmc is specifically mentioned.
|
||||
*/
|
||||
FILE *N3DS_FileOpen(const char *file, const char *mode)
|
||||
{
|
||||
// romfs are read-only
|
||||
if (!IsReadMode(mode)) {
|
||||
return fopen(file, mode);
|
||||
}
|
||||
|
||||
// If the path has an explicit prefix, we skip the guess work
|
||||
if (HasPrefix(file, "romfs:/") || HasPrefix(file, "sdmc:/")) {
|
||||
return fopen(file, mode);
|
||||
}
|
||||
|
||||
return TryOpenFile(file, mode);
|
||||
}
|
||||
|
||||
static bool IsReadMode(const char *mode)
|
||||
{
|
||||
return SDL_strchr(mode, 'r') != NULL;
|
||||
}
|
||||
|
||||
static bool HasPrefix(const char *file, const char *prefix)
|
||||
{
|
||||
return SDL_strncmp(prefix, file, SDL_strlen(prefix)) == 0;
|
||||
}
|
||||
|
||||
static FILE *TryOpenFile(const char *file, const char *mode)
|
||||
{
|
||||
FILE *fp = NULL;
|
||||
|
||||
fp = TryOpenInRomfs(file, mode);
|
||||
if (!fp) {
|
||||
fp = fopen(file, mode);
|
||||
}
|
||||
|
||||
return fp;
|
||||
}
|
||||
|
||||
FILE *TryOpenInRomfs(const char *file, const char *mode)
|
||||
{
|
||||
FILE *fp = NULL;
|
||||
char *prefixed_filepath = NULL;
|
||||
|
||||
if (SDL_asprintf(&prefixed_filepath, "romfs:/%s", file) < 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
fp = fopen(prefixed_filepath, mode);
|
||||
|
||||
SDL_free(prefixed_filepath);
|
||||
return fp;
|
||||
}
|
||||
28
src/io/n3ds/SDL_iostreamromfs.h
Normal file
28
src/io/n3ds/SDL_iostreamromfs.h
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
Simple DirectMedia Layer
|
||||
Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
*/
|
||||
#include "SDL_internal.h"
|
||||
|
||||
#ifndef SDL_iostreamromfs_h_
|
||||
#define SDL_iostreamromfs_h_
|
||||
|
||||
FILE *N3DS_FileOpen(const char *file, const char *mode);
|
||||
|
||||
#endif // SDL_iostreamromfs_h_
|
||||
550
src/io/windows/SDL_asyncio_windows_ioring.c
Normal file
550
src/io/windows/SDL_asyncio_windows_ioring.c
Normal file
@@ -0,0 +1,550 @@
|
||||
/*
|
||||
Simple DirectMedia Layer
|
||||
Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
*/
|
||||
|
||||
// The Windows backend uses IoRing for asynchronous i/o, and falls back to
|
||||
// the "generic" threadpool implementation if it isn't available or
|
||||
// fails for some other reason. IoRing was introduced in Windows 11.
|
||||
|
||||
#include "SDL_internal.h"
|
||||
#include "../SDL_sysasyncio.h"
|
||||
|
||||
#ifdef HAVE_IORINGAPI_H
|
||||
|
||||
#include "../../core/windows/SDL_windows.h"
|
||||
#include <ioringapi.h>
|
||||
|
||||
// Don't know what the lowest usable version is, but this seems safe.
|
||||
#define SDL_REQUIRED_IORING_VERSION IORING_VERSION_3
|
||||
|
||||
static SDL_InitState ioring_init;
|
||||
|
||||
// We could add a whole bootstrap thing like the audio/video/etc subsystems use, but let's keep this simple for now.
|
||||
static bool (*CreateAsyncIOQueue)(SDL_AsyncIOQueue *queue);
|
||||
static void (*QuitAsyncIO)(void);
|
||||
static bool (*AsyncIOFromFile)(const char *file, const char *mode, SDL_AsyncIO *asyncio);
|
||||
|
||||
// we never link directly to ioring.
|
||||
static const char *ioring_library = "KernelBase.dll";
|
||||
static void *ioring_handle = NULL;
|
||||
|
||||
#define SDL_IORING_FUNCS \
|
||||
SDL_IORING_FUNC(HRESULT, QueryIoRingCapabilities, (IORING_CAPABILITIES *capabilities)) \
|
||||
SDL_IORING_FUNC(BOOL, IsIoRingOpSupported, (HIORING ioRing, IORING_OP_CODE op)) \
|
||||
SDL_IORING_FUNC(HRESULT, CreateIoRing, (IORING_VERSION ioringVersion, IORING_CREATE_FLAGS flags, UINT32 submissionQueueSize, UINT32 completionQueueSize, HIORING* h)) \
|
||||
SDL_IORING_FUNC(HRESULT, GetIoRingInfo, (HIORING ioRing, IORING_INFO* info)) \
|
||||
SDL_IORING_FUNC(HRESULT, SubmitIoRing, (HIORING ioRing, UINT32 waitOperations, UINT32 milliseconds, UINT32* submittedEntries)) \
|
||||
SDL_IORING_FUNC(HRESULT, CloseIoRing, (HIORING ioRing)) \
|
||||
SDL_IORING_FUNC(HRESULT, PopIoRingCompletion, (HIORING ioRing, IORING_CQE* cqe)) \
|
||||
SDL_IORING_FUNC(HRESULT, SetIoRingCompletionEvent, (HIORING ioRing, HANDLE hEvent)) \
|
||||
SDL_IORING_FUNC(HRESULT, BuildIoRingCancelRequest, (HIORING ioRing, IORING_HANDLE_REF file, UINT_PTR opToCancel, UINT_PTR userData)) \
|
||||
SDL_IORING_FUNC(HRESULT, BuildIoRingReadFile, (HIORING ioRing, IORING_HANDLE_REF fileRef, IORING_BUFFER_REF dataRef, UINT32 numberOfBytesToRead, UINT64 fileOffset, UINT_PTR userData, IORING_SQE_FLAGS sqeFlags)) \
|
||||
SDL_IORING_FUNC(HRESULT, BuildIoRingWriteFile, (HIORING ioRing, IORING_HANDLE_REF fileRef, IORING_BUFFER_REF bufferRef, UINT32 numberOfBytesToWrite, UINT64 fileOffset, FILE_WRITE_FLAGS writeFlags, UINT_PTR userData, IORING_SQE_FLAGS sqeFlags)) \
|
||||
SDL_IORING_FUNC(HRESULT, BuildIoRingFlushFile, (HIORING ioRing, IORING_HANDLE_REF fileRef, FILE_FLUSH_MODE flushMode, UINT_PTR userData, IORING_SQE_FLAGS sqeFlags)) \
|
||||
|
||||
#define SDL_IORING_FUNC(ret, fn, args) typedef ret (WINAPI *SDL_fntype_##fn) args;
|
||||
SDL_IORING_FUNCS
|
||||
#undef SDL_IORING_FUNC
|
||||
|
||||
typedef struct SDL_WinIoRingFunctions
|
||||
{
|
||||
#define SDL_IORING_FUNC(ret, fn, args) SDL_fntype_##fn fn;
|
||||
SDL_IORING_FUNCS
|
||||
#undef SDL_IORING_FUNC
|
||||
} SDL_WinIoRingFunctions;
|
||||
|
||||
static SDL_WinIoRingFunctions ioring;
|
||||
|
||||
|
||||
typedef struct WinIoRingAsyncIOQueueData
|
||||
{
|
||||
SDL_Mutex *sqe_lock;
|
||||
SDL_Mutex *cqe_lock;
|
||||
HANDLE event;
|
||||
HIORING ring;
|
||||
SDL_AtomicInt num_waiting;
|
||||
} WinIoRingAsyncIOQueueData;
|
||||
|
||||
|
||||
static void UnloadWinIoRingLibrary(void)
|
||||
{
|
||||
if (ioring_library) {
|
||||
SDL_UnloadObject(ioring_handle);
|
||||
ioring_library = NULL;
|
||||
}
|
||||
SDL_zero(ioring);
|
||||
}
|
||||
|
||||
static bool LoadWinIoRingSyms(void)
|
||||
{
|
||||
#define SDL_IORING_FUNC(ret, fn, args) { \
|
||||
ioring.fn = (SDL_fntype_##fn) SDL_LoadFunction(ioring_handle, #fn); \
|
||||
if (!ioring.fn) { \
|
||||
return false; \
|
||||
} \
|
||||
}
|
||||
SDL_IORING_FUNCS
|
||||
#undef SDL_IORING_FUNC
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool LoadWinIoRing(void)
|
||||
{
|
||||
bool result = true;
|
||||
|
||||
if (!ioring_handle) {
|
||||
ioring_handle = SDL_LoadObject(ioring_library);
|
||||
if (!ioring_handle) {
|
||||
result = false;
|
||||
// Don't call SDL_SetError(): SDL_LoadObject already did.
|
||||
} else {
|
||||
result = LoadWinIoRingSyms();
|
||||
if (result) {
|
||||
IORING_CAPABILITIES caps;
|
||||
HRESULT hr = ioring.QueryIoRingCapabilities(&caps);
|
||||
if (FAILED(hr)) {
|
||||
result = false;
|
||||
} else if (caps.MaxVersion < SDL_REQUIRED_IORING_VERSION) {
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
UnloadWinIoRingLibrary();
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static Sint64 ioring_asyncio_size(void *userdata)
|
||||
{
|
||||
HANDLE handle = (HANDLE) userdata;
|
||||
LARGE_INTEGER size;
|
||||
if (!GetFileSizeEx(handle, &size)) {
|
||||
WIN_SetError("GetFileSizeEx");
|
||||
return -1;
|
||||
}
|
||||
return (Sint64) size.QuadPart;
|
||||
}
|
||||
|
||||
// you must hold sqe_lock when calling this!
|
||||
static bool ioring_asyncioqueue_queue_task(void *userdata, SDL_AsyncIOTask *task)
|
||||
{
|
||||
WinIoRingAsyncIOQueueData *queuedata = (WinIoRingAsyncIOQueueData *) userdata;
|
||||
const HRESULT hr = ioring.SubmitIoRing(queuedata->ring, 0, 0, NULL);
|
||||
return (FAILED(hr) ? WIN_SetErrorFromHRESULT("SubmitIoRing", hr) : true);
|
||||
}
|
||||
|
||||
static void ioring_asyncioqueue_cancel_task(void *userdata, SDL_AsyncIOTask *task)
|
||||
{
|
||||
if (!task->asyncio || !task->asyncio->userdata) {
|
||||
return; // Windows IoRing needs the file handle in question, so we'll just have to let it complete if unknown.
|
||||
}
|
||||
|
||||
SDL_AsyncIOTask *cancel_task = (SDL_AsyncIOTask *) SDL_calloc(1, sizeof (*cancel_task));
|
||||
if (!cancel_task) {
|
||||
return; // oh well, the task can just finish on its own.
|
||||
}
|
||||
|
||||
WinIoRingAsyncIOQueueData *queuedata = (WinIoRingAsyncIOQueueData *) userdata;
|
||||
HANDLE handle = (HANDLE) task->asyncio->userdata;
|
||||
IORING_HANDLE_REF href = IoRingHandleRefFromHandle(handle);
|
||||
|
||||
// have to hold a lock because otherwise two threads could get_sqe and submit while one request isn't fully set up.
|
||||
SDL_LockMutex(queuedata->sqe_lock);
|
||||
const HRESULT hr = ioring.BuildIoRingCancelRequest(queuedata->ring, href, (UINT_PTR) task, (UINT_PTR) cancel_task);
|
||||
if (FAILED(hr)) {
|
||||
SDL_UnlockMutex(queuedata->sqe_lock);
|
||||
SDL_free(cancel_task); // oh well, the task can just finish on its own.
|
||||
return;
|
||||
}
|
||||
|
||||
cancel_task->app_userdata = task;
|
||||
ioring_asyncioqueue_queue_task(userdata, task);
|
||||
SDL_UnlockMutex(queuedata->sqe_lock);
|
||||
}
|
||||
|
||||
static SDL_AsyncIOTask *ProcessCQE(WinIoRingAsyncIOQueueData *queuedata, IORING_CQE *cqe)
|
||||
{
|
||||
if (!cqe) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
SDL_AsyncIOTask *task = (SDL_AsyncIOTask *) cqe->UserData;
|
||||
if (task) { // can be NULL if this was just a wakeup message, a NOP, etc.
|
||||
if (!task->queue) { // We leave `queue` blank to signify this was a task cancellation.
|
||||
SDL_AsyncIOTask *cancel_task = task;
|
||||
task = (SDL_AsyncIOTask *) cancel_task->app_userdata;
|
||||
SDL_free(cancel_task);
|
||||
if (SUCCEEDED(cqe->ResultCode)) { // cancel was successful?
|
||||
task->result = SDL_ASYNCIO_CANCELED;
|
||||
} else {
|
||||
task = NULL; // it already finished or was too far along to cancel, so we'll pick up the actual results later.
|
||||
}
|
||||
} else if (FAILED(cqe->ResultCode)) {
|
||||
task->result = SDL_ASYNCIO_FAILURE;
|
||||
// !!! FIXME: fill in task->error.
|
||||
} else {
|
||||
if ((task->type == SDL_ASYNCIO_TASK_WRITE) && (((Uint64) cqe->Information) < task->requested_size)) {
|
||||
task->result = SDL_ASYNCIO_FAILURE; // it's always a failure on short writes.
|
||||
}
|
||||
|
||||
// don't explicitly mark it as COMPLETE; that's the default value and a linked task might have failed in an earlier operation and this would overwrite it.
|
||||
|
||||
if ((task->type == SDL_ASYNCIO_TASK_READ) || (task->type == SDL_ASYNCIO_TASK_WRITE)) {
|
||||
task->result_size = (Uint64) cqe->Information;
|
||||
}
|
||||
}
|
||||
|
||||
// we currently send all close operations through as flushes, requested or not, so the actually closing is (in theory) fast. We do that here.
|
||||
// if a later IoRing interface version offers an asynchronous close operation, revisit this to only flush if requested, like we do in the Linux io_uring code.
|
||||
if (task->type == SDL_ASYNCIO_TASK_CLOSE) {
|
||||
SDL_assert(task->asyncio != NULL);
|
||||
SDL_assert(task->asyncio->userdata != NULL);
|
||||
HANDLE handle = (HANDLE) task->asyncio->userdata;
|
||||
if (!CloseHandle(handle)) {
|
||||
task->result = SDL_ASYNCIO_FAILURE; // shrug.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
static SDL_AsyncIOTask *ioring_asyncioqueue_get_results(void *userdata)
|
||||
{
|
||||
WinIoRingAsyncIOQueueData *queuedata = (WinIoRingAsyncIOQueueData *) userdata;
|
||||
|
||||
// unlike liburing's io_uring_peek_cqe(), it's possible PopIoRingCompletion() is thread safe, but for now we wrap it in a mutex just in case.
|
||||
SDL_LockMutex(queuedata->cqe_lock);
|
||||
IORING_CQE cqe;
|
||||
const HRESULT hr = ioring.PopIoRingCompletion(queuedata->ring, &cqe);
|
||||
SDL_UnlockMutex(queuedata->cqe_lock);
|
||||
|
||||
if ((hr == S_FALSE) || FAILED(hr)) {
|
||||
return NULL; // nothing available at the moment.
|
||||
}
|
||||
|
||||
return ProcessCQE(queuedata, &cqe);
|
||||
}
|
||||
|
||||
static SDL_AsyncIOTask *ioring_asyncioqueue_wait_results(void *userdata, Sint32 timeoutMS)
|
||||
{
|
||||
WinIoRingAsyncIOQueueData *queuedata = (WinIoRingAsyncIOQueueData *) userdata;
|
||||
|
||||
// the event only signals when the IoRing moves from empty to non-empty, so you have to try a (non-blocking) get_results first or risk eternal hangs.
|
||||
SDL_AsyncIOTask *task = ioring_asyncioqueue_get_results(userdata);
|
||||
if (!task) {
|
||||
SDL_AddAtomicInt(&queuedata->num_waiting, 1);
|
||||
WaitForSingleObject(queuedata->event, (timeoutMS < 0) ? INFINITE : (DWORD) timeoutMS);
|
||||
SDL_AddAtomicInt(&queuedata->num_waiting, -1);
|
||||
|
||||
// (we don't care if the wait failed for any reason, as the upcoming get_results will report valid information. We just wanted the wait operation to block.)
|
||||
task = ioring_asyncioqueue_get_results(userdata);
|
||||
}
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
static void ioring_asyncioqueue_signal(void *userdata)
|
||||
{
|
||||
WinIoRingAsyncIOQueueData *queuedata = (WinIoRingAsyncIOQueueData *) userdata;
|
||||
const int num_waiting = SDL_GetAtomicInt(&queuedata->num_waiting);
|
||||
for (int i = 0; i < num_waiting; i++) {
|
||||
SetEvent(queuedata->event);
|
||||
}
|
||||
}
|
||||
|
||||
static void ioring_asyncioqueue_destroy(void *userdata)
|
||||
{
|
||||
WinIoRingAsyncIOQueueData *queuedata = (WinIoRingAsyncIOQueueData *) userdata;
|
||||
ioring.CloseIoRing(queuedata->ring);
|
||||
CloseHandle(queuedata->event);
|
||||
SDL_DestroyMutex(queuedata->sqe_lock);
|
||||
SDL_DestroyMutex(queuedata->cqe_lock);
|
||||
SDL_free(queuedata);
|
||||
}
|
||||
|
||||
static bool SDL_SYS_CreateAsyncIOQueue_ioring(SDL_AsyncIOQueue *queue)
|
||||
{
|
||||
WinIoRingAsyncIOQueueData *queuedata = (WinIoRingAsyncIOQueueData *) SDL_calloc(1, sizeof (*queuedata));
|
||||
if (!queuedata) {
|
||||
return false;
|
||||
}
|
||||
|
||||
HRESULT hr;
|
||||
IORING_CREATE_FLAGS flags;
|
||||
|
||||
SDL_SetAtomicInt(&queuedata->num_waiting, 0);
|
||||
|
||||
queuedata->sqe_lock = SDL_CreateMutex();
|
||||
if (!queuedata->sqe_lock) {
|
||||
goto failed;
|
||||
}
|
||||
|
||||
queuedata->cqe_lock = SDL_CreateMutex();
|
||||
if (!queuedata->cqe_lock) {
|
||||
goto failed;
|
||||
}
|
||||
|
||||
queuedata->event = CreateEventW(NULL, FALSE, FALSE, NULL);
|
||||
if (!queuedata->event) {
|
||||
WIN_SetError("CreateEventW");
|
||||
goto failed;
|
||||
}
|
||||
|
||||
// !!! FIXME: no idea how large the queue should be. Is 128 overkill or too small?
|
||||
flags.Required = IORING_CREATE_REQUIRED_FLAGS_NONE;
|
||||
flags.Advisory = IORING_CREATE_ADVISORY_FLAGS_NONE;
|
||||
hr = ioring.CreateIoRing(SDL_REQUIRED_IORING_VERSION, flags, 128, 128, &queuedata->ring);
|
||||
if (FAILED(hr)) {
|
||||
WIN_SetErrorFromHRESULT("CreateIoRing", hr);
|
||||
goto failed;
|
||||
}
|
||||
|
||||
hr = ioring.SetIoRingCompletionEvent(queuedata->ring, queuedata->event);
|
||||
if (FAILED(hr)) {
|
||||
WIN_SetErrorFromHRESULT("SetIoRingCompletionEvent", hr);
|
||||
goto failed;
|
||||
}
|
||||
|
||||
static const IORING_OP_CODE needed_ops[] = {
|
||||
IORING_OP_NOP,
|
||||
IORING_OP_FLUSH,
|
||||
IORING_OP_READ,
|
||||
IORING_OP_WRITE,
|
||||
IORING_OP_CANCEL
|
||||
};
|
||||
|
||||
for (int i = 0; i < SDL_arraysize(needed_ops); i++) {
|
||||
if (!ioring.IsIoRingOpSupported(queuedata->ring, needed_ops[i])) {
|
||||
SDL_SetError("Created IoRing doesn't support op %u", (unsigned int) needed_ops[i]);
|
||||
goto failed;
|
||||
}
|
||||
}
|
||||
|
||||
static const SDL_AsyncIOQueueInterface SDL_AsyncIOQueue_ioring = {
|
||||
ioring_asyncioqueue_queue_task,
|
||||
ioring_asyncioqueue_cancel_task,
|
||||
ioring_asyncioqueue_get_results,
|
||||
ioring_asyncioqueue_wait_results,
|
||||
ioring_asyncioqueue_signal,
|
||||
ioring_asyncioqueue_destroy
|
||||
};
|
||||
|
||||
SDL_copyp(&queue->iface, &SDL_AsyncIOQueue_ioring);
|
||||
queue->userdata = queuedata;
|
||||
return true;
|
||||
|
||||
failed:
|
||||
if (queuedata->ring) {
|
||||
ioring.CloseIoRing(queuedata->ring);
|
||||
}
|
||||
if (queuedata->event) {
|
||||
CloseHandle(queuedata->event);
|
||||
}
|
||||
if (queuedata->sqe_lock) {
|
||||
SDL_DestroyMutex(queuedata->sqe_lock);
|
||||
}
|
||||
if (queuedata->cqe_lock) {
|
||||
SDL_DestroyMutex(queuedata->cqe_lock);
|
||||
}
|
||||
SDL_free(queuedata);
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool ioring_asyncio_read(void *userdata, SDL_AsyncIOTask *task)
|
||||
{
|
||||
// !!! FIXME: UINT32 smaller than requested_size's Uint64. If we overflow it, we could try submitting multiple SQEs
|
||||
// !!! FIXME: and make a note in the task that there are several in sequence.
|
||||
if (task->requested_size > 0xFFFFFFFF) {
|
||||
return SDL_SetError("ioring: i/o task is too large");
|
||||
}
|
||||
|
||||
HANDLE handle = (HANDLE) userdata;
|
||||
WinIoRingAsyncIOQueueData *queuedata = (WinIoRingAsyncIOQueueData *) task->queue->userdata;
|
||||
IORING_HANDLE_REF href = IoRingHandleRefFromHandle(handle);
|
||||
IORING_BUFFER_REF bref = IoRingBufferRefFromPointer(task->buffer);
|
||||
|
||||
// have to hold a lock because otherwise two threads could get_sqe and submit while one request isn't fully set up.
|
||||
SDL_LockMutex(queuedata->sqe_lock);
|
||||
bool retval;
|
||||
const HRESULT hr = ioring.BuildIoRingReadFile(queuedata->ring, href, bref, (UINT32) task->requested_size, task->offset, (UINT_PTR) task, IOSQE_FLAGS_NONE);
|
||||
if (FAILED(hr)) {
|
||||
retval = WIN_SetErrorFromHRESULT("BuildIoRingReadFile", hr);
|
||||
} else {
|
||||
retval = task->queue->iface.queue_task(task->queue->userdata, task);
|
||||
}
|
||||
SDL_UnlockMutex(queuedata->sqe_lock);
|
||||
return retval;
|
||||
}
|
||||
|
||||
static bool ioring_asyncio_write(void *userdata, SDL_AsyncIOTask *task)
|
||||
{
|
||||
// !!! FIXME: UINT32 smaller than requested_size's Uint64. If we overflow it, we could try submitting multiple SQEs
|
||||
// !!! FIXME: and make a note in the task that there are several in sequence.
|
||||
if (task->requested_size > 0xFFFFFFFF) {
|
||||
return SDL_SetError("ioring: i/o task is too large");
|
||||
}
|
||||
|
||||
HANDLE handle = (HANDLE) userdata;
|
||||
WinIoRingAsyncIOQueueData *queuedata = (WinIoRingAsyncIOQueueData *) task->queue->userdata;
|
||||
IORING_HANDLE_REF href = IoRingHandleRefFromHandle(handle);
|
||||
IORING_BUFFER_REF bref = IoRingBufferRefFromPointer(task->buffer);
|
||||
|
||||
// have to hold a lock because otherwise two threads could get_sqe and submit while one request isn't fully set up.
|
||||
SDL_LockMutex(queuedata->sqe_lock);
|
||||
bool retval;
|
||||
const HRESULT hr = ioring.BuildIoRingWriteFile(queuedata->ring, href, bref, (UINT32) task->requested_size, task->offset, 0 /*FILE_WRITE_FLAGS_NONE*/, (UINT_PTR) task, IOSQE_FLAGS_NONE);
|
||||
if (FAILED(hr)) {
|
||||
retval = WIN_SetErrorFromHRESULT("BuildIoRingWriteFile", hr);
|
||||
} else {
|
||||
retval = task->queue->iface.queue_task(task->queue->userdata, task);
|
||||
}
|
||||
SDL_UnlockMutex(queuedata->sqe_lock);
|
||||
return retval;
|
||||
}
|
||||
|
||||
static bool ioring_asyncio_close(void *userdata, SDL_AsyncIOTask *task)
|
||||
{
|
||||
// current IoRing operations don't offer asynchronous closing, but let's assume most of the potential work is flushing to disk, so just do it for everything, explicit flush or not. We'll close when it finishes.
|
||||
HANDLE handle = (HANDLE) userdata;
|
||||
WinIoRingAsyncIOQueueData *queuedata = (WinIoRingAsyncIOQueueData *)task->queue->userdata;
|
||||
IORING_HANDLE_REF href = IoRingHandleRefFromHandle(handle);
|
||||
|
||||
// have to hold a lock because otherwise two threads could get_sqe and submit while one request isn't fully set up.
|
||||
SDL_LockMutex(queuedata->sqe_lock);
|
||||
bool retval;
|
||||
const HRESULT hr = ioring.BuildIoRingFlushFile(queuedata->ring, href, FILE_FLUSH_DEFAULT, (UINT_PTR) task, IOSQE_FLAGS_NONE);
|
||||
if (FAILED(hr)) {
|
||||
retval = WIN_SetErrorFromHRESULT("BuildIoRingFlushFile", hr);
|
||||
} else {
|
||||
retval = task->queue->iface.queue_task(task->queue->userdata, task);
|
||||
}
|
||||
SDL_UnlockMutex(queuedata->sqe_lock);
|
||||
return retval;
|
||||
}
|
||||
|
||||
static void ioring_asyncio_destroy(void *userdata)
|
||||
{
|
||||
// this is only a Win32 file HANDLE, should have been closed elsewhere.
|
||||
}
|
||||
|
||||
static bool Win32OpenModeFromString(const char *mode, DWORD *access_mode, DWORD *create_mode)
|
||||
{
|
||||
// this is exactly the set of strings that SDL_AsyncIOFromFile promises will work.
|
||||
static const struct { const char *str; DWORD amode; WORD cmode; } mappings[] = {
|
||||
{ "rb", GENERIC_READ, OPEN_EXISTING },
|
||||
{ "wb", GENERIC_WRITE, CREATE_ALWAYS },
|
||||
{ "r+b", GENERIC_READ | GENERIC_WRITE, OPEN_EXISTING },
|
||||
{ "w+b", GENERIC_READ | GENERIC_WRITE, CREATE_ALWAYS }
|
||||
};
|
||||
|
||||
for (int i = 0; i < SDL_arraysize(mappings); i++) {
|
||||
if (SDL_strcmp(mappings[i].str, mode) == 0) {
|
||||
*access_mode = mappings[i].amode;
|
||||
*create_mode = mappings[i].cmode;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
SDL_assert(!"Shouldn't have reached this code");
|
||||
return SDL_SetError("Invalid file open mode");
|
||||
}
|
||||
|
||||
static bool SDL_SYS_AsyncIOFromFile_ioring(const char *file, const char *mode, SDL_AsyncIO *asyncio)
|
||||
{
|
||||
DWORD access_mode, create_mode;
|
||||
if (!Win32OpenModeFromString(mode, &access_mode, &create_mode)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
LPWSTR wstr = WIN_UTF8ToStringW(file);
|
||||
if (!wstr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
HANDLE handle = CreateFileW(wstr, access_mode, FILE_SHARE_READ, NULL, create_mode, FILE_ATTRIBUTE_NORMAL, NULL);
|
||||
SDL_free(wstr);
|
||||
if (!handle) {
|
||||
return WIN_SetError("CreateFileW");
|
||||
}
|
||||
|
||||
static const SDL_AsyncIOInterface SDL_AsyncIOFile_ioring = {
|
||||
ioring_asyncio_size,
|
||||
ioring_asyncio_read,
|
||||
ioring_asyncio_write,
|
||||
ioring_asyncio_close,
|
||||
ioring_asyncio_destroy
|
||||
};
|
||||
|
||||
SDL_copyp(&asyncio->iface, &SDL_AsyncIOFile_ioring);
|
||||
|
||||
asyncio->userdata = (void *) handle;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void SDL_SYS_QuitAsyncIO_ioring(void)
|
||||
{
|
||||
UnloadWinIoRingLibrary();
|
||||
}
|
||||
|
||||
static void MaybeInitializeWinIoRing(void)
|
||||
{
|
||||
if (SDL_ShouldInit(&ioring_init)) {
|
||||
if (LoadWinIoRing()) {
|
||||
CreateAsyncIOQueue = SDL_SYS_CreateAsyncIOQueue_ioring;
|
||||
QuitAsyncIO = SDL_SYS_QuitAsyncIO_ioring;
|
||||
AsyncIOFromFile = SDL_SYS_AsyncIOFromFile_ioring;
|
||||
} else { // can't use ioring? Use the "generic" threadpool implementation instead.
|
||||
CreateAsyncIOQueue = SDL_SYS_CreateAsyncIOQueue_Generic;
|
||||
QuitAsyncIO = SDL_SYS_QuitAsyncIO_Generic;
|
||||
AsyncIOFromFile = SDL_SYS_AsyncIOFromFile_Generic;
|
||||
}
|
||||
SDL_SetInitialized(&ioring_init, true);
|
||||
}
|
||||
}
|
||||
|
||||
bool SDL_SYS_CreateAsyncIOQueue(SDL_AsyncIOQueue *queue)
|
||||
{
|
||||
MaybeInitializeWinIoRing();
|
||||
return CreateAsyncIOQueue(queue);
|
||||
}
|
||||
|
||||
bool SDL_SYS_AsyncIOFromFile(const char *file, const char *mode, SDL_AsyncIO *asyncio)
|
||||
{
|
||||
MaybeInitializeWinIoRing();
|
||||
return AsyncIOFromFile(file, mode, asyncio);
|
||||
}
|
||||
|
||||
void SDL_SYS_QuitAsyncIO(void)
|
||||
{
|
||||
if (SDL_ShouldQuit(&ioring_init)) {
|
||||
QuitAsyncIO();
|
||||
CreateAsyncIOQueue = NULL;
|
||||
QuitAsyncIO = NULL;
|
||||
AsyncIOFromFile = NULL;
|
||||
SDL_SetInitialized(&ioring_init, false);
|
||||
}
|
||||
}
|
||||
|
||||
#endif // defined HAVE_IORINGAPI_H
|
||||
|
||||
Reference in New Issue
Block a user