SDL: Add the Windows notification driver

From 984b03680fa747697240ce8e6579cf670d551abd Mon Sep 17 00:00:00 2001
From: Frank Praznik <[EMAIL REDACTED]>
Date: Mon, 6 Apr 2026 12:45:56 -0400
Subject: [PATCH] Add the Windows notification driver

Notifications are supported on Win10 and above.
---
 VisualC-GDK/SDL/SDL.vcxproj.filters           |    7 +-
 VisualC/SDL/SDL.vcxproj                       |    7 +-
 VisualC/SDL/SDL.vcxproj.filters               |   21 +
 src/core/windows/SDL_windows.c                |    5 +
 src/core/windows/SDL_windows.h                |    3 +
 .../windows/SDL_windowsnotification.c         | 1161 +++++++++++++++++
 6 files changed, 1202 insertions(+), 2 deletions(-)
 create mode 100644 src/notification/windows/SDL_windowsnotification.c

diff --git a/VisualC-GDK/SDL/SDL.vcxproj.filters b/VisualC-GDK/SDL/SDL.vcxproj.filters
index 9970a0834c22d..6843eff2eca41 100644
--- a/VisualC-GDK/SDL/SDL.vcxproj.filters
+++ b/VisualC-GDK/SDL/SDL.vcxproj.filters
@@ -4,6 +4,9 @@
     <ClCompile Include="..\..\src\core\gdk\SDL_gdk.cpp" />
     <ClCompile Include="..\..\src\core\windows\pch.c" />
     <ClCompile Include="..\..\src\core\windows\pch_cpp.cpp" />
+    <ClCompile Include="..\..\src\events\SDL_notificationevents.c" />
+    <ClCompile Include="..\..\src\notification\dummy\SDL_dummynotification.c" />
+    <ClCompile Include="..\..\src\notification\SDL_notification.c" />
     <ClCompile Include="..\..\src\render\direct3d12\SDL_render_d3d12_xbox.cpp" />
     <ClCompile Include="..\..\src\render\direct3d12\SDL_shaders_d3d12_xboxone.cpp" />
     <ClCompile Include="..\..\src\render\direct3d12\SDL_shaders_d3d12_xboxseries.cpp" />
@@ -54,6 +57,7 @@
     <ClCompile Include="..\..\src\gpu\vulkan\SDL_gpu_vulkan.c" />
     <ClCompile Include="..\..\src\gpu\xr\SDL_gpu_openxr.c" />
     <ClCompile Include="..\..\src\gpu\xr\SDL_openxrdyn.c" />
+    <ClInclude Include="..\..\src\events\SDL_notificationevents_c.h" />
     <ClInclude Include="..\..\src\gpu\xr\SDL_openxr_internal.h" />
     <ClCompile Include="..\..\src\haptic\dummy\SDL_syshaptic.c" />
     <ClCompile Include="..\..\src\haptic\SDL_haptic.c" />
@@ -241,6 +245,7 @@
     <ClCompile Include="..\..\src\video\yuv2rgb\yuv_rgb_lsx.c" />
     <ClCompile Include="..\..\src\video\yuv2rgb\yuv_rgb_sse.c" />
     <ClCompile Include="..\..\src\video\yuv2rgb\yuv_rgb_std.c" />
+    <ClInclude Include="..\..\src\notification\SDL_notification_c.h" />
   </ItemGroup>
   <ItemGroup>
     <ClInclude Include="..\..\include\SDL3\SDL.h" />
@@ -513,4 +518,4 @@
   <ItemGroup>
     <ResourceCompile Include="..\..\src\core\windows\version.rc" />
   </ItemGroup>
-</Project>
+</Project>
\ No newline at end of file
diff --git a/VisualC/SDL/SDL.vcxproj b/VisualC/SDL/SDL.vcxproj
index db8a11f436519..643bffef8f719 100644
--- a/VisualC/SDL/SDL.vcxproj
+++ b/VisualC/SDL/SDL.vcxproj
@@ -466,6 +466,7 @@
     <ClInclude Include="..\..\src\events\SDL_keysym_to_keycode_c.h" />
     <ClInclude Include="..\..\src\events\SDL_keysym_to_scancode_c.h" />
     <ClInclude Include="..\..\src\events\SDL_mouse_c.h" />
+    <ClInclude Include="..\..\src\events\SDL_notificationevents_c.h" />
     <ClInclude Include="..\..\src\events\SDL_pen_c.h" />
     <ClInclude Include="..\..\src\events\SDL_scancode_tables_c.h" />
     <ClInclude Include="..\..\src\events\SDL_touch_c.h" />
@@ -522,6 +523,7 @@
     <ClInclude Include="..\..\src\main\SDL_main_callbacks.h" />
     <ClInclude Include="..\..\src\misc\SDL_libusb.h" />
     <ClInclude Include="..\..\src\misc\SDL_sysurl.h" />
