From bcc7ab71cb0e489d93ecd5c7bb113dfa2856d9cb Mon Sep 17 00:00:00 2001
From: Stenzek <stenzek@gmail.com>
Date: Thu, 21 Mar 2024 01:53:34 +1000
Subject: [PATCH] NoGUI: Add SDL platform

---
 src/duckstation-nogui/CMakeLists.txt          |   9 +
 .../duckstation-nogui.vcxproj                 |   2 +
 .../duckstation-nogui.vcxproj.filters         |   2 +
 src/duckstation-nogui/nogui_host.cpp          |  13 +-
 src/duckstation-nogui/nogui_platform.h        |   3 +
 src/duckstation-nogui/sdl_key_names.h         | 274 +++++++++++
 src/duckstation-nogui/sdl_nogui_platform.cpp  | 449 ++++++++++++++++++
 src/duckstation-nogui/sdl_nogui_platform.h    |  57 +++
 src/duckstation-nogui/win32_key_names.h       |   5 +
 src/util/sdl_input_source.cpp                 |  29 +-
 src/util/sdl_input_source.h                   |   4 +
 11 files changed, 843 insertions(+), 4 deletions(-)
 create mode 100644 src/duckstation-nogui/sdl_key_names.h
 create mode 100644 src/duckstation-nogui/sdl_nogui_platform.cpp
 create mode 100644 src/duckstation-nogui/sdl_nogui_platform.h

diff --git a/src/duckstation-nogui/CMakeLists.txt b/src/duckstation-nogui/CMakeLists.txt
index 2615eed09..d5feb7387 100644
--- a/src/duckstation-nogui/CMakeLists.txt
+++ b/src/duckstation-nogui/CMakeLists.txt
@@ -86,3 +86,12 @@ if(ENABLE_WAYLAND)
     X11::xkbcommon
   )
 endif()
+
+if(ENABLE_SDL2)
+  message(STATUS "Building SDL NoGUI Platform.")
+  target_sources(duckstation-nogui PRIVATE
+    sdl_nogui_platform.cpp
+    sdl_nogui_platform.h
+  )
+  target_link_libraries(duckstation-nogui PUBLIC SDL2::SDL2)
+endif()
diff --git a/src/duckstation-nogui/duckstation-nogui.vcxproj b/src/duckstation-nogui/duckstation-nogui.vcxproj
index cc5525a36..fbbb40c64 100644
--- a/src/duckstation-nogui/duckstation-nogui.vcxproj
+++ b/src/duckstation-nogui/duckstation-nogui.vcxproj
@@ -6,6 +6,7 @@
     <ClCompile Include="pch.cpp">
       <PrecompiledHeader>Create</PrecompiledHeader>
     </ClCompile>
+    <ClCompile Include="sdl_nogui_platform.cpp" />
     <ClCompile Include="wayland_nogui_platform.cpp">
       <ExcludedFromBuild>true</ExcludedFromBuild>
     </ClCompile>
@@ -19,6 +20,7 @@
     <ClInclude Include="nogui_platform.h" />
     <ClInclude Include="pch.h" />
     <ClInclude Include="resource.h" />
+    <ClInclude Include="sdl_nogui_platform.h" />
     <ClInclude Include="wayland_nogui_platform.h">
       <ExcludedFromBuild>true</ExcludedFromBuild>
     </ClInclude>
diff --git a/src/duckstation-nogui/duckstation-nogui.vcxproj.filters b/src/duckstation-nogui/duckstation-nogui.vcxproj.filters
index d08b60d6b..5cdac5f86 100644
--- a/src/duckstation-nogui/duckstation-nogui.vcxproj.filters
+++ b/src/duckstation-nogui/duckstation-nogui.vcxproj.filters
@@ -6,6 +6,7 @@
     <ClCompile Include="wayland_nogui_platform.cpp" />
     <ClCompile Include="x11_nogui_platform.cpp" />
     <ClCompile Include="pch.cpp" />
+    <ClCompile Include="sdl_nogui_platform.cpp" />
   </ItemGroup>
   <ItemGroup>
     <ClInclude Include="resource.h" />
@@ -15,6 +16,7 @@
     <ClInclude Include="wayland_nogui_platform.h" />
     <ClInclude Include="x11_nogui_platform.h" />
     <ClInclude Include="pch.h" />
+    <ClInclude Include="sdl_nogui_platform.h" />
   </ItemGroup>
   <ItemGroup>
     <Manifest Include="duckstation-nogui.manifest" />
