Просмотр исходного кода

Add the Windows notification driver

Notifications are supported on Win10 and above.
Frank Praznik 2 месяцев назад
Родитель
Сommit
984b03680f

+ 6 - 1
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>

+ 6 - 1
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>
+</Project>

+ 21 - 0
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" />

+ 5 - 0
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);

+ 3 - 0
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);
 

+ 1161 - 0
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, path_len);
+                SDL_wcslcat(path_format, path, path_len);
+                SDL_free(path);
+            }
+        }
+    }
+
+    for (;;) {
+        const int len = SDL_swprintf(buf_ptr, buf_len, L"<audio src=\"%ls\" loop=\"false\" silent=\"%ls\"/>", sound_path, silent_str);
+        if (len < buf_len) {
+            buf_len = len;
+            break;
+        }
+
+        buf_ptr = SDL_malloc((len + 1) * sizeof(WCHAR));
+        buf_len = len;
+    }
+
+    SDL_free(path_format);
+
+    const size_t write_size = buf_len * sizeof(WCHAR);
+    const bool res = SDL_WriteIO(dst, buf_ptr, write_size) == write_size;
+
+    if (buf_ptr != buf) {
+        SDL_free(buf_ptr);
+    }
+
+    return res;
+}
+
+static bool AppendXmlAction(SDL_IOStream *dst, const char *label, const char *action)
+{
+    char static_buf[512];
+    char *buf = static_buf;
+    int buf_len = SDL_arraysize(static_buf);
+
+    for (;;) {
+        int ret = SDL_snprintf(buf, buf_len, "<action content=\"%s\" activationType=\"foreground\" arguments=\"%s\"/>", label, action);
+        if (ret < buf_len) {
+            buf_len = ret + 1;
+            break;
+        }
+
+        buf_len = ret + 1;
+        buf = SDL_realloc(buf, ret);
+        if (!buf) {
+            return false;
+        }
+    }
+
+    // We know that, at most, an equal number of wide chars are needed for conversion.
+    WCHAR *wcbuf = SDL_malloc(buf_len * sizeof(WCHAR));
+    bool ret = true;
+
+    if (!wcbuf) {
+        ret = false;
+        goto done;
+    }
+    int wclen = MultiByteToWideChar(CP_UTF8, 0, buf, -1, wcbuf, buf_len);
+    if (wclen <= 0) {
+        ret = false;
+        goto done;
+    }
+
+    const size_t size = (wclen - 1) * sizeof(WCHAR);
+    if (SDL_WriteIO(dst, wcbuf, size) < size) {
+        ret = false;
+    }
+
+done:
+    SDL_free(wcbuf);
+    if (buf != static_buf) {
+        SDL_free(buf);
+    }
+
+    return ret;
+}
+
+static bool AppendXmlImage(SDL_IOStream *dst, const WCHAR *image_path)
+{
+    WCHAR static_buf[512];
+    WCHAR *buf = static_buf;
+    int buf_len = SDL_arraysize(static_buf);
+    bool ret = true;
+
+    for (;;) {
+        const int len = SDL_swprintf(buf, buf_len, L"<image id=\"1\" placement=\"appLogoOverride\" src=\"file:///%ls\"></image>", image_path);
+        if (len < buf_len) {
+            const size_t size = len * sizeof(WCHAR);
+            if (SDL_WriteIO(dst, buf, size) < size) {
+                ret = false;
+            }
+            break;
+        }
+
+        buf_len = len + 1;
+        buf = SDL_realloc(buf, len * sizeof(WCHAR));
+        if (!buf) {
+            return false;
+        }
+    }
+
+    if (buf != static_buf) {
+        SDL_free(buf);
+    }
+
+    return ret;
+}
+
+#define XML_TOAST_OPENING_STR L"<toast scenario=\"default\" activationType=\"foreground\" launch=\"default\">" \
+                              L"<visual>"                                                                      \
+                              L"<binding template=\"ToastGeneric\">"
+#define XML_TOAST_CLOSING_STR   L"</toast>"
+#define XML_TEXT_OPENING_STR    L"<text><![CDATA["
+#define XML_TEXT_CLOSING_STR    L"]]></text>"
+#define XML_ACTIONS_OPENING_STR L"<actions>"
+#define XML_ACTIONS_CLOSING_STR L"</actions>"
+#define XML_VISUAL_CLOSING_STR  L"</binding></visual>"
+
+static bool AppendXmlText(SDL_IOStream *dst, const char *text)
+{
+    const int wclen = MultiByteToWideChar(CP_UTF8, 0, text, -1, NULL, 0);
+    if (wclen <= 0) {
+        return false;
+    }
+    WCHAR *wcbuf = SDL_malloc(wclen * sizeof(WCHAR));
+    if (!wcbuf) {
+        return false;
+    }
+    MultiByteToWideChar(CP_UTF8, 0, text, -1, wcbuf, wclen);
+
+    size_t size = sizeof(XML_TEXT_OPENING_STR) - sizeof(WCHAR);
+    if (SDL_WriteIO(dst, XML_TEXT_OPENING_STR, size) < size) {
+        SDL_free(wcbuf);
+        return false;
+    }
+
+    size = (wclen - 1) * sizeof(WCHAR);
+    if (SDL_WriteIO(dst, wcbuf, size) < size) {
+        SDL_free(wcbuf);
+        return false;
+    }
+    SDL_free(wcbuf);
+
+    size = sizeof(XML_TEXT_CLOSING_STR) - sizeof(WCHAR);
+    if (SDL_WriteIO(dst, XML_TEXT_CLOSING_STR, size) < size) {
+        return false;
+    }
+
+    return true;
+}
+
+static WCHAR *BuildNotificationXml(SDL_PropertiesID props, const WCHAR *icon_path, SDL_NotificationID id, Uint32 *wchar_count)
+{
+    WCHAR *xml = NULL;
+    SDL_IOStream *dst = SDL_IOFromDynamicMem();
+    if (!dst) {
+        return NULL;
+    }
+
+    const char *title = SDL_GetStringProperty(props, SDL_PROP_NOTIFICATION_TITLE_STRING, NULL);
+    const char *message = SDL_GetStringProperty(props, SDL_PROP_NOTIFICATION_MESSAGE_STRING, NULL);
+    const char *sound = SDL_GetStringProperty(props, SDL_PROP_NOTIFICATION_SOUND_STRING, "default");
+    const SDL_NotificationAction *actions = SDL_GetPointerProperty(props, SDL_PROP_NOTIFICATION_ACTIONS_POINTER, NULL);
+    const int num_actions = (int)SDL_GetNumberProperty(props, SDL_PROP_NOTIFICATION_ACTION_COUNT_NUMBER, 0);
+
+    size_t size = sizeof(XML_TOAST_OPENING_STR) - sizeof(WCHAR);
+    if (SDL_WriteIO(dst, XML_TOAST_OPENING_STR, size) < size) {
+        goto done;
+    }
+    if (!AppendXmlImage(dst, icon_path)) {
+        goto done;
+    }
+    if (!AppendXmlText(dst, title)) {
+        goto done;
+    }
+    if (!AppendXmlText(dst, message)) {
+        goto done;
+    }
+
+    size = sizeof(XML_VISUAL_CLOSING_STR) - sizeof(WCHAR);
+    if (SDL_WriteIO(dst, XML_VISUAL_CLOSING_STR, size) < size) {
+        goto done;
+    }
+
+    if (!AppendXmlAudio(dst, sound)) {
+        goto done;
+    }
+
+    if (actions && num_actions) {
+        size = sizeof(XML_ACTIONS_OPENING_STR) - sizeof(WCHAR);
+        if (SDL_WriteIO(dst, XML_ACTIONS_OPENING_STR, size) < size) {
+            goto done;
+        }
+
+        for (int i = 0; i < num_actions; ++i) {
+            if (actions[i].type == SDL_NOTIFICATION_ACTION_TYPE_BUTTON) {
+                if (!AppendXmlAction(dst, actions[i].button.action_label, actions[i].button.action_id)) {
+                    goto done;
+                }
+            }
+        }
+
+        size = sizeof(XML_ACTIONS_CLOSING_STR) - sizeof(WCHAR);
+        if (SDL_WriteIO(dst, XML_ACTIONS_CLOSING_STR, size) < size) {
+            goto done;
+        }
+    }
+
+    // The XML string *must* be null-terminated for WindowsCreateStringReference().
+    size = sizeof(XML_TOAST_CLOSING_STR);
+    if (SDL_WriteIO(dst, XML_TOAST_CLOSING_STR, size) < size) {
+        goto done;
+    }
+    *wchar_count = (Uint32)((SDL_TellIO(dst) / sizeof(WCHAR)) - 1);
+
+    // Get the pointer to the XML string.
+    SDL_PropertiesID io_props = SDL_GetIOProperties(dst);
+    xml = SDL_GetPointerProperty(io_props, SDL_PROP_IOSTREAM_DYNAMIC_MEMORY_POINTER, NULL);
+    SDL_SetPointerProperty(io_props, SDL_PROP_IOSTREAM_DYNAMIC_MEMORY_POINTER, NULL);
+
+done:
+    SDL_CloseIO(dst);
+    return xml;
+}
+
+static void ClearNotificationWithID(SDL_NotificationID id)
+{
+    __x_ABI_CWindows_CUI_CNotifications_CIToastNotificationHistory *pToastNotificationHistory = NULL;
+    __x_ABI_CWindows_CUI_CNotifications_CIToastNotificationManagerStatics2 *pToastNotificationManagerStatics2 = NULL;
+    HSTRING_HEADER hshTag;
+    HSTRING hsTag = NULL;
+    WCHAR tag[32];
+
+    HRESULT hr = pToastNotificationManager->lpVtbl->QueryInterface(pToastNotificationManager, &IID_IToastNotificationManagerStatics2, (LPVOID *)&pToastNotificationManagerStatics2);
+    if (FAILED(hr)) {
+        return;
+    }
+    hr = pToastNotificationManagerStatics2->lpVtbl->get_History(pToastNotificationManagerStatics2, &pToastNotificationHistory);
+    if (FAILED(hr)) {
+        goto cleanup;
+    }
+
+    SDL_swprintf(tag, SDL_arraysize(tag), L"%" SDL_PRIu32, id);
+    hr = WIN_WindowsCreateStringReference(tag, (UINT32)SDL_wcslen(tag), &hshTag, &hsTag);
+    if (FAILED(hr)) {
+        goto cleanup;
+    }
+
+    pToastNotificationHistory->lpVtbl->RemoveGroupedTagWithId(pToastNotificationHistory, hsTag, hsGroupId, hsAppId);
+
+cleanup:
+    WIN_WindowsDeleteString(hsTag);
+    if (pToastNotificationHistory) {
+        pToastNotificationHistory->lpVtbl->Release(pToastNotificationHistory);
+    }
+    if (pToastNotificationManagerStatics2) {
+        pToastNotificationManagerStatics2->lpVtbl->Release(pToastNotificationManagerStatics2);
+    }
+}
+
+NTSTATUS WIN_BCryptGenRandom(BCRYPT_ALG_HANDLE hAlgorithm, PUCHAR pbBuffer, ULONG cbBuffer, ULONG dwFlags)
+{
+    static bool s_bLoaded;
+    static HMODULE s_hBCrypt;
+    static BCryptGenRandom_t pBCryptGenRandom;
+
+    if (!s_bLoaded) {
+        s_hBCrypt = LoadLibraryEx(TEXT("bcrypt.dll"), NULL, LOAD_LIBRARY_SEARCH_SYSTEM32);
+        s_bLoaded = true;
+    }
+    if (!pBCryptGenRandom) {
+        pBCryptGenRandom = (BCryptGenRandom_t)GetProcAddress(s_hBCrypt, "BCryptGenRandom");
+    }
+
+    if (pBCryptGenRandom) {
+        return pBCryptGenRandom(hAlgorithm, pbBuffer, cbBuffer, dwFlags);
+    }
+
+    return -1;
+}
+
+SDL_NotificationID SDL_SYS_ShowNotification(SDL_PropertiesID props)
+{
+    HRESULT hr = S_OK;
+    SDL_NotificationID ret = 0;
+
+    // Need Win10 or higher for notifications.
+    if (!WIN_IsWindows10OrGreater()) {
+        SDL_SetError("Notifications require Windows 10 or higher");
+        return 0;
+    }
+
+    if (!InitToastSystem()) {
+        SDL_CleanupNotifications();
+        return 0;
+    }
+
+    SDL_Surface *image = SDL_GetPointerProperty(props, SDL_PROP_NOTIFICATION_IMAGE_POINTER, NULL);
+    const SDL_NotificationID replaces = SDL_GetNumberProperty(props, SDL_PROP_NOTIFICATION_REPLACES_NUMBER, 0);
+    const SDL_NotificationPriority priority = SDL_GetNumberProperty(props, SDL_PROP_NOTIFICATION_PRIORITY_NUMBER, SDL_NOTIFICATION_PRIORITY_NORMAL);
+    const bool transient = SDL_GetBooleanProperty(props, SDL_PROP_NOTIFICATION_TRANSIENT_BOOLEAN, false);
+
+    if (replaces) {
+        ClearNotificationWithID(replaces);
+    }
+
+    SDL_NotificationID new_id = 0;
+    // Generate a unique notification ID.
+    if (WIN_BCryptGenRandom(NULL, (PUCHAR)&new_id, sizeof(new_id), BCRYPT_USE_SYSTEM_PREFERRED_RNG) != 0) { // STATUS_SUCCESS == 0
+        // No RNG? Use the low 32 bits of the current time.
+        new_id = (SDL_NotificationID)SDL_GetTicksNS();
+    }
+
+    // Windows notifications load images from disk, so save it to temporary storage.
+    WCHAR *image_path = SaveToastImage(image);
+
+    // Build the XML description for the notification.
+    Uint32 xml_len = 0;
+    WCHAR *xml = BuildNotificationXml(props, image_path, new_id, &xml_len);
+    SDL_free(image_path);
+    if (!xml) {
+        return 0;
+    }
+
+    HSTRING_HEADER hshXmlDocument;
+    HSTRING hsXmlDocument = NULL;
+    HSTRING_HEADER hshBanner;
+    HSTRING hsBanner = NULL;
+    IInspectable *pInspectable = NULL;
+    __x_ABI_CWindows_CData_CXml_CDom_CIXmlDocument *pXmlDocument = NULL;
+    __x_ABI_CWindows_CData_CXml_CDom_CIXmlDocumentIO *pXmlDocumentIO = NULL;
+    __x_ABI_CWindows_CUI_CNotifications_CIToastNotification *pToastNotification = NULL;
+
+    hr = WIN_WindowsCreateStringReference(RuntimeClass_Windows_Data_Xml_Dom_XmlDocument, (UINT32)(sizeof(RuntimeClass_Windows_Data_Xml_Dom_XmlDocument) / sizeof(WCHAR) - 1), &hshXmlDocument, &hsXmlDocument);
+    if (FAILED(hr)) {
+        goto cleanup;
+    }
+
+    hr = WIN_WindowsCreateStringReference(xml, xml_len, &hshBanner, &hsBanner);
+    if (FAILED(hr)) {
+        goto cleanup;
+    }
+
+    hr = WIN_RoActivateInstance(hsXmlDocument, &pInspectable);
+    if (FAILED(hr)) {
+        goto cleanup;
+    }
+
+    hr = pInspectable->lpVtbl->QueryInterface(pInspectable, &IID_IXmlDocument, (void **)(&pXmlDocument));
+    if (FAILED(hr)) {
+        goto cleanup;
+    }
+
+    hr = pXmlDocument->lpVtbl->QueryInterface(pXmlDocument, &IID_IXmlDocumentIO, (void **)(&pXmlDocumentIO));
+    if (FAILED(hr)) {
+        goto cleanup;
+    }
+
+    hr = pXmlDocumentIO->lpVtbl->LoadXml(pXmlDocumentIO, hsBanner);
+    if (FAILED(hr)) {
+        goto cleanup;
+    }
+
+    hr = pNotificationFactory->lpVtbl->CreateToastNotification(pNotificationFactory, pXmlDocument, &pToastNotification);
+    if (FAILED(hr)) {
+        goto cleanup;
+    }
+
+    // Register the OnDismissed notifier to clear transient notifications when cancelled or timed out.
+    if (transient) {
+        EventRegistrationToken dismissedToken;
+        hr = pToastNotification->lpVtbl->add_Dismissed(pToastNotification, &OnDismissed, &dismissedToken);
+        if (FAILED(hr)) {
+            goto cleanup;
+        }
+    }
+    {
+        EventRegistrationToken activatedToken;
+        hr = pToastNotification->lpVtbl->add_Activated(pToastNotification, &OnActivated, &activatedToken);
+        if (FAILED(hr)) {
+            goto cleanup;
+        }
+    }
+
+    // Tag with the ID for future replacement.
+    {
+        __x_ABI_CWindows_CUI_CNotifications_CIToastNotification2 *pToastNotification2;
+        HSTRING_HEADER hshTag;
+        HSTRING hsTag = NULL;
+        WCHAR tag[32];
+
+        hr = pToastNotification->lpVtbl->QueryInterface(pToastNotification, &IID_IToastNotification2, (LPVOID *)&pToastNotification2);
+        if (FAILED(hr)) {
+            goto cleanup;
+        }
+
+        SDL_swprintf(tag, SDL_arraysize(tag), L"%" SDL_PRIu32, new_id);
+        hr = WIN_WindowsCreateStringReference(tag, (UINT32)SDL_wcslen(tag), &hshTag, &hsTag);
+        if (FAILED(hr)) {
+            goto cleanup;
+        }
+
+        hr = pToastNotification2->lpVtbl->put_Group(pToastNotification2, hsGroupId);
+        if (FAILED(hr)) {
+            goto cleanup;
+        }
+
+        hr = pToastNotification2->lpVtbl->put_Tag(pToastNotification2, hsTag);
+        if (FAILED(hr)) {
+            goto cleanup;
+        }
+
+        pToastNotification2->lpVtbl->Release(pToastNotification2);
+    }
+
+    /* Set the priority.
+     * All levels except for SDL_NOTIFICATION_PRIORITY_CRITICAL map to normal on Windows, as selecting
+     * high priority can wake the screen, and should only be done when absolutely necessary.
+     */
+    {
+        const __x_ABI_CWindows_CUI_CNotifications_CToastNotificationPriority toast_priority = priority != SDL_NOTIFICATION_PRIORITY_CRITICAL ? ToastNotificationPriority_Default : ToastNotificationPriority_High;
+        __x_ABI_CWindows_CUI_CNotifications_CIToastNotification4 *pToastNotification4;
+        hr = pToastNotification->lpVtbl->QueryInterface(pToastNotification, &IID_IToastNotification4, (LPVOID *)&pToastNotification4);
+        if (FAILED(hr)) {
+            goto cleanup;
+        }
+
+        hr = pToastNotification4->lpVtbl->put_Priority(pToastNotification4, toast_priority);
+        if (FAILED(hr)) {
+            goto cleanup;
+        }
+
+        pToastNotification4->lpVtbl->Release(pToastNotification4);
+    }
+
+    // Finally, show the notification.
+    hr = pToastNotifier->lpVtbl->Show(pToastNotifier, pToastNotification);
+    if (SUCCEEDED(hr)) {
+        ret = new_id;
+    }
+
+cleanup:
+    if (pToastNotification) {
+        pToastNotification->lpVtbl->Release(pToastNotification);
+    }
+    if (pXmlDocumentIO) {
+        pXmlDocumentIO->lpVtbl->Release(pXmlDocumentIO);
+    }
+    if (pXmlDocument) {
+        pXmlDocument->lpVtbl->Release(pXmlDocument);
+    }
+    if (pInspectable) {
+        pInspectable->lpVtbl->Release(pInspectable);
+    }
+    if (hsBanner) {
+        WIN_WindowsDeleteString(hsBanner);
+    }
+    if (hsXmlDocument) {
+        WIN_WindowsDeleteString(hsXmlDocument);
+    }
+
+    SDL_free(xml);
+
+    return ret;
+}
+
+bool SDL_RemoveNotification(SDL_NotificationID notification)
+{
+    if (!WIN_IsWindows10OrGreater()) {
+        return SDL_Unsupported();
+    }
+
+    ClearNotificationWithID(notification);
+    return true;
+}
+
+void SDL_CleanupNotifications()
+{
+    if (pNotificationFactory) {
+        pNotificationFactory->lpVtbl->Release(pNotificationFactory);
+        pNotificationFactory = NULL;
+    }
+    if (pToastNotifier) {
+        pToastNotifier->lpVtbl->Release(pToastNotifier);
+        pToastNotifier = NULL;
+    }
+    if (pToastNotificationManager) {
+        pToastNotificationManager->lpVtbl->Release(pToastNotificationManager);
+        pToastNotificationManager = NULL;
+    }
+    if (pClassFactory) {
+        pClassFactory->lpVtbl->Release((IUnknown *)pClassFactory);
+        pClassFactory = NULL;
+    }
+    if (hsAppId) {
+        WIN_WindowsDeleteString(hsAppId);
+        hsAppId = NULL;
+    }
+    if (hsGroupId) {
+        WIN_WindowsDeleteString(hsGroupId);
+        hsGroupId = NULL;
+    }
+
+    CleanupIcons();
+
+    if (ro_initialized) {
+        WIN_RoUninitialize();
+        ro_initialized = false;
+    }
+    if (co_initialized) {
+        WIN_CoUninitialize();
+        co_initialized = false;
+    }
+
+    SDL_free(app_reg_key);
+    app_reg_key = NULL;
+
+    SDL_free(app_icon_path);
+    app_icon_path = NULL;
+}
+
+bool SDL_RequestNotificationPermission(void)
+{
+    // Notifications are supported on Win10 or higher.
+    return (bool)WIN_IsWindows10OrGreater();
+}