diff options
| author | 3gg <3gg@shellblade.net> | 2025-12-27 12:03:39 -0800 |
|---|---|---|
| committer | 3gg <3gg@shellblade.net> | 2025-12-27 12:03:39 -0800 |
| commit | 5a079a2d114f96d4847d1ee305d5b7c16eeec50e (patch) | |
| tree | 8926ab44f168acf787d8e19608857b3af0f82758 /contrib/SDL-3.2.8/src/io/io_uring | |
Initial commit
Diffstat (limited to 'contrib/SDL-3.2.8/src/io/io_uring')
| -rw-r--r-- | contrib/SDL-3.2.8/src/io/io_uring/SDL_asyncio_liburing.c | 551 |
1 files changed, 551 insertions, 0 deletions
diff --git a/contrib/SDL-3.2.8/src/io/io_uring/SDL_asyncio_liburing.c b/contrib/SDL-3.2.8/src/io/io_uring/SDL_asyncio_liburing.c new file mode 100644 index 0000000..07e8c24 --- /dev/null +++ b/contrib/SDL-3.2.8/src/io/io_uring/SDL_asyncio_liburing.c | |||
| @@ -0,0 +1,551 @@ | |||
| 1 | /* | ||
| 2 | Simple DirectMedia Layer | ||
| 3 | Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org> | ||
| 4 | |||
| 5 | This software is provided 'as-is', without any express or implied | ||
| 6 | warranty. In no event will the authors be held liable for any damages | ||
| 7 | arising from the use of this software. | ||
| 8 | |||
| 9 | Permission is granted to anyone to use this software for any purpose, | ||
| 10 | including commercial applications, and to alter it and redistribute it | ||
| 11 | freely, subject to the following restrictions: | ||
| 12 | |||
| 13 | 1. The origin of this software must not be misrepresented; you must not | ||
| 14 | claim that you wrote the original software. If you use this software | ||
| 15 | in a product, an acknowledgment in the product documentation would be | ||
| 16 | appreciated but is not required. | ||
| 17 | 2. Altered source versions must be plainly marked as such, and must not be | ||
| 18 | misrepresented as being the original software. | ||
| 19 | 3. This notice may not be removed or altered from any source distribution. | ||
| 20 | */ | ||
| 21 | |||
| 22 | // The Linux backend uses io_uring for asynchronous i/o, and falls back to | ||
| 23 | // the "generic" threadpool implementation if liburing isn't available or | ||
| 24 | // fails for some other reason. | ||
| 25 | |||
| 26 | #include "SDL_internal.h" | ||
| 27 | |||
| 28 | #ifdef HAVE_LIBURING_H | ||
| 29 | |||
| 30 | #include "../SDL_sysasyncio.h" | ||
| 31 | |||
| 32 | #include <liburing.h> | ||
| 33 | #include <errno.h> | ||
| 34 | #include <fcntl.h> | ||
| 35 | #include <string.h> // for strerror() | ||
| 36 | |||
| 37 | static SDL_InitState liburing_init; | ||
| 38 | |||
| 39 | // We could add a whole bootstrap thing like the audio/video/etc subsystems use, but let's keep this simple for now. | ||
| 40 | static bool (*CreateAsyncIOQueue)(SDL_AsyncIOQueue *queue); | ||
| 41 | static void (*QuitAsyncIO)(void); | ||
| 42 | static bool (*AsyncIOFromFile)(const char *file, const char *mode, SDL_AsyncIO *asyncio); | ||
| 43 | |||
| 44 | // we never link directly to liburing. | ||
| 45 | // (this says "-ffi" which sounds like a scripting language binding thing, but the non-ffi version | ||
| 46 | // is static-inline code we can't lookup with dlsym. This is by design.) | ||
| 47 | static const char *liburing_library = "liburing-ffi.so.2"; | ||
| 48 | static void *liburing_handle = NULL; | ||
| 49 | |||
| 50 | #define SDL_LIBURING_FUNCS \ | ||
| 51 | SDL_LIBURING_FUNC(int, io_uring_queue_init, (unsigned entries, struct io_uring *ring, unsigned flags)) \ | ||
| 52 | SDL_LIBURING_FUNC(struct io_uring_probe *,io_uring_get_probe,(void)) \ | ||
| 53 | SDL_LIBURING_FUNC(void, io_uring_free_probe, (struct io_uring_probe *probe)) \ | ||
| 54 | SDL_LIBURING_FUNC(int, io_uring_opcode_supported, (const struct io_uring_probe *p, int op)) \ | ||
| 55 | SDL_LIBURING_FUNC(struct io_uring_sqe *, io_uring_get_sqe, (struct io_uring *ring)) \ | ||
| 56 | SDL_LIBURING_FUNC(void, io_uring_prep_read,(struct io_uring_sqe *sqe, int fd, void *buf, unsigned nbytes, __u64 offset)) \ | ||
| 57 | SDL_LIBURING_FUNC(void, io_uring_prep_write,(struct io_uring_sqe *sqe, int fd, const void *buf, unsigned nbytes, __u64 offset)) \ | ||
| 58 | SDL_LIBURING_FUNC(void, io_uring_prep_close, (struct io_uring_sqe *sqe, int fd)) \ | ||
| 59 | SDL_LIBURING_FUNC(void, io_uring_prep_fsync, (struct io_uring_sqe *sqe, int fd, unsigned fsync_flags)) \ | ||
| 60 | SDL_LIBURING_FUNC(void, io_uring_prep_cancel, (struct io_uring_sqe *sqe, void *user_data, int flags)) \ | ||
| 61 | SDL_LIBURING_FUNC(void, io_uring_prep_timeout, (struct io_uring_sqe *sqe, struct __kernel_timespec *ts, unsigned count, unsigned flags)) \ | ||
| 62 | SDL_LIBURING_FUNC(void, io_uring_prep_nop, (struct io_uring_sqe *sqe)) \ | ||
| 63 | SDL_LIBURING_FUNC(void, io_uring_sqe_set_data, (struct io_uring_sqe *sqe, void *data)) \ | ||
| 64 | SDL_LIBURING_FUNC(void, io_uring_sqe_set_flags, (struct io_uring_sqe *sqe, unsigned flags)) \ | ||
| 65 | SDL_LIBURING_FUNC(int, io_uring_submit, (struct io_uring *ring)) \ | ||
| 66 | SDL_LIBURING_FUNC(int, io_uring_peek_cqe, (struct io_uring *ring, struct io_uring_cqe **cqe_ptr)) \ | ||
| 67 | SDL_LIBURING_FUNC(int, io_uring_wait_cqe, (struct io_uring *ring, struct io_uring_cqe **cqe_ptr)) \ | ||
| 68 | SDL_LIBURING_FUNC(int, io_uring_wait_cqe_timeout, (struct io_uring *ring, struct io_uring_cqe **cqe_ptr, struct __kernel_timespec *ts)) \ | ||
| 69 | SDL_LIBURING_FUNC(void, io_uring_cqe_seen, (struct io_uring *ring, struct io_uring_cqe *cqe)) \ | ||
| 70 | SDL_LIBURING_FUNC(void, io_uring_queue_exit, (struct io_uring *ring)) \ | ||
| 71 | |||
| 72 | |||
| 73 | #define SDL_LIBURING_FUNC(ret, fn, args) typedef ret (*SDL_fntype_##fn) args; | ||
| 74 | SDL_LIBURING_FUNCS | ||
| 75 | #undef SDL_LIBURING_FUNC | ||
| 76 | |||
| 77 | typedef struct SDL_LibUringFunctions | ||
| 78 | { | ||
| 79 | #define SDL_LIBURING_FUNC(ret, fn, args) SDL_fntype_##fn fn; | ||
| 80 | SDL_LIBURING_FUNCS | ||
| 81 | #undef SDL_LIBURING_FUNC | ||
| 82 | } SDL_LibUringFunctions; | ||
| 83 | |||
| 84 | static SDL_LibUringFunctions liburing; | ||
| 85 | |||
| 86 | |||
| 87 | typedef struct LibUringAsyncIOQueueData | ||
| 88 | { | ||
| 89 | SDL_Mutex *sqe_lock; | ||
| 90 | SDL_Mutex *cqe_lock; | ||
| 91 | struct io_uring ring; | ||
| 92 | SDL_AtomicInt num_waiting; | ||
| 93 | } LibUringAsyncIOQueueData; | ||
| 94 | |||
| 95 | |||
| 96 | static void UnloadLibUringLibrary(void) | ||
| 97 | { | ||
| 98 | if (liburing_library) { | ||
| 99 | SDL_UnloadObject(liburing_handle); | ||
| 100 | liburing_library = NULL; | ||
| 101 | } | ||
| 102 | SDL_zero(liburing); | ||
| 103 | } | ||
| 104 | |||
| 105 | static bool LoadLibUringSyms(void) | ||
| 106 | { | ||
| 107 | #define SDL_LIBURING_FUNC(ret, fn, args) { \ | ||
| 108 | liburing.fn = (SDL_fntype_##fn) SDL_LoadFunction(liburing_handle, #fn); \ | ||
| 109 | if (!liburing.fn) { \ | ||
| 110 | return false; \ | ||
| 111 | } \ | ||
| 112 | } | ||
| 113 | SDL_LIBURING_FUNCS | ||
| 114 | #undef SDL_LIBURING_FUNC | ||
| 115 | return true; | ||
| 116 | } | ||
| 117 | |||
| 118 | // we rely on the presence of liburing to handle io_uring for us. The alternative is making | ||
| 119 | // direct syscalls into the kernel, which is undesirable. liburing both shields us from this, | ||
| 120 | // but also smooths over some kernel version differences, etc. | ||
| 121 | static bool LoadLibUring(void) | ||
| 122 | { | ||
| 123 | bool result = true; | ||
| 124 | |||
| 125 | if (!liburing_handle) { | ||
| 126 | liburing_handle = SDL_LoadObject(liburing_library); | ||
| 127 | if (!liburing_handle) { | ||
| 128 | result = false; | ||
| 129 | // Don't call SDL_SetError(): SDL_LoadObject already did. | ||
| 130 | } else { | ||
| 131 | result = LoadLibUringSyms(); | ||
| 132 | if (result) { | ||
| 133 | static const int needed_ops[] = { | ||
| 134 | IORING_OP_NOP, | ||
| 135 | IORING_OP_FSYNC, | ||
| 136 | IORING_OP_TIMEOUT, | ||
| 137 | IORING_OP_CLOSE, | ||
| 138 | IORING_OP_READ, | ||
| 139 | IORING_OP_WRITE, | ||
| 140 | IORING_OP_ASYNC_CANCEL | ||
| 141 | }; | ||
| 142 | |||
| 143 | struct io_uring_probe *probe = liburing.io_uring_get_probe(); | ||
| 144 | if (!probe) { | ||
| 145 | result = false; | ||
| 146 | } else { | ||
| 147 | for (int i = 0; i < SDL_arraysize(needed_ops); i++) { | ||
| 148 | if (!io_uring_opcode_supported(probe, needed_ops[i])) { | ||
| 149 | result = false; | ||
| 150 | break; | ||
| 151 | } | ||
| 152 | } | ||
| 153 | liburing.io_uring_free_probe(probe); | ||
| 154 | } | ||
| 155 | } | ||
| 156 | |||
| 157 | if (!result) { | ||
| 158 | UnloadLibUringLibrary(); | ||
| 159 | } | ||
| 160 | } | ||
| 161 | } | ||
| 162 | return result; | ||
| 163 | } | ||
| 164 | |||
| 165 | static bool liburing_SetError(const char *what, int err) | ||
| 166 | { | ||
| 167 | SDL_assert(err <= 0); | ||
| 168 | return SDL_SetError("%s failed: %s", what, strerror(-err)); | ||
| 169 | } | ||
| 170 | |||
| 171 | static Sint64 liburing_asyncio_size(void *userdata) | ||
| 172 | { | ||
| 173 | const int fd = (int) (intptr_t) userdata; | ||
| 174 | struct stat statbuf; | ||
| 175 | if (fstat(fd, &statbuf) < 0) { | ||
| 176 | SDL_SetError("fstat failed: %s", strerror(errno)); | ||
| 177 | return -1; | ||
| 178 | } | ||
| 179 | return ((Sint64) statbuf.st_size); | ||
| 180 | } | ||
| 181 | |||
| 182 | // you must hold sqe_lock when calling this! | ||
| 183 | static bool liburing_asyncioqueue_queue_task(void *userdata, SDL_AsyncIOTask *task) | ||
| 184 | { | ||
| 185 | LibUringAsyncIOQueueData *queuedata = (LibUringAsyncIOQueueData *) userdata; | ||
| 186 | const int rc = liburing.io_uring_submit(&queuedata->ring); | ||
| 187 | return (rc < 0) ? liburing_SetError("io_uring_submit", rc) : true; | ||
| 188 | } | ||
| 189 | |||
| 190 | static void liburing_asyncioqueue_cancel_task(void *userdata, SDL_AsyncIOTask *task) | ||
| 191 | { | ||
| 192 | SDL_AsyncIOTask *cancel_task = (SDL_AsyncIOTask *) SDL_calloc(1, sizeof (*cancel_task)); | ||
| 193 | if (!cancel_task) { | ||
| 194 | return; // oh well, the task can just finish on its own. | ||
| 195 | } | ||
| 196 | |||
| 197 | LibUringAsyncIOQueueData *queuedata = (LibUringAsyncIOQueueData *) userdata; | ||
| 198 | |||
| 199 | // have to hold a lock because otherwise two threads could get_sqe and submit while one request isn't fully set up. | ||
| 200 | SDL_LockMutex(queuedata->sqe_lock); | ||
| 201 | struct io_uring_sqe *sqe = liburing.io_uring_get_sqe(&queuedata->ring); | ||
| 202 | if (!sqe) { | ||
| 203 | SDL_UnlockMutex(queuedata->sqe_lock); | ||
| 204 | SDL_free(cancel_task); // oh well, the task can just finish on its own. | ||
| 205 | return; | ||
| 206 | } | ||
| 207 | |||
| 208 | cancel_task->app_userdata = task; | ||
| 209 | liburing.io_uring_prep_cancel(sqe, task, 0); | ||
| 210 | liburing.io_uring_sqe_set_data(sqe, cancel_task); | ||
| 211 | liburing_asyncioqueue_queue_task(userdata, task); | ||
| 212 | SDL_UnlockMutex(queuedata->sqe_lock); | ||
| 213 | } | ||
| 214 | |||
| 215 | static SDL_AsyncIOTask *ProcessCQE(LibUringAsyncIOQueueData *queuedata, struct io_uring_cqe *cqe) | ||
| 216 | { | ||
| 217 | if (!cqe) { | ||
| 218 | return NULL; | ||
| 219 | } | ||
| 220 | |||
| 221 | SDL_AsyncIOTask *task = (SDL_AsyncIOTask *) io_uring_cqe_get_data(cqe); | ||
| 222 | if (task) { // can be NULL if this was just a wakeup message, a NOP, etc. | ||
| 223 | if (!task->queue) { // We leave `queue` blank to signify this was a task cancellation. | ||
| 224 | SDL_AsyncIOTask *cancel_task = task; | ||
| 225 | task = (SDL_AsyncIOTask *) cancel_task->app_userdata; | ||
| 226 | SDL_free(cancel_task); | ||
| 227 | if (cqe->res >= 0) { // cancel was successful? | ||
| 228 | task->result = SDL_ASYNCIO_CANCELED; | ||
| 229 | } else { | ||
| 230 | task = NULL; // it already finished or was too far along to cancel, so we'll pick up the actual results later. | ||
| 231 | } | ||
| 232 | } else if (cqe->res < 0) { | ||
| 233 | task->result = SDL_ASYNCIO_FAILURE; | ||
| 234 | // !!! FIXME: fill in task->error. | ||
| 235 | } else { | ||
| 236 | if ((task->type == SDL_ASYNCIO_TASK_WRITE) && (((Uint64) cqe->res) < task->requested_size)) { | ||
| 237 | task->result = SDL_ASYNCIO_FAILURE; // it's always a failure on short writes. | ||
| 238 | } | ||
| 239 | |||
| 240 | // 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. | ||
| 241 | |||
| 242 | if ((task->type == SDL_ASYNCIO_TASK_READ) || (task->type == SDL_ASYNCIO_TASK_WRITE)) { | ||
| 243 | task->result_size = (Uint64) cqe->res; | ||
| 244 | } | ||
| 245 | } | ||
| 246 | |||
| 247 | if ((task->type == SDL_ASYNCIO_TASK_CLOSE) && task->flush) { | ||
| 248 | task->flush = false; | ||
| 249 | task = NULL; // don't return this one, it's a linked task, so it'll arrive in a later CQE. | ||
| 250 | } | ||
| 251 | } | ||
| 252 | |||
| 253 | return task; | ||
| 254 | } | ||
| 255 | |||
| 256 | static SDL_AsyncIOTask *liburing_asyncioqueue_get_results(void *userdata) | ||
| 257 | { | ||
| 258 | LibUringAsyncIOQueueData *queuedata = (LibUringAsyncIOQueueData *) userdata; | ||
| 259 | |||
| 260 | // 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. | ||
| 261 | SDL_LockMutex(queuedata->cqe_lock); | ||
| 262 | struct io_uring_cqe *cqe = NULL; | ||
| 263 | const int rc = liburing.io_uring_peek_cqe(&queuedata->ring, &cqe); | ||
| 264 | if (rc != 0) { | ||
| 265 | SDL_assert(rc == -EAGAIN); // should only fail because nothing is available at the moment. | ||
| 266 | SDL_UnlockMutex(queuedata->cqe_lock); | ||
| 267 | return NULL; | ||
| 268 | } | ||
| 269 | |||
| 270 | struct io_uring_cqe cqe_copy; | ||
| 271 | SDL_copyp(&cqe_copy, cqe); // this is only a few bytes. | ||
| 272 | liburing.io_uring_cqe_seen(&queuedata->ring, cqe); // let io_uring use this slot again. | ||
| 273 | SDL_UnlockMutex(queuedata->cqe_lock); | ||
| 274 | |||
| 275 | return ProcessCQE(queuedata, &cqe_copy); | ||
| 276 | } | ||
| 277 | |||
| 278 | static SDL_AsyncIOTask *liburing_asyncioqueue_wait_results(void *userdata, Sint32 timeoutMS) | ||
| 279 | { | ||
| 280 | LibUringAsyncIOQueueData *queuedata = (LibUringAsyncIOQueueData *) userdata; | ||
| 281 | struct io_uring_cqe *cqe = NULL; | ||
| 282 | |||
| 283 | SDL_AddAtomicInt(&queuedata->num_waiting, 1); | ||
| 284 | if (timeoutMS < 0) { | ||
| 285 | liburing.io_uring_wait_cqe(&queuedata->ring, &cqe); | ||
| 286 | } else { | ||
| 287 | struct __kernel_timespec ts = { (Sint64) timeoutMS / SDL_MS_PER_SECOND, (Sint64) SDL_MS_TO_NS(timeoutMS % SDL_MS_PER_SECOND) }; | ||
| 288 | liburing.io_uring_wait_cqe_timeout(&queuedata->ring, &cqe, &ts); | ||
| 289 | } | ||
| 290 | SDL_AddAtomicInt(&queuedata->num_waiting, -1); | ||
| 291 | |||
| 292 | // (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.) | ||
| 293 | |||
| 294 | // 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 | ||
| 295 | // then use peek to make sure we have a unique cqe, and other competing threads either get their own or nothing. | ||
| 296 | return liburing_asyncioqueue_get_results(userdata); // this just happens to do all those things. | ||
| 297 | } | ||
| 298 | |||
| 299 | static void liburing_asyncioqueue_signal(void *userdata) | ||
| 300 | { | ||
| 301 | LibUringAsyncIOQueueData *queuedata = (LibUringAsyncIOQueueData *) userdata; | ||
| 302 | const int num_waiting = SDL_GetAtomicInt(&queuedata->num_waiting); | ||
| 303 | |||
| 304 | SDL_LockMutex(queuedata->sqe_lock); | ||
| 305 | 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? | ||
| 306 | struct io_uring_sqe *sqe = liburing.io_uring_get_sqe(&queuedata->ring); | ||
| 307 | if (sqe) { | ||
| 308 | static struct __kernel_timespec ts; // no wait, just wake a thread as fast as this can land in the completion queue. | ||
| 309 | liburing.io_uring_prep_timeout(sqe, &ts, 0, 0); | ||
| 310 | liburing.io_uring_sqe_set_data(sqe, NULL); | ||
| 311 | } | ||
| 312 | } | ||
| 313 | liburing.io_uring_submit(&queuedata->ring); | ||
| 314 | |||
| 315 | SDL_UnlockMutex(queuedata->sqe_lock); | ||
| 316 | } | ||
| 317 | |||
| 318 | static void liburing_asyncioqueue_destroy(void *userdata) | ||
| 319 | { | ||
| 320 | LibUringAsyncIOQueueData *queuedata = (LibUringAsyncIOQueueData *) userdata; | ||
| 321 | liburing.io_uring_queue_exit(&queuedata->ring); | ||
| 322 | SDL_DestroyMutex(queuedata->sqe_lock); | ||
| 323 | SDL_DestroyMutex(queuedata->cqe_lock); | ||
| 324 | SDL_free(queuedata); | ||
| 325 | } | ||
| 326 | |||
| 327 | static bool SDL_SYS_CreateAsyncIOQueue_liburing(SDL_AsyncIOQueue *queue) | ||
| 328 | { | ||
| 329 | LibUringAsyncIOQueueData *queuedata = (LibUringAsyncIOQueueData *) SDL_calloc(1, sizeof (*queuedata)); | ||
| 330 | if (!queuedata) { | ||
| 331 | return false; | ||
| 332 | } | ||
| 333 | |||
| 334 | SDL_SetAtomicInt(&queuedata->num_waiting, 0); | ||
| 335 | |||
| 336 | queuedata->sqe_lock = SDL_CreateMutex(); | ||
| 337 | if (!queuedata->sqe_lock) { | ||
| 338 | SDL_free(queuedata); | ||
| 339 | return false; | ||
| 340 | } | ||
| 341 | |||
| 342 | queuedata->cqe_lock = SDL_CreateMutex(); | ||
| 343 | if (!queuedata->cqe_lock) { | ||
| 344 | SDL_DestroyMutex(queuedata->sqe_lock); | ||
| 345 | SDL_free(queuedata); | ||
| 346 | return false; | ||
| 347 | } | ||
| 348 | |||
| 349 | // !!! FIXME: no idea how large the queue should be. Is 128 overkill or too small? | ||
| 350 | const int rc = liburing.io_uring_queue_init(128, &queuedata->ring, 0); | ||
| 351 | if (rc != 0) { | ||
| 352 | SDL_DestroyMutex(queuedata->sqe_lock); | ||
| 353 | SDL_DestroyMutex(queuedata->cqe_lock); | ||
| 354 | SDL_free(queuedata); | ||
| 355 | return liburing_SetError("io_uring_queue_init", rc); | ||
| 356 | } | ||
| 357 | |||
| 358 | static const SDL_AsyncIOQueueInterface SDL_AsyncIOQueue_liburing = { | ||
| 359 | liburing_asyncioqueue_queue_task, | ||
| 360 | liburing_asyncioqueue_cancel_task, | ||
| 361 | liburing_asyncioqueue_get_results, | ||
| 362 | liburing_asyncioqueue_wait_results, | ||
| 363 | liburing_asyncioqueue_signal, | ||
| 364 | liburing_asyncioqueue_destroy | ||
| 365 | }; | ||
| 366 | |||
| 367 | SDL_copyp(&queue->iface, &SDL_AsyncIOQueue_liburing); | ||
| 368 | queue->userdata = queuedata; | ||
| 369 | return true; | ||
| 370 | } | ||
| 371 | |||
| 372 | |||
| 373 | static bool liburing_asyncio_read(void *userdata, SDL_AsyncIOTask *task) | ||
| 374 | { | ||
| 375 | LibUringAsyncIOQueueData *queuedata = (LibUringAsyncIOQueueData *) task->queue->userdata; | ||
| 376 | const int fd = (int) (intptr_t) userdata; | ||
| 377 | |||
| 378 | // !!! FIXME: `unsigned` is likely smaller than requested_size's Uint64. If we overflow it, we could try submitting multiple SQEs | ||
| 379 | // !!! FIXME: and make a note in the task that there are several in sequence. | ||
| 380 | if (task->requested_size > ((Uint64) ~((unsigned) 0))) { | ||
| 381 | return SDL_SetError("io_uring: i/o task is too large"); | ||
| 382 | } | ||
| 383 | |||
| 384 | // have to hold a lock because otherwise two threads could get_sqe and submit while one request isn't fully set up. | ||
| 385 | SDL_LockMutex(queuedata->sqe_lock); | ||
| 386 | bool retval; | ||
| 387 | struct io_uring_sqe *sqe = liburing.io_uring_get_sqe(&queuedata->ring); | ||
| 388 | if (!sqe) { | ||
| 389 | retval = SDL_SetError("io_uring: submission queue is full"); | ||
| 390 | } else { | ||
| 391 | liburing.io_uring_prep_read(sqe, fd, task->buffer, (unsigned) task->requested_size, task->offset); | ||
| 392 | liburing.io_uring_sqe_set_data(sqe, task); | ||
| 393 | retval = task->queue->iface.queue_task(task->queue->userdata, task); | ||
| 394 | } | ||
| 395 | SDL_UnlockMutex(queuedata->sqe_lock); | ||
| 396 | return retval; | ||
| 397 | } | ||
| 398 | |||
| 399 | static bool liburing_asyncio_write(void *userdata, SDL_AsyncIOTask *task) | ||
| 400 | { | ||
| 401 | LibUringAsyncIOQueueData *queuedata = (LibUringAsyncIOQueueData *) task->queue->userdata; | ||
| 402 | const int fd = (int) (intptr_t) userdata; | ||
| 403 | |||
| 404 | // !!! FIXME: `unsigned` is likely smaller than requested_size's Uint64. If we overflow it, we could try submitting multiple SQEs | ||
| 405 | // !!! FIXME: and make a note in the task that there are several in sequence. | ||
| 406 | if (task->requested_size > ((Uint64) ~((unsigned) 0))) { | ||
| 407 | return SDL_SetError("io_uring: i/o task is too large"); | ||
| 408 | } | ||
| 409 | |||
| 410 | // have to hold a lock because otherwise two threads could get_sqe and submit while one request isn't fully set up. | ||
| 411 | SDL_LockMutex(queuedata->sqe_lock); | ||
| 412 | bool retval; | ||
| 413 | struct io_uring_sqe *sqe = liburing.io_uring_get_sqe(&queuedata->ring); | ||
| 414 | if (!sqe) { | ||
| 415 | retval = SDL_SetError("io_uring: submission queue is full"); | ||
| 416 | } else { | ||
| 417 | liburing.io_uring_prep_write(sqe, fd, task->buffer, (unsigned) task->requested_size, task->offset); | ||
| 418 | liburing.io_uring_sqe_set_data(sqe, task); | ||
| 419 | retval = task->queue->iface.queue_task(task->queue->userdata, task); | ||
| 420 | } | ||
| 421 | SDL_UnlockMutex(queuedata->sqe_lock); | ||
| 422 | return retval; | ||
| 423 | } | ||
| 424 | |||
| 425 | static bool liburing_asyncio_close(void *userdata, SDL_AsyncIOTask *task) | ||
| 426 | { | ||
| 427 | LibUringAsyncIOQueueData *queuedata = (LibUringAsyncIOQueueData *) task->queue->userdata; | ||
| 428 | const int fd = (int) (intptr_t) userdata; | ||
| 429 | |||
| 430 | // have to hold a lock because otherwise two threads could get_sqe and submit while one request isn't fully set up. | ||
| 431 | SDL_LockMutex(queuedata->sqe_lock); | ||
| 432 | bool retval; | ||
| 433 | struct io_uring_sqe *sqe = liburing.io_uring_get_sqe(&queuedata->ring); | ||
| 434 | if (!sqe) { | ||
| 435 | retval = SDL_SetError("io_uring: submission queue is full"); | ||
| 436 | } else { | ||
| 437 | if (task->flush) { | ||
| 438 | struct io_uring_sqe *flush_sqe = sqe; | ||
| 439 | sqe = liburing.io_uring_get_sqe(&queuedata->ring); // this will be our actual close task. | ||
| 440 | if (!sqe) { | ||
| 441 | liburing.io_uring_prep_nop(flush_sqe); // we already have the first sqe, just make it a NOP. | ||
| 442 | liburing.io_uring_sqe_set_data(flush_sqe, NULL); | ||
| 443 | task->queue->iface.queue_task(task->queue->userdata, task); | ||
| 444 | SDL_UnlockMutex(queuedata->sqe_lock); | ||
| 445 | return SDL_SetError("io_uring: submission queue is full"); | ||
| 446 | } | ||
| 447 | liburing.io_uring_prep_fsync(flush_sqe, fd, IORING_FSYNC_DATASYNC); | ||
| 448 | liburing.io_uring_sqe_set_data(flush_sqe, task); | ||
| 449 | 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. | ||
| 450 | } | ||
| 451 | |||
| 452 | liburing.io_uring_prep_close(sqe, fd); | ||
| 453 | liburing.io_uring_sqe_set_data(sqe, task); | ||
| 454 | |||
| 455 | retval = task->queue->iface.queue_task(task->queue->userdata, task); | ||
| 456 | } | ||
| 457 | SDL_UnlockMutex(queuedata->sqe_lock); | ||
| 458 | return retval; | ||
| 459 | } | ||
| 460 | |||
| 461 | static void liburing_asyncio_destroy(void *userdata) | ||
| 462 | { | ||
| 463 | // this is only a Unix file descriptor, should have been closed elsewhere. | ||
| 464 | } | ||
| 465 | |||
| 466 | static int PosixOpenModeFromString(const char *mode) | ||
| 467 | { | ||
| 468 | // this is exactly the set of strings that SDL_AsyncIOFromFile promises will work. | ||
| 469 | static const struct { const char *str; int flags; } mappings[] = { | ||
| 470 | { "rb", O_RDONLY }, | ||
| 471 | { "wb", O_WRONLY | O_CREAT | O_TRUNC }, | ||
| 472 | { "r+b", O_RDWR }, | ||
| 473 | { "w+b", O_RDWR | O_CREAT | O_TRUNC } | ||
| 474 | }; | ||
| 475 | |||
| 476 | for (int i = 0; i < SDL_arraysize(mappings); i++) { | ||
| 477 | if (SDL_strcmp(mappings[i].str, mode) == 0) { | ||
| 478 | return mappings[i].flags; | ||
| 479 | } | ||
| 480 | } | ||
| 481 | |||
| 482 | SDL_assert(!"Shouldn't have reached this code"); | ||
| 483 | return 0; | ||
| 484 | } | ||
| 485 | |||
| 486 | static bool SDL_SYS_AsyncIOFromFile_liburing(const char *file, const char *mode, SDL_AsyncIO *asyncio) | ||
| 487 | { | ||
| 488 | const int fd = open(file, PosixOpenModeFromString(mode), 0644); | ||
| 489 | if (fd == -1) { | ||
| 490 | return SDL_SetError("open failed: %s", strerror(errno)); | ||
| 491 | } | ||
| 492 | |||
| 493 | static const SDL_AsyncIOInterface SDL_AsyncIOFile_liburing = { | ||
| 494 | liburing_asyncio_size, | ||
| 495 | liburing_asyncio_read, | ||
| 496 | liburing_asyncio_write, | ||
| 497 | liburing_asyncio_close, | ||
| 498 | liburing_asyncio_destroy | ||
| 499 | }; | ||
| 500 | |||
| 501 | SDL_copyp(&asyncio->iface, &SDL_AsyncIOFile_liburing); | ||
| 502 | asyncio->userdata = (void *) (intptr_t) fd; | ||
| 503 | return true; | ||
| 504 | } | ||
| 505 | |||
| 506 | static void SDL_SYS_QuitAsyncIO_liburing(void) | ||
| 507 | { | ||
| 508 | UnloadLibUringLibrary(); | ||
| 509 | } | ||
| 510 | |||
| 511 | static void MaybeInitializeLibUring(void) | ||
| 512 | { | ||
| 513 | if (SDL_ShouldInit(&liburing_init)) { | ||
| 514 | if (LoadLibUring()) { | ||
| 515 | CreateAsyncIOQueue = SDL_SYS_CreateAsyncIOQueue_liburing; | ||
| 516 | QuitAsyncIO = SDL_SYS_QuitAsyncIO_liburing; | ||
| 517 | AsyncIOFromFile = SDL_SYS_AsyncIOFromFile_liburing; | ||
| 518 | } else { // can't use liburing? Use the "generic" threadpool implementation instead. | ||
| 519 | CreateAsyncIOQueue = SDL_SYS_CreateAsyncIOQueue_Generic; | ||
| 520 | QuitAsyncIO = SDL_SYS_QuitAsyncIO_Generic; | ||
| 521 | AsyncIOFromFile = SDL_SYS_AsyncIOFromFile_Generic; | ||
| 522 | } | ||
| 523 | SDL_SetInitialized(&liburing_init, true); | ||
| 524 | } | ||
| 525 | } | ||
| 526 | |||
| 527 | bool SDL_SYS_CreateAsyncIOQueue(SDL_AsyncIOQueue *queue) | ||
| 528 | { | ||
| 529 | MaybeInitializeLibUring(); | ||
| 530 | return CreateAsyncIOQueue(queue); | ||
| 531 | } | ||
| 532 | |||
| 533 | bool SDL_SYS_AsyncIOFromFile(const char *file, const char *mode, SDL_AsyncIO *asyncio) | ||
| 534 | { | ||
| 535 | MaybeInitializeLibUring(); | ||
| 536 | return AsyncIOFromFile(file, mode, asyncio); | ||
| 537 | } | ||
| 538 | |||
| 539 | void SDL_SYS_QuitAsyncIO(void) | ||
| 540 | { | ||
| 541 | if (SDL_ShouldQuit(&liburing_init)) { | ||
| 542 | QuitAsyncIO(); | ||
| 543 | CreateAsyncIOQueue = NULL; | ||
| 544 | QuitAsyncIO = NULL; | ||
| 545 | AsyncIOFromFile = NULL; | ||
| 546 | SDL_SetInitialized(&liburing_init, false); | ||
| 547 | } | ||
| 548 | } | ||
| 549 | |||
| 550 | #endif // defined HAVE_LIBURING_H | ||
| 551 | |||
