From 395e9a934bffb9305d2fd9adedea14aae580d29d Mon Sep 17 00:00:00 2001
From: Connor McLaughlin <stenzek@gmail.com>
Date: Sun, 15 Jan 2023 14:00:51 +1000
Subject: [PATCH] InputManager: Support inverted full axis

i.e. pedals
---
 src/common/heterogeneous_containers.h         |  58 ++-
 src/common/layered_settings_interface.cpp     |  33 ++
 src/common/layered_settings_interface.h       |   5 +-
 src/common/memory_settings_interface.cpp      |  71 ++--
 src/common/memory_settings_interface.h        |   4 +
 src/common/settings_interface.h               |   3 +
 src/core/CMakeLists.txt                       |   1 +
 src/core/analog_controller.cpp                |  12 +-
 src/core/analog_joystick.cpp                  |  12 +-
 src/core/controller.cpp                       |   4 +-
 src/core/controller.h                         |  15 +-
 src/core/core.vcxproj                         |   1 +
 src/core/core.vcxproj.filters                 |   1 +
 src/core/digital_controller.cpp               |   2 +-
 src/core/guncon.cpp                           |   2 +-
 src/core/host.h                               |  43 ---
 src/core/input_types.h                        |  71 ++++
 src/core/negcon.cpp                           |   4 +-
 src/core/playstation_mouse.cpp                |   2 +-
 .../controllerbindingwidgets.cpp              |  26 +-
 .../controllersettingsdialog.cpp              |   2 +-
 src/duckstation-qt/hotkeysettingswidget.cpp   |   4 +-
 src/duckstation-qt/inputbindingdialog.cpp     |  59 ++-
 src/duckstation-qt/inputbindingdialog.h       |   8 +-
 src/duckstation-qt/inputbindingwidgets.cpp    |  67 +++-
 src/duckstation-qt/inputbindingwidgets.h      |  10 +-
 src/frontend-common/dinput_source.cpp         |  75 ++--
 src/frontend-common/dinput_source.h           |   4 +-
 src/frontend-common/fullscreen_ui.cpp         | 103 +++--
 src/frontend-common/imgui_overlays.cpp        |  13 +-
 src/frontend-common/input_manager.cpp         | 131 +++++--
 src/frontend-common/input_manager.h           |  43 ++-
 src/frontend-common/input_source.cpp          |  43 ++-
 src/frontend-common/input_source.h            |   4 +
 src/frontend-common/sdl_input_source.cpp      | 361 ++++++++++++++----
 src/frontend-common/sdl_input_source.h        |  28 +-
 src/frontend-common/xinput_source.cpp         |  13 +-
 src/util/ini_settings_interface.cpp           |  47 ++-
 src/util/ini_settings_interface.h             |   3 +
 39 files changed, 1022 insertions(+), 366 deletions(-)
 create mode 100644 src/core/input_types.h

diff --git a/src/common/heterogeneous_containers.h b/src/common/heterogeneous_containers.h
index 8f7a3a9d7..ad769f49c 100644
--- a/src/common/heterogeneous_containers.h
+++ b/src/common/heterogeneous_containers.h
@@ -72,6 +72,32 @@ UnorderedStringMapFind(UnorderedStringMap<ValueType>& map, const KeyType& key)
 {
   return map.find(key);
 }
+template<typename KeyType, typename ValueType>
+ALWAYS_INLINE typename UnorderedStringMultimap<ValueType>::const_iterator
+UnorderedStringMultiMapFind(const UnorderedStringMultimap<ValueType>& map, const KeyType& key)
+{
+  return map.find(key);
+}
+template<typename KeyType, typename ValueType>
+ALWAYS_INLINE std::pair<typename UnorderedStringMultimap<ValueType>::const_iterator,
+                        typename UnorderedStringMultimap<ValueType>::const_iterator>
+UnorderedStringMultiMapEqualRange(const UnorderedStringMultimap<ValueType>& map, const KeyType& key)
+{
+  return map.equal_range(key);
+}
+template<typename KeyType, typename ValueType>
+ALWAYS_INLINE typename UnorderedStringMultimap<ValueType>::iterator
+UnorderedStringMultiMapFind(UnorderedStringMultimap<ValueType>& map, const KeyType& key)
+{
+  return map.find(key);
+}
+template<typename KeyType, typename ValueType>
+ALWAYS_INLINE std::pair<typename UnorderedStringMultimap<ValueType>::iterator,
+                        typename UnorderedStringMultimap<ValueType>::iterator>
+UnorderedStringMultiMapEqualRange(UnorderedStringMultimap<ValueType>& map, const KeyType& key)
+{
+  return map.equal_range(key);
+}
 #else
 template<typename ValueType>
 using UnorderedStringMap = std::unordered_map<std::string, ValueType>;
@@ -81,15 +107,43 @@ using UnorderedStringSet = std::unordered_set<std::string>;
 using UnorderedStringMultiSet = std::unordered_multiset<std::string>;
 
 template<typename KeyType, typename ValueType>
-ALWAYS_INLINE typename UnorderedStringMap<ValueType>::const_iterator UnorderedStringMapFind(const UnorderedStringMap<ValueType>& map, const KeyType& key)
+ALWAYS_INLINE typename UnorderedStringMap<ValueType>::const_iterator
+UnorderedStringMapFind(const UnorderedStringMap<ValueType>& map, const KeyType& key)
 {
   return map.find(std::string(key));
 }
 template<typename KeyType, typename ValueType>
-ALWAYS_INLINE typename UnorderedStringMap<ValueType>::iterator UnorderedStringMapFind(UnorderedStringMap<ValueType>& map, const KeyType& key)
+ALWAYS_INLINE typename UnorderedStringMap<ValueType>::iterator
+UnorderedStringMapFind(UnorderedStringMap<ValueType>& map, const KeyType& key)
 {
   return map.find(std::string(key));
 }
+template<typename KeyType, typename ValueType>
+ALWAYS_INLINE typename UnorderedStringMultimap<ValueType>::const_iterator
+UnorderedStringMultiMapFind(const UnorderedStringMultimap<ValueType>& map, const KeyType& key)
+{
+  return map.find(std::string(key));
+}
+template<typename KeyType, typename ValueType>
+ALWAYS_INLINE std::pair<typename UnorderedStringMultimap<ValueType>::const_iterator,
+                        typename UnorderedStringMultimap<ValueType>::const_iterator>
+UnorderedStringMultiMapEqualRange(const UnorderedStringMultimap<ValueType>& map, const KeyType& key)
+{
+  return map.equal_range(std::string(key));
+}
+template<typename KeyType, typename ValueType>
+ALWAYS_INLINE typename UnorderedStringMultimap<ValueType>::iterator
+UnorderedStringMultiMapFind(UnorderedStringMultimap<ValueType>& map, const KeyType& key)
+{
+  return map.find(std::string(key));
+}
+template<typename KeyType, typename ValueType>
+ALWAYS_INLINE std::pair<typename UnorderedStringMultimap<ValueType>::iterator,
+                        typename UnorderedStringMultimap<ValueType>::iterator>
+UnorderedStringMultiMapEqualRange(UnorderedStringMultimap<ValueType>& map, const KeyType& key)
+{
+  return map.equal_range(std::string(key));
+}
 #endif
 
 template<typename ValueType>
diff --git a/src/common/layered_settings_interface.cpp b/src/common/layered_settings_interface.cpp
index 6ce655d28..d56994985 100644
--- a/src/common/layered_settings_interface.cpp
+++ b/src/common/layered_settings_interface.cpp
@@ -3,6 +3,7 @@
 
 #include "layered_settings_interface.h"
 #include "common/assert.h"
+#include <unordered_set>
 
 LayeredSettingsInterface::LayeredSettingsInterface() = default;
 
@@ -190,3 +191,35 @@ bool LayeredSettingsInterface::AddToStringList(const char* section, const char*
   Panic("Attempt to call AddToStringList() on layered settings interface");
   return true;
 }
+
+std::vector<std::pair<std::string, std::string>> LayeredSettingsInterface::GetKeyValueList(const char* section) const
+{
+  std::unordered_set<std::string_view> seen;
+  std::vector<std::pair<std::string, std::string>> ret;
+  for (u32 layer = FIRST_LAYER; layer <= LAST_LAYER; layer++)
+  {
+    if (SettingsInterface* sif = m_layers[layer])
+    {
+      const size_t newly_added_begin = ret.size();
+      std::vector<std::pair<std::string, std::string>> entries = sif->GetKeyValueList(section);
+      for (std::pair<std::string, std::string>& entry : entries)
+      {
+        if (seen.find(entry.first) != seen.end())
+          continue;
+        ret.push_back(std::move(entry));
+      }
+
+      // Mark keys as seen after processing all entries in case the layer has multiple entries for a specific key
+      for (auto cur = ret.begin() + newly_added_begin, end = ret.end(); cur < end; cur++)
+        seen.insert(cur->first);
+    }
+  }
+
+  return ret;
+}
+
+void LayeredSettingsInterface::SetKeyValueList(const char* section,
+                                               const std::vector<std::pair<std::string, std::string>>& items)
+{
+  Panic("Attempt to call SetKeyValueList() on layered settings interface");
+}
diff --git a/src/common/layered_settings_interface.h b/src/common/layered_settings_interface.h
index b94d15fe6..ec46ca56a 100644
--- a/src/common/layered_settings_interface.h
+++ b/src/common/layered_settings_interface.h
@@ -49,6 +49,9 @@ public:
   bool RemoveFromStringList(const char* section, const char* key, const char* item) override;
   bool AddToStringList(const char* section, const char* key, const char* item) override;
 
+  std::vector<std::pair<std::string, std::string>> GetKeyValueList(const char* section) const override;
+  void SetKeyValueList(const char* section, const std::vector<std::pair<std::string, std::string>>& items) override;
+
   // default parameter overloads
   using SettingsInterface::GetBoolValue;
   using SettingsInterface::GetDoubleValue;
@@ -62,4 +65,4 @@ private:
   static constexpr Layer LAST_LAYER = LAYER_BASE;
 
   std::array<SettingsInterface*, NUM_LAYERS> m_layers{};
