SDL: asyncio: don't report failures on closing read-only files with Windows IoRing. (c3e92)

From c3e92cf1c4f54da2b4b0b6d5f26fa1b699e740a3 Mon Sep 17 00:00:00 2001
From: "Ryan C. Gordon" <[EMAIL REDACTED]>
Date: Sat, 31 Jan 2026 03:58:25 -0500
Subject: [PATCH] asyncio: don't report failures on closing read-only files
 with Windows IoRing.

We still need the task to go through the IoRing, even though the flush
operation we use to get it there will always fail on a read-only file. So
check for this specific case and don't report failure.

Fixes #14878.

(cherry picked from commit 4df13e880671ca910cb6757ceb9851025a2fd50e)
---
 src/io/SDL_asyncio.c                        | 20 +++++++++++++-------
 src/io/SDL_sysasyncio.h                     |  1 +
 src/io/windows/SDL_asyncio_windows_ioring.c | 10 ++++++++--
 3 files changed, 22 insertions(+), 9 deletions(-)

diff --git a/src/io/SDL_asyncio.c b/src/io/SDL_asyncio.c
index 4c350379bbe4c..3146137725b3f 100644
--- a/src/io/SDL_asyncio.c
+++ b/src/io/SDL_asyncio.c
@@ -23,20 +23,23 @@
 #include "SDL_sysasyncio.h"
 #include "SDL_asyncio_c.h"
 
-static const char *AsyncFileModeValid(const char *mode)
+static const char *AsyncFileModeValid(const char *mode, bool *readonly)
 {
-    static const struct { const char *valid; const char *with_binary; } mode_map[] = {
-        { "r", "rb" },
-        { "w", "wb" },
-        { "r+","r+b" },
-        { "w+", "w+b" }
+    static const struct { const char *valid; const char *with_binary; bool readonly; } mode_map[] = {
+        { "r", "rb", true },
+        { "w", "wb", false },
+        { "r+","r+b", false },
+        { "w+", "w+b", false }
     };
 
     for (int i = 0; i < SDL_arraysize(mode_map); i++) {
         if (SDL_strcmp(mode, mode_map[i].valid) == 0) {
+            *readonly = mode_map[i].readonly;
             return mode_map[i].with_binary;
         }
     }
+
+    *readonly = false;
     return NULL;
 }
 
@@ -51,7 +54,8 @@ SDL_AsyncIO *SDL_AsyncIOFromFile(const char *file, const char *mode)
         return NULL;
     }
 
-    const char *binary_mode = AsyncFileModeValid(mode);
+    bool readonly = false;
+    const char *binary_mode = AsyncFileModeValid(mode, &readonly);
     if (!binary_mode) {
         SDL_SetError("Unsupported file mode");
         return NULL;
@@ -62,6 +66,8 @@ SDL_AsyncIO *SDL_AsyncIOFromFile(const char *file, const char *mode)
         return NULL;
     }
 
+    asyncio->readonly = readonly;
+
     asyncio->lock = SDL_CreateMutex();
     if (!asyncio->lock) {
         SDL_free(asyncio);
diff --git a/src/io/SDL_sysasyncio.h b/src/io/SDL_sysasyncio.h
index efb727b69b5f3..b0ca21b91607f 100644
--- a/src/io/SDL_sysasyncio.h
+++ b/src/io/SDL_sysasyncio.h
@@ -124,6 +124,7 @@ struct SDL_AsyncIO
     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.
+    bool readonly;  // true if this file is opened read-only.
 };
 
 // This is implemented for various platforms; param validation is done before calling this. Open file, fill in iface and userdata.
diff --git a/src/io/windows/SDL_asyncio_windows_ioring.c b/src/io/windows/SDL_asyncio_windows_ioring.c
index f427e9c2bb177..81a5d84096352 100644
--- a/src/io/windows/SDL_asyncio_windows_ioring.c
+++ b/src/io/windows/SDL_asyncio_windows_ioring.c
@@ -200,8 +200,14 @@ static SDL_AsyncIOTask *ProcessCQE(WinIoRingAsyncIOQueueData *queuedata, IORING_
                 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.
+            if ((task->type == SDL_ASYNCIO_TASK_CLOSE) && (cqe->ResultCode == E_ACCESSDENIED) && task->asyncio->readonly) {
+                // we push all close requests through as flushes, as there is currently no async close operation and flushing writes to disk is the time-consuming part.
+                // However, flushing a read-only handle generates an error, so we catch this specific situation and ignore it. This approach still makes the task go
+                // through the IoRing so we can handle this all in the same place otherwise. The actual close happens below.
+            } else {
+                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.