+    <ClInclude Include="..\..\src\notification\SDL_notification_c.h" />
     <ClInclude Include="..\..\src\power\SDL_syspower.h" />
     <ClInclude Include="..\..\src\process\SDL_sysprocess.h" />
     <ClInclude Include="..\..\src\render\direct3d11\D3D11_PixelShader_Advanced.h" />
@@ -624,6 +626,7 @@
     <ClCompile Include="..\..\src\events\imKStoUCS.c" />
     <ClCompile Include="..\..\src\events\SDL_keysym_to_keycode.c" />
     <ClCompile Include="..\..\src\events\SDL_keysym_to_scancode.c" />
+    <ClCompile Include="..\..\src\events\SDL_notificationevents.c" />
     <ClCompile Include="..\..\src\events\SDL_scancode_tables.c" />
     <ClCompile Include="..\..\src\filesystem\SDL_filesystem.c" />
     <ClCompile Include="..\..\src\filesystem\windows\SDL_sysfsops.c" />
@@ -639,6 +642,8 @@
     <ClCompile Include="..\..\src\main\SDL_main_callbacks.c" />
     <ClCompile Include="..\..\src\main\SDL_runapp.c" />
     <ClCompile Include="..\..\src\main\windows\SDL_sysmain_runapp.c" />
+    <ClCompile Include="..\..\src\notification\SDL_notification.c" />
+    <ClCompile Include="..\..\src\notification\windows\SDL_windowsnotification.c" />
     <ClCompile Include="..\..\src\render\vulkan\SDL_render_vulkan.c" />
     <ClCompile Include="..\..\src\render\vulkan\SDL_shaders_vulkan.c" />
     <ClCompile Include="..\..\src\SDL_guid.c" />
@@ -1000,4 +1005,4 @@
   <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
   <ImportGroup Label="ExtensionTargets">
   </ImportGroup>
-</Project>
\ No newline at end of file
+</Project>
diff --git a/VisualC/SDL/SDL.vcxproj.filters b/VisualC/SDL/SDL.vcxproj.filters
index 0798f3d750eb6..655fca14fdcbc 100644
--- a/VisualC/SDL/SDL.vcxproj.filters
+++ b/VisualC/SDL/SDL.vcxproj.filters
@@ -262,6 +262,12 @@
     <Filter Include="render\gpu\shaders">
       <UniqueIdentifier>{107d41e6-7c7e-4d9a-a3b3-b6f4abfde0c1}</UniqueIdentifier>
     </Filter>
+    <Filter Include="notification">
+      <UniqueIdentifier>{0000b1b2d12877646f6147a5a7510000}</UniqueIdentifier>
+    </Filter>
+    <Filter Include="notification\windows">
+      <UniqueIdentifier>{0000b6590eb19c11945581d0479b0000}</UniqueIdentifier>
+    </Filter>
   </ItemGroup>
   <ItemGroup>
     <ClInclude Include="..\..\include\SDL3\SDL.h">
@@ -489,6 +495,9 @@
     <ClInclude Include="..\..\src\camera\SDL_syscamera.h">
       <Filter>camera</Filter>
     </ClInclude>
+    <ClInclude Include="..\..\src\events\SDL_notificationevents_c.h">
+      <Filter>events</Filter>
+    </ClInclude>
     <ClInclude Include="..\..\src\filesystem\SDL_sysfilesystem.h">
       <Filter>filesystem</Filter>
     </ClInclude>
@@ -501,6 +510,9 @@
     <ClInclude Include="..\..\src\main\SDL_main_callbacks.h">
       <Filter>main</Filter>
     </ClInclude>
+    <ClInclude Include="..\..\src\notification\SDL_notification_c.h">
+      <Filter>notification</Filter>
+    </ClInclude>
     <ClInclude Include="..\..\src\SDL_error_c.h" />
     <ClInclude Include="..\..\src\SDL_hashtable.h" />
     <ClInclude Include="..\..\src\SDL_list.h" />
@@ -1351,6 +1363,9 @@
     <ClCompile Include="..\..\src\dialog\SDL_dialog_utils.c">
       <Filter>dialog</Filter>
     </ClCompile>
+    <ClCompile Include="..\..\src\events\SDL_notificationevents.c">
+      <Filter>events</Filter>
+    </ClCompile>
     <ClCompile Include="..\..\src\filesystem\SDL_filesystem.c">
       <Filter>filesystem</Filter>
     </ClCompile>
@@ -1378,6 +1393,12 @@
     <ClCompile Include="..\..\src\main\windows\SDL_sysmain_runapp.c">
       <Filter>main\windows</Filter>
     </ClCompile>
+    <ClCompile Include="..\..\src\notification\SDL_notification.c">
+      <Filter>notification</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\src\notification\windows\SDL_windowsnotification.c">
+      <Filter>notification\windows</Filter>
+    </ClCompile>
     <ClCompile Include="..\..\src\SDL.c" />
     <ClCompile Include="..\..\src\SDL_assert.c" />
     <ClCompile Include="..\..\src\SDL_error.c" />