-};
+};
\ No newline at end of file
diff --git a/src/common/memory_settings_interface.cpp b/src/common/memory_settings_interface.cpp
index 05a19c98a..e580599a1 100644
--- a/src/common/memory_settings_interface.cpp
+++ b/src/common/memory_settings_interface.cpp
@@ -21,7 +21,7 @@ void MemorySettingsInterface::Clear()
 
 bool MemorySettingsInterface::GetIntValue(const char* section, const char* key, s32* value) const
 {
-  const auto sit = m_sections.find(section);
+  const auto sit = UnorderedStringMapFind(m_sections, section);
   if (sit == m_sections.end())
     return false;
 
@@ -39,11 +39,11 @@ bool MemorySettingsInterface::GetIntValue(const char* section, const char* key,
 
 bool MemorySettingsInterface::GetUIntValue(const char* section, const char* key, u32* value) const
 {
-  const auto sit = m_sections.find(section);
+  const auto sit = UnorderedStringMapFind(m_sections, section);
   if (sit == m_sections.end())
     return false;
 
-  const auto iter = sit->second.find(key);
+  const auto iter = UnorderedStringMultiMapFind(sit->second, key);
   if (iter == sit->second.end())
     return false;
 
@@ -57,11 +57,11 @@ bool MemorySettingsInterface::GetUIntValue(const char* section, const char* key,
 
 bool MemorySettingsInterface::GetFloatValue(const char* section, const char* key, float* value) const
 {
-  const auto sit = m_sections.find(section);
+  const auto sit = UnorderedStringMapFind(m_sections, section);
   if (sit == m_sections.end())
     return false;
 
-  const auto iter = sit->second.find(key);
+  const auto iter = UnorderedStringMultiMapFind(sit->second, key);
   if (iter == sit->second.end())
     return false;
 
@@ -75,11 +75,11 @@ bool MemorySettingsInterface::GetFloatValue(const char* section, const char* key
 
 bool MemorySettingsInterface::GetDoubleValue(const char* section, const char* key, double* value) const
 {
-  const auto sit = m_sections.find(section);
+  const auto sit = UnorderedStringMapFind(m_sections, section);
   if (sit == m_sections.end())
     return false;
 
-  const auto iter = sit->second.find(key);
+  const auto iter = UnorderedStringMultiMapFind(sit->second, key);
   if (iter == sit->second.end())
     return false;
 
@@ -93,11 +93,11 @@ bool MemorySettingsInterface::GetDoubleValue(const char* section, const char* ke
 
 bool MemorySettingsInterface::GetBoolValue(const char* section, const char* key, bool* value) const
 {
-  const auto sit = m_sections.find(section);
+  const auto sit = UnorderedStringMapFind(m_sections, section);
   if (sit == m_sections.end())
     return false;
 
-  const auto iter = sit->second.find(key);
+  const auto iter = UnorderedStringMultiMapFind(sit->second, key);
   if (iter == sit->second.end())
     return false;
 
@@ -111,11 +111,11 @@ bool MemorySettingsInterface::GetBoolValue(const char* section, const char* key,
 
 bool MemorySettingsInterface::GetStringValue(const char* section, const char* key, std::string* value) const
 {
-  const auto sit = m_sections.find(section);
+  const auto sit = UnorderedStringMapFind(m_sections, section);
   if (sit == m_sections.end())
     return false;
 
-  const auto iter = sit->second.find(key);
+  const auto iter = UnorderedStringMultiMapFind(sit->second, key);
   if (iter == sit->second.end())
     return false;
 
@@ -153,13 +153,34 @@ void MemorySettingsInterface::SetStringValue(const char* section, const char* ke
   SetValue(section, key, value);
 }
 
+std::vector<std::pair<std::string, std::string>> MemorySettingsInterface::GetKeyValueList(const char* section) const
+{
+  std::vector<std::pair<std::string, std::string>> output;
+  auto sit = UnorderedStringMapFind(m_sections, section);
+  if (sit != m_sections.end())
+  {
+    for (const auto& it : sit->second)
+      output.emplace_back(it.first, it.second);
+  }
+  return output;
+}
+
+void MemorySettingsInterface::SetKeyValueList(const char* section,
+                                              const std::vector<std::pair<std::string, std::string>>& items)
+{
+  auto sit = UnorderedStringMapFind(m_sections, section);
+  sit->second.clear();
+  for (const auto& [key, value] : items)
+    sit->second.emplace(key, value);
+}
+
 void MemorySettingsInterface::SetValue(const char* section, const char* key, std::string value)
 {
-  auto sit = m_sections.find(section);
+  auto sit = UnorderedStringMapFind(m_sections, section);
   if (sit == m_sections.end())
     sit = m_sections.emplace(std::make_pair(std::string(section), KeyMap())).first;
 
-  const auto range = sit->second.equal_range(key);
+  const auto range = UnorderedStringMultiMapEqualRange(sit->second, key);
   if (range.first == sit->second.end())
   {
     sit->second.emplace(std::string(key), std::move(value));
@@ -182,10 +203,10 @@ std::vector<std::string> MemorySettingsInterface::GetStringList(const char* sect
 {
   std::vector<std::string> ret;
 
-  const auto sit = m_sections.find(section);
+  const auto sit = UnorderedStringMapFind(m_sections, section);
   if (sit != m_sections.end())
   {
-    const auto range = sit->second.equal_range(key);
+    const auto range = UnorderedStringMultiMapEqualRange(sit->second, key);
     for (auto iter = range.first; iter != range.second; ++iter)
       ret.emplace_back(iter->second);
   }
@@ -195,11 +216,11 @@ std::vector<std::string> MemorySettingsInterface::GetStringList(const char* sect
 
 void MemorySettingsInterface::SetStringList(const char* section, const char* key, const std::vector<std::string>& items)
 {
-  auto sit = m_sections.find(section);
+  auto sit = UnorderedStringMapFind(m_sections, section);
   if (sit == m_sections.end())
     sit = m_sections.emplace(std::make_pair(std::string(section), KeyMap())).first;
 
-  const auto range = sit->second.equal_range(key);
+  const auto range = UnorderedStringMultiMapEqualRange(sit->second, key);
   for (auto iter = range.first; iter != range.second;)
     sit->second.erase(iter++);
 
@@ -210,11 +231,11 @@ void MemorySettingsInterface::SetStringList(const char* section, const char* key
 
 bool MemorySettingsInterface::RemoveFromStringList(const char* section, const char* key, const char* item)
 {
-  auto sit = m_sections.find(section);
+  auto sit = UnorderedStringMapFind(m_sections, section);
   if (sit == m_sections.end())
     sit = m_sections.emplace(std::make_pair(std::string(section), KeyMap())).first;
 
-  const auto range = sit->second.equal_range(key);
+  const auto range = UnorderedStringMultiMapEqualRange(sit->second, key);
   bool result = false;
   for (auto iter = range.first; iter != range.second;)
   {
@@ -234,11 +255,11 @@ bool MemorySettingsInterface::RemoveFromStringList(const char* section, const ch
 
 bool MemorySettingsInterface::AddToStringList(const char* section, const char* key, const char* item)
 {
-  auto sit = m_sections.find(section);
+  auto sit = UnorderedStringMapFind(m_sections, section);
   if (sit == m_sections.end())
     sit = m_sections.emplace(std::make_pair(std::string(section), KeyMap())).first;
 
-  const auto range = sit->second.equal_range(key);
+  const auto range = UnorderedStringMultiMapEqualRange(sit->second, key);
   for (auto iter = range.first; iter != range.second; ++iter)
   {
     if (iter->second == item)
@@ -251,7 +272,7 @@ bool MemorySettingsInterface::AddToStringList(const char* section, const char* k
 
 bool MemorySettingsInterface::ContainsValue(const char* section, const char* key) const
 {
-  const auto sit = m_sections.find(section);
+  const auto sit = UnorderedStringMapFind(m_sections, section);
   if (sit == m_sections.end())
     return false;
 
@@ -260,18 +281,18 @@ bool MemorySettingsInterface::ContainsValue(const char* section, const char* key
 
 void MemorySettingsInterface::DeleteValue(const char* section, const char* key)
 {
-  auto sit = m_sections.find(section);
+  auto sit = UnorderedStringMapFind(m_sections, section);
   if (sit == m_sections.end())
     return;
 
-  const auto range = sit->second.equal_range(key);
+  const auto range = UnorderedStringMultiMapEqualRange(sit->second, key);
   for (auto iter = range.first; iter != range.second;)
     sit->second.erase(iter++);
 }
 
 void MemorySettingsInterface::ClearSection(const char* section)
 {
-  auto sit = m_sections.find(section);
+  auto sit = UnorderedStringMapFind(m_sections, section);
   if (sit == m_sections.end())
     return;
 
diff --git a/src/common/memory_settings_interface.h b/src/common/memory_settings_interface.h
index 49fe3e117..c0a3a85f3 100644
--- a/src/common/memory_settings_interface.h
+++ b/src/common/memory_settings_interface.h
@@ -29,6 +29,10 @@ public:
   void SetDoubleValue(const char* section, const char* key, double value) override;
   void SetBoolValue(const char* section, const char* key, bool value) override;
   void SetStringValue(const char* section, const char* key, const char* value) override;
+
+  std::vector<std::pair<std::string, std::string>> GetKeyValueList(const char* section) const override;
+  void SetKeyValueList(const char* section, const std::vector<std::pair<std::string, std::string>>& items) override;
+
   bool ContainsValue(const char* section, const char* key) const override;
   void DeleteValue(const char* section, const char* key) override;
   void ClearSection(const char* section) override;
diff --git a/src/common/settings_interface.h b/src/common/settings_interface.h
index 591979a91..9ba4c96b7 100644
--- a/src/common/settings_interface.h
+++ b/src/common/settings_interface.h
@@ -35,6 +35,9 @@ public:
   virtual bool RemoveFromStringList(const char* section, const char* key, const char* item) = 0;
   virtual bool AddToStringList(const char* section, const char* key, const char* item) = 0;
 
+  virtual std::vector<std::pair<std::string, std::string>> GetKeyValueList(const char* section) const = 0;
+  virtual void SetKeyValueList(const char* section, const std::vector<std::pair<std::string, std::string>>& items) = 0;
+
   virtual bool ContainsValue(const char* section, const char* key) const = 0;
   virtual void DeleteValue(const char* section, const char* key) = 0;
   virtual void ClearSection(const char* section) = 0;
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index 29c4b22d6..31f4d449f 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -59,6 +59,7 @@ add_library(core
     host_interface_progress_callback.cpp
     host_interface_progress_callback.h
     host_settings.h
+    input_types.h
     interrupt_controller.cpp
     interrupt_controller.h
     libcrypt_serials.cpp
diff --git a/src/core/analog_controller.cpp b/src/core/analog_controller.cpp
index 044b54abd..1d0328570 100644
--- a/src/core/analog_controller.cpp
+++ b/src/core/analog_controller.cpp
@@ -789,12 +789,12 @@ std::unique_ptr<AnalogController> AnalogController::Create(u32 index)
 static const Controller::ControllerBindingInfo s_binding_info[] = {
 #define BUTTON(name, display_name, button, genb)                                                                       \
   {                                                                                                                    \
-    name, display_name, static_cast<u32>(button), Controller::ControllerBindingType::Button, genb                      \
+    name, display_name, static_cast<u32>(button), InputBindingInfo::Type::Button, genb                                 \
   }
 #define AXIS(name, display_name, halfaxis, genb)                                                                       \
   {                                                                                                                    \
     name, display_name, static_cast<u32>(AnalogController::Button::Count) + static_cast<u32>(halfaxis),                \
-      Controller::ControllerBindingType::HalfAxis, genb                                                                \
+      InputBindingInfo::Type::HalfAxis, genb                                                                           \
   }
 
   BUTTON("Up", "D-Pad Up", AnalogController::Button::Up, GenericInputBinding::DPadUp),
@@ -862,11 +862,11 @@ static const SettingInfo s_settings[] = {
                                     "functioning, try increasing this value."),
    "8", "0", "255", "1", "%d", nullptr, 1.0f},
   {SettingInfo::Type::IntegerList, "InvertLeftStick", TRANSLATABLE("AnalogController", "Invert Left Stick"),
-   TRANSLATABLE("AnalogController", "Inverts the direction of the left analog stick."),
-   "0", "0", "3", nullptr, nullptr, s_invert_settings, 0.0f},
+   TRANSLATABLE("AnalogController", "Inverts the direction of the left analog stick."), "0", "0", "3", nullptr, nullptr,
+   s_invert_settings, 0.0f},
   {SettingInfo::Type::IntegerList, "InvertRightStick", TRANSLATABLE("AnalogController", "Invert Right Stick"),
-   TRANSLATABLE("AnalogController", "Inverts the direction of the right analog stick."),
-   "0", "0", "3", nullptr, nullptr, s_invert_settings, 0.0f},
+   TRANSLATABLE("AnalogController", "Inverts the direction of the right analog stick."), "0", "0", "3", nullptr,
+   nullptr, s_invert_settings, 0.0f},
 };
 
 const Controller::ControllerInfo AnalogController::INFO = {ControllerType::AnalogController,
diff --git a/src/core/analog_joystick.cpp b/src/core/analog_joystick.cpp
index a07332cbc..82082fe7c 100644
--- a/src/core/analog_joystick.cpp
+++ b/src/core/analog_joystick.cpp
@@ -335,12 +335,12 @@ std::unique_ptr<AnalogJoystick> AnalogJoystick::Create(u32 index)
 static const Controller::ControllerBindingInfo s_binding_info[] = {
 #define BUTTON(name, display_name, button, genb)                                                                       \
   {                                                                                                                    \
-    name, display_name, static_cast<u32>(button), Controller::ControllerBindingType::Button, genb                      \
+    name, display_name, static_cast<u32>(button), InputBindingInfo::Type::Button, genb                                 \
   }
 #define AXIS(name, display_name, halfaxis, genb)                                                                       \
   {                                                                                                                    \
     name, display_name, static_cast<u32>(AnalogJoystick::Button::Count) + static_cast<u32>(halfaxis),                  \
-      Controller::ControllerBindingType::HalfAxis, genb                                                                \
+      InputBindingInfo::Type::HalfAxis, genb                                                                           \
   }
 
   BUTTON("Up", "D-Pad Up", AnalogJoystick::Button::Up, GenericInputBinding::DPadUp),
@@ -391,11 +391,11 @@ static const SettingInfo s_settings[] = {
      "controllers, e.g. DualShock 4, Xbox One Controller."),
    "1.33f", "0.01f", "2.00f", "0.01f", "%.0f%%", nullptr, 100.0f},
   {SettingInfo::Type::IntegerList, "InvertLeftStick", TRANSLATABLE("AnalogJoystick", "Invert Left Stick"),
-   TRANSLATABLE("AnalogJoystick", "Inverts the direction of the left analog stick."),
-   "0", "0", "3", nullptr, nullptr, s_invert_settings, 0.0f},
+   TRANSLATABLE("AnalogJoystick", "Inverts the direction of the left analog stick."), "0", "0", "3", nullptr, nullptr,
+   s_invert_settings, 0.0f},
   {SettingInfo::Type::IntegerList, "InvertRightStick", TRANSLATABLE("AnalogJoystick", "Invert Right Stick"),
-   TRANSLATABLE("AnalogJoystick", "Inverts the direction of the right analog stick."),
-   "0", "0", "3", nullptr, nullptr, s_invert_settings, 0.0f},
+   TRANSLATABLE("AnalogJoystick", "Inverts the direction of the right analog stick."), "0", "0", "3", nullptr, nullptr,
+   s_invert_settings, 0.0f},
 };
 
 const Controller::ControllerInfo AnalogJoystick::INFO = {ControllerType::AnalogJoystick,
diff --git a/src/core/controller.cpp b/src/core/controller.cpp
index cc25cd0cf..757025a23 100644
--- a/src/core/controller.cpp
+++ b/src/core/controller.cpp
@@ -148,7 +148,7 @@ std::vector<std::string> Controller::GetControllerBinds(const std::string_view&
     for (u32 i = 0; i < info->num_bindings; i++)
     {
       const ControllerBindingInfo& bi = info->bindings[i];
-      if (bi.type == ControllerBindingType::Unknown || bi.type == ControllerBindingType::Motor)
+      if (bi.type == InputBindingInfo::Type::Unknown || bi.type == InputBindingInfo::Type::Motor)
         continue;
 
       ret.emplace_back(info->bindings[i].name);
@@ -168,7 +168,7 @@ std::vector<std::string> Controller::GetControllerBinds(ControllerType type)
     for (u32 i = 0; i < info->num_bindings; i++)
     {
       const ControllerBindingInfo& bi = info->bindings[i];
-      if (bi.type == ControllerBindingType::Unknown || bi.type == ControllerBindingType::Motor)
+      if (bi.type == InputBindingInfo::Type::Unknown || bi.type == InputBindingInfo::Type::Motor)
         continue;
 
       ret.emplace_back(info->bindings[i].name);
diff --git a/src/core/controller.h b/src/core/controller.h
index dbc04441b..282894c59 100644
--- a/src/core/controller.h
+++ b/src/core/controller.h
@@ -3,6 +3,7 @@
 
 #pragma once
 #include "common/image.h"
+#include "input_types.h"
 #include "settings.h"
 #include "types.h"
 #include <memory>
@@ -16,21 +17,9 @@ class SettingsInterface;
 class StateWrapper;
 class HostInterface;
 
-enum class GenericInputBinding : u8;
-
 class Controller
 {
 public:
-  enum class ControllerBindingType : u8
-  {
-    Unknown,
-    Button,
-    Axis,
-    HalfAxis,
-    Motor,
-    Macro
-  };
-
   enum class VibrationCapabilities : u8
   {
     NoVibration,
@@ -44,7 +33,7 @@ public:
     const char* name;
     const char* display_name;
     u32 bind_index;
-    ControllerBindingType type;
+    InputBindingInfo::Type type;
     GenericInputBinding generic_mapping;
   };
 
diff --git a/src/core/core.vcxproj b/src/core/core.vcxproj
index af3ec2215..fc2d34c2a 100644
--- a/src/core/core.vcxproj
+++ b/src/core/core.vcxproj
@@ -128,6 +128,7 @@
     <ClInclude Include="host_display.h" />
     <ClInclude Include="host_interface_progress_callback.h" />
     <ClInclude Include="host_settings.h" />
+    <ClInclude Include="input_types.h" />
     <ClInclude Include="interrupt_controller.h" />
     <ClInclude Include="libcrypt_serials.h" />
     <ClInclude Include="mdec.h" />
diff --git a/src/core/core.vcxproj.filters b/src/core/core.vcxproj.filters
index f809c9330..b0951bd14 100644
--- a/src/core/core.vcxproj.filters
+++ b/src/core/core.vcxproj.filters
@@ -124,5 +124,6 @@
     <ClInclude Include="host_settings.h" />
     <ClInclude Include="achievements.h" />
     <ClInclude Include="game_database.h" />
+    <ClInclude Include="input_types.h" />
   </ItemGroup>
 </Project>
\ No newline at end of file
diff --git a/src/core/digital_controller.cpp b/src/core/digital_controller.cpp
index 888032422..33c4357da 100644
--- a/src/core/digital_controller.cpp
+++ b/src/core/digital_controller.cpp
@@ -146,7 +146,7 @@ std::unique_ptr<DigitalController> DigitalController::Create(u32 index)
 static const Controller::ControllerBindingInfo s_binding_info[] = {
 #define BUTTON(name, display_name, button, genb)                                                                       \
   {                                                                                                                    \
-    name, display_name, static_cast<u32>(button), Controller::ControllerBindingType::Button, genb                      \
+    name, display_name, static_cast<u32>(button), InputBindingInfo::Type::Button, genb                                 \
   }
 
   BUTTON("Up", "D-Pad Up", DigitalController::Button::Up, GenericInputBinding::DPadUp),
diff --git a/src/core/guncon.cpp b/src/core/guncon.cpp
index eca985e67..d80efd24a 100644
--- a/src/core/guncon.cpp
+++ b/src/core/guncon.cpp
@@ -208,7 +208,7 @@ std::unique_ptr<GunCon> GunCon::Create(u32 index)
 static const Controller::ControllerBindingInfo s_binding_info[] = {
 #define BUTTON(name, display_name, button, genb)                                                                       \
   {                                                                                                                    \
-    name, display_name, static_cast<u32>(button), Controller::ControllerBindingType::Button, genb                      \
+    name, display_name, static_cast<u32>(button), InputBindingInfo::Type::Button, genb                                 \
   }
 
   BUTTON("Trigger", "Trigger", GunCon::Button::Trigger, GenericInputBinding::R2),
diff --git a/src/core/host.h b/src/core/host.h
index dda90ee85..c1eb545a1 100644
--- a/src/core/host.h
+++ b/src/core/host.h
@@ -23,49 +23,6 @@ class CDImage;
 /// Marks a core string as being translatable.
 #define TRANSLATABLE(context, str) str
 
-/// Generic input bindings. These roughly match a DualShock 4 or XBox One controller.
-/// They are used for automatic binding to PS2 controller types, and for big picture mode navigation.
-enum class GenericInputBinding : u8
-{
-  Unknown,
-
-  DPadUp,
-  DPadRight,
-  DPadLeft,
-  DPadDown,
-
-  LeftStickUp,
-  LeftStickRight,
-  LeftStickDown,
-  LeftStickLeft,
-  L3,
-
-  RightStickUp,
-  RightStickRight,
-  RightStickDown,
-  RightStickLeft,
-  R3,
-
-  Triangle, // Y on XBox pads.
-  Circle,   // B on XBox pads.
-  Cross,    // A on XBox pads.
-  Square,   // X on XBox pads.
-
-  Select, // Share on DS4, View on XBox pads.
-  Start,  // Options on DS4, Menu on XBox pads.
-  System, // PS button on DS4, Guide button on XBox pads.
-
-  L1, // LB on Xbox pads.
-  L2, // Left trigger on XBox pads.
-  R1, // RB on XBox pads.
-  R2, // Right trigger on Xbox pads.
-
-  SmallMotor, // High frequency vibration.
-  LargeMotor, // Low frequency vibration.
-
-  Count,
-};
-
 namespace Host {
 /// Reads a file from the resources directory of the application.
 /// This may be outside of the "normal" filesystem on platforms such as Mac.
diff --git a/src/core/input_types.h b/src/core/input_types.h
new file mode 100644
index 000000000..8e8957454
--- /dev/null
+++ b/src/core/input_types.h
@@ -0,0 +1,71 @@
+// SPDX-FileCopyrightText: 2022-2023 Connor McLaughlin <stenzek@gmail.com>
+// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
+
+#pragma once
+#include "types.h"
+
+enum class GenericInputBinding : u8;
+
+struct InputBindingInfo
+{
+  enum class Type : u8
+  {
+    Unknown,
+    Button,
+    Axis,
+    HalfAxis,
+    Motor,
+    Pointer,  // Receive relative mouse movement events, bind_index is offset by the axis.
+    Macro,
+  };
+
+  const char* name;
+  const char* display_name;
+  Type bind_type;
+  u16 bind_index;
+  GenericInputBinding generic_mapping;
+};
+
+/// Generic input bindings. These roughly match a DualShock 4 or XBox One controller.
+/// They are used for automatic binding to PS2 controller types, and for big picture mode navigation.
+enum class GenericInputBinding : u8
+{
+  Unknown,
+
+  DPadUp,
+  DPadRight,
+  DPadLeft,
+  DPadDown,
+
+  LeftStickUp,
+  LeftStickRight,
+  LeftStickDown,
+  LeftStickLeft,
+  L3,
+
+  RightStickUp,
+  RightStickRight,
+  RightStickDown,
+  RightStickLeft,
+  R3,
+
+  Triangle, // Y on XBox pads.
+  Circle,   // B on XBox pads.
+  Cross,    // A on XBox pads.
+  Square,   // X on XBox pads.
+
+  Select, // Share on DS4, View on XBox pads.
+  Start,  // Options on DS4, Menu on XBox pads.
+  System, // PS button on DS4, Guide button on XBox pads.
+
+  L1, // LB on Xbox pads.
+  L2, // Left trigger on XBox pads.
+  R1, // RB on XBox pads.
+  R2, // Right trigger on Xbox pads.
+
+  SmallMotor, // High frequency vibration.
+  LargeMotor, // Low frequency vibration.
+
+  Count,
+};
+
diff --git a/src/core/negcon.cpp b/src/core/negcon.cpp
index 765511b29..a1756c84a 100644
--- a/src/core/negcon.cpp
+++ b/src/core/negcon.cpp
@@ -228,12 +228,12 @@ std::unique_ptr<NeGcon> NeGcon::Create(u32 index)
 static const Controller::ControllerBindingInfo s_binding_info[] = {
 #define BUTTON(name, display_name, button, genb)                                                                       \
   {                                                                                                                    \
-    name, display_name, static_cast<u32>(button), Controller::ControllerBindingType::Button, genb                      \
+    name, display_name, static_cast<u32>(button), InputBindingInfo::Type::Button, genb                                 \
   }
 #define AXIS(name, display_name, halfaxis, genb)                                                                       \
   {                                                                                                                    \
     name, display_name, static_cast<u32>(NeGcon::Button::Count) + static_cast<u32>(halfaxis),                          \
-      Controller::ControllerBindingType::HalfAxis, genb                                                                \
+      InputBindingInfo::Type::HalfAxis, genb                                                                           \
   }
 
   BUTTON("Up", "D-Pad Up", NeGcon::Button::Up, GenericInputBinding::DPadUp),
diff --git a/src/core/playstation_mouse.cpp b/src/core/playstation_mouse.cpp
index c620b3902..ae01ef32e 100644
--- a/src/core/playstation_mouse.cpp
+++ b/src/core/playstation_mouse.cpp
@@ -179,7 +179,7 @@ std::unique_ptr<PlayStationMouse> PlayStationMouse::Create(u32 index)
 static const Controller::ControllerBindingInfo s_binding_info[] = {
 #define BUTTON(name, display_name, button, genb)                                                                       \
   {                                                                                                                    \
-    name, display_name, static_cast<u32>(button), Controller::ControllerBindingType::Button, genb                      \
+    name, display_name, static_cast<u32>(button), InputBindingInfo::Type::Button, genb                                 \
   }
 
   BUTTON("Left", "Left Button", PlayStationMouse::Button::Left, GenericInputBinding::Cross),
diff --git a/src/duckstation-qt/controllerbindingwidgets.cpp b/src/duckstation-qt/controllerbindingwidgets.cpp
index 00e576796..8bc208485 100644
--- a/src/duckstation-qt/controllerbindingwidgets.cpp
+++ b/src/duckstation-qt/controllerbindingwidgets.cpp
@@ -370,7 +370,7 @@ ControllerMacroEditWidget::ControllerMacroEditWidget(ControllerMacroWidget* pare
   for (u32 i = 0; i < cinfo->num_bindings; i++)
   {
     const Controller::ControllerBindingInfo& bi = cinfo->bindings[i];
-    if (bi.type == Controller::ControllerBindingType::Motor)
+    if (bi.type == InputBindingInfo::Type::Motor)
       continue;
 
     QListWidgetItem* item = new QListWidgetItem();
@@ -383,7 +383,8 @@ ControllerMacroEditWidget::ControllerMacroEditWidget(ControllerMacroWidget* pare
   m_frequency = dialog->getIntValue(section.c_str(), fmt::format("Macro{}Frequency", index + 1u).c_str(), 0);
   updateFrequencyText();
 
-  m_ui.trigger->initialize(dialog->getProfileSettingsInterface(), section, fmt::format("Macro{}", index + 1u));
+  m_ui.trigger->initialize(dialog->getProfileSettingsInterface(), InputBindingInfo::Type::Macro, section,
+                           fmt::format("Macro{}", index + 1u));
 
   connect(m_ui.increaseFrequency, &QAbstractButton::clicked, this, [this]() { modFrequency(1); });
   connect(m_ui.decreateFrequency, &QAbstractButton::clicked, this, [this]() { modFrequency(-1); });
@@ -453,7 +454,7 @@ void ControllerMacroEditWidget::updateBinds()
   for (u32 i = 0, bind_index = 0; i < cinfo->num_bindings; i++)
   {
     const Controller::ControllerBindingInfo& bi = cinfo->bindings[i];
-    if (bi.type == Controller::ControllerBindingType::Motor)
+    if (bi.type == InputBindingInfo::Type::Motor)
       continue;
 
     const QListWidgetItem* item = m_ui.bindList->item(static_cast<int>(bind_index));
@@ -740,17 +741,18 @@ void ControllerBindingWidget_Base::initBindingWidgets()
   for (u32 i = 0; i < cinfo->num_bindings; i++)
   {
     const Controller::ControllerBindingInfo& bi = cinfo->bindings[i];
-    if (bi.type == Controller::ControllerBindingType::Unknown || bi.type == Controller::ControllerBindingType::Motor)
-      continue;
-
-    InputBindingWidget* widget = findChild<InputBindingWidget*>(QString::fromUtf8(bi.name));
-    if (!widget)
+    if (bi.type == InputBindingInfo::Type::Axis || bi.type == InputBindingInfo::Type::HalfAxis ||
+        bi.type == InputBindingInfo::Type::Button || bi.type == InputBindingInfo::Type::Pointer)
     {
-      Log_ErrorPrintf("No widget found for '%s' (%s)", bi.name, cinfo->name);
-      continue;
-    }
+      InputBindingWidget* widget = findChild<InputBindingWidget*>(QString::fromUtf8(bi.name));
+      if (!widget)
+      {
+        Log_ErrorPrintf("No widget found for '%s' (%s)", bi.name, cinfo->name);
+        continue;
+      }
 
-    widget->initialize(sif, config_section, bi.name);
+      widget->initialize(sif, bi.type, config_section, bi.name);
+    }
   }
 
   switch (cinfo->vibration_caps)
diff --git a/src/duckstation-qt/controllersettingsdialog.cpp b/src/duckstation-qt/controllersettingsdialog.cpp
index f7e5c97be..ac07b3e2c 100644
--- a/src/duckstation-qt/controllersettingsdialog.cpp
+++ b/src/duckstation-qt/controllersettingsdialog.cpp
@@ -237,7 +237,7 @@ void ControllerSettingsDialog::onVibrationMotorsEnumerated(const QList<InputBind
 
   for (const InputBindingKey key : motors)
   {
-    const std::string key_str(InputManager::ConvertInputBindingKeyToString(key));
+    const std::string key_str(InputManager::ConvertInputBindingKeyToString(InputBindingInfo::Type::Motor, key));
     if (!key_str.empty())
       m_vibration_motors.push_back(QString::fromStdString(key_str));
   }
diff --git a/src/duckstation-qt/hotkeysettingswidget.cpp b/src/duckstation-qt/hotkeysettingswidget.cpp
index 50fcec6e8..a7a968a50 100644
--- a/src/duckstation-qt/hotkeysettingswidget.cpp
+++ b/src/duckstation-qt/hotkeysettingswidget.cpp
@@ -73,8 +73,8 @@ void HotkeySettingsWidget::createButtons()
     QLabel* label = new QLabel(qApp->translate("Hotkeys", hotkey->display_name), m_container);
     layout->addWidget(label, target_row, 0);
 
-    InputBindingWidget* bind =
-      new InputBindingWidget(m_container, m_dialog->getProfileSettingsInterface(), "Hotkeys", hotkey->name);
+    InputBindingWidget* bind = new InputBindingWidget(m_container, m_dialog->getProfileSettingsInterface(),
+                                                      InputBindingInfo::Type::Button, "Hotkeys", hotkey->name);
     bind->setMinimumWidth(300);
     layout->addWidget(bind, target_row, 1);
   }
diff --git a/src/duckstation-qt/inputbindingdialog.cpp b/src/duckstation-qt/inputbindingdialog.cpp
index de1c7946e..da5bd12a8 100644
--- a/src/duckstation-qt/inputbindingdialog.cpp
+++ b/src/duckstation-qt/inputbindingdialog.cpp
@@ -11,10 +11,11 @@
 #include <QtGui/QMouseEvent>
 #include <QtGui/QWheelEvent>
 
-InputBindingDialog::InputBindingDialog(SettingsInterface* sif, std::string section_name, std::string key_name,
+InputBindingDialog::InputBindingDialog(SettingsInterface* sif, InputBindingInfo::Type bind_type,
+                                       std::string section_name, std::string key_name,
                                        std::vector<std::string> bindings, QWidget* parent)
-  : QDialog(parent), m_sif(sif), m_section_name(std::move(section_name)), m_key_name(std::move(key_name)),
-    m_bindings(std::move(bindings))
+  : QDialog(parent), m_sif(sif), m_bind_type(bind_type), m_section_name(std::move(section_name)),
+    m_key_name(std::move(key_name)), m_bindings(std::move(bindings))
 {
   m_ui.setupUi(this);
   m_ui.title->setText(
@@ -53,8 +54,9 @@ bool InputBindingDialog::eventFilter(QObject* watched, QEvent* event)
   else if (event_type == QEvent::MouseButtonPress || event_type == QEvent::MouseButtonDblClick)
   {
     // double clicks get triggered if we click bind, then click again quickly.
-    unsigned button_index = CountTrailingZeros(static_cast<u32>(static_cast<const QMouseEvent*>(event)->button()));
-    m_new_bindings.push_back(InputManager::MakePointerButtonKey(0, button_index));
+    unsigned long button_index;
+    if (_BitScanForward(&button_index, static_cast<u32>(static_cast<const QMouseEvent*>(event)->button())))
+      m_new_bindings.push_back(InputManager::MakePointerButtonKey(0, button_index));
     return true;
   }
   else if (event_type == QEvent::Wheel)
@@ -64,7 +66,7 @@ bool InputBindingDialog::eventFilter(QObject* watched, QEvent* event)
     if (dx != 0.0f)
     {
       InputBindingKey key(InputManager::MakePointerAxisKey(0, InputPointerAxis::WheelX));
-      key.negative = (dx < 0.0f);
+      key.modifier = dx < 0.0f ? InputModifier::Negate : InputModifier::None;
       m_new_bindings.push_back(key);
     }
 
@@ -72,7 +74,7 @@ bool InputBindingDialog::eventFilter(QObject* watched, QEvent* event)
     if (dy != 0.0f)
     {
       InputBindingKey key(InputManager::MakePointerAxisKey(0, InputPointerAxis::WheelY));
-      key.negative = (dy < 0.0f);
+      key.modifier = dy < 0.0f ? InputModifier::Negate : InputModifier::None;
       m_new_bindings.push_back(key);
     }
 
@@ -89,20 +91,20 @@ bool InputBindingDialog::eventFilter(QObject* watched, QEvent* event)
     // if we've moved more than a decent distance from the center of the widget, bind it.
     // this is so we don't accidentally bind to the mouse if you bump it while reaching for your pad.
     static constexpr const s32 THRESHOLD = 50;
-    const QPointF diff(static_cast<QMouseEvent*>(event)->globalPosition() - m_input_listen_start_position);
+    const QPoint diff(static_cast<QMouseEvent*>(event)->globalPosition().toPoint() - m_input_listen_start_position);
     bool has_one = false;
 
     if (std::abs(diff.x()) >= THRESHOLD)
     {
       InputBindingKey key(InputManager::MakePointerAxisKey(0, InputPointerAxis::X));
-      key.negative = (diff.x() < 0);
+      key.modifier = diff.x() < 0 ? InputModifier::Negate : InputModifier::None;
       m_new_bindings.push_back(key);
       has_one = true;
     }
     if (std::abs(diff.y()) >= THRESHOLD)
     {
       InputBindingKey key(InputManager::MakePointerAxisKey(0, InputPointerAxis::Y));
-      key.negative = (diff.y() < 0);
+      key.modifier = diff.y() < 0 ? InputModifier::Negate : InputModifier::None;
       m_new_bindings.push_back(key);
       has_one = true;
     }
@@ -132,6 +134,7 @@ void InputBindingDialog::onInputListenTimerTimeout()
 
 void InputBindingDialog::startListeningForInput(u32 timeout_in_seconds)
 {
+  m_value_ranges.clear();
   m_new_bindings.clear();
   m_mouse_mapping_enabled = InputBindingWidget::isMouseMappingEnabled();
   m_input_listen_start_position = QCursor::pos();
@@ -179,7 +182,7 @@ void InputBindingDialog::addNewBinding()
     return;
 
   const std::string new_binding(
-    InputManager::ConvertInputBindingKeysToString(m_new_bindings.data(), m_new_bindings.size()));
+    InputManager::ConvertInputBindingKeysToString(m_bind_type, m_new_bindings.data(), m_new_bindings.size()));
   if (!new_binding.empty())
   {
     if (std::find(m_bindings.begin(), m_bindings.end(), new_binding) != m_bindings.end())
@@ -248,14 +251,37 @@ void InputBindingDialog::saveListToSettings()
 
 void InputBindingDialog::inputManagerHookCallback(InputBindingKey key, float value)
 {
-  const float abs_value = std::abs(value);
+  if (!isListeningForInput())
+    return;
 
-  for (InputBindingKey other_key : m_new_bindings)
+  float initial_value = value;
+  float min_value = value;
+  auto it = std::find_if(m_value_ranges.begin(), m_value_ranges.end(),
+                         [key](const auto& it) { return it.first.bits == key.bits; });
+  if (it != m_value_ranges.end())
+  {
+    initial_value = it->second.first;
+    min_value = it->second.second = std::min(it->second.second, value);
+  }
+  else
+  {
+    m_value_ranges.emplace_back(key, std::make_pair(initial_value, min_value));
+  }
+
+  const float abs_value = std::abs(value);
+  const bool reverse_threshold = (key.source_subtype == InputSubclass::ControllerAxis && initial_value > 0.5f);
+
+  for (InputBindingKey& other_key : m_new_bindings)
   {
     if (other_key.MaskDirection() == key.MaskDirection())
     {
-      if (abs_value < 0.5f)
+      // for pedals, we wait for it to go back to near its starting point to commit the binding
+      if ((reverse_threshold ? ((initial_value - value) <= 0.25f) : (abs_value < 0.5f)))
       {
+        // did we go the full range?
+        if (reverse_threshold && initial_value > 0.5f && min_value <= -0.5f)
+          other_key.modifier = InputModifier::FullAxis;
+
         // if this key is in our new binding list, it's a "release", and we're done
         addNewBinding();
         stopListeningForInput();
@@ -268,10 +294,11 @@ void InputBindingDialog::inputManagerHookCallback(InputBindingKey key, float val
   }
 
   // new binding, add it to the list, but wait for a decent distance first, and then wait for release
-  if (abs_value >= 0.5f)
+  if ((reverse_threshold ? (abs_value < 0.5f) : (abs_value >= 0.5f)))
   {
     InputBindingKey key_to_add = key;
-    key_to_add.negative = (value < 0.0f);
+    key_to_add.modifier = (value < 0.0f && !reverse_threshold) ? InputModifier::Negate : InputModifier::None;
+    key_to_add.invert = reverse_threshold;
     m_new_bindings.push_back(key_to_add);
   }
 }
diff --git a/src/duckstation-qt/inputbindingdialog.h b/src/duckstation-qt/inputbindingdialog.h
index 2ac4c0a51..4747e0328 100644
--- a/src/duckstation-qt/inputbindingdialog.h
+++ b/src/duckstation-qt/inputbindingdialog.h
@@ -17,8 +17,8 @@ class InputBindingDialog : public QDialog
   Q_OBJECT
 
 public:
-  InputBindingDialog(SettingsInterface* sif, std::string section_name, std::string key_name,
-                     std::vector<std::string> bindings, QWidget* parent);
+  InputBindingDialog(SettingsInterface* sif, InputBindingInfo::Type bind_type, std::string section_name,
+                     std::string key_name, std::vector<std::string> bindings, QWidget* parent);
   ~InputBindingDialog();
 
 protected Q_SLOTS:
@@ -51,13 +51,15 @@ protected:
   Ui::InputBindingDialog m_ui;
 
   SettingsInterface* m_sif;
+  InputBindingInfo::Type m_bind_type;
   std::string m_section_name;
   std::string m_key_name;
   std::vector<std::string> m_bindings;
   std::vector<InputBindingKey> m_new_bindings;
+  std::vector<std::pair<InputBindingKey, std::pair<float, float>>> m_value_ranges;
 
   QTimer* m_input_listen_timer = nullptr;
   u32 m_input_listen_remaining_seconds = 0;
-  QPointF m_input_listen_start_position{};
+  QPoint m_input_listen_start_position{};
   bool m_mouse_mapping_enabled = false;
 };
diff --git a/src/duckstation-qt/inputbindingwidgets.cpp b/src/duckstation-qt/inputbindingwidgets.cpp
index e1a8b0554..a1a9ac749 100644
--- a/src/duckstation-qt/inputbindingwidgets.cpp
+++ b/src/duckstation-qt/inputbindingwidgets.cpp
@@ -22,8 +22,8 @@ InputBindingWidget::InputBindingWidget(QWidget* parent) : QPushButton(parent)
   connect(this, &QPushButton::clicked, this, &InputBindingWidget::onClicked);
 }
 
-InputBindingWidget::InputBindingWidget(QWidget* parent, SettingsInterface* sif, std::string section_name,
-                                       std::string key_name)
+InputBindingWidget::InputBindingWidget(QWidget* parent, SettingsInterface* sif, InputBindingInfo::Type bind_type,
+                                       std::string section_name, std::string key_name)
   : QPushButton(parent)
 {
   setMinimumWidth(225);
@@ -31,7 +31,7 @@ InputBindingWidget::InputBindingWidget(QWidget* parent, SettingsInterface* sif,
 
   connect(this, &QPushButton::clicked, this, &InputBindingWidget::onClicked);
 
-  initialize(sif, std::move(section_name), std::move(key_name));
+  initialize(sif, bind_type, std::move(section_name), std::move(key_name));
 }
 
 InputBindingWidget::~InputBindingWidget()
@@ -44,9 +44,11 @@ bool InputBindingWidget::isMouseMappingEnabled()
   return Host::GetBaseBoolSettingValue("UI", "EnableMouseMapping", false);
 }
 
-void InputBindingWidget::initialize(SettingsInterface* sif, std::string section_name, std::string key_name)
+void InputBindingWidget::initialize(SettingsInterface* sif, InputBindingInfo::Type bind_type, std::string section_name,
+                                    std::string key_name)
 {
   m_sif = sif;
+  m_bind_type = bind_type;
   m_section_name = std::move(section_name);
   m_key_name = std::move(key_name);
   reloadBinding();
@@ -109,8 +111,9 @@ bool InputBindingWidget::eventFilter(QObject* watched, QEvent* event)
   else if (event_type == QEvent::MouseButtonPress || event_type == QEvent::MouseButtonDblClick)
   {
     // double clicks get triggered if we click bind, then click again quickly.
-    const u32 button_index = CountTrailingZeros(static_cast<u32>(static_cast<const QMouseEvent*>(event)->button()));
-    m_new_bindings.push_back(InputManager::MakePointerButtonKey(0, button_index));
+    unsigned long button_index;
+    if (_BitScanForward(&button_index, static_cast<u32>(static_cast<const QMouseEvent*>(event)->button())))
+      m_new_bindings.push_back(InputManager::MakePointerButtonKey(0, button_index));
     return true;
   }
   else if (event_type == QEvent::Wheel)
@@ -120,7 +123,7 @@ bool InputBindingWidget::eventFilter(QObject* watched, QEvent* event)
     if (dx != 0.0f)
     {
       InputBindingKey key(InputManager::MakePointerAxisKey(0, InputPointerAxis::WheelX));
-      key.negative = (dx < 0.0f);
+      key.modifier = dx < 0.0f ? InputModifier::Negate : InputModifier::None;
       m_new_bindings.push_back(key);
     }
 
@@ -128,7 +131,7 @@ bool InputBindingWidget::eventFilter(QObject* watched, QEvent* event)
     if (dy != 0.0f)
     {
       InputBindingKey key(InputManager::MakePointerAxisKey(0, InputPointerAxis::WheelY));
-      key.negative = (dy < 0.0f);
+      key.modifier = dy < 0.0f ? InputModifier::Negate : InputModifier::None;
       m_new_bindings.push_back(key);
     }
 
@@ -145,20 +148,20 @@ bool InputBindingWidget::eventFilter(QObject* watched, QEvent* event)
     // if we've moved more than a decent distance from the center of the widget, bind it.
     // this is so we don't accidentally bind to the mouse if you bump it while reaching for your pad.
     static constexpr const s32 THRESHOLD = 50;
-    const QPointF diff(static_cast<QMouseEvent*>(event)->globalPosition() - m_input_listen_start_position);
+    const QPoint diff(static_cast<QMouseEvent*>(event)->globalPosition().toPoint() - m_input_listen_start_position);
     bool has_one = false;
 
     if (std::abs(diff.x()) >= THRESHOLD)
     {
       InputBindingKey key(InputManager::MakePointerAxisKey(0, InputPointerAxis::X));
-      key.negative = (diff.x() < 0);
+      key.modifier = diff.x() < 0 ? InputModifier::Negate : InputModifier::None;
       m_new_bindings.push_back(key);
       has_one = true;
     }
     if (std::abs(diff.y()) >= THRESHOLD)
     {
       InputBindingKey key(InputManager::MakePointerAxisKey(0, InputPointerAxis::Y));
-      key.negative = (diff.y() < 0);
+      key.modifier = diff.y() < 0 ? InputModifier::Negate : InputModifier::None;
       m_new_bindings.push_back(key);
       has_one = true;
     }
@@ -205,8 +208,8 @@ void InputBindingWidget::setNewBinding()
   if (m_new_bindings.empty())
     return;
 
-  const std::string new_binding(
-    InputManager::ConvertInputBindingKeysToString(m_new_bindings.data(), m_new_bindings.size()));
+  std::string new_binding(
+    InputManager::ConvertInputBindingKeysToString(m_bind_type, m_new_bindings.data(), m_new_bindings.size()));
   if (!new_binding.empty())
   {
     if (m_sif)
@@ -280,6 +283,7 @@ void InputBindingWidget::onInputListenTimerTimeout()
 
 void InputBindingWidget::startListeningForInput(u32 timeout_in_seconds)
 {
+  m_value_ranges.clear();
   m_new_bindings.clear();
   m_mouse_mapping_enabled = isMouseMappingEnabled();
   m_input_listen_start_position = QCursor::pos();
@@ -315,14 +319,37 @@ void InputBindingWidget::stopListeningForInput()
 
 void InputBindingWidget::inputManagerHookCallback(InputBindingKey key, float value)
 {
-  const float abs_value = std::abs(value);
+  if (!isListeningForInput())
+    return;
 
-  for (InputBindingKey other_key : m_new_bindings)
+  float initial_value = value;
+  float min_value = value;
+  auto it = std::find_if(m_value_ranges.begin(), m_value_ranges.end(),
+                         [key](const auto& it) { return it.first.bits == key.bits; });
+  if (it != m_value_ranges.end())
+  {
+    initial_value = it->second.first;
+    min_value = it->second.second = std::min(it->second.second, value);
+  }
+  else
+  {
+    m_value_ranges.emplace_back(key, std::make_pair(initial_value, min_value));
+  }
+
+  const float abs_value = std::abs(value);
+  const bool reverse_threshold = (key.source_subtype == InputSubclass::ControllerAxis && initial_value > 0.5f);
+
+  for (InputBindingKey& other_key : m_new_bindings)
   {
     if (other_key.MaskDirection() == key.MaskDirection())
     {
-      if (abs_value < 0.5f)
+      // for pedals, we wait for it to go back to near its starting point to commit the binding
+      if ((reverse_threshold ? ((initial_value - value) <= 0.25f) : (abs_value < 0.5f)))
       {
+        // did we go the full range?
+        if (reverse_threshold && initial_value > 0.5f && min_value <= -0.5f)
+          other_key.modifier = InputModifier::FullAxis;
+
         // if this key is in our new binding list, it's a "release", and we're done
         setNewBinding();
         stopListeningForInput();
@@ -335,10 +362,11 @@ void InputBindingWidget::inputManagerHookCallback(InputBindingKey key, float val
   }
 
   // new binding, add it to the list, but wait for a decent distance first, and then wait for release
-  if (abs_value >= 0.5f)
+  if ((reverse_threshold ? (abs_value < 0.5f) : (abs_value >= 0.5f)))
   {
     InputBindingKey key_to_add = key;
-    key_to_add.negative = (value < 0.0f);
+    key_to_add.modifier = (value < 0.0f && !reverse_threshold) ? InputModifier::Negate : InputModifier::None;
+    key_to_add.invert = reverse_threshold;
     m_new_bindings.push_back(key_to_add);
   }
 }
@@ -359,7 +387,8 @@ void InputBindingWidget::unhookInputManager()
 
 void InputBindingWidget::openDialog()
 {
-  InputBindingDialog binding_dialog(m_sif, m_section_name, m_key_name, m_bindings, QtUtils::GetRootWidget(this));
+  InputBindingDialog binding_dialog(m_sif, m_bind_type, m_section_name, m_key_name, m_bindings,
+                                    QtUtils::GetRootWidget(this));
   binding_dialog.exec();
   reloadBinding();
 }
diff --git a/src/duckstation-qt/inputbindingwidgets.h b/src/duckstation-qt/inputbindingwidgets.h
index 8e182263e..9b331480b 100644
--- a/src/duckstation-qt/inputbindingwidgets.h
+++ b/src/duckstation-qt/inputbindingwidgets.h
@@ -18,12 +18,14 @@ class InputBindingWidget : public QPushButton
 
 public:
   InputBindingWidget(QWidget* parent);
-  InputBindingWidget(QWidget* parent, SettingsInterface* sif, std::string section_name, std::string key_name);
+  InputBindingWidget(QWidget* parent, SettingsInterface* sif, InputBindingInfo::Type bind_type,
+                     std::string section_name, std::string key_name);
   ~InputBindingWidget();
 
   static bool isMouseMappingEnabled();
 
-  void initialize(SettingsInterface* sif, std::string section_name, std::string key_name);
+  void initialize(SettingsInterface* sif, InputBindingInfo::Type bind_type, std::string section_name,
+                  std::string key_name);
 
 public Q_SLOTS:
   void clearBinding();
@@ -57,13 +59,15 @@ protected:
   void unhookInputManager();
 
   SettingsInterface* m_sif = nullptr;
+  InputBindingInfo::Type m_bind_type = InputBindingInfo::Type::Unknown;
   std::string m_section_name;
   std::string m_key_name;
   std::vector<std::string> m_bindings;
   std::vector<InputBindingKey> m_new_bindings;
+  std::vector<std::pair<InputBindingKey, std::pair<float, float>>> m_value_ranges;
   QTimer* m_input_listen_timer = nullptr;
   u32 m_input_listen_remaining_seconds = 0;
-  QPointF m_input_listen_start_position{};
+  QPoint m_input_listen_start_position{};
   bool m_mouse_mapping_enabled = false;
 };
 
diff --git a/src/frontend-common/dinput_source.cpp b/src/frontend-common/dinput_source.cpp
index 03b091489..44ddd268b 100644
--- a/src/frontend-common/dinput_source.cpp
+++ b/src/frontend-common/dinput_source.cpp
@@ -56,7 +56,7 @@ std::string DInputSource::GetDeviceIdentifier(u32 index)
 }
 
 static constexpr std::array<const char*, DInputSource::NUM_HAT_DIRECTIONS> s_hat_directions = {
-  {"Up", "Right", "Down", "Left"}};
+  {"Up", "Down", "Left", "Right"}};
 
 bool DInputSource::Initialize(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock)
 {
@@ -88,14 +88,15 @@ bool DInputSource::Initialize(SettingsInterface& si, std::unique_lock<std::mutex
 
   // need to release the lock while we're enumerating, because we call winId().
   settings_lock.unlock();
-	const std::optional<WindowInfo> toplevel_wi(Host::GetTopLevelWindowInfo());
-	if (!toplevel_wi.has_value() || toplevel_wi->type != WindowInfo::Type::Win32)
-	{
-		Log_ErrorPrintf("Missing top level window, cannot add DInput devices.");
-		return false;
-	}
+  const std::optional<WindowInfo> toplevel_wi(Host::GetTopLevelWindowInfo());
   settings_lock.lock();
 
+  if (!toplevel_wi.has_value() || toplevel_wi->type != WindowInfo::Type::Win32)
+  {
+    Log_ErrorPrintf("Missing top level window, cannot add DInput devices.");
+    return false;
+  }
+
   m_toplevel_window = static_cast<HWND>(toplevel_wi->window_handle);
   ReloadDevices();
   return true;
@@ -106,15 +107,6 @@ void DInputSource::UpdateSettings(SettingsInterface& si, std::unique_lock<std::m
   // noop
 }
 
-void DInputSource::Shutdown()
-{
-  while (!m_controllers.empty())
-  {
-    Host::OnInputDeviceDisconnected(GetDeviceIdentifier(static_cast<u32>(m_controllers.size() - 1)));
-    m_controllers.pop_back();
-  }
-}
-
 static BOOL CALLBACK EnumCallback(LPCDIDEVICEINSTANCEW lpddi, LPVOID pvRef)
 {
   static_cast<std::vector<DIDEVICEINSTANCEW>*>(pvRef)->push_back(*lpddi);
@@ -126,6 +118,7 @@ bool DInputSource::ReloadDevices()
   // detect any removals
   PollEvents();
 
+  // look for new devices
   std::vector<DIDEVICEINSTANCEW> devices;
   m_dinput->EnumDevices(DI8DEVCLASS_GAMECTRL, EnumCallback, &devices, DIEDFL_ATTACHEDONLY);
 
@@ -156,7 +149,7 @@ bool DInputSource::ReloadDevices()
     {
       const u32 index = static_cast<u32>(m_controllers.size());
       m_controllers.push_back(std::move(cd));
-      Host::OnInputDeviceConnected(GetDeviceIdentifier(index), name);
+      InputManager::OnInputDeviceConnected(GetDeviceIdentifier(index), name);
       changed = true;
     }
   }
@@ -164,6 +157,15 @@ bool DInputSource::ReloadDevices()
   return changed;
 }
 
+void DInputSource::Shutdown()
+{
+  while (!m_controllers.empty())
+  {
+    InputManager::OnInputDeviceDisconnected(GetDeviceIdentifier(static_cast<u32>(m_controllers.size() - 1)));
+    m_controllers.pop_back();
+  }
+}
+
 bool DInputSource::AddDevice(ControllerData& cd, const std::string& name)
 {
   HRESULT hr = cd.device->SetCooperativeLevel(m_toplevel_window, DISCL_BACKGROUND | DISCL_EXCLUSIVE);
@@ -204,11 +206,11 @@ bool DInputSource::AddDevice(ControllerData& cd, const std::string& name)
 
   cd.num_buttons = caps.dwButtons;
 
-  static constexpr auto axis_offsets =
-    make_array(offsetof(DIJOYSTATE, lX), offsetof(DIJOYSTATE, lY), offsetof(DIJOYSTATE, lZ), offsetof(DIJOYSTATE, lRz),
-               offsetof(DIJOYSTATE, lRx), offsetof(DIJOYSTATE, lRy), offsetof(DIJOYSTATE, rglSlider[0]),
-               offsetof(DIJOYSTATE, rglSlider[1]));
-  for (const auto offset : axis_offsets)
+  static constexpr const u32 axis_offsets[] = {offsetof(DIJOYSTATE, lX),           offsetof(DIJOYSTATE, lY),
+                                               offsetof(DIJOYSTATE, lZ),           offsetof(DIJOYSTATE, lRz),
+                                               offsetof(DIJOYSTATE, lRx),          offsetof(DIJOYSTATE, lRy),
+                                               offsetof(DIJOYSTATE, rglSlider[0]), offsetof(DIJOYSTATE, rglSlider[1])};
+  for (const u32 offset : axis_offsets)
   {
     // ask for 16 bits of axis range
     DIPROPRANGE range = {};
@@ -222,9 +224,7 @@ bool DInputSource::AddDevice(ControllerData& cd, const std::string& name)
 
     // did it apply?
     if (SUCCEEDED(cd.device->GetProperty(DIPROP_RANGE, &range.diph)))
-    {
-      cd.axis_offsets.push_back(static_cast<u32>(offset));
-    }
+      cd.axis_offsets.push_back(offset);
   }
 
   cd.num_hats = caps.dwPOVs;
@@ -263,7 +263,7 @@ void DInputSource::PollEvents()
 
       if (hr != DI_OK)
       {
-        Host::OnInputDeviceDisconnected(GetDeviceIdentifier(static_cast<u32>(i)));
+        InputManager::OnInputDeviceDisconnected(GetDeviceIdentifier(static_cast<u32>(i)));
         m_controllers.erase(m_controllers.begin() + i);
         continue;
       }
@@ -336,13 +336,28 @@ std::optional<InputBindingKey> DInputSource::ParseKeyString(const std::string_vi
 
   if (StringUtil::StartsWith(binding, "+Axis") || StringUtil::StartsWith(binding, "-Axis"))
   {
-    const std::optional<u32> axis_index = StringUtil::FromChars<u32>(binding.substr(5));
+    std::string_view end;
+    const std::optional<u32> axis_index = StringUtil::FromChars<u32>(binding.substr(5), 10, &end);
     if (!axis_index.has_value())
       return std::nullopt;
 
     key.source_subtype = InputSubclass::ControllerAxis;
     key.data = axis_index.value();
-    key.negative = (binding[0] == '-');
+    key.modifier = (binding[0] == '-') ? InputModifier::Negate : InputModifier::None;
+    key.invert = (end == "~");
+    return key;
+  }
+  else if (StringUtil::StartsWith(binding, "FullAxis"))
+  {
+    std::string_view end;
+    const std::optional<u32> axis_index = StringUtil::FromChars<u32>(binding.substr(8), 10, &end);
+    if (!axis_index.has_value())
+      return std::nullopt;
+
+    key.source_subtype = InputSubclass::ControllerAxis;
+    key.data = axis_index.value();
+    key.modifier = InputModifier::FullAxis;
+    key.invert = (end == "~");
     return key;
   }
   else if (StringUtil::StartsWith(binding, "Hat"))
@@ -388,7 +403,9 @@ std::string DInputSource::ConvertKeyToString(InputBindingKey key)
   {
     if (key.source_subtype == InputSubclass::ControllerAxis)
     {
-      ret = fmt::format("DInput-{}/{}Axis{}", u32(key.source_index), key.negative ? '-' : '+', u32(key.data));
+      const char* modifier =
+        (key.modifier == InputModifier::FullAxis ? "Full" : (key.modifier == InputModifier::Negate ? "-" : "+"));
+      ret = fmt::format("DInput-{}/{}Axis{}{}", u32(key.source_index), modifier, u32(key.data), key.invert ? "~" : "");
     }
     else if (key.source_subtype == InputSubclass::ControllerButton && key.data >= MAX_NUM_BUTTONS)
     {
diff --git a/src/frontend-common/dinput_source.h b/src/frontend-common/dinput_source.h
index 1d3872ba5..9151a26be 100644
--- a/src/frontend-common/dinput_source.h
+++ b/src/frontend-common/dinput_source.h
@@ -4,8 +4,8 @@
 #pragma once
 #define DIRECTINPUT_VERSION 0x0800
 #include "common/windows_headers.h"
-#include "input_source.h"
 #include "core/types.h"
+#include "input_source.h"
 #include <array>
 #include <dinput.h>
 #include <functional>
@@ -80,7 +80,7 @@ private:
   ControllerDataArray m_controllers;
 
   HMODULE m_dinput_module{};
-  LPCDIDATAFORMAT m_joystick_data_format{};
   ComPtr<IDirectInput8W> m_dinput;
+  LPCDIDATAFORMAT m_joystick_data_format{};
   HWND m_toplevel_window = NULL;
 };
diff --git a/src/frontend-common/fullscreen_ui.cpp b/src/frontend-common/fullscreen_ui.cpp
index d62a0b2b0..3ec2dec5d 100644
--- a/src/frontend-common/fullscreen_ui.cpp
+++ b/src/frontend-common/fullscreen_ui.cpp
@@ -44,6 +44,8 @@
 #include <bitset>
 #include <thread>
 #include <unordered_map>
+#include <utility>
+#include <vector>
 Log_SetChannel(FullscreenUI);
 
 #ifdef WITH_CHEEVOS
@@ -362,11 +364,10 @@ static void PopulateGraphicsAdapterList();
 static void PopulateGameListDirectoryCache(SettingsInterface* si);
 static void PopulatePostProcessingChain();
 static void SavePostProcessingChain();
-static void BeginInputBinding(SettingsInterface* bsi, Controller::ControllerBindingType type,
-                              const std::string_view& section, const std::string_view& key,
-                              const std::string_view& display_name);
+static void BeginInputBinding(SettingsInterface* bsi, InputBindingInfo::Type type, const std::string_view& section,
+                              const std::string_view& key, const std::string_view& display_name);
 static void DrawInputBindingWindow();
-static void DrawInputBindingButton(SettingsInterface* bsi, Controller::ControllerBindingType type, const char* section,
+static void DrawInputBindingButton(SettingsInterface* bsi, InputBindingInfo::Type type, const char* section,
                                    const char* name, const char* display_name, bool show_type = true);
 static void ClearInputBindingVariables();
 static void StartAutomaticBinding(u32 port);
@@ -381,11 +382,12 @@ static FrontendCommon::PostProcessingChain s_postprocessing_chain;
 static std::vector<const HotkeyInfo*> s_hotkey_list_cache;
 static std::atomic_bool s_settings_changed{false};
 static std::atomic_bool s_game_settings_changed{false};
-static Controller::ControllerBindingType s_input_binding_type = Controller::ControllerBindingType::Unknown;
+static InputBindingInfo::Type s_input_binding_type = InputBindingInfo::Type::Unknown;
 static std::string s_input_binding_section;
 static std::string s_input_binding_key;
 static std::string s_input_binding_display_name;
 static std::vector<InputBindingKey> s_input_binding_new_bindings;
+static std::vector<std::pair<InputBindingKey, std::pair<float, float>>> s_input_binding_value_ranges;
 static Common::Timer s_input_binding_timer;
 
 //////////////////////////////////////////////////////////////////////////
@@ -788,7 +790,7 @@ void FullscreenUI::Render()
   if (s_about_window_open)
     DrawAboutWindow();
 
-  if (s_input_binding_type != Controller::ControllerBindingType::Unknown)
+  if (s_input_binding_type != InputBindingInfo::Type::Unknown)
     DrawInputBindingWindow();
 
   ImGuiFullscreen::EndLayout();
@@ -1277,9 +1279,8 @@ std::string FullscreenUI::GetEffectiveStringSetting(SettingsInterface* bsi, cons
   return ret;
 }
 
-void FullscreenUI::DrawInputBindingButton(SettingsInterface* bsi, Controller::ControllerBindingType type,
-                                          const char* section, const char* name, const char* display_name,
-                                          bool show_type)
+void FullscreenUI::DrawInputBindingButton(SettingsInterface* bsi, InputBindingInfo::Type type, const char* section,
+                                          const char* name, const char* display_name, bool show_type)
 {
   TinyString title;
   title.Fmt("{}/{}", section, name);
@@ -1299,17 +1300,17 @@ void FullscreenUI::DrawInputBindingButton(SettingsInterface* bsi, Controller::Co
   {
     switch (type)
     {
-      case Controller::ControllerBindingType::Button:
+      case InputBindingInfo::Type::Button:
         title = fmt::format(ICON_FA_DOT_CIRCLE " {}", display_name);
         break;
-      case Controller::ControllerBindingType::Axis:
-      case Controller::ControllerBindingType::HalfAxis:
+      case InputBindingInfo::Type::Axis:
+      case InputBindingInfo::Type::HalfAxis:
         title = fmt::format(ICON_FA_BULLSEYE " {}", display_name);
         break;
-      case Controller::ControllerBindingType::Motor:
+      case InputBindingInfo::Type::Motor:
         title = fmt::format(ICON_FA_BELL " {}", display_name);
         break;
-      case Controller::ControllerBindingType::Macro:
+      case InputBindingInfo::Type::Macro:
         title = fmt::format(ICON_FA_PIZZA_SLICE " {}", display_name);
         break;
       default:
@@ -1343,18 +1344,19 @@ void FullscreenUI::DrawInputBindingButton(SettingsInterface* bsi, Controller::Co
 
 void FullscreenUI::ClearInputBindingVariables()
 {
-  s_input_binding_type = Controller::ControllerBindingType::Unknown;
+  s_input_binding_type = InputBindingInfo::Type::Unknown;
   s_input_binding_section = {};
   s_input_binding_key = {};
   s_input_binding_display_name = {};
   s_input_binding_new_bindings = {};
+  s_input_binding_value_ranges = {};
 }
 
-void FullscreenUI::BeginInputBinding(SettingsInterface* bsi, Controller::ControllerBindingType type,
+void FullscreenUI::BeginInputBinding(SettingsInterface* bsi, InputBindingInfo::Type type,
                                      const std::string_view& section, const std::string_view& key,
                                      const std::string_view& display_name)
 {
-  if (s_input_binding_type != Controller::ControllerBindingType::Unknown)
+  if (s_input_binding_type != InputBindingInfo::Type::Unknown)
   {
     InputManager::RemoveHook();
     ClearInputBindingVariables();
@@ -1365,25 +1367,50 @@ void FullscreenUI::BeginInputBinding(SettingsInterface* bsi, Controller::Control
   s_input_binding_key = key;
   s_input_binding_display_name = display_name;
   s_input_binding_new_bindings = {};
+  s_input_binding_value_ranges = {};
   s_input_binding_timer.Reset();
 
-  InputManager::SetHook([game_settings = IsEditingGameSettings(bsi)](
-                          InputBindingKey key, float value) -> InputInterceptHook::CallbackResult {
+  const bool game_settings = IsEditingGameSettings(bsi);
+
+  InputManager::SetHook([game_settings](InputBindingKey key, float value) -> InputInterceptHook::CallbackResult {
+    if (s_input_binding_type == InputBindingInfo::Type::Unknown)
+      return InputInterceptHook::CallbackResult::StopProcessingEvent;
+
     // holding the settings lock here will protect the input binding list
     auto lock = Host::GetSettingsLock();
 
-    const float abs_value = std::abs(value);
-
-    for (InputBindingKey other_key : s_input_binding_new_bindings)
+    float initial_value = value;
+    float min_value = value;
+    auto it = std::find_if(s_input_binding_value_ranges.begin(), s_input_binding_value_ranges.end(),
+                           [key](const auto& it) { return it.first.bits == key.bits; });
+    if (it != s_input_binding_value_ranges.end())
     {
+      initial_value = it->second.first;
+      min_value = it->second.second = std::min(it->second.second, value);
+    }
+    else
+    {
+      s_input_binding_value_ranges.emplace_back(key, std::make_pair(initial_value, min_value));
+    }
+
+    const float abs_value = std::abs(value);
+    const bool reverse_threshold = (key.source_subtype == InputSubclass::ControllerAxis && initial_value > 0.5f);
+
+    for (InputBindingKey& other_key : s_input_binding_new_bindings)
+    {
+      // if this key is in our new binding list, it's a "release", and we're done
       if (other_key.MaskDirection() == key.MaskDirection())
       {
-        if (abs_value < 0.5f)
+        // for pedals, we wait for it to go back to near its starting point to commit the binding
+        if ((reverse_threshold ? ((initial_value - value) <= 0.25f) : (abs_value < 0.5f)))
         {
-          // if this key is in our new binding list, it's a "release", and we're done
+          // did we go the full range?
+          if (reverse_threshold && initial_value > 0.5f && min_value <= -0.5f)
+            other_key.modifier = InputModifier::FullAxis;
+
           SettingsInterface* bsi = GetEditingSettingsInterface(game_settings);
           const std::string new_binding(InputManager::ConvertInputBindingKeysToString(
-            s_input_binding_new_bindings.data(), s_input_binding_new_bindings.size()));
+            s_input_binding_type, s_input_binding_new_bindings.data(), s_input_binding_new_bindings.size()));
           bsi->SetStringValue(s_input_binding_section.c_str(), s_input_binding_key.c_str(), new_binding.c_str());
           SetSettingsChanged(bsi);
           ClearInputBindingVariables();
@@ -1396,10 +1423,11 @@ void FullscreenUI::BeginInputBinding(SettingsInterface* bsi, Controller::Control
     }
 
     // new binding, add it to the list, but wait for a decent distance first, and then wait for release
-    if (abs_value >= 0.5f)
+    if ((reverse_threshold ? (abs_value < 0.5f) : (abs_value >= 0.5f)))
     {
       InputBindingKey key_to_add = key;
-      key_to_add.negative = (value < 0.0f);
+      key_to_add.modifier = (value < 0.0f && !reverse_threshold) ? InputModifier::Negate : InputModifier::None;
+      key_to_add.invert = reverse_threshold;
       s_input_binding_new_bindings.push_back(key_to_add);
     }
 
@@ -1409,7 +1437,7 @@ void FullscreenUI::BeginInputBinding(SettingsInterface* bsi, Controller::Control
 
 void FullscreenUI::DrawInputBindingWindow()
 {
-  DebugAssert(s_input_binding_type != Controller::ControllerBindingType::Unknown);
+  DebugAssert(s_input_binding_type != InputBindingInfo::Type::Unknown);
 
   const double time_remaining = INPUT_BINDING_TIMEOUT_SECONDS - s_input_binding_timer.GetTimeSeconds();
   if (time_remaining <= 0.0)
@@ -3180,7 +3208,7 @@ void FullscreenUI::DrawControllerSettingsPage()
 
     for (u32 macro_index = 0; macro_index < InputManager::NUM_MACRO_BUTTONS_PER_CONTROLLER; macro_index++)
     {
-      DrawInputBindingButton(bsi, Controller::ControllerBindingType::Macro, section.c_str(),
+      DrawInputBindingButton(bsi, InputBindingInfo::Type::Macro, section.c_str(),
                              fmt::format("Macro{}", macro_index + 1).c_str(),
                              fmt::format("Macro {} Trigger", macro_index + 1).c_str());
 
@@ -3194,9 +3222,8 @@ void FullscreenUI::DrawControllerSettingsPage()
         for (u32 i = 0; i < ci->num_bindings; i++)
         {
           const Controller::ControllerBindingInfo& bi = ci->bindings[i];
-          if (bi.type != Controller::ControllerBindingType::Button &&
-              bi.type != Controller::ControllerBindingType::Axis &&
-              bi.type != Controller::ControllerBindingType::HalfAxis)
+          if (bi.type != InputBindingInfo::Type::Button && bi.type != InputBindingInfo::Type::Axis &&
+              bi.type != InputBindingInfo::Type::HalfAxis)
           {
             continue;
           }
@@ -3350,8 +3377,7 @@ void FullscreenUI::DrawHotkeySettingsPage()
       last_category = hotkey;
     }
 
-    DrawInputBindingButton(bsi, Controller::ControllerBindingType::Button, "Hotkeys", hotkey->name,
-                           hotkey->display_name, false);
+    DrawInputBindingButton(bsi, InputBindingInfo::Type::Button, "Hotkeys", hotkey->name, hotkey->display_name, false);
   }
 
   EndMenuButtons();
@@ -5831,9 +5857,12 @@ void FullscreenUI::HandleGameListActivate(const GameList::Entry* entry)
 void FullscreenUI::HandleGameListOptions(const GameList::Entry* entry)
 {
   ImGuiFullscreen::ChoiceDialogOptions options = {
-    {ICON_FA_WRENCH " Game Properties", false},  {ICON_FA_PLAY " Resume Game", false},
-    {ICON_FA_UNDO " Load State", false},         {ICON_FA_COMPACT_DISC " Default Boot", false},
-    {ICON_FA_LIGHTBULB " Fast Boot", false},     {ICON_FA_MAGIC " Slow Boot", false},
+    {ICON_FA_WRENCH " Game Properties", false},
+    {ICON_FA_PLAY " Resume Game", false},
+    {ICON_FA_UNDO " Load State", false},
+    {ICON_FA_COMPACT_DISC " Default Boot", false},
+    {ICON_FA_LIGHTBULB " Fast Boot", false},
+    {ICON_FA_MAGIC " Slow Boot", false},
     {ICON_FA_FOLDER_MINUS " Reset Play Time", false},
     {ICON_FA_WINDOW_CLOSE " Close Menu", false},
   };
diff --git a/src/frontend-common/imgui_overlays.cpp b/src/frontend-common/imgui_overlays.cpp
index b5a8ae9a8..0a8c2d368 100644
--- a/src/frontend-common/imgui_overlays.cpp
+++ b/src/frontend-common/imgui_overlays.cpp
@@ -523,8 +523,8 @@ void ImGuiManager::DrawInputsOverlay()
       const Controller::ControllerBindingInfo& bi = cinfo->bindings[bind];
       switch (bi.type)
       {
-        case Controller::ControllerBindingType::Axis:
-        case Controller::ControllerBindingType::HalfAxis:
+        case InputBindingInfo::Type::Axis:
+        case InputBindingInfo::Type::HalfAxis:
         {
           // axes are always shown
           const float value = controller->GetBindState(bi.bind_index);
@@ -535,7 +535,7 @@ void ImGuiManager::DrawInputsOverlay()
         }
         break;
 
-        case Controller::ControllerBindingType::Button:
+        case InputBindingInfo::Type::Button:
         {
           // buttons only shown when active
           const float value = controller->GetBindState(bi.bind_index);
@@ -544,9 +544,10 @@ void ImGuiManager::DrawInputsOverlay()
         }
         break;
 
-        case Controller::ControllerBindingType::Motor:
-        case Controller::ControllerBindingType::Macro:
-        case Controller::ControllerBindingType::Unknown:
+        case InputBindingInfo::Type::Motor:
+        case InputBindingInfo::Type::Macro:
+        case InputBindingInfo::Type::Unknown:
+        case InputBindingInfo::Type::Pointer:
         default:
           break;
       }
diff --git a/src/frontend-common/input_manager.cpp b/src/frontend-common/input_manager.cpp
index ef51637f5..98893e08b 100644
--- a/src/frontend-common/input_manager.cpp
+++ b/src/frontend-common/input_manager.cpp
@@ -281,48 +281,72 @@ bool InputManager::ParseBindingAndGetSource(const std::string_view& binding, Inp
   return false;
 }
 
-std::string InputManager::ConvertInputBindingKeyToString(InputBindingKey key)
+std::string InputManager::ConvertInputBindingKeyToString(InputBindingInfo::Type binding_type, InputBindingKey key)
 {
-  if (key.source_type == InputSourceType::Keyboard)
+  if (binding_type == InputBindingInfo::Type::Pointer)
   {
-    const std::optional<std::string> str(ConvertHostKeyboardCodeToString(key.data));
-    if (str.has_value() && !str->empty())
-      return fmt::format("Keyboard/{}", str->c_str());
-  }
-  else if (key.source_type == InputSourceType::Pointer)
-  {
-    if (key.source_subtype == InputSubclass::PointerButton)
+    // pointer and device bindings don't have a data part
+    if (key.source_type == InputSourceType::Pointer)
     {
-      if (key.data < s_pointer_button_names.size())
-        return fmt::format("Pointer-{}/{}", u32{key.source_index}, s_pointer_button_names[key.data]);
-      else
-        return fmt::format("Pointer-{}/Button{}", u32{key.source_index}, key.data);
+      return GetPointerDeviceName(key.data);
     }
-    else if (key.source_subtype == InputSubclass::PointerAxis)
+    else if (key.source_type < InputSourceType::Count && s_input_sources[static_cast<u32>(key.source_type)])
     {
-      return fmt::format("Pointer-{}/{}{:c}", u32{key.source_index}, s_pointer_axis_names[key.data],
-                         key.negative ? '-' : '+');
+      // This assumes that it always follows the Type/Binding form.
+      std::string keystr(s_input_sources[static_cast<u32>(key.source_type)]->ConvertKeyToString(key));
+      std::string::size_type pos = keystr.find('/');
+      if (pos != std::string::npos)
+        keystr.erase(pos);
+      return keystr;
     }
   }
-  else if (key.source_type == InputSourceType::Sensor)
+  else
   {
-    if (key.source_subtype == InputSubclass::SensorAccelerometer && key.data < s_sensor_accelerometer_names.size())
-      return fmt::format("Sensor/{}", s_sensor_accelerometer_names[key.data]);
-  }
-  else if (key.source_type < InputSourceType::Count && s_input_sources[static_cast<u32>(key.source_type)])
-  {
-    return s_input_sources[static_cast<u32>(key.source_type)]->ConvertKeyToString(key);
+    if (key.source_type == InputSourceType::Keyboard)
+    {
+      const std::optional<std::string> str(ConvertHostKeyboardCodeToString(key.data));
+      if (str.has_value() && !str->empty())
+        return fmt::format("Keyboard/{}", str->c_str());
+    }
+    else if (key.source_type == InputSourceType::Pointer)
+    {
+      if (key.source_subtype == InputSubclass::PointerButton)
+      {
+        if (key.data < s_pointer_button_names.size())
+          return fmt::format("Pointer-{}/{}", u32{key.source_index}, s_pointer_button_names[key.data]);
+        else
+          return fmt::format("Pointer-{}/Button{}", u32{key.source_index}, key.data);
+      }
+      else if (key.source_subtype == InputSubclass::PointerAxis)
+      {
+        return fmt::format("Pointer-{}/{}{:c}", u32{key.source_index}, s_pointer_axis_names[key.data],
+                           key.modifier == InputModifier::Negate ? '-' : '+');
+      }
+    }
+    else if (key.source_type < InputSourceType::Count && s_input_sources[static_cast<u32>(key.source_type)])
+    {
+      return s_input_sources[static_cast<u32>(key.source_type)]->ConvertKeyToString(key);
+    }
   }
 
   return {};
 }
 
-std::string InputManager::ConvertInputBindingKeysToString(const InputBindingKey* keys, size_t num_keys)
+std::string InputManager::ConvertInputBindingKeysToString(InputBindingInfo::Type binding_type,
+                                                          const InputBindingKey* keys, size_t num_keys)
 {
+  // can't have a chord of devices/pointers
+  if (binding_type == InputBindingInfo::Type::Pointer)
+  {
+    // so only take the first
+    if (num_keys > 0)
+      return ConvertInputBindingKeyToString(binding_type, keys[0]);
+  }
+
   std::stringstream ss;
   for (size_t i = 0; i < num_keys; i++)
   {
-    const std::string keystr(ConvertInputBindingKeyToString(keys[i]));
+    const std::string keystr(ConvertInputBindingKeyToString(binding_type, keys[i]));
     if (keystr.empty())
       return std::string();
 
@@ -574,9 +598,9 @@ std::optional<InputBindingKey> InputManager::ParsePointerKey(const std::string_v
 
       const std::string_view dir_part(sub_binding.substr(std::strlen(s_pointer_axis_names[i])));
       if (dir_part == "+")
-        key.negative = false;
+        key.modifier = InputModifier::None;
       else if (dir_part == "-")
-        key.negative = true;
+        key.modifier = InputModifier::Negate;
       else
         return std::nullopt;
 
@@ -597,6 +621,23 @@ std::optional<InputBindingKey> InputManager::ParsePointerKey(const std::string_v
   return std::nullopt;
 }
 
+std::optional<u32> InputManager::GetIndexFromPointerBinding(const std::string_view& source)
+{
+  if (!StringUtil::StartsWith(source, "Pointer-"))
+    return std::nullopt;
+
+  const std::optional<s32> pointer_index = StringUtil::FromChars<s32>(source.substr(8));
+  if (!pointer_index.has_value() || pointer_index.value() < 0)
+    return std::nullopt;
+
+  return static_cast<u32>(pointer_index.value());
+}
+
+std::string InputManager::GetPointerDeviceName(u32 pointer_index)
+{
+  return fmt::format("Pointer-{}", pointer_index);
+}
+
 std::optional<InputBindingKey> InputManager::ParseSensorKey(const std::string_view& source,
                                                             const std::string_view& sub_binding)
 {
@@ -616,9 +657,9 @@ std::optional<InputBindingKey> InputManager::ParseSensorKey(const std::string_vi
 
       const std::string_view dir_part(sub_binding.substr(std::strlen(s_sensor_accelerometer_names[i])));
       if (dir_part == "+")
-        key.negative = false;
+        key.modifier = InputModifier::None;
       else if (dir_part == "-")
-        key.negative = true;
+        key.modifier = InputModifier::Negate;
       else
         return std::nullopt;
 
@@ -777,7 +818,6 @@ bool InputManager::InvokeEvents(InputBindingKey key, float value, GenericInputBi
   for (auto it = range.first; it != range.second; ++it)
   {
     InputBinding* binding = it->second.get();
-
     // find the key which matches us
     for (u32 i = 0; i < binding->num_keys; i++)
     {
@@ -785,11 +825,26 @@ bool InputManager::InvokeEvents(InputBindingKey key, float value, GenericInputBi
         continue;
 
       const u8 bit = static_cast<u8>(1) << i;
-      const bool negative = binding->keys[i].negative;
+      const bool negative = binding->keys[i].modifier == InputModifier::Negate;
       const bool new_state = (negative ? (value < 0.0f) : (value > 0.0f));
+      float value_to_pass = 0.0f;
+      switch (binding->keys[i].modifier)
+      {
+        case InputModifier::None:
+          if (value > 0.0f)
+            value_to_pass = value;
+          break;
+        case InputModifier::Negate:
+          if (value < 0.0f)
+            value_to_pass = -value;
+          break;
+        case InputModifier::FullAxis:
+          value_to_pass = value * 0.5f + 0.5f;
+          break;
+      }
 
-      // invert if we're negative, since the handler expects 0..1
-      const float value_to_pass = (negative ? ((value < 0.0f) ? -value : 0.0f) : (value > 0.0f) ? value : 0.0f);
+      // handle inverting, needed for some wheels.
+      value_to_pass = binding->keys[i].invert ? (1.0f - value_to_pass) : value_to_pass;
 
       // axes are fired regardless of a state change, unless they're zero
       // (but going from not-zero to zero will still fire, because of the full state)
@@ -1130,6 +1185,16 @@ std::vector<std::string> InputManager::GetInputProfileNames()
   return ret;
 }
 
+void InputManager::OnInputDeviceConnected(const std::string_view& identifier, const std::string_view& device_name)
+{
+  Host::OnInputDeviceConnected(identifier, device_name);
+}
+
+void InputManager::OnInputDeviceDisconnected(const std::string_view& identifier)
+{
+  Host::OnInputDeviceDisconnected(identifier);
+}
+
 // ------------------------------------------------------------------------
 // Vibration
 // ------------------------------------------------------------------------
diff --git a/src/frontend-common/input_manager.h b/src/frontend-common/input_manager.h
index 65950278e..41a506a0c 100644
--- a/src/frontend-common/input_manager.h
+++ b/src/frontend-common/input_manager.h
@@ -14,6 +14,8 @@
 #include "common/types.h"
 #include "common/window_info.h"
 
+#include "core/input_types.h"
+
 /// Class, or source of an input event.
 enum class InputSourceType : u32
 {
@@ -47,12 +49,20 @@ enum class InputSubclass : u32
 
   ControllerButton = 0,
   ControllerAxis = 1,
-  ControllerMotor = 2,
-  ControllerHaptic = 3,
+  ControllerHat = 2,
+  ControllerMotor = 3,
+  ControllerHaptic = 4,
 
   SensorAccelerometer = 0,
 };
 
+enum class InputModifier : u32
+{
+  None = 0,
+  Negate,   ///< Input * -1, gets the negative side of the axis
+  FullAxis, ///< (Input * 0.5) + 0.5, uses both the negative and positive side of the axis together
+};
+
 /// A composite type representing a full input key which is part of an event.
 union InputBindingKey
 {
@@ -60,9 +70,10 @@ union InputBindingKey
   {
     InputSourceType source_type : 4;
     u32 source_index : 8;             ///< controller number
-    InputSubclass source_subtype : 2; ///< if 1, binding is for an axis and not a button (used for controllers)
-    u32 negative : 1;                 ///< if 1, binding is for the negative side of the axis
-    u32 unused : 17;
+    InputSubclass source_subtype : 3; ///< if 1, binding is for an axis and not a button (used for controllers)
+    InputModifier modifier : 2;
+    u32 invert : 1; ///< if 1, value is inverted prior to being sent to the sink
+    u32 unused : 14;
     u32 data;
   };
 
@@ -77,7 +88,8 @@ union InputBindingKey
   {
     InputBindingKey r;
     r.bits = bits;
-    r.negative = false;
+    r.modifier = InputModifier::None;
+    r.invert = 0;
     return r;
   }
 };
@@ -181,6 +193,12 @@ bool GetInputSourceDefaultEnabled(InputSourceType type);
 /// Parses an input class string.
 std::optional<InputSourceType> ParseInputSourceString(const std::string_view& str);
 
+/// Parses a pointer device string, i.e. tells you which pointer is specified.
+std::optional<u32> GetIndexFromPointerBinding(const std::string_view& str);
+
+/// Returns the device name for a pointer index (e.g. Pointer-0).
+std::string GetPointerDeviceName(u32 pointer_index);
+
 /// Converts a key code from a human-readable string to an identifier.
 std::optional<u32> ConvertHostKeyboardStringToCode(const std::string_view& str);
 
@@ -204,10 +222,11 @@ InputBindingKey MakeSensorAxisKey(InputSubclass sensor, u32 axis);
 std::optional<InputBindingKey> ParseInputBindingKey(const std::string_view& binding);
 
 /// Converts a input key to a string.
-std::string ConvertInputBindingKeyToString(InputBindingKey key);
+std::string ConvertInputBindingKeyToString(InputBindingInfo::Type binding_type, InputBindingKey key);
 
 /// Converts a chord of binding keys to a string.
-std::string ConvertInputBindingKeysToString(const InputBindingKey* keys, size_t num_keys);
+std::string ConvertInputBindingKeysToString(InputBindingInfo::Type binding_type, const InputBindingKey* keys,
+                                            size_t num_keys);
 
 /// Returns a list of all hotkeys.
 std::vector<const HotkeyInfo*> GetHotkeyList();
@@ -263,7 +282,7 @@ void AddVibrationBinding(u32 pad_index, const InputBindingKey* motor_0_binding,
 
 /// Updates internal state for any binds for this key, and fires callbacks as needed.
 /// Returns true if anything was bound to this key, otherwise false.
-bool InvokeEvents(InputBindingKey key, float value, GenericInputBinding generic_key);
+bool InvokeEvents(InputBindingKey key, float value, GenericInputBinding generic_key = GenericInputBinding::Unknown);
 
 /// Sets a hook which can be used to intercept events before they're processed by the normal bindings.
 /// This is typically used when binding new controls to detect what gets pressed.
@@ -315,6 +334,12 @@ bool MapController(SettingsInterface& si, u32 controller,
 
 /// Returns a list of input profiles available.
 std::vector<std::string> GetInputProfileNames();
+
+/// Called when a new input device is connected.
+void OnInputDeviceConnected(const std::string_view& identifier, const std::string_view& device_name);
+
+/// Called when an input device is disconnected.
+void OnInputDeviceDisconnected(const std::string_view& identifier);
 } // namespace InputManager
 
 namespace Host {
diff --git a/src/frontend-common/input_source.cpp b/src/frontend-common/input_source.cpp
index c8baf60c6..c89b7a91e 100644
--- a/src/frontend-common/input_source.cpp
+++ b/src/frontend-common/input_source.cpp
@@ -38,6 +38,17 @@ InputBindingKey InputSource::MakeGenericControllerButtonKey(InputSourceType claz
   return key;
 }
 
+InputBindingKey InputSource::MakeGenericControllerHatKey(InputSourceType clazz, u32 controller_index, s32 hat_index,
+                                                         u8 hat_direction, u32 num_directions)
+{
+  InputBindingKey key = {};
+  key.source_type = clazz;
+  key.source_index = controller_index;
+  key.source_subtype = InputSubclass::ControllerHat;
+  key.data = static_cast<u32>(hat_index) * num_directions + hat_direction;
+  return key;
+}
+
 InputBindingKey InputSource::MakeGenericControllerMotorKey(InputSourceType clazz, u32 controller_index, s32 motor_index)
 {
   InputBindingKey key = {};
@@ -81,12 +92,21 @@ std::optional<InputBindingKey> InputSource::ParseGenericControllerKey(InputSourc
     key.data = static_cast<u32>(axis_number.value());
 
     if (sub_binding[0] == '+')
-      key.negative = false;
+      key.modifier = InputModifier::None;
     else if (sub_binding[0] == '-')
-      key.negative = true;
+      key.modifier = InputModifier::Negate;
     else
       return std::nullopt;
   }
+  else if (StringUtil::StartsWith(sub_binding, "FullAxis"))
+  {
+    const std::optional<s32> axis_number = StringUtil::FromChars<s32>(sub_binding.substr(8));
+    if (!axis_number.has_value() || axis_number.value() < 0)
+      return std::nullopt;
+    key.source_subtype = InputSubclass::ControllerAxis;
+    key.data = static_cast<u32>(axis_number.value());
+    key.modifier = InputModifier::FullAxis;
+  }
   else if (StringUtil::StartsWith(sub_binding, "Button"))
   {
     const std::optional<s32> button_number = StringUtil::FromChars<s32>(sub_binding.substr(6));
@@ -108,8 +128,21 @@ std::string InputSource::ConvertGenericControllerKeyToString(InputBindingKey key
 {
   if (key.source_subtype == InputSubclass::ControllerAxis)
   {
-    return StringUtil::StdStringFromFormat("%s-%u/%cAxis%u", InputManager::InputSourceToString(key.source_type),
-                                           key.source_index, key.negative ? '+' : '-', key.data);
+    const char* modifier = "";
+    switch (key.modifier)
+    {
+      case InputModifier::None:
+        modifier = "+";
+        break;
+      case InputModifier::Negate:
+        modifier = "-";
+        break;
+      case InputModifier::FullAxis:
+        modifier = "Full";
+        break;
+    }
+    return StringUtil::StdStringFromFormat("%s-%u/%sAxis%u", InputManager::InputSourceToString(key.source_type),
+                                           key.source_index, modifier, key.data);
   }
   else if (key.source_subtype == InputSubclass::ControllerButton)
   {
@@ -120,4 +153,4 @@ std::string InputSource::ConvertGenericControllerKeyToString(InputBindingKey key
   {
     return {};
   }
-}
+}
\ No newline at end of file
diff --git a/src/frontend-common/input_source.h b/src/frontend-common/input_source.h
index 7749c94e5..0a5784924 100644
--- a/src/frontend-common/input_source.h
+++ b/src/frontend-common/input_source.h
@@ -55,6 +55,10 @@ public:
   /// Creates a key for a generic controller button event.
   static InputBindingKey MakeGenericControllerButtonKey(InputSourceType clazz, u32 controller_index, s32 button_index);
 
+  /// Creates a key for a generic controller hat event.
+  static InputBindingKey MakeGenericControllerHatKey(InputSourceType clazz, u32 controller_index, s32 hat_index,
+                                                     u8 hat_direction, u32 num_directions);
+
   /// Creates a key for a generic controller motor event.
   static InputBindingKey MakeGenericControllerMotorKey(InputSourceType clazz, u32 controller_index, s32 motor_index);
 
diff --git a/src/frontend-common/sdl_input_source.cpp b/src/frontend-common/sdl_input_source.cpp
index ad175c78c..bc7d2ffa0 100644
--- a/src/frontend-common/sdl_input_source.cpp
+++ b/src/frontend-common/sdl_input_source.cpp
@@ -14,7 +14,7 @@
 #endif
 Log_SetChannel(SDLInputSource);
 
-static const char* s_sdl_axis_names[] = {
+static constexpr const char* s_sdl_axis_names[] = {
   "LeftX",        // SDL_CONTROLLER_AXIS_LEFTX
   "LeftY",        // SDL_CONTROLLER_AXIS_LEFTY
   "RightX",       // SDL_CONTROLLER_AXIS_RIGHTX
@@ -22,7 +22,7 @@ static const char* s_sdl_axis_names[] = {
   "LeftTrigger",  // SDL_CONTROLLER_AXIS_TRIGGERLEFT
   "RightTrigger", // SDL_CONTROLLER_AXIS_TRIGGERRIGHT
 };
-static const GenericInputBinding s_sdl_generic_binding_axis_mapping[][2] = {
+static constexpr const GenericInputBinding s_sdl_generic_binding_axis_mapping[][2] = {
   {GenericInputBinding::LeftStickLeft, GenericInputBinding::LeftStickRight},   // SDL_CONTROLLER_AXIS_LEFTX
   {GenericInputBinding::LeftStickUp, GenericInputBinding::LeftStickDown},      // SDL_CONTROLLER_AXIS_LEFTY
   {GenericInputBinding::RightStickLeft, GenericInputBinding::RightStickRight}, // SDL_CONTROLLER_AXIS_RIGHTX
@@ -31,7 +31,7 @@ static const GenericInputBinding s_sdl_generic_binding_axis_mapping[][2] = {
   {GenericInputBinding::Unknown, GenericInputBinding::R2},                     // SDL_CONTROLLER_AXIS_TRIGGERRIGHT
 };
 
-static const char* s_sdl_button_names[] = {
+static constexpr const char* s_sdl_button_names[] = {
   "A",             // SDL_CONTROLLER_BUTTON_A
   "B",             // SDL_CONTROLLER_BUTTON_B
   "X",             // SDL_CONTROLLER_BUTTON_X
@@ -54,7 +54,7 @@ static const char* s_sdl_button_names[] = {
   "Paddle4",       // SDL_CONTROLLER_BUTTON_PADDLE4
   "Touchpad",      // SDL_CONTROLLER_BUTTON_TOUCHPAD
 };
-static const GenericInputBinding s_sdl_generic_binding_button_mapping[] = {
+static constexpr const GenericInputBinding s_sdl_generic_binding_button_mapping[] = {
   GenericInputBinding::Cross,     // SDL_CONTROLLER_BUTTON_A
   GenericInputBinding::Circle,    // SDL_CONTROLLER_BUTTON_B
   GenericInputBinding::Square,    // SDL_CONTROLLER_BUTTON_X
@@ -78,11 +78,20 @@ static const GenericInputBinding s_sdl_generic_binding_button_mapping[] = {
   GenericInputBinding::Unknown,   // SDL_CONTROLLER_BUTTON_TOUCHPAD
 };
 
+static constexpr const char* s_sdl_hat_direction_names[] = {
+  // clang-format off
+	"North",
+	"East",
+	"South",
+	"West",
+  // clang-format on
+};
+
 SDLInputSource::SDLInputSource() = default;
 
 SDLInputSource::~SDLInputSource()
 {
-  DebugAssert(m_controllers.empty());
+  Assert(m_controllers.empty());
 }
 
 bool SDLInputSource::Initialize(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock)
@@ -123,6 +132,13 @@ void SDLInputSource::UpdateSettings(SettingsInterface& si, std::unique_lock<std:
   }
 }
 
+bool SDLInputSource::ReloadDevices()
+{
+  // We'll get a GC added/removed event here.
+  PollEvents();
+  return false;
+}
+
 void SDLInputSource::Shutdown()
 {
   ShutdownSubsystem();
@@ -131,36 +147,32 @@ void SDLInputSource::Shutdown()
 void SDLInputSource::LoadSettings(SettingsInterface& si)
 {
   m_controller_enhanced_mode = si.GetBoolValue("InputSources", "SDLControllerEnhancedMode", false);
-}
-
-bool SDLInputSource::ReloadDevices()
-{
-  // We'll get a GC added/removed event here.
-  PollEvents();
-  return false;
+  m_sdl_hints = si.GetKeyValueList("SDLHints");
 }
 
 void SDLInputSource::SetHints()
 {
   SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, m_controller_enhanced_mode ? "1" : "0");
   SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, m_controller_enhanced_mode ? "1" : "0");
+  // Enable Wii U Pro Controller support
+  // New as of SDL 2.26, so use string
+  SDL_SetHint("SDL_JOYSTICK_HIDAPI_WII", "1");
+#ifndef _WIN32
+  // Gets us pressure sensitive button support on Linux
+  // Apparently doesn't work on Windows, so leave it off there
+  // New as of SDL 2.26, so use string
+  SDL_SetHint("SDL_JOYSTICK_HIDAPI_PS3", "1");
+#endif
+
+  for (const std::pair<std::string, std::string>& hint : m_sdl_hints)
+    SDL_SetHint(hint.first.c_str(), hint.second.c_str());
 }
 
 bool SDLInputSource::InitializeSubsystem()
 {
-  int result;
-#ifdef __APPLE__
-  // On macOS, SDL_InitSubSystem runs a main-thread-only call to some GameController framework method
-  // So send this to be run on the main thread
-  dispatch_sync_f(dispatch_get_main_queue(), &result, [](void* ctx) {
-    *static_cast<int*>(ctx) = SDL_InitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC);
-  });
-#else
-  result = SDL_InitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC);
-#endif
-  if (result < 0)
+  if (SDL_InitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC) < 0)
   {
-    Log_ErrorPrintf("SDL_InitSubSystem(SDL_INIT_JOYSTICK |SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC) failed");
+    Log_ErrorPrint("SDL_InitSubSystem(SDL_INIT_JOYSTICK |SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC) failed");
     return false;
   }
 
@@ -172,16 +184,11 @@ bool SDLInputSource::InitializeSubsystem()
 void SDLInputSource::ShutdownSubsystem()
 {
   while (!m_controllers.empty())
-    CloseGameController(m_controllers.begin()->joystick_id);
+    CloseDevice(m_controllers.begin()->joystick_id);
 
   if (m_sdl_subsystem_initialized)
   {
-#ifdef __APPLE__
-    dispatch_sync_f(dispatch_get_main_queue(), nullptr,
-                    [](void*) { SDL_QuitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC); });
-#else
     SDL_QuitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC);
-#endif
     m_sdl_subsystem_initialized = false;
   }
 }
@@ -206,7 +213,7 @@ std::vector<std::pair<std::string, std::string>> SDLInputSource::EnumerateDevice
   {
     std::string id(StringUtil::StdStringFromFormat("SDL-%d", cd.player_id));
 
-    const char* name = SDL_GameControllerName(cd.game_controller);
+    const char* name = cd.game_controller ? SDL_GameControllerName(cd.game_controller) : SDL_JoystickName(cd.joystick);
     if (name)
       ret.emplace_back(std::move(id), name);
     else
@@ -258,6 +265,19 @@ std::optional<InputBindingKey> SDLInputSource::ParseKeyString(const std::string_
   {
     // likely an axis
     const std::string_view axis_name(binding.substr(1));
+
+    if (StringUtil::StartsWith(axis_name, "Axis"))
+    {
+      std::string_view end;
+      if (auto value = StringUtil::FromChars<u32>(axis_name.substr(4), 10, &end))
+      {
+        key.source_subtype = InputSubclass::ControllerAxis;
+        key.data = *value + static_cast<u32>(std::size(s_sdl_axis_names));
+        key.modifier = (binding[0] == '-') ? InputModifier::Negate : InputModifier::None;
+        key.invert = (end == "~");
+        return key;
+      }
+    }
     for (u32 i = 0; i < std::size(s_sdl_axis_names); i++)
     {
       if (axis_name == s_sdl_axis_names[i])
@@ -265,14 +285,51 @@ std::optional<InputBindingKey> SDLInputSource::ParseKeyString(const std::string_
         // found an axis!
         key.source_subtype = InputSubclass::ControllerAxis;
         key.data = i;
-        key.negative = (binding[0] == '-');
+        key.modifier = (binding[0] == '-') ? InputModifier::Negate : InputModifier::None;
         return key;
       }
     }
   }
+  else if (StringUtil::StartsWith(binding, "FullAxis"))
+  {
+    std::string_view end;
+    if (auto value = StringUtil::FromChars<u32>(binding.substr(8), 10, &end))
+    {
+      key.source_subtype = InputSubclass::ControllerAxis;
+      key.data = *value + static_cast<u32>(std::size(s_sdl_axis_names));
+      key.modifier = InputModifier::FullAxis;
+      key.invert = (end == "~");
+      return key;
+    }
+  }
+  else if (StringUtil::StartsWith(binding, "Hat"))
+  {
+    std::string_view hat_dir;
+    if (auto value = StringUtil::FromChars<u32>(binding.substr(3), 10, &hat_dir); value.has_value() && !hat_dir.empty())
+    {
+      for (u8 dir = 0; dir < static_cast<u8>(std::size(s_sdl_hat_direction_names)); dir++)
+      {
+        if (hat_dir == s_sdl_hat_direction_names[dir])
+        {
+          key.source_subtype = InputSubclass::ControllerHat;
+          key.data = value.value() * static_cast<u32>(std::size(s_sdl_hat_direction_names)) + dir;
+          return key;
+        }
+      }
+    }
+  }
   else
   {
     // must be a button
+    if (StringUtil::StartsWith(binding, "Button"))
+    {
+      if (auto value = StringUtil::FromChars<u32>(binding.substr(6)))
+      {
+        key.source_subtype = InputSubclass::ControllerButton;
+        key.data = *value + static_cast<u32>(std::size(s_sdl_button_names));
+        return key;
+      }
+    }
     for (u32 i = 0; i < std::size(s_sdl_button_names); i++)
     {
       if (binding == s_sdl_button_names[i])
@@ -294,14 +351,39 @@ std::string SDLInputSource::ConvertKeyToString(InputBindingKey key)
 
   if (key.source_type == InputSourceType::SDL)
   {
-    if (key.source_subtype == InputSubclass::ControllerAxis && key.data < std::size(s_sdl_axis_names))
+    if (key.source_subtype == InputSubclass::ControllerAxis)
     {
-      ret = StringUtil::StdStringFromFormat("SDL-%u/%c%s", key.source_index, key.negative ? '-' : '+',
-                                            s_sdl_axis_names[key.data]);
+      const char* modifier =
+        (key.modifier == InputModifier::FullAxis ? "Full" : (key.modifier == InputModifier::Negate ? "-" : "+"));
+      if (key.data < std::size(s_sdl_axis_names))
+      {
+        ret = StringUtil::StdStringFromFormat("SDL-%u/%s%s", key.source_index, modifier, s_sdl_axis_names[key.data]);
+      }
+      else
+      {
+        ret = StringUtil::StdStringFromFormat("SDL-%u/%sAxis%u%s", key.source_index, modifier,
+                                              key.data - static_cast<u32>(std::size(s_sdl_axis_names)),
+                                              key.invert ? "~" : "");
+      }
     }
-    else if (key.source_subtype == InputSubclass::ControllerButton && key.data < std::size(s_sdl_button_names))
+    else if (key.source_subtype == InputSubclass::ControllerButton)
     {
-      ret = StringUtil::StdStringFromFormat("SDL-%u/%s", key.source_index, s_sdl_button_names[key.data]);
+      if (key.data < std::size(s_sdl_button_names))
+      {
+        ret = StringUtil::StdStringFromFormat("SDL-%u/%s", key.source_index, s_sdl_button_names[key.data]);
+      }
+      else
+      {
+        ret = StringUtil::StdStringFromFormat("SDL-%u/Button%u", key.source_index,
+                                              key.data - static_cast<u32>(std::size(s_sdl_button_names)));
+      }
+    }
+    else if (key.source_subtype == InputSubclass::ControllerHat)
+    {
+      const u32 hat_index = key.data / static_cast<u32>(std::size(s_sdl_hat_direction_names));
+      const u32 hat_direction = key.data % static_cast<u32>(std::size(s_sdl_hat_direction_names));
+      ret = StringUtil::StdStringFromFormat("SDL-%u/Hat%u%s", key.source_index, hat_index,
+                                            s_sdl_hat_direction_names[hat_direction]);
     }
     else if (key.source_subtype == InputSubclass::ControllerMotor)
     {
@@ -323,14 +405,37 @@ bool SDLInputSource::ProcessSDLEvent(const SDL_Event* event)
     case SDL_CONTROLLERDEVICEADDED:
     {
       Log_InfoPrintf("(SDLInputSource) Controller %d inserted", event->cdevice.which);
-      OpenGameController(event->cdevice.which);
+      OpenDevice(event->cdevice.which, true);
       return true;
     }
 
     case SDL_CONTROLLERDEVICEREMOVED:
     {
       Log_InfoPrintf("(SDLInputSource) Controller %d removed", event->cdevice.which);
-      CloseGameController(event->cdevice.which);
+      CloseDevice(event->cdevice.which);
+      return true;
+    }
+
+    case SDL_JOYDEVICEADDED:
+    {
+      // Let game controller handle.. well.. game controllers.
+      if (SDL_IsGameController(event->jdevice.which))
+        return false;
+
+      Log_InfoPrintf("(SDLInputSource) Joystick %d inserted", event->jdevice.which);
+      OpenDevice(event->cdevice.which, false);
+      return true;
+    }
+    break;
+
+    case SDL_JOYDEVICEREMOVED:
+    {
+      if (auto it = GetControllerDataForJoystickId(event->cdevice.which);
+          it != m_controllers.end() && it->game_controller)
+        return false;
+
+      Log_InfoPrintf("(SDLInputSource) Joystick %d removed", event->jdevice.which);
+      CloseDevice(event->cdevice.which);
       return true;
     }
 
@@ -341,11 +446,37 @@ bool SDLInputSource::ProcessSDLEvent(const SDL_Event* event)
     case SDL_CONTROLLERBUTTONUP:
       return HandleControllerButtonEvent(&event->cbutton);
 
+    case SDL_JOYAXISMOTION:
+      return HandleJoystickAxisEvent(&event->jaxis);
+
+    case SDL_JOYBUTTONDOWN:
+    case SDL_JOYBUTTONUP:
+      return HandleJoystickButtonEvent(&event->jbutton);
+
+    case SDL_JOYHATMOTION:
+      return HandleJoystickHatEvent(&event->jhat);
+
     default:
       return false;
   }
 }
 
+SDL_Joystick* SDLInputSource::GetJoystickForDevice(const std::string_view& device)
+{
+  if (!StringUtil::StartsWith(device, "SDL-"))
+    return nullptr;
+
+  const std::optional<s32> player_id = StringUtil::FromChars<s32>(device.substr(4));
+  if (!player_id.has_value() || player_id.value() < 0)
+    return nullptr;
+
+  auto it = GetControllerDataForPlayerId(player_id.value());
+  if (it == m_controllers.end())
+    return nullptr;
+
+  return it->joystick;
+}
+
 SDLInputSource::ControllerDataVector::iterator SDLInputSource::GetControllerDataForJoystickId(int id)
 {
   return std::find_if(m_controllers.begin(), m_controllers.end(),
@@ -375,11 +506,23 @@ int SDLInputSource::GetFreePlayerId() const
   return 0;
 }
 
-bool SDLInputSource::OpenGameController(int index)
+bool SDLInputSource::OpenDevice(int index, bool is_gamecontroller)
 {
-  SDL_GameController* gcontroller = SDL_GameControllerOpen(index);
-  SDL_Joystick* joystick = gcontroller ? SDL_GameControllerGetJoystick(gcontroller) : nullptr;
-  if (!gcontroller || !joystick)
+  SDL_GameController* gcontroller;
+  SDL_Joystick* joystick;
+
+  if (is_gamecontroller)
+  {
+    gcontroller = SDL_GameControllerOpen(index);
+    joystick = gcontroller ? SDL_GameControllerGetJoystick(gcontroller) : nullptr;
+  }
+  else
+  {
+    gcontroller = nullptr;
+    joystick = SDL_JoystickOpen(index);
+  }
+
+  if (!gcontroller && !joystick)
   {
     Log_ErrorPrintf("(SDLInputSource) Failed to open controller %d", index);
     if (gcontroller)
@@ -389,7 +532,7 @@ bool SDLInputSource::OpenGameController(int index)
   }
 
   const int joystick_id = SDL_JoystickInstanceID(joystick);
-  int player_id = SDL_GameControllerGetPlayerIndex(gcontroller);
+  int player_id = gcontroller ? SDL_GameControllerGetPlayerIndex(gcontroller) : SDL_JoystickGetPlayerIndex(joystick);
   if (player_id < 0 || GetControllerDataForPlayerId(player_id) != m_controllers.end())
   {
     const int free_player_id = GetFreePlayerId();
@@ -399,20 +542,49 @@ bool SDLInputSource::OpenGameController(int index)
     player_id = free_player_id;
   }
 
-  Log_InfoPrintf("(SDLInputSource) Opened controller %d (instance id %d, player id %d): %s", index, joystick_id,
-                 player_id, SDL_GameControllerName(gcontroller));
+  const char* name = gcontroller ? SDL_GameControllerName(gcontroller) : SDL_JoystickName(joystick);
+  if (!name)
+    name = "Unknown Device";
+
+  Log_VerbosePrintf("(SDLInputSource) Opened %s %d (instance id %d, player id %d): %s",
+                    is_gamecontroller ? "game controller" : "joystick", index, joystick_id, player_id, name);
 
   ControllerData cd = {};
   cd.player_id = player_id;
   cd.joystick_id = joystick_id;
   cd.haptic_left_right_effect = -1;
   cd.game_controller = gcontroller;
+  cd.joystick = joystick;
 
-  cd.use_game_controller_rumble = (SDL_GameControllerRumble(gcontroller, 0, 0, 0) == 0);
+  if (gcontroller)
+  {
+    const int num_axes = SDL_JoystickNumAxes(joystick);
+    const int num_buttons = SDL_JoystickNumButtons(joystick);
+    cd.joy_axis_used_in_gc.resize(num_axes, false);
+    cd.joy_button_used_in_gc.resize(num_buttons, false);
+    auto mark_bind = [&](SDL_GameControllerButtonBind bind) {
+      if (bind.bindType == SDL_CONTROLLER_BINDTYPE_AXIS && bind.value.axis < num_axes)
+        cd.joy_axis_used_in_gc[bind.value.axis] = true;
+      if (bind.bindType == SDL_CONTROLLER_BINDTYPE_BUTTON && bind.value.button < num_buttons)
+        cd.joy_button_used_in_gc[bind.value.button] = true;
+    };
+    for (size_t i = 0; i < std::size(s_sdl_axis_names); i++)
+      mark_bind(SDL_GameControllerGetBindForAxis(gcontroller, static_cast<SDL_GameControllerAxis>(i)));
+    for (size_t i = 0; i < std::size(s_sdl_button_names); i++)
+      mark_bind(SDL_GameControllerGetBindForButton(gcontroller, static_cast<SDL_GameControllerButton>(i)));
+  }
+  else
+  {
+    // GC doesn't have the concept of hats, so we only need to do this for joysticks.
+    const int num_hats = SDL_JoystickNumHats(joystick);
+    if (num_hats > 0)
+      cd.last_hat_state.resize(static_cast<size_t>(num_hats), u8(0));
+  }
+
+  cd.use_game_controller_rumble = (gcontroller && SDL_GameControllerRumble(gcontroller, 0, 0, 0) == 0);
   if (cd.use_game_controller_rumble)
   {
-    Log_DevPrintf("(SDLInputSource) Rumble is supported on '%s' via gamecontroller",
-                  SDL_GameControllerName(gcontroller));
+    Log_VerbosePrintf("(SDLInputSource) Rumble is supported on '%s' via gamecontroller", name);
   }
   else
   {
@@ -431,7 +603,7 @@ bool SDLInputSource::OpenGameController(int index)
       }
       else
       {
-        Log_WarningPrintf("(SDLInputSource) Failed to create haptic left/right effect: %s", SDL_GetError());
+        Log_ErrorPrintf("(SDLInputSource) Failed to create haptic left/right effect: %s", SDL_GetError());
         if (SDL_HapticRumbleSupported(haptic) && SDL_HapticRumbleInit(haptic) != 0)
         {
           cd.haptic = haptic;
@@ -445,37 +617,43 @@ bool SDLInputSource::OpenGameController(int index)
     }
 
     if (cd.haptic)
-      Log_DevPrintf("(SDLInputSource) Rumble is supported on '%s' via haptic", SDL_GameControllerName(gcontroller));
+      Log_VerbosePrintf("(SDLInputSource) Rumble is supported on '%s' via haptic", name);
   }
 
   if (!cd.haptic && !cd.use_game_controller_rumble)
-    Log_WarningPrintf("(SDLInputSource) Rumble is not supported on '%s'", SDL_GameControllerName(gcontroller));
+    Log_VerbosePrintf("(SDLInputSource) Rumble is not supported on '%s'", name);
 
   m_controllers.push_back(std::move(cd));
 
-  const char* name = SDL_GameControllerName(cd.game_controller);
-  Host::OnInputDeviceConnected(StringUtil::StdStringFromFormat("SDL-%d", player_id), name ? name : "Unknown Device");
+  InputManager::OnInputDeviceConnected(StringUtil::StdStringFromFormat("SDL-%d", player_id), name);
   return true;
 }
 
-bool SDLInputSource::CloseGameController(int joystick_index)
+bool SDLInputSource::CloseDevice(int joystick_index)
 {
   auto it = GetControllerDataForJoystickId(joystick_index);
   if (it == m_controllers.end())
     return false;
 
+  InputManager::OnInputDeviceDisconnected(StringUtil::StdStringFromFormat("SDL-%d", it->player_id));
+
   if (it->haptic)
-    SDL_HapticClose(static_cast<SDL_Haptic*>(it->haptic));
+    SDL_HapticClose(it->haptic);
 
-  SDL_GameControllerClose(static_cast<SDL_GameController*>(it->game_controller));
+  if (it->game_controller)
+    SDL_GameControllerClose(it->game_controller);
+  else
+    SDL_JoystickClose(it->joystick);
 
-  const int player_id = it->player_id;
   m_controllers.erase(it);
-
-  Host::OnInputDeviceDisconnected(StringUtil::StdStringFromFormat("SDL-%d", player_id));
   return true;
 }
 
+static float NormalizeS16(s16 value)
+{
+  return static_cast<float>(value) / (value < 0 ? 32768.0f : 32767.0f);
+}
+
 bool SDLInputSource::HandleControllerAxisEvent(const SDL_ControllerAxisEvent* ev)
 {
   auto it = GetControllerDataForJoystickId(ev->which);
@@ -483,8 +661,8 @@ bool SDLInputSource::HandleControllerAxisEvent(const SDL_ControllerAxisEvent* ev
     return false;
 
   const InputBindingKey key(MakeGenericControllerAxisKey(InputSourceType::SDL, it->player_id, ev->axis));
-  const float value = static_cast<float>(ev->value) / (ev->value < 0 ? 32768.0f : 32767.0f);
-  return InputManager::InvokeEvents(key, value, GenericInputBinding::Unknown);
+  InputManager::InvokeEvents(key, NormalizeS16(ev->value));
+  return true;
 }
 
 bool SDLInputSource::HandleControllerButtonEvent(const SDL_ControllerButtonEvent* ev)
@@ -497,7 +675,62 @@ bool SDLInputSource::HandleControllerButtonEvent(const SDL_ControllerButtonEvent
   const GenericInputBinding generic_key = (ev->button < std::size(s_sdl_generic_binding_button_mapping)) ?
                                             s_sdl_generic_binding_button_mapping[ev->button] :
                                             GenericInputBinding::Unknown;
-  return InputManager::InvokeEvents(key, (ev->state == SDL_PRESSED) ? 1.0f : 0.0f, generic_key);
+  InputManager::InvokeEvents(key, (ev->state == SDL_PRESSED) ? 1.0f : 0.0f, generic_key);
+  return true;
+}
+
+bool SDLInputSource::HandleJoystickAxisEvent(const SDL_JoyAxisEvent* ev)
+{
+  auto it = GetControllerDataForJoystickId(ev->which);
+  if (it == m_controllers.end())
+    return false;
+  if (ev->axis < it->joy_axis_used_in_gc.size() && it->joy_axis_used_in_gc[ev->axis])
+    return false;                                                            // Will get handled by GC event
+  const u32 axis = ev->axis + static_cast<u32>(std::size(s_sdl_axis_names)); // Ensure we don't conflict with GC axes
+  const InputBindingKey key(MakeGenericControllerAxisKey(InputSourceType::SDL, it->player_id, axis));
+  InputManager::InvokeEvents(key, NormalizeS16(ev->value));
+  return true;
+}
+
+bool SDLInputSource::HandleJoystickButtonEvent(const SDL_JoyButtonEvent* ev)
+{
+  auto it = GetControllerDataForJoystickId(ev->which);
+  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
+  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));
+  InputManager::InvokeEvents(key, (ev->state == SDL_PRESSED) ? 1.0f : 0.0f);
+  return true;
+}
+
+bool SDLInputSource::HandleJoystickHatEvent(const SDL_JoyHatEvent* ev)
+{
+  auto it = GetControllerDataForJoystickId(ev->which);
+  if (it == m_controllers.end() || ev->hat >= it->last_hat_state.size())
+    return false;
+
+  const unsigned long last_direction = it->last_hat_state[ev->hat];
+  it->last_hat_state[ev->hat] = ev->value;
+
+  unsigned long changed_direction = last_direction ^ ev->value;
+  while (changed_direction != 0)
+  {
+    unsigned long pos;
+    _BitScanForward(&pos, changed_direction);
+
+    const unsigned long mask = (1u << pos);
+    changed_direction &= ~mask;
+
+    const InputBindingKey key(MakeGenericControllerHatKey(InputSourceType::SDL, it->player_id, ev->hat,
+                                                          static_cast<u8>(pos),
+                                                          static_cast<u32>(std::size(s_sdl_hat_direction_names))));
+    InputManager::InvokeEvents(key, (last_direction & mask) ? 0.0f : 1.0f);
+  }
+
+  return true;
 }
 
 std::vector<InputBindingKey> SDLInputSource::EnumerateMotors()
@@ -581,7 +814,7 @@ bool SDLInputSource::GetGenericBindingMapping(const std::string_view& device, Ge
   }
   else
   {
-    // joysticks, which we haven't implemented yet anyway.
+    // joysticks have arbitrary axis numbers, so automapping isn't going to work here.
     return false;
   }
 }
diff --git a/src/frontend-common/sdl_input_source.h b/src/frontend-common/sdl_input_source.h
index 3ecdd3c8c..5daf1d341 100644
--- a/src/frontend-common/sdl_input_source.h
+++ b/src/frontend-common/sdl_input_source.h
@@ -36,22 +36,26 @@ public:
 
   bool ProcessSDLEvent(const SDL_Event* event);
 
-private:
-  enum : int
-  {
-    MAX_NUM_AXES = 7,
-    MAX_NUM_BUTTONS = 16,
-  };
+  SDL_Joystick* GetJoystickForDevice(const std::string_view& device);
 
+private:
   struct ControllerData
   {
     SDL_Haptic* haptic;
     SDL_GameController* game_controller;
+    SDL_Joystick* joystick;
     u16 rumble_intensity[2];
     int haptic_left_right_effect;
     int joystick_id;
     int player_id;
     bool use_game_controller_rumble;
+
+    // Used to disable Joystick controls that are used in GameController inputs so we don't get double events
+    std::vector<bool> joy_button_used_in_gc;
+    std::vector<bool> joy_axis_used_in_gc;
+
+    // Track last hat state so we can send "unpressed" events.
+    std::vector<u8> last_hat_state;
   };
 
   using ControllerDataVector = std::vector<ControllerData>;
@@ -65,14 +69,18 @@ private:
   ControllerDataVector::iterator GetControllerDataForPlayerId(int id);
   int GetFreePlayerId() const;
 
-  bool OpenGameController(int index);
-  bool CloseGameController(int joystick_index);
-  bool HandleControllerAxisEvent(const SDL_ControllerAxisEvent* event);
-  bool HandleControllerButtonEvent(const SDL_ControllerButtonEvent* event);
+  bool OpenDevice(int index, bool is_gamecontroller);
+  bool CloseDevice(int joystick_index);
+  bool HandleControllerAxisEvent(const SDL_ControllerAxisEvent* ev);
+  bool HandleControllerButtonEvent(const SDL_ControllerButtonEvent* ev);
+  bool HandleJoystickAxisEvent(const SDL_JoyAxisEvent* ev);
+  bool HandleJoystickButtonEvent(const SDL_JoyButtonEvent* ev);
+  bool HandleJoystickHatEvent(const SDL_JoyHatEvent* ev);
   void SendRumbleUpdate(ControllerData* cd);
 
   ControllerDataVector m_controllers;
 
   bool m_sdl_subsystem_initialized = false;
   bool m_controller_enhanced_mode = false;
+  std::vector<std::pair<std::string, std::string>> m_sdl_hints;
 };
diff --git a/src/frontend-common/xinput_source.cpp b/src/frontend-common/xinput_source.cpp
index ee271f466..b461281af 100644
--- a/src/frontend-common/xinput_source.cpp
+++ b/src/frontend-common/xinput_source.cpp
@@ -260,7 +260,7 @@ std::optional<InputBindingKey> XInputSource::ParseKeyString(const std::string_vi
         // found an axis!
         key.source_subtype = InputSubclass::ControllerAxis;
         key.data = i;
-        key.negative = (binding[0] == '-');
+        key.modifier = binding[0] == '-' ? InputModifier::Negate : InputModifier::None;
         return key;
       }
     }
@@ -291,8 +291,8 @@ std::string XInputSource::ConvertKeyToString(InputBindingKey key)
   {
     if (key.source_subtype == InputSubclass::ControllerAxis && key.data < std::size(s_axis_names))
     {
-      ret = StringUtil::StdStringFromFormat("XInput-%u/%c%s", key.source_index, key.negative ? '-' : '+',
-                                            s_axis_names[key.data]);
+      const char modifier = key.modifier == InputModifier::Negate ? '-' : '+';
+      ret = StringUtil::StdStringFromFormat("XInput-%u/%c%s", key.source_index, modifier, s_axis_names[key.data]);
     }
     else if (key.source_subtype == InputSubclass::ControllerButton && key.data < std::size(s_button_names))
     {
@@ -382,16 +382,15 @@ void XInputSource::HandleControllerConnection(u32 index)
   cd.has_small_motor = caps.Vibration.wRightMotorSpeed != 0;
   cd.last_state = {};
 
-  Host::OnInputDeviceConnected(StringUtil::StdStringFromFormat("XInput-%u", index),
-                               StringUtil::StdStringFromFormat("XInput Controller %u", index));
+  InputManager::OnInputDeviceConnected(StringUtil::StdStringFromFormat("XInput-%u", index),
+                                       StringUtil::StdStringFromFormat("XInput Controller %u", index));
 }
 
 void XInputSource::HandleControllerDisconnection(u32 index)
 {
   Log_InfoPrintf("XInput controller %u disconnected.", index);
+  InputManager::OnInputDeviceDisconnected(StringUtil::StdStringFromFormat("XInput-%u", index));
   m_controllers[index] = {};
-
-  Host::OnInputDeviceDisconnected(StringUtil::StdStringFromFormat("XInput-%u", index));
 }
 
 void XInputSource::CheckForStateChanges(u32 index, const XINPUT_STATE& new_state)
diff --git a/src/util/ini_settings_interface.cpp b/src/util/ini_settings_interface.cpp
index 48ce74dc9..758546e1d 100644
--- a/src/util/ini_settings_interface.cpp
+++ b/src/util/ini_settings_interface.cpp
@@ -186,28 +186,28 @@ bool INISettingsInterface::GetStringValue(const char* section, const char* key,
   return true;
 }
 
-void INISettingsInterface::SetIntValue(const char* section, const char* key, int value)
+void INISettingsInterface::SetIntValue(const char* section, const char* key, s32 value)
 {
   m_dirty = true;
-  m_ini.SetLongValue(section, key, static_cast<long>(value), nullptr, false, true);
+  m_ini.SetValue(section, key, StringUtil::ToChars(value).c_str(), nullptr, true);
 }
 
 void INISettingsInterface::SetUIntValue(const char* section, const char* key, u32 value)
 {
   m_dirty = true;
-  m_ini.SetLongValue(section, key, static_cast<long>(value), nullptr, false, true);
+  m_ini.SetValue(section, key, StringUtil::ToChars(value).c_str(), nullptr, true);
 }
 
 void INISettingsInterface::SetFloatValue(const char* section, const char* key, float value)
 {
   m_dirty = true;
-  m_ini.SetDoubleValue(section, key, static_cast<double>(value), nullptr, true);
+  m_ini.SetValue(section, key, StringUtil::ToChars(value).c_str(), nullptr, true);
 }
 
 void INISettingsInterface::SetDoubleValue(const char* section, const char* key, double value)
 {
   m_dirty = true;
-  m_ini.SetDoubleValue(section, key, value, nullptr, true);
+  m_ini.SetValue(section, key, StringUtil::ToChars(value).c_str(), nullptr, true);
 }
 
 void INISettingsInterface::SetBoolValue(const char* section, const char* key, bool value)
@@ -282,3 +282,40 @@ bool INISettingsInterface::AddToStringList(const char* section, const char* key,
   m_ini.SetValue(section, key, item, nullptr, false);
   return true;
 }
+
+std::vector<std::pair<std::string, std::string>> INISettingsInterface::GetKeyValueList(const char* section) const
+{
+  using Entry = CSimpleIniA::Entry;
+  using KVEntry = std::pair<const char*, Entry>;
+  std::vector<KVEntry> entries;
+  std::vector<std::pair<std::string, std::string>> output;
+  std::list<Entry> keys, values;
+  if (m_ini.GetAllKeys(section, keys))
+  {
+    for (Entry& key : keys)
+    {
+      if (!m_ini.GetAllValues(section, key.pItem, values)) // [[unlikely]]
+      {
+        Log_ErrorPrintf("Got no values for a key returned from GetAllKeys!");
+        continue;
+      }
+      for (const Entry& value : values)
+        entries.emplace_back(key.pItem, value);
+    }
+  }
+
+  std::sort(entries.begin(), entries.end(),
+            [](const KVEntry& a, const KVEntry& b) { return a.second.nOrder < b.second.nOrder; });
+  for (const KVEntry& entry : entries)
+    output.emplace_back(entry.first, entry.second.pItem);
+
+  return output;
+}
+
+void INISettingsInterface::SetKeyValueList(const char* section,
+                                           const std::vector<std::pair<std::string, std::string>>& items)
+{
+  m_ini.Delete(section, nullptr);
+  for (const std::pair<std::string, std::string>& item : items)
+    m_ini.SetValue(section, item.first.c_str(), item.second.c_str(), nullptr, false);
+}
diff --git a/src/util/ini_settings_interface.h b/src/util/ini_settings_interface.h
index 64d095451..8aa178db3 100644
--- a/src/util/ini_settings_interface.h
+++ b/src/util/ini_settings_interface.h
@@ -45,6 +45,9 @@ public:
   bool RemoveFromStringList(const char* section, const char* key, const char* item) override;
   bool AddToStringList(const char* section, const char* key, const char* item) override;
 
+  std::vector<std::pair<std::string, std::string>> GetKeyValueList(const char* section) const override;
+  void SetKeyValueList(const char* section, const std::vector<std::pair<std::string, std::string>>& items) override;
+
   // default parameter overloads
   using SettingsInterface::GetBoolValue;
   using SettingsInterface::GetDoubleValue;