diff --git a/src/duckstation-nogui/nogui_host.cpp b/src/duckstation-nogui/nogui_host.cpp
index 126ac4de7..7e3c6ab1a 100644
--- a/src/duckstation-nogui/nogui_host.cpp
+++ b/src/duckstation-nogui/nogui_host.cpp
@@ -870,13 +870,20 @@ std::unique_ptr<NoGUIPlatform> NoGUIHost::CreatePlatform()
 {
   std::unique_ptr<NoGUIPlatform> ret;
 
+  const char* platform = std::getenv("DUCKSTATION_NOGUI_PLATFORM");
+#ifdef ENABLE_SDL2
+  if (platform && StringUtil::Strcasecmp(platform, "sdl") == 0)
+    ret = NoGUIPlatform::CreateSDLPlatform();
+#endif
+
 #if defined(_WIN32)
-  ret = NoGUIPlatform::CreateWin32Platform();
+  if (!ret)
+    ret = NoGUIPlatform::CreateWin32Platform();
 #elif defined(__APPLE__)
-  ret = NoGUIPlatform::CreateCocoaPlatform();
+  if (!ret)
+    ret = NoGUIPlatform::CreateCocoaPlatform();
 #else
   // linux
-  const char* platform = std::getenv("DUCKSTATION_NOGUI_PLATFORM");
 #ifdef NOGUI_PLATFORM_WAYLAND
   if (!ret && (!platform || StringUtil::Strcasecmp(platform, "wayland") == 0) && std::getenv("WAYLAND_DISPLAY"))
     ret = NoGUIPlatform::CreateWaylandPlatform();
diff --git a/src/duckstation-nogui/nogui_platform.h b/src/duckstation-nogui/nogui_platform.h
index 2810d2e68..452e545e5 100644
--- a/src/duckstation-nogui/nogui_platform.h
+++ b/src/duckstation-nogui/nogui_platform.h
@@ -52,6 +52,9 @@ public:
 #ifdef __APPLE__
   static std::unique_ptr<NoGUIPlatform> CreateCocoaPlatform();
 #endif
+#ifdef ENABLE_SDL2
+  static std::unique_ptr<NoGUIPlatform> CreateSDLPlatform();
+#endif
 #ifdef NOGUI_PLATFORM_WAYLAND
   static std::unique_ptr<NoGUIPlatform> CreateWaylandPlatform();
 #endif
diff --git a/src/duckstation-nogui/sdl_key_names.h b/src/duckstation-nogui/sdl_key_names.h
new file mode 100644
index 000000000..efc587ea7
--- /dev/null
+++ b/src/duckstation-nogui/sdl_key_names.h
@@ -0,0 +1,274 @@
+// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
+// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
+
+#pragma once
+
+#include "common/types.h"
+#include "common/windows_headers.h"
+
+#include <array>
+#include <cstring>
+#include <map>
+#include <optional>
+#include <string_view>
+
+namespace SDLKeyNames {
+
+static const std::map<int, const char*> s_sdl_key_names = {{SDLK_RETURN, "Return"},
+                                                           {SDLK_ESCAPE, "Escape"},
+                                                           {SDLK_BACKSPACE, "Backspace"},
+                                                           {SDLK_TAB, "Tab"},
+                                                           {SDLK_SPACE, "Space"},
+                                                           {SDLK_EXCLAIM, "Exclam"},
+                                                           {SDLK_QUOTEDBL, "QuoteDbl"},
+                                                           {SDLK_HASH, "Hash"},
+                                                           {SDLK_PERCENT, "Percent"},
+                                                           {SDLK_DOLLAR, "Dollar"},
+                                                           {SDLK_AMPERSAND, "Ampersand"},
+                                                           {SDLK_QUOTE, "Apostrophe"},
+                                                           {SDLK_LEFTPAREN, "ParenLeft"},
+                                                           {SDLK_RIGHTPAREN, "ParenRight"},
+                                                           {SDLK_ASTERISK, "Asterisk"},
+                                                           {SDLK_PLUS, "PLus"},
+                                                           {SDLK_COMMA, "Comma"},
+                                                           {SDLK_MINUS, "Minus"},
+                                                           {SDLK_PERIOD, "Period"},
+                                                           {SDLK_SLASH, "Slash"},
+                                                           {SDLK_0, "0"},
+                                                           {SDLK_1, "1"},
+                                                           {SDLK_2, "2"},
+                                                           {SDLK_3, "3"},
+                                                           {SDLK_4, "4"},
+                                                           {SDLK_5, "5"},
+                                                           {SDLK_6, "6"},
+                                                           {SDLK_7, "7"},
+                                                           {SDLK_8, "8"},
+                                                           {SDLK_9, "9"},
+                                                           {SDLK_COLON, "Colon"},
+                                                           {SDLK_SEMICOLON, "Semcolon"},
+                                                           {SDLK_LESS, "Less"},
+                                                           {SDLK_EQUALS, "Equal"},
+                                                           {SDLK_GREATER, "Greater"},
+                                                           {SDLK_QUESTION, "Question"},
+                                                           {SDLK_AT, "AT"},
+                                                           {SDLK_LEFTBRACKET, "BracketLeft"},
+                                                           {SDLK_BACKSLASH, "Backslash"},
+                                                           {SDLK_RIGHTBRACKET, "BracketRight"},
+                                                           {SDLK_CARET, "Caret"},
+                                                           {SDLK_UNDERSCORE, "Underscore"},
+                                                           {SDLK_BACKQUOTE, "Backquote"},
+                                                           {SDLK_a, "A"},
+                                                           {SDLK_b, "B"},
+                                                           {SDLK_c, "C"},
+                                                           {SDLK_d, "D"},
+                                                           {SDLK_e, "E"},
+                                                           {SDLK_f, "F"},
+                                                           {SDLK_g, "G"},
+                                                           {SDLK_h, "H"},
+                                                           {SDLK_i, "I"},
+                                                           {SDLK_j, "J"},
+                                                           {SDLK_k, "K"},
+                                                           {SDLK_l, "L"},
+                                                           {SDLK_m, "M"},
+                                                           {SDLK_n, "N"},
+                                                           {SDLK_o, "O"},
+                                                           {SDLK_p, "P"},
+                                                           {SDLK_q, "Q"},
+                                                           {SDLK_r, "R"},
+                                                           {SDLK_s, "S"},
+                                                           {SDLK_t, "T"},
+                                                           {SDLK_u, "U"},
+                                                           {SDLK_v, "V"},
+                                                           {SDLK_w, "W"},
+                                                           {SDLK_x, "X"},
+                                                           {SDLK_y, "Y"},
+                                                           {SDLK_z, "Z"},
+                                                           {SDLK_CAPSLOCK, "CapsLock"},
+                                                           {SDLK_F1, "F1"},
+                                                           {SDLK_F2, "F2"},
+                                                           {SDLK_F3, "F3"},
+                                                           {SDLK_F4, "F4"},
+                                                           {SDLK_F5, "F5"},
+                                                           {SDLK_F6, "F6"},
+                                                           {SDLK_F7, "F7"},
+                                                           {SDLK_F8, "F8"},
+                                                           {SDLK_F9, "F9"},
+                                                           {SDLK_F10, "F10"},
+                                                           {SDLK_F11, "F11"},
+                                                           {SDLK_F12, "F12"},
+                                                           {SDLK_PRINTSCREEN, "Print"},
+                                                           {SDLK_SCROLLLOCK, "ScrollLock"},
+                                                           {SDLK_PAUSE, "Pause"},
+                                                           {SDLK_INSERT, "Insert"},
+                                                           {SDLK_HOME, "Home"},
+                                                           {SDLK_PAGEUP, "PageUp"},
+                                                           {SDLK_DELETE, "Delete"},
+                                                           {SDLK_END, "End"},
+                                                           {SDLK_PAGEDOWN, "PageDown"},
+                                                           {SDLK_RIGHT, "Right"},
+                                                           {SDLK_LEFT, "Left"},
+                                                           {SDLK_DOWN, "Down"},
+                                                           {SDLK_UP, "Up"},
+                                                           {SDLK_NUMLOCKCLEAR, "NumLock"},
+                                                           {SDLK_KP_DIVIDE, "Keypad+Divide"},
+                                                           {SDLK_KP_MULTIPLY, "Keypad+Multiply"},
+                                                           {SDLK_KP_MINUS, "Keypad+Minus"},
+                                                           {SDLK_KP_PLUS, "Keypad+Plus"},
+                                                           {SDLK_KP_ENTER, "Keypad+Return"},
+                                                           {SDLK_KP_1, "Keypad+1"},
+                                                           {SDLK_KP_2, "Keypad+2"},
+                                                           {SDLK_KP_3, "Keypad+3"},
+                                                           {SDLK_KP_4, "Keypad+4"},
+                                                           {SDLK_KP_5, "Keypad+5"},
+                                                           {SDLK_KP_6, "Keypad+6"},
+                                                           {SDLK_KP_7, "Keypad+7"},
+                                                           {SDLK_KP_8, "Keypad+8"},
+                                                           {SDLK_KP_9, "Keypad+9"},
+                                                           {SDLK_KP_0, "Keypad+0"},
+                                                           {SDLK_KP_PERIOD, "Keypad+Period"},
+                                                           {SDLK_APPLICATION, "Application"},
+                                                           {SDLK_POWER, "Power"},
+                                                           {SDLK_KP_EQUALS, "Keypad+Equal"},
+                                                           {SDLK_F13, "F13"},
+                                                           {SDLK_F14, "F14"},
+                                                           {SDLK_F15, "F15"},
+                                                           {SDLK_F16, "F16"},
+                                                           {SDLK_F17, "F17"},
+                                                           {SDLK_F18, "F18"},
+                                                           {SDLK_F19, "F19"},
+                                                           {SDLK_F20, "F20"},
+                                                           {SDLK_F21, "F21"},
+                                                           {SDLK_F22, "F22"},
+                                                           {SDLK_F23, "F23"},
+                                                           {SDLK_F24, "F24"},
+                                                           {SDLK_EXECUTE, "Execute"},
+                                                           {SDLK_HELP, "Help"},
+                                                           {SDLK_MENU, "Menu"},
+                                                           {SDLK_SELECT, "Select"},
+                                                           {SDLK_STOP, "Stop"},
+                                                           {SDLK_AGAIN, "Again"},
+                                                           {SDLK_UNDO, "Undo"},
+                                                           {SDLK_CUT, "Cut"},
+                                                           {SDLK_COPY, "Copy"},
+                                                           {SDLK_PASTE, "Paste"},
+                                                           {SDLK_FIND, "Find"},
+                                                           {SDLK_MUTE, "Mute"},
+                                                           {SDLK_VOLUMEUP, "VolumeUp"},
+                                                           {SDLK_VOLUMEDOWN, "VolumeDown"},
+                                                           {SDLK_KP_COMMA, "Keypad+Comma"},
+                                                           {SDLK_KP_EQUALSAS400, "Keypad+EqualAS400"},
+                                                           {SDLK_ALTERASE, "AltErase"},
+                                                           {SDLK_SYSREQ, "SysReq"},
+                                                           {SDLK_CANCEL, "Cancel"},
+                                                           {SDLK_CLEAR, "Clear"},
+                                                           {SDLK_PRIOR, "Prior"},
+                                                           {SDLK_RETURN2, "Return2"},
+                                                           {SDLK_SEPARATOR, "Separator"},
+                                                           {SDLK_OUT, "Out"},
+                                                           {SDLK_OPER, "Oper"},
+                                                           {SDLK_CLEARAGAIN, "ClearAgain"},
+                                                           {SDLK_CRSEL, "CrSel"},
+                                                           {SDLK_EXSEL, "ExSel"},
+                                                           {SDLK_KP_00, "Keypad+00"},
+                                                           {SDLK_KP_000, "Keypad+000"},
+                                                           {SDLK_THOUSANDSSEPARATOR, "ThousandsSeparator"},
+                                                           {SDLK_DECIMALSEPARATOR, "DecimalSeparator"},
+                                                           {SDLK_CURRENCYUNIT, "CurrencyUnit"},
+                                                           {SDLK_CURRENCYSUBUNIT, "CurrencySubunit"},
+                                                           {SDLK_KP_LEFTPAREN, "Keypad+ParenLeft"},
+                                                           {SDLK_KP_RIGHTPAREN, "Keypad+ParenRight"},
+                                                           {SDLK_KP_LEFTBRACE, "Keypad+LeftBrace"},
+                                                           {SDLK_KP_RIGHTBRACE, "Keypad+RightBrace"},
+                                                           {SDLK_KP_TAB, "Keypad+Tab"},
+                                                           {SDLK_KP_BACKSPACE, "Keypad+Backspace"},
+                                                           {SDLK_KP_A, "Keypad+A"},
+                                                           {SDLK_KP_B, "Keypad+B"},
+                                                           {SDLK_KP_C, "Keypad+C"},
+                                                           {SDLK_KP_D, "Keypad+D"},
+                                                           {SDLK_KP_E, "Keypad+E"},
+                                                           {SDLK_KP_F, "Keypad+F"},
+                                                           {SDLK_KP_XOR, "Keypad+XOR"},
+                                                           {SDLK_KP_POWER, "Keypad+Power"},
+                                                           {SDLK_KP_PERCENT, "Keypad+Percent"},
+                                                           {SDLK_KP_LESS, "Keypad+Less"},
+                                                           {SDLK_KP_GREATER, "Keypad+Greater"},
+                                                           {SDLK_KP_AMPERSAND, "Keypad+Ampersand"},
+                                                           {SDLK_KP_DBLAMPERSAND, "Keypad+AmpersandDbl"},
+                                                           {SDLK_KP_VERTICALBAR, "Keypad+Bar"},
+                                                           {SDLK_KP_DBLVERTICALBAR, "Keypad+BarDbl"},
+                                                           {SDLK_KP_COLON, "Keypad+Colon"},
+                                                           {SDLK_KP_HASH, "Keypad+Hash"},
+                                                           {SDLK_KP_SPACE, "Keypad+Space"},
+                                                           {SDLK_KP_AT, "Keypad+At"},
+                                                           {SDLK_KP_EXCLAM, "Keypad+Exclam"},
+                                                           {SDLK_KP_MEMSTORE, "Keypad+MemStore"},
+                                                           {SDLK_KP_MEMRECALL, "Keypad+MemRecall"},
+                                                           {SDLK_KP_MEMCLEAR, "Keypad+MemClear"},
+                                                           {SDLK_KP_MEMADD, "Keypad+MemAdd"},
+                                                           {SDLK_KP_MEMSUBTRACT, "Keypad+MemSubtract"},
+                                                           {SDLK_KP_MEMMULTIPLY, "Keypad+MemMultiply"},
+                                                           {SDLK_KP_MEMDIVIDE, "Keypad+MemDivide"},
+                                                           {SDLK_KP_PLUSMINUS, "Keypad+PlusMinus"},
+                                                           {SDLK_KP_CLEAR, "Keypad+Clear"},
+                                                           {SDLK_KP_CLEARENTRY, "Keypad+ClearEntry"},
+                                                           {SDLK_KP_BINARY, "Keypad+Binary"},
+                                                           {SDLK_KP_OCTAL, "Keypad+Octal"},
+                                                           {SDLK_KP_DECIMAL, "Keypad+Decimal"},
+                                                           {SDLK_KP_HEXADECIMAL, "Keypad+Hexadecimal"},
+                                                           {SDLK_LCTRL, "LeftControl"},
+                                                           {SDLK_LSHIFT, "LeftShift"},
+                                                           {SDLK_LALT, "LeftAlt"},
+                                                           {SDLK_LGUI, "Super_L"},
+                                                           {SDLK_RCTRL, "RightCtrl"},
+                                                           {SDLK_RSHIFT, "RightShift"},
+                                                           {SDLK_RALT, "RightAlt"},
+                                                           {SDLK_RGUI, "RightSuper"},
+                                                           {SDLK_MODE, "Mode"},
+                                                           {SDLK_AUDIONEXT, "MediaNext"},
+                                                           {SDLK_AUDIOPREV, "MediaPrevious"},
+                                                           {SDLK_AUDIOSTOP, "MediaStop"},
+                                                           {SDLK_AUDIOPLAY, "MediaPlay"},
+                                                           {SDLK_AUDIOMUTE, "VolumeMute"},
+                                                           {SDLK_MEDIASELECT, "MediaSelect"},
+                                                           {SDLK_WWW, "WWW"},
+                                                           {SDLK_MAIL, "Mail"},
+                                                           {SDLK_CALCULATOR, "Calculator"},
+                                                           {SDLK_COMPUTER, "Computer"},
+                                                           {SDLK_AC_SEARCH, "Search"},
+                                                           {SDLK_AC_HOME, "Home"},
+                                                           {SDLK_AC_BACK, "Back"},
+                                                           {SDLK_AC_FORWARD, "Forward"},
+                                                           {SDLK_AC_STOP, "Stop"},
+                                                           {SDLK_AC_REFRESH, "Refresh"},
+                                                           {SDLK_AC_BOOKMARKS, "Bookmarks"},
+                                                           {SDLK_BRIGHTNESSDOWN, "BrightnessDown"},
+                                                           {SDLK_BRIGHTNESSUP, "BrightnessUp"},
+                                                           {SDLK_DISPLAYSWITCH, "DisplaySwitch"},
+                                                           {SDLK_KBDILLUMTOGGLE, "IllumToggle"},
+                                                           {SDLK_KBDILLUMDOWN, "IllumDown"},
+                                                           {SDLK_KBDILLUMUP, "IllumUp"},
+                                                           {SDLK_EJECT, "Eject"},
+                                                           {SDLK_SLEEP, "Sleep"},
+                                                           {SDLK_APP1, "App1"},
+                                                           {SDLK_APP2, "App2"},
+                                                           {SDLK_AUDIOREWIND, "MediaRewind"},
+                                                           {SDLK_AUDIOFASTFORWARD, "MediaFastForward"}};
+
+static const char* GetKeyName(DWORD key)
+{
+  const auto it = s_sdl_key_names.find(key);
+  return it == s_sdl_key_names.end() ? nullptr : it->second;
+}
+
+static std::optional<DWORD> GetKeyCodeForName(const std::string_view& key_name)
+{
+  for (const auto& it : s_sdl_key_names)
+  {
+    if (key_name == it.second)
+      return it.first;
+  }
+
+  return std::nullopt;
+}
+
+} // namespace SDLKeyNames
diff --git a/src/duckstation-nogui/sdl_nogui_platform.cpp b/src/duckstation-nogui/sdl_nogui_platform.cpp
new file mode 100644
index 000000000..3afb1e5b2
--- /dev/null
+++ b/src/duckstation-nogui/sdl_nogui_platform.cpp
@@ -0,0 +1,449 @@
+// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
+// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
+
+#include "sdl_nogui_platform.h"
+#include "nogui_host.h"
+#include "sdl_key_names.h"
+
+#include "core/host.h"
+
+#include "util/imgui_manager.h"
+#include "util/sdl_input_source.h"
+
+#include "common/log.h"
+#include "common/scoped_guard.h"
+#include "common/string_util.h"
+#include "common/threading.h"
+
+#include <SDL.h>
+#include <SDL_syswm.h>
+
+Log_SetChannel(SDLNoGUIPlatform);
+
+static constexpr float DEFAULT_WINDOW_DPI = 96.0f;
+
+SDLNoGUIPlatform::SDLNoGUIPlatform()
+{
+  m_message_loop_running.store(true, std::memory_order_release);
+}
+
+SDLNoGUIPlatform::~SDLNoGUIPlatform()
+{
+  SDL_QuitSubSystem(SDL_INIT_VIDEO | SDL_INIT_EVENTS);
+}
+
+bool SDLNoGUIPlatform::Initialize()
+{
+  if (SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_EVENTS) != 0)
+  {
+    Log_ErrorFmt("SDL_InitSubSystem() failed: {}", SDL_GetError());
+    SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error",
+                             TinyString::from_format("SDL_InitSubSystem() failed: {}", SDL_GetError()), nullptr);
+    return false;
+  }
+
+  m_func_event_id = SDL_RegisterEvents(1);
+  m_wakeup_event_id = SDL_RegisterEvents(1);
+  if (m_func_event_id == static_cast<u32>(-1) || m_wakeup_event_id == static_cast<u32>(-1))
+  {
+    Log_ErrorFmt("SDL_RegisterEvents() failed: {}", SDL_GetError());
+    return false;
+  }
+
+  // prevent input source polling on main thread...
+  SDLInputSource::ALLOW_EVENT_POLLING = false;
+
+  return true;
+}
+
+void SDLNoGUIPlatform::ReportError(const std::string_view& title, const std::string_view& message)
+{
+  SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, SmallString(title).c_str(), SmallString(message).c_str(), m_window);
+}
+
+bool SDLNoGUIPlatform::ConfirmMessage(const std::string_view& title, const std::string_view& message)
+{
+  const SmallString title_copy(title);
+  const SmallString message_copy(message);
+
+  static constexpr SDL_MessageBoxButtonData bd[2] = {
+    {SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT, 1, "Yes"},
+    {SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT, 2, "No"},
+  };
+  const SDL_MessageBoxData md = {SDL_MESSAGEBOX_INFORMATION,
+                                 m_window,
+                                 title_copy.c_str(),
+                                 message_copy.c_str(),
+                                 static_cast<int>(std::size(bd)),
+                                 bd,
+                                 nullptr};
+
+  int buttonid = -1;
+  SDL_ShowMessageBox(&md, &buttonid);
+  return (buttonid == 1);
+}
+
+void SDLNoGUIPlatform::SetDefaultConfig(SettingsInterface& si)
+{
+  // noop
+}
+
+bool SDLNoGUIPlatform::CreatePlatformWindow(std::string title)
+{
+  s32 window_x, window_y, window_width, window_height;
+  if (!NoGUIHost::GetSavedPlatformWindowGeometry(&window_x, &window_y, &window_width, &window_height))
+  {
+    window_x = SDL_WINDOWPOS_UNDEFINED;
+    window_y = SDL_WINDOWPOS_UNDEFINED;
+    window_width = DEFAULT_WINDOW_WIDTH;
+    window_height = DEFAULT_WINDOW_HEIGHT;
+  }
+
+  m_window = SDL_CreateWindow(title.c_str(), window_x, window_y, window_width, window_height,
+                              SDL_WINDOW_RESIZABLE | SDL_WINDOW_INPUT_FOCUS | SDL_WINDOW_MOUSE_FOCUS |
+                                SDL_WINDOW_SHOWN | SDL_WINDOW_ALLOW_HIGHDPI);
+  if (!m_window)
+  {
+    Log_ErrorFmt("SDL_CreateWindow() failed: {}", SDL_GetError());
+    ReportError("Error", TinyString::from_format("SDL_CreateWindow() failed: {}", SDL_GetError()));
+    return false;
+  }
+
+  if (m_fullscreen.load(std::memory_order_acquire))
+    SetFullscreen(true);
+
+  return true;
+}
+
+bool SDLNoGUIPlatform::HasPlatformWindow() const
+{
+  return (m_window != nullptr);
+}
+
+void SDLNoGUIPlatform::DestroyPlatformWindow()
+{
+  if (!m_window)
+    return;
+
+  if (!m_fullscreen.load(std::memory_order_acquire))
+  {
+    int window_x = SDL_WINDOWPOS_UNDEFINED, window_y = SDL_WINDOWPOS_UNDEFINED;
+    int window_width = DEFAULT_WINDOW_WIDTH, window_height = DEFAULT_WINDOW_HEIGHT;
+    SDL_GetWindowPosition(m_window, &window_x, &window_y);
+    SDL_GetWindowSize(m_window, &window_width, &window_height);
+    NoGUIHost::SavePlatformWindowGeometry(window_x, window_y, window_width, window_height);
+  }
+
+  SDL_DestroyWindow(m_window);
+  m_window = nullptr;
+}
+
+std::optional<WindowInfo> SDLNoGUIPlatform::GetPlatformWindowInfo()
+{
+  if (!m_window)
+    return std::nullopt;
+
+  SDL_SysWMinfo swi = {};
+  SDL_VERSION(&swi.version);
+
+  if (!SDL_GetWindowWMInfo(m_window, &swi))
+  {
+    Log_ErrorFmt("SDL_GetWindowWMInfo() failed: {}", SDL_GetError());
+    return std::nullopt;
+  }
+
+  int window_width = 1, window_height = 1;
+  int window_px_width = 1, window_px_height = 1;
+  SDL_GetWindowSize(m_window, &window_width, &window_height);
+  SDL_GetWindowSizeInPixels(m_window, &window_px_width, &window_px_height);
+  m_window_scale = static_cast<float>(std::max(window_px_width, 1)) / static_cast<float>(std::max(window_width, 1));
+
+  if (const int display_index = SDL_GetWindowDisplayIndex(m_window); display_index >= 0)
+  {
+    float ddpi, hdpi, vdpi;
+    if (SDL_GetDisplayDPI(display_index, &ddpi, &hdpi, &vdpi) == 0)
+      m_window_scale = std::max(ddpi / DEFAULT_WINDOW_DPI, 0.5f);
+  }
+
+  WindowInfo wi;
+  wi.surface_width = static_cast<u32>(window_px_width);
+  wi.surface_height = static_cast<u32>(window_px_height);
+  wi.surface_scale = m_window_scale;
+
+  switch (swi.subsystem)
+  {
+#ifdef SDL_VIDEO_DRIVER_WINDOWS
+    case SDL_SYSWM_WINDOWS:
+      wi.type = WindowInfo::Type::Win32;
+      wi.window_handle = swi.info.win.window;
+      break;
+#endif
+
+#ifdef SDL_VIDEO_DRIVER_X11
+    case SDL_SYSWM_X11:
+      wi.type = WindowInfo::Type::X11;
+      wi.display_connection = swi.info.x11.display;
+      wi.window_handle = swi.info.x11.window;
+      break;
+#endif
+
+#ifdef SDL_VIDEO_DRIVER_WAYLAND
+    case SDL_SYSWM_WAYLAND:
+      wi.type = WindowInfo::Type::Wayland;
+      wi.display_connection = swi.info.wl.display;
+      wi.window_handle = swi.info.wl.surface;
+      break;
+#endif
+
+#ifdef SDL_VIDEO_DRIVER_COCOA
+    case SDL_SYSWM_COCOA:
+      wi.type = WindowInfo::Type::MacOS;
+      wi.window_handle = swi.info.cocoa.window;
+      break;
+#endif
+
+    default:
+      Log_ErrorFmt("Unhandled WM subsystem {}", static_cast<int>(swi.subsystem));
+      return std::nullopt;
+  }
+
+  return wi;
+}
+
+void SDLNoGUIPlatform::SetPlatformWindowTitle(std::string title)
+{
+  if (!m_window)
+    return;
+
+  SDL_SetWindowTitle(m_window, title.c_str());
+}
+
+std::optional<u32> SDLNoGUIPlatform::ConvertHostKeyboardStringToCode(const std::string_view& str)
+{
+  std::optional<DWORD> converted(SDLKeyNames::GetKeyCodeForName(str));
+  return converted.has_value() ? std::optional<u32>(static_cast<u32>(converted.value())) : std::nullopt;
+  return std::nullopt;
+}
+
+std::optional<std::string> SDLNoGUIPlatform::ConvertHostKeyboardCodeToString(u32 code)
+{
+  const char* converted = SDLKeyNames::GetKeyName(code);
+  return converted ? std::optional<std::string>(converted) : std::nullopt;
+}
+
+void SDLNoGUIPlatform::RunMessageLoop()
+{
+  while (m_message_loop_running.load(std::memory_order_acquire))
+  {
+    SDL_Event ev;
+    if (!SDL_WaitEvent(&ev))
+      continue;
+
+    ProcessEvent(&ev);
+  }
+}
+
+void SDLNoGUIPlatform::ExecuteInMessageLoop(std::function<void()> func)
+{
+  std::function<void()>* pfunc = new std::function<void()>(std::move(func));
+
+  SDL_Event ev;
+  ev.user = {};
+  ev.type = m_func_event_id;
+  ev.user.data1 = pfunc;
+  SDL_PushEvent(&ev);
+}
+
+void SDLNoGUIPlatform::QuitMessageLoop()
+{
+  m_message_loop_running.store(false, std::memory_order_release);
+
+  SDL_Event ev;
+  ev.user = {};
+  ev.type = m_wakeup_event_id;
+  SDL_PushEvent(&ev);
+}
+
+void SDLNoGUIPlatform::SetFullscreen(bool enabled)
+{
+  if (!m_window || m_fullscreen.load(std::memory_order_acquire) == enabled)
+    return;
+
+  if (SDL_SetWindowFullscreen(m_window, enabled ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0) != 0)
+  {
+    Log_ErrorFmt("SDL_SetWindowFullscreen() failed: {}", SDL_GetError());
+    return;
+  }
+
+  m_fullscreen.store(enabled, std::memory_order_release);
+}
+
+bool SDLNoGUIPlatform::RequestRenderWindowSize(s32 new_window_width, s32 new_window_height)
+{
+  if (!m_window || m_fullscreen.load(std::memory_order_acquire))
+    return false;
+
+  SDL_SetWindowSize(m_window, new_window_width, new_window_height);
+  return true;
+}
+
+bool SDLNoGUIPlatform::OpenURL(const std::string_view& url)
+{
+  if (SDL_OpenURL(SmallString(url).c_str()) != 0)
+  {
+    Log_ErrorFmt("SDL_OpenURL() failed: {}", SDL_GetError());
+    return false;
+  }
+
+  return true;
+}
+
+bool SDLNoGUIPlatform::CopyTextToClipboard(const std::string_view& text)
+{
+  if (SDL_SetClipboardText(SmallString(text).c_str()) != 0)
+  {
+    Log_ErrorFmt("SDL_SetClipboardText() failed: {}", SDL_GetError());
+    return false;
+  }
+
+  return true;
+}
+
+void SDLNoGUIPlatform::ProcessEvent(const SDL_Event* ev)
+{
+  switch (ev->type)
+  {
+    case SDL_WINDOWEVENT:
+    {
+      switch (ev->window.event)
+      {
+        case SDL_WINDOWEVENT_SIZE_CHANGED:
+        {
+          int window_width = ev->window.data1, window_height = ev->window.data2;
+          SDL_GetWindowSizeInPixels(m_window, &window_width, &window_height);
+          NoGUIHost::ProcessPlatformWindowResize(window_width, window_height, m_window_scale);
+        }
+        break;
+
+        case SDL_WINDOWEVENT_DISPLAY_CHANGED:
+        {
+          const int new_display = ev->window.data1;
+          float ddpi, hdpi, vdpi;
+          if (SDL_GetDisplayDPI(new_display, &ddpi, &hdpi, &vdpi) == 0)
+          {
+            if (const float new_scale = std::max(ddpi / DEFAULT_WINDOW_DPI, 0.5f); new_scale != m_window_scale)
+            {
+              m_window_scale = new_scale;
+
+              int window_width = 1, window_height = 1;
+              SDL_GetWindowSizeInPixels(m_window, &window_width, &window_height);
+              NoGUIHost::ProcessPlatformWindowResize(window_width, window_height, m_window_scale);
+            }
+          }
+        }
+        break;
+
+        case SDL_WINDOWEVENT_CLOSE:
+        {
+          Host::RunOnCPUThread([]() { Host::RequestExit(false); });
+        }
+        break;
+
+        case SDL_WINDOWEVENT_FOCUS_GAINED:
+        {
+          NoGUIHost::PlatformWindowFocusGained();
+        }
+        break;
+
+        case SDL_WINDOWEVENT_FOCUS_LOST:
+        {
+          NoGUIHost::PlatformWindowFocusLost();
+        }
+        break;
+
+        default:
+          break;
+      }
+    }
+    break;
+
+    case SDL_KEYDOWN:
+    case SDL_KEYUP:
+    {
+      const bool pressed = (ev->type == SDL_KEYDOWN);
+      NoGUIHost::ProcessPlatformKeyEvent(static_cast<s32>(ev->key.keysym.sym), pressed);
+    }
+    break;
+
+    case SDL_TEXTINPUT:
+    {
+      if (ImGuiManager::WantsTextInput())
+        NoGUIHost::ProcessPlatformTextEvent(ev->text.text);
+    }
+    break;
+
+    case SDL_MOUSEMOTION:
+    {
+      const float x = static_cast<float>(ev->motion.x);
+      const float y = static_cast<float>(ev->motion.y);
+      NoGUIHost::ProcessPlatformMouseMoveEvent(x, y);
+    }
+    break;
+
+    case SDL_MOUSEBUTTONDOWN:
+    case SDL_MOUSEBUTTONUP:
+    {
+      const bool pressed = (ev->type == SDL_MOUSEBUTTONDOWN);
+      if (ev->button.button > 0)
+        NoGUIHost::ProcessPlatformMouseButtonEvent(ev->button.button - 1, pressed);
+    }
+    break;
+
+    case SDL_MOUSEWHEEL:
+    {
+      NoGUIHost::ProcessPlatformMouseWheelEvent(ev->wheel.preciseX, ev->wheel.preciseY);
+    }
+    break;
+
+    case SDL_QUIT:
+    {
+      Host::RunOnCPUThread([]() { Host::RequestExit(false); });
+    }
+    break;
+
+    default:
+    {
+      if (ev->type == m_func_event_id)
+      {
+        std::function<void()>* pfunc = reinterpret_cast<std::function<void()>*>(ev->user.data1);
+        if (pfunc)
+        {
+          (*pfunc)();
+          delete pfunc;
+        }
+      }
+      else if (ev->type == m_wakeup_event_id)
+      {
+      }
+      else if (SDLInputSource::IsHandledInputEvent(ev) && InputManager::GetInputSourceInterface(InputSourceType::SDL))
+      {
+        Host::RunOnCPUThread([event_copy = *ev]() {
+          SDLInputSource* is =
+            static_cast<SDLInputSource*>(InputManager::GetInputSourceInterface(InputSourceType::SDL));
+          if (is) [[likely]]
+            is->ProcessSDLEvent(&event_copy);
+        });
+      }
+    }
+    break;
+  }
+}
+
+std::unique_ptr<NoGUIPlatform> NoGUIPlatform::CreateSDLPlatform()
+{
+  std::unique_ptr<SDLNoGUIPlatform> ret(new SDLNoGUIPlatform());
+  if (!ret->Initialize())
+    return {};
+
+  return ret;
+}
diff --git a/src/duckstation-nogui/sdl_nogui_platform.h b/src/duckstation-nogui/sdl_nogui_platform.h
new file mode 100644
index 000000000..ea8a1b83f
--- /dev/null
+++ b/src/duckstation-nogui/sdl_nogui_platform.h
@@ -0,0 +1,57 @@
+// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
+// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
+
+#pragma once
+
+#include <atomic>
+
+#include "common/windows_headers.h"
+
+#include "nogui_platform.h"
+
+#include <SDL.h>
+
+class SDLNoGUIPlatform : public NoGUIPlatform
+{
+public:
+  SDLNoGUIPlatform();
+  ~SDLNoGUIPlatform();
+
+  bool Initialize();
+
+  void ReportError(const std::string_view& title, const std::string_view& message) override;
+  bool ConfirmMessage(const std::string_view& title, const std::string_view& message) override;
+
+  void SetDefaultConfig(SettingsInterface& si) override;
+
+  bool CreatePlatformWindow(std::string title) override;
+  bool HasPlatformWindow() const override;
+  void DestroyPlatformWindow() override;
+  std::optional<WindowInfo> GetPlatformWindowInfo() override;
+  void SetPlatformWindowTitle(std::string title) override;
+
+  std::optional<u32> ConvertHostKeyboardStringToCode(const std::string_view& str) override;
+  std::optional<std::string> ConvertHostKeyboardCodeToString(u32 code) override;
+
+  void RunMessageLoop() override;
+  void ExecuteInMessageLoop(std::function<void()> func) override;
+  void QuitMessageLoop() override;
+
+  void SetFullscreen(bool enabled) override;
+
+  bool RequestRenderWindowSize(s32 new_window_width, s32 new_window_height) override;
+
+  bool OpenURL(const std::string_view& url) override;
+  bool CopyTextToClipboard(const std::string_view& text) override;
+
+private:
+  void ProcessEvent(const SDL_Event* ev);
+
+  SDL_Window* m_window = nullptr;
+  float m_window_scale = 1.0f;
+  u32 m_func_event_id = 0;
+  u32 m_wakeup_event_id = 0;
+
+  std::atomic_bool m_message_loop_running{false};
+  std::atomic_bool m_fullscreen{false};
+};
\ No newline at end of file
diff --git a/src/duckstation-nogui/win32_key_names.h b/src/duckstation-nogui/win32_key_names.h
index fafea7564..ffe701158 100644
--- a/src/duckstation-nogui/win32_key_names.h
+++ b/src/duckstation-nogui/win32_key_names.h
@@ -1,6 +1,11 @@
+// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
+// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
+
 #pragma once