diff --git a/src/core/windows/SDL_windows.c b/src/core/windows/SDL_windows.c
index 3f51cbd5fab2b..4ece1bd46a21a 100644
--- a/src/core/windows/SDL_windows.c
+++ b/src/core/windows/SDL_windows.c
@@ -379,6 +379,11 @@ BOOL WIN_IsWindows81OrGreater(void)
     CHECKWINVER(TRUE, IsWindowsVersionOrGreater(HIBYTE(_WIN32_WINNT_WINBLUE), LOBYTE(_WIN32_WINNT_WINBLUE), 0));
 }
 
+BOOL WIN_IsWindows10OrGreater(void)
+{
+    CHECKWINVER(TRUE, IsWindowsVersionOrGreater(HIBYTE(_WIN32_WINNT_WIN10), LOBYTE(_WIN32_WINNT_WIN10), 0));
+}
+
 BOOL WIN_IsWindows11OrGreater(void)
 {
     return IsWindowsBuildVersionAtLeast(22000);
diff --git a/src/core/windows/SDL_windows.h b/src/core/windows/SDL_windows.h
index e102f200c3d08..2c18c5cd1badb 100644
--- a/src/core/windows/SDL_windows.h
+++ b/src/core/windows/SDL_windows.h
@@ -196,6 +196,9 @@ extern BOOL WIN_IsWindows8OrGreater(void);
 // Returns true if we're running on Windows 8.1 and newer
 extern BOOL WIN_IsWindows81OrGreater(void);
 
+// Returns true if we're running on Windows 10 and newer
+extern BOOL WIN_IsWindows10OrGreater(void);
+
 // Returns true if we're running on Windows 11 and newer
 extern BOOL WIN_IsWindows11OrGreater(void);
 
diff --git a/src/notification/windows/SDL_windowsnotification.c b/src/notification/windows/SDL_windowsnotification.c
new file mode 100644
index 0000000000000..e3441bcbc0222
--- /dev/null
+++ b/src/notification/windows/SDL_windowsnotification.c
@@ -0,0 +1,1161 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2026 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 "../../events/SDL_notificationevents_c.h"
+#include "notification/SDL_notification_c.h"
+#include "video/SDL_surface_c.h"
+
+#include "../../core/windows/SDL_windows.h"
+#define COBJMACROS
+#include <Windows.ui.notifications.h>
+#include <bcrypt.h>
+#include <initguid.h>
+#include <roapi.h>
+
+typedef HRESULT(WINAPI *RoGetActivationFactory_t)(HSTRING activatableClassId, REFIID iid, void **factory);
+typedef HRESULT(WINAPI *RoActivateInstance_t)(HSTRING activatableClassId, IInspectable **instance);
+typedef HRESULT(WINAPI *WindowsCreateString_t)(PCWSTR sourceString, UINT32 length, HSTRING *string);
+typedef HRESULT(WINAPI *WindowsCreateStringReference_t)(PCWSTR sourceString, UINT32 length, HSTRING_HEADER *hstringHeader, HSTRING *string);
+typedef HRESULT(WINAPI *WindowsDeleteString_t)(HSTRING string);
+typedef PCWSTR(WINAPI *WindowsGetStringRawBuffer_t)(HSTRING string, UINT32 *length);
+typedef NTSTATUS(WINAPI *BCryptGenRandom_t)(BCRYPT_ALG_HANDLE hAlgorithm, PUCHAR pbBuffer, ULONG cbBuffer, ULONG dwFlags);
+typedef LONG(WINAPI *GetCurrentPackageFullName_t)(UINT32 *packageFullNameLength, PWSTR packageFullName);
+
+static RoGetActivationFactory_t WIN_RoGetActivationFactory;
+static RoActivateInstance_t WIN_RoActivateInstance;
+static WindowsCreateString_t WIN_WindowsCreateString;
+static WindowsCreateStringReference_t WIN_WindowsCreateStringReference;
+static WindowsDeleteString_t WIN_WindowsDeleteString;
+static WindowsGetStringRawBuffer_t WIN_WindowsGetStringRawBuffer;
+static GetCurrentPackageFullName_t WIN_GetCurrentPackageFullName;
+
+// The registry key base needed to register the app instance so notifications can be sent.
+#define REG_KEY_BASE L"SOFTWARE\\Classes\\AppUserModelId\\"
+#define GROUP_ID_STR L"SDL_LocalNotification"
+
+// IIDs for the interfaces.
+DEFINE_GUID(IID_IToastNotificationManagerStatics,
+            0x50ac103f, 0xd235, 0x4598, 0xbb, 0xef, 0x98, 0xfe, 0x4d, 0x1a, 0x3a, 0xd4);
+
+DEFINE_GUID(IID_IToastNotificationManagerStatics2,
+            0x7ab93c52, 0x0e48, 0x4750, 0xba, 0x9d, 0x1a, 0x41, 0x13, 0x98, 0x18, 0x47);
+
+DEFINE_GUID(IID_IToastNotificationFactory,
+            0x04124b20, 0x82c6, 0x4229, 0xb1, 0x09, 0xfd, 0x9e, 0xd4, 0x66, 0x2b, 0x53);
+
+DEFINE_GUID(IID_IToastNotification2,
+            0x9dfb9fd1, 0x143a, 0x490e, 0x90, 0xbf, 0xb9, 0xfb, 0xa7, 0x13, 0x2d, 0xe7);
+
+DEFINE_GUID(IID_IToastNotification4,
+            0x15154935, 0x28ea, 0x4727, 0x88, 0xe9, 0xc5, 0x86, 0x80, 0xe2, 0xd1, 0x18);
+
+DEFINE_GUID(IID_INotificationActivationCallback,
+            0x53e31837, 0x6600, 0x4a81, 0x93, 0x95, 0x75, 0xcf, 0xfe, 0x74, 0x6f, 0x94);
+
+DEFINE_GUID(IID_IXmlDocument,
+            0xf7f3a506, 0x1e87, 0x42d6, 0xbc, 0xfb, 0xb8, 0xc8, 0x09, 0xfa, 0x54, 0x94);
+
+DEFINE_GUID(IID_IXmlDocumentIO,
+            0x6cd0e74e, 0xee65, 0x4489, 0x9e, 0xbf, 0xca, 0x43, 0xe8, 0x7b, 0xa6, 0x37);
+
+DEFINE_GUID(IID_IToastActivatedEventArgs,
+            0xe3bf92f3, 0xc197, 0x436f, 0x82, 0x65, 0x06, 0x25, 0x82, 0x4f, 0x8d, 0xac);
+
+DEFINE_GUID(IID_IToastActivatedEventHandler,
+            0xab54de2d, 0x97d9, 0x5528, 0xb6, 0xad, 0x10, 0x5a, 0xfe, 0x15, 0x65, 0x30);
+
+DEFINE_GUID(IID_IToastDismissedEventHandler,
+            0x61c2402f, 0x0ed0, 0x5a18, 0xab, 0x69, 0x59, 0xf4, 0xaa, 0x99, 0xa3, 0x68);
+
+static struct Impl_IGeneric *pClassFactory = NULL;
+
+static HSTRING hsGroupId = NULL;
+static HSTRING hsAppId = NULL;
+
+static __x_ABI_CWindows_CUI_CNotifications_CIToastNotificationManagerStatics *pToastNotificationManager = NULL;
+static __x_ABI_CWindows_CUI_CNotifications_CIToastNotifier *pToastNotifier = NULL;
+static __x_ABI_CWindows_CUI_CNotifications_CIToastNotificationFactory *pNotificationFactory = NULL;
+
+static WCHAR *app_reg_key = NULL;
+static WCHAR *app_icon_path = NULL;
+
+static bool ro_initialized = false;
+static bool co_initialized = false;
+
+// IUnknown implementation
+typedef struct Impl_IGeneric
+{
+    IUnknownVtbl *lpVtbl;
+    SDL_AtomicInt refCount;
+} Impl_IGeneric;
+
+static ULONG STDMETHODCALLTYPE Impl_IGeneric_AddRef(Impl_IGeneric *_this)
+{
+    return SDL_AddAtomicInt(&_this->refCount, 1) + 1;
+}
+
+// OnActivated interface
+static HRESULT STDMETHODCALLTYPE Impl_OnActivated_QueryInterface(__FITypedEventHandler_2_Windows__CUI__CNotifications__CToastNotification_IInspectable *_this, REFIID riid, void **ppvObject)
+{
+    if (ppvObject == NULL) {
+        return E_POINTER;
+    }
+    if (IsEqualGUID(riid, &IID_IToastActivatedEventHandler) ||
+        IsEqualGUID(riid, &IID_IAgileObject) ||
+        IsEqualGUID(riid, &IID_IUnknown)) {
+        *ppvObject = _this;
+        _this->lpVtbl->AddRef(_this);
+        return S_OK;
+    }
+    return E_NOINTERFACE;
+}
+
+static HRESULT STDMETHODCALLTYPE Impl_OnActivated_Invoke(__FITypedEventHandler_2_Windows__CUI__CNotifications__CToastNotification_IInspectable *_this, __x_ABI_CWindows_CUI_CNotifications_CIToastNotification *Sender, IInspectable *Args)
+{
+    __x_ABI_CWindows_CUI_CNotifications_CIToastActivatedEventArgs *pEventArgs;
+    HRESULT hr = Args->lpVtbl->QueryInterface(Args, &IID_IToastActivatedEventArgs, (LPVOID *)&pEventArgs);
+    if (SUCCEEDED(hr)) {
+        SDL_NotificationID id = 0;
+
+        __x_ABI_CWindows_CUI_CNotifications_CIToastNotification2 *pToastNotification2;
+        hr = Sender->lpVtbl->QueryInterface(Sender, &IID_IToastNotification2, (LPVOID *)&pToastNotification2);
+        if (SUCCEEDED(hr)) {
+            HSTRING hsTag;
+            hr = pToastNotification2->lpVtbl->get_Tag(pToastNotification2, &hsTag);
+            if (SUCCEEDED(hr)) {
+                PCWSTR tag = WIN_WindowsGetStringRawBuffer(hsTag, NULL);
+                id = (SDL_NotificationID)SDL_wcstoul(tag, NULL, 10);
+            }
+            pToastNotification2->lpVtbl->Release(pToastNotification2);
+        }
+
+        if (id) {
+            HSTRING hsEventString;
+            hr = pEventArgs->lpVtbl->get_Arguments(pEventArgs, &hsEventString);
+            if (SUCCEEDED(hr)) {
+                PCWCHAR wstr = WIN_WindowsGetStringRawBuffer(hsEventString, NULL);
+                if (wstr) {
+                    char tmp[512];
+                    const int len = WideCharToMultiByte(CP_UTF8, 0, wstr, -1, tmp, sizeof(tmp), NULL, NULL);
+                    if (len > 0 && len <= sizeof(tmp)) {
+                        SDL_SendNotificationAction(id, tmp);
+                    }
+                }
+            }
+        }
+    }
+
+    return S_OK;
+}
+
+static __FITypedEventHandler_2_Windows__CUI__CNotifications__CToastNotification_IInspectableVtbl WindowsToast__OnActivatedVtbl = {
+    .QueryInterface = &Impl_OnActivated_QueryInterface,
+    .AddRef = (void *)Impl_IGeneric_AddRef,
+    .Release = (void *)Impl_IGeneric_AddRef,
+    .Invoke = &Impl_OnActivated_Invoke,
+};
+
+static __FITypedEventHandler_2_Windows__CUI__CNotifications__CToastNotification_IInspectable OnActivated = {
+    .lpVtbl = &WindowsToast__OnActivatedVtbl
+};
+
+// OnDismissed interface
+static HRESULT STDMETHODCALLTYPE Impl_OnDismissed_QueryInterface(__FITypedEventHandler_2_Windows__CUI__CNotifications__CToastNotification_Windows__CUI__CNotifications__CToastDismissedEventArgs *_this, REFIID riid, void **ppvObject)
+{
+    if (ppvObject == NULL) {
+        return E_POINTER;
+    }
+    if (IsEqualGUID(riid, &IID_IToastDismissedEventHandler) ||
+        IsEqualGUID(riid, &IID_IAgileObject) ||
+        IsEqualGUID(riid, &IID_IUnknown)) {
+        *ppvObject = _this;
+        _this->lpVtbl->AddRef(_this);
+        return S_OK;
+    }
+    return E_NOINTERFACE;
+}
+
+static HRESULT STDMETHODCALLTYPE Impl_OnDismissed_Invoke(__FITypedEventHandler_2_Windows__CUI__CNotifications__CToastNotification_Windows__CUI__CNotifications__CToastDismissedEventArgs *_this, __x_ABI_CWindows_CUI_CNotifications_CIToastNotification *Sender, __x_ABI_CWindows_CUI_CNotifications_CIToastDismissedEventArgs *Args)
+{
+    __x_ABI_CWindows_CUI_CNotifications_CToastDismissalReason Reason;
+    Args->lpVtbl->get_Reason(Args, &Reason);
+
+    /* Remove transient notifications that were cancelled or timed out,
+     * so they won't persist in the notification center.
+     */
+    switch (Reason) {
+    case ToastDismissalReason_TimedOut:
+    case ToastDismissalReason_UserCanceled:
+        pToastNotifier->lpVtbl->Hide(pToastNotifier, Sender);
+        break;
+
+    default:
+        break;
+    }
+
+    return S_OK;
+}
+
+static __FITypedEventHandler_2_Windows__CUI__CNotifications__CToastNotification_Windows__CUI__CNotifications__CToastDismissedEventArgsVtbl WindowsToast__OnDismissedVtbl = {
+    .QueryInterface = &Impl_OnDismissed_QueryInterface,
+    .AddRef = (void *)Impl_IGeneric_AddRef,
+    .Release = (void *)Impl_IGeneric_AddRef,
+    .Invoke = &Impl_OnDismissed_Invoke,
+};
+
+static __FITypedEventHandler_2_Windows__CUI__CNotifications__CToastNotification_Windows__CUI__CNotifications__CToastDismissedEventArgs OnDismissed = {
+    .lpVtbl = &WindowsToast__OnDismissedVtbl
+};
+
+static bool IsInPackage()
+{
+    if (!WIN_GetCurrentPackageFullName) {
+        HMODULE kernel32 = GetModuleHandle(TEXT("kernel32.dll"));
+        WIN_GetCurrentPackageFullName = (GetCurrentPackageFullName_t)GetProcAddress(kernel32, "GetCurrentPackageFullName");
+    }
+
+    if (WIN_GetCurrentPackageFullName) {
+        UINT32 length = 0;
+        LONG rc = WIN_GetCurrentPackageFullName(&length, NULL);
+        if (rc != ERROR_INSUFFICIENT_BUFFER) {
+            if (rc == APPMODEL_ERROR_NO_PACKAGE) {
+                return false;
+            } else {
+                return true;
+            }
+        }
+    }
+
+    return false;
+}
+
+static WCHAR *GetExePath()
+{
+    DWORD buflen = MAX_PATH;
+    WCHAR *path = NULL;
+    DWORD len = 0;
+
+    for (;;) {
+        WCHAR *ptr = SDL_realloc(path, buflen * sizeof(WCHAR));
+        if (!ptr) {
+            SDL_free(path);
+            return NULL;
+        }
+
+        path = ptr;
+
+        len = GetModuleFileNameW(NULL, path, buflen);
+        // If this was truncated, then len >= buflen - 1
+        if (len < buflen - 1) {
+            break;
+        }
+
+        // buffer too small? Try again.
+        buflen *= 2;
+    }
+
+    if (len == 0) {
+        SDL_free(path);
+        return NULL;
+    }
+
+    return path;
+}
+
+/* There is no way to pass an image as a byte stream when creating Windows
+ * notifications, so surfaces are saved as PNG files to temporary storage
+ * while the notification is being shown, then cleaned up a few seconds later.
+ */
+typedef struct ToastIcon
+{
+    struct ToastIcon *next;
+    WCHAR icon_file[1];
+} ToastIcon;
+
+static ToastIcon *toast_icons = NULL;
+static UINT_PTR cleanup_timer_id = 0;
+
+static void CleanupIcons()
+{
+    KillTimer(NULL, cleanup_timer_id);
+    cleanup_timer_id = 0;
+
+    for (ToastIcon *i = toast_icons; i; i = toast_icons) {
+        DeleteFileW(i->icon_file);
+        toast_icons = i->next;
+        SDL_free(i);
+    }
+}
+
+static void CALLBACK IconCleanupCallback(HWND hWnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime)
+{
+    CleanupIcons();
+}
+
+static WCHAR *GetFullPath(const char *path)
+{
+    const size_t conv_len = SDL_strlen(path) + 1;
+
+    WCHAR *conv_path = SDL_malloc(conv_len * sizeof(WCHAR));
+    if (!conv_path) {
+        return NULL;
+    }
+    MultiByteToWideChar(CP_UTF8, 0, path, -1, conv_path, (int)conv_len);
+
+    const DWORD full_len = GetFullPathNameW(conv_path, 0, NULL, NULL);
+    if (!full_len) {
+        SDL_free(conv_path);
+        return NULL;
+    }
+
+    WCHAR *full_path = SDL_malloc(full_len * sizeof(WCHAR));
+    if (!full_path) {
+        SDL_free(conv_path);
+        return NULL;
+    }
+    GetFullPathNameW(conv_path, full_len, full_path, NULL);
+    SDL_free(conv_path);
+
+    // Make sure the file actually exists.
+    WIN32_FILE_ATTRIBUTE_DATA attr;
+    if (GetFileAttributesExW(full_path, GetFileExInfoStandard, &attr)) {
+        return full_path;
+    }
+
+    SDL_free(full_path);
+    return NULL;
+}
+
+static const WCHAR *GetToastIconPath()
+{
+    if (app_icon_path) {
+        return app_icon_path;
+    }
+
+    const char *icon = SDL_GetStringProperty(SDL_GetGlobalProperties(), SDL_PROP_GLOBAL_NOTIFICATION_HEADER_ICON_STRING, NULL);
+    if (icon) {
+        app_icon_path = GetFullPath(icon);
+    }
+
+    return app_icon_path;
+}
+
+static WCHAR *SaveToastImage(SDL_Surface *icon)
+{
+    if (icon) {
+        // The documentation states that the buffers for these should be MAX_PATH.
+        WCHAR *temp_path = NULL;
+        size_t path_len = 0;
+
+        // Stop any timers so they won't fire in the middle of this.
+        if (cleanup_timer_id) {
+            KillTimer(NULL, cleanup_timer_id);
+        }
+
+        path_len = GetTempPath2W(0, NULL);
+        if (!path_len) {
+            return NULL;
+        }
+        temp_path = SDL_realloc(temp_path, (path_len + 1) * sizeof(WCHAR));
+        path_len = GetTempPath2W((DWORD)path_len + 1, temp_path);
+        if (!path_len) {
+            SDL_free(temp_path);
+            return NULL;
+        }
+
+        WCHAR file_name[MAX_PATH];
+        const UINT name_ret = GetTempFileNameW(temp_path, L"SDL", 0, file_name);
+        SDL_free(temp_path);
+        if (!name_ret) {
+            return NULL;
+        }
+
+        path_len += SDL_wcslen(file_name) + 5;
+
+        WCHAR *path_buf = NULL;
+        ToastIcon *toast_icon = SDL_calloc(1, sizeof(ToastIcon) + (path_len * sizeof(WCHAR)));
+        toast_icon->next = toast_icons;
+        toast_icons = toast_icon;
+        path_buf = toast_icon->icon_file;
+
+        SDL_wcslcat(path_buf, file_name, path_len);
+        SDL_wcslcat(path_buf, L".png", path_len);
+
+        const int len = WideCharToMultiByte(CP_UTF8, 0, path_buf, -1, NULL, 0, NULL, NULL);
+        char *png_path = SDL_malloc(len * sizeof(char));
+        WideCharToMultiByte(CP_UTF8, 0, path_buf, -1, png_path, len, NULL, NULL);
+        SDL_SavePNG(icon, png_path);
+        SDL_free(png_path);
+
+        // Duplicate the path, since the source object will be destroyed by a timer.
+        path_buf = SDL_wcsdup(path_buf);
+
+        // Schedule a cleanup of icons 5 seconds from now.
+        cleanup_timer_id = SetTimer(NULL, 0, 5000, IconCleanupCallback);
+
+        return path_buf;
+    }
+
+    return NULL;
+}
+
+static WCHAR *GetAppMetadata(const char *metadata_name)
+{
+    WCHAR *metadata = NULL;
+    int id_len = 0;
+
+    const char *app_metadata = SDL_GetAppMetadataProperty(metadata_name);
+    if (app_metadata && *app_metadata != '\0') {
+        id_len = MultiByteToWideChar(CP_UTF8, 0, app_metadata, -1, NULL, 0);
+        if (id_len > 0) {
+            metadata = SDL_malloc(id_len * sizeof(WCHAR));
+            if (!metadata) {
+                return NULL;
+            }
+
+            MultiByteToWideChar(CP_UTF8, 0, app_metadata, -1, metadata, id_len);
+            return metadata;
+        }
+    } else {
+        WCHAR *wszExePath = GetExePath();
+
+        for (WCHAR *c = wszExePath + SDL_wcslen(wszExePath); c >= wszExePath; --c) {
+            if (*c == L'/' || *c == L'\\') {
+                metadata = c + 1;
+                break;
+            }
+        }
+
+        if (!metadata) {
+            metadata = wszExePath;
+        }
+
+        metadata = SDL_wcsdup(metadata);
+        SDL_free(wszExePath);
+
+        return metadata;
+    }
+
+    return NULL;
+}
+
+static bool InitToastSystem()
+{
+    static bool initialized = false;
+
+    if (initialized) {
+        return true;
+    }
+
+#define RESOLVE(x)                                \
+    WIN_##x = (x##_t)WIN_LoadComBaseFunction(#x); \
+    if (!WIN_##x)                                 \
+    return WIN_SetError("GetProcAddress failed for " #x)
+    RESOLVE(RoGetActivationFactory);
+    RESOLVE(RoActivateInstance);
+    RESOLVE(WindowsCreateString);
+    RESOLVE(WindowsCreateStringReference);
+    RESOLVE(WindowsGetStringRawBuffer);
+    RESOLVE(WindowsDeleteString);
+#undef RESOLVE
+
+    HSTRING_HEADER hshToastNotificationManager;
+    HSTRING hsToastNotificationManager = NULL;
+    HSTRING_HEADER hshToastNotification;
+    HSTRING hsToastNotification = NULL;
+    WCHAR *image_path = NULL;
+
+    // Initialize COM and Windows runtime with the same threading model.
+    HRESULT hr = WIN_CoInitialize();
+    if (FAILED(hr)) {
+        return false;
+    }
+    co_initialized = true;
+
+    hr = WIN_RoInitialize();
+    if (FAILED(hr)) {
+        return false;
+    }
+    ro_initialized = true;
+
+    // Get the application ID and name.
+    WCHAR *app_id = GetAppMetadata(SDL_PROP_APP_METADATA_IDENTIFIER_STRING);
+    WCHAR *app_name = GetAppMetadata(SDL_PROP_APP_METADATA_NAME_STRING);
+
+    // Create the persistent appID string.
+    hr = WIN_WindowsCreateString(app_id, (UINT32)SDL_wcslen(app_id), &hsAppId);
+    if (FAILED(hr)) {
+        goto cleanup;
+    }
+
+    // Create the persistent groupID string.
+    hr = WIN_WindowsCreateString(GROUP_ID_STR, (UINT32)SDL_wcslen(GROUP_ID_STR), &hsGroupId);
+    if (FAILED(hr)) {
+        goto cleanup;
+    }
+
+    // Build the registry key.
+    {
+        size_t reg_key_len = SDL_wcslen(REG_KEY_BASE) + SDL_wcslen(app_id) + 1;
+        app_reg_key = SDL_malloc(reg_key_len * sizeof(WCHAR));
+        if (!app_reg_key) {
+            goto cleanup;
+        }
+        SDL_swprintf(app_reg_key, reg_key_len, L"%ls%ls", REG_KEY_BASE, app_id);
+    }
+
+    // Set the app name and icon.
+    {
+        const WCHAR *icon_path = GetToastIconPath();
+        if (icon_path) {
+            hr = HRESULT_FROM_WIN32(RegSetKeyValueW(HKEY_CURRENT_USER, app_reg_key, L"IconUri", REG_SZ, icon_path, (DWORD)(SDL_wcslen(icon_path) * sizeof(WCHAR))));
+            if (FAILED(hr)) {
+                goto cleanup;
+            }
+        } else {
+            // This will "fail" if the key already doesn't exist, which is fine.
+            RegDeleteKeyValueW(HKEY_CURRENT_USER, app_reg_key, L"IconUri");
+        }
+    }
+
+    hr = HRESULT_FROM_WIN32(RegSetKeyValueW(HKEY_CURRENT_USER, app_reg_key, L"DisplayName", REG_SZ, app_name, (DWORD)(SDL_wcslen(app_name) * sizeof(WCHAR))));
+    if (FAILED(hr)) {
+        goto cleanup;
+    }
+
+    hr = WIN_WindowsCreateStringReference(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager, sizeof(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager) / sizeof(WCHAR) - 1, &hshToastNotificationManager, &hsToastNotificationManager);
+    if (FAILED(hr)) {
+        goto cleanup;
+    }
+
+    hr = WIN_RoGetActivationFactory(hsToastNotificationManager, &IID_IToastNotificationManagerStatics, (LPVOID *)&pToastNotificationManager);
+    if (FAILED(hr)) {
+        goto cleanup;
+    }
+
+    hr = pToastNotificationManager->lpVtbl->CreateToastNotifierWithId(pToastNotificationManager, hsAppId, &pToastNotifier);
+    if (FAILED(hr)) {
+        goto cleanup;
+    }
+
+    hr = WIN_WindowsCreateStringReference(RuntimeClass_Windows_UI_Notifications_ToastNotification, (UINT32)(sizeof(RuntimeClass_Windows_UI_Notifications_ToastNotification) / sizeof(wchar_t) - 1), &hshToastNotification, &hsToastNotification);
+    if (FAILED(hr)) {
+        goto cleanup;
+    }
+
+    hr = WIN_RoGetActivationFactory(hsToastNotification, &IID_IToastNotificationFactory, (LPVOID *)&pNotificationFactory);
+    if (FAILED(hr)) {
+        goto cleanup;
+    }
+
+    initialized = true;
+
+cleanup:
+    WIN_WindowsDeleteString(hsToastNotificationManager);
+    WIN_WindowsDeleteString(hsToastNotification);
+    SDL_free(image_path);
+    SDL_free(app_id);
+    SDL_free(app_name);
+
+    return initialized;
+}
+
+static bool AppendXmlAudio(SDL_IOStream *dst, const char *sound)
+{
+    static const WCHAR *default_sound_path = L"ms-winsoundevent:Notification.Default";
+
+    WCHAR buf[512];
+    WCHAR *buf_ptr = buf;
+    WCHAR *path_format = NULL;
+    const WCHAR *silent_str = L"false";
+    const WCHAR *sound_path = default_sound_path;
+    int buf_len = SDL_arraysize(buf);
+
+    if (SDL_strcmp(sound, "silent") == 0) {
+        silent_str = L"true";
+    } else if (SDL_strcmp(sound, "default") != 0) {
+        /* Windows currently only loads custom notification sounds when the app is
+         * in an MSIX package. We'll prepend a 'file:///' prefix when not in a package
+         * in case this changes in the future, but for now, it just plays the default
+         * sound.
+         */
+        if (IsInPackage()) {
+            const WCHAR *prefix = L"ms-appx:///";
+            const size_t prefix_len = 11;
+
+            const size_t path_len = SDL_strlen(sound) + prefix_len + 1;
+            if (path_len) {
+                path_format = SDL_malloc(sizeof(WCHAR *) * path_len);
+                if (!path_format) {
+                    return false;
+                }
+                SDL_wcslcpy(path_format, prefix, path_len);
+
+                if (*sound == '/') {
+                    ++sound;
+                }
+                MultiByteToWideChar(CP_UTF8, 0, sound, -1, path_format + prefix_len, (int)(path_len - prefix_len));
+                sound_path = path_format;
+            }
+        } else {
+            SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Windows does not support custom notification sounds outside an MSIX package");
+
+            WCHAR *path = GetFullPath(sound);
+            if (path) {
+                const WCHAR *prefix = L"file:///";
+                const DWORD prefix_len = 8;
+
+                const size_t path_len = SDL_wcslen(path) + prefix_len + 1;
+                sound_path = path_format = SDL_malloc(sizeof(WCHAR *) * path_len);
+                if (!path_format) {
+                    SDL_free(path);
+                    return false;
+                }
+
+                SDL_wcslcpy(path_format, prefix,

(Patch may be truncated, please check the link at the top of this post.)