+
 #include "common/types.h"
 #include "common/windows_headers.h"
+
 #include <array>
 #include <cstring>
 #include <map>
diff --git a/src/util/sdl_input_source.cpp b/src/util/sdl_input_source.cpp
index 1e401e89e..c6973577e 100644
--- a/src/util/sdl_input_source.cpp
+++ b/src/util/sdl_input_source.cpp
@@ -151,6 +151,8 @@ static void SDLLogCallback(void* userdata, int category, SDL_LogPriority priorit
   Log::Write("SDL", "SDL", priority_map[priority], message);
 }
 
+bool SDLInputSource::ALLOW_EVENT_POLLING = true;
+
 SDLInputSource::SDLInputSource() = default;
 
 SDLInputSource::~SDLInputSource()
@@ -322,6 +324,9 @@ void SDLInputSource::ShutdownSubsystem()
 
 void SDLInputSource::PollEvents()
 {
+  if (!ALLOW_EVENT_POLLING)
+    return;
+
   for (;;)
   {
     SDL_Event ev;
@@ -548,6 +553,28 @@ TinyString SDLInputSource::ConvertKeyToIcon(InputBindingKey key)
   return ret;
 }
 
+bool SDLInputSource::IsHandledInputEvent(const SDL_Event* ev)
+{
+  switch (ev->type)
+  {
+    case SDL_CONTROLLERDEVICEADDED:
+    case SDL_CONTROLLERDEVICEREMOVED:
+    case SDL_JOYDEVICEADDED:
+    case SDL_JOYDEVICEREMOVED:
+    case SDL_CONTROLLERAXISMOTION:
+    case SDL_CONTROLLERBUTTONDOWN:
+    case SDL_CONTROLLERBUTTONUP:
+    case SDL_JOYAXISMOTION:
+    case SDL_JOYBUTTONDOWN:
+    case SDL_JOYBUTTONUP:
+    case SDL_JOYHATMOTION:
+      return true;
+
+    default:
+      return false;
+  }
+}
+
 bool SDLInputSource::ProcessSDLEvent(const SDL_Event* event)
 {
   switch (event->type)
@@ -859,7 +886,7 @@ bool SDLInputSource::HandleJoystickButtonEvent(const SDL_JoyButtonEvent* ev)
   if (it == m_controllers.end())
     return false;
   if (ev->button < it->joy_button_used_in_gc.size() && it->joy_button_used_in_gc[ev->button])
-    return false;                                                 // Will get handled by GC event
+    return false; // Will get handled by GC event
   const u32 button =
     ev->button + static_cast<u32>(std::size(s_sdl_button_names)); // Ensure we don't conflict with GC buttons
   const InputBindingKey key(MakeGenericControllerButtonKey(InputSourceType::SDL, it->player_id, button));
diff --git a/src/util/sdl_input_source.h b/src/util/sdl_input_source.h
index 7ed6185d1..25971c7ab 100644
--- a/src/util/sdl_input_source.h
+++ b/src/util/sdl_input_source.h
@@ -46,6 +46,10 @@ public:
   static u32 GetRGBForPlayerId(SettingsInterface& si, u32 player_id);
   static u32 ParseRGBForPlayerId(const std::string_view& str, u32 player_id);
 
+  static bool IsHandledInputEvent(const SDL_Event* ev);
+
+  static bool ALLOW_EVENT_POLLING;
+
 private:
   struct ControllerData
   {