From 99ec667b208500f1d59d7a4650e414d28f26110b Mon Sep 17 00:00:00 2001
From: Silent <zdanio95@gmail.com>
Date: Sat, 14 Nov 2020 00:32:35 +0100
Subject: [PATCH 1/3] Factorize input hooks into reusable monitor classes

Those monitors will need some context so this will be useful
in the nearby future.
---
 src/duckstation-qt/CMakeLists.txt             |  2 +
 src/duckstation-qt/duckstation-qt.vcxproj     |  2 +
 .../duckstation-qt.vcxproj.filters            |  2 +
 src/duckstation-qt/inputbindingdialog.cpp     | 45 +-------------
 src/duckstation-qt/inputbindingmonitor.cpp    | 60 +++++++++++++++++++
 src/duckstation-qt/inputbindingmonitor.h      | 45 ++++++++++++++
 src/duckstation-qt/inputbindingwidgets.cpp    | 55 ++---------------
 7 files changed, 118 insertions(+), 93 deletions(-)
 create mode 100644 src/duckstation-qt/inputbindingmonitor.cpp
 create mode 100644 src/duckstation-qt/inputbindingmonitor.h

diff --git a/src/duckstation-qt/CMakeLists.txt b/src/duckstation-qt/CMakeLists.txt
index b83be92fe..a8a5aeff3 100644
--- a/src/duckstation-qt/CMakeLists.txt
+++ b/src/duckstation-qt/CMakeLists.txt
@@ -55,6 +55,8 @@ set(SRCS
   inputbindingdialog.cpp
   inputbindingdialog.h
   inputbindingdialog.ui
+  inputbindingmonitor.cpp
+  inputbindingmonitor.h
   inputbindingwidgets.cpp
   inputbindingwidgets.h
   main.cpp
diff --git a/src/duckstation-qt/duckstation-qt.vcxproj b/src/duckstation-qt/duckstation-qt.vcxproj
index 9ef490d3c..3d7fe4e40 100644
--- a/src/duckstation-qt/duckstation-qt.vcxproj
+++ b/src/duckstation-qt/duckstation-qt.vcxproj
@@ -66,6 +66,7 @@
     <ClCompile Include="displaysettingswidget.cpp" />
     <ClCompile Include="hotkeysettingswidget.cpp" />
     <ClCompile Include="inputbindingdialog.cpp" />
+    <ClCompile Include="inputbindingmonitor.cpp" />
     <ClCompile Include="inputbindingwidgets.cpp" />
     <ClCompile Include="qtdisplaywidget.cpp" />
     <ClCompile Include="gamelistsettingswidget.cpp" />
@@ -105,6 +106,7 @@
     <QtMoc Include="gamelistmodel.h" />
     <QtMoc Include="gamelistsearchdirectoriesmodel.h" />
     <QtMoc Include="autoupdaterdialog.h" />
+    <ClInclude Include="inputbindingmonitor.h" />
     <ClInclude Include="resource.h" />
     <ClInclude Include="settingwidgetbinder.h" />
     <QtMoc Include="consolesettingswidget.h" />
diff --git a/src/duckstation-qt/duckstation-qt.vcxproj.filters b/src/duckstation-qt/duckstation-qt.vcxproj.filters
index 3dd288d97..82bc61a72 100644
--- a/src/duckstation-qt/duckstation-qt.vcxproj.filters
+++ b/src/duckstation-qt/duckstation-qt.vcxproj.filters
@@ -60,11 +60,13 @@
     <ClCompile Include="$(IntDir)moc_postprocessingchainconfigwidget.cpp" />
     <ClCompile Include="$(IntDir)moc_postprocessingshaderconfigwidget.cpp" />
     <ClCompile Include="$(IntDir)moc_postprocessingsettingswidget.cpp" />
+    <ClCompile Include="inputbindingmonitor.cpp" />
   </ItemGroup>
   <ItemGroup>
     <ClInclude Include="qtutils.h" />
     <ClInclude Include="settingwidgetbinder.h" />
     <ClInclude Include="resource.h" />
+    <ClInclude Include="inputbindingmonitor.h" />
   </ItemGroup>
   <ItemGroup>
     <Filter Include="resources">
diff --git a/src/duckstation-qt/inputbindingdialog.cpp b/src/duckstation-qt/inputbindingdialog.cpp
index 0edded305..294a0af19 100644
--- a/src/duckstation-qt/inputbindingdialog.cpp
+++ b/src/duckstation-qt/inputbindingdialog.cpp
@@ -3,6 +3,7 @@
 #include "common/string_util.h"
 #include "core/settings.h"
 #include "frontend-common/controller_interface.h"
+#include "inputbindingmonitor.h"
 #include "qthostinterface.h"
 #include "qtutils.h"
 #include <QtCore/QTimer>
@@ -210,27 +211,7 @@ void InputButtonBindingDialog::hookControllerInput()
   if (!controller_interface)
     return;
 
-  controller_interface->SetHook([this](const ControllerInterface::Hook& ei) {
-    if (ei.type == ControllerInterface::Hook::Type::Axis)
-    {
-      // wait until it's at least half pushed so we don't get confused between axises with small movement
-      if (std::abs(ei.value) < 0.5f)
-        return ControllerInterface::Hook::CallbackResult::ContinueMonitoring;
-
-      // TODO: this probably should consider the "last value"
-      QMetaObject::invokeMethod(this, "bindToControllerAxis", Q_ARG(int, ei.controller_index),
-                                Q_ARG(int, ei.button_or_axis_number), Q_ARG(std::optional<bool>, ei.value > 0));
-      return ControllerInterface::Hook::CallbackResult::StopMonitoring;
-    }
-    else if (ei.type == ControllerInterface::Hook::Type::Button && ei.value > 0.0f)
-    {
-      QMetaObject::invokeMethod(this, "bindToControllerButton", Q_ARG(int, ei.controller_index),
-                                Q_ARG(int, ei.button_or_axis_number));
-      return ControllerInterface::Hook::CallbackResult::StopMonitoring;
-    }
-
-    return ControllerInterface::Hook::CallbackResult::ContinueMonitoring;
-  });
+  controller_interface->SetHook(InputButtonBindingMonitor(this));
 }
 
 void InputButtonBindingDialog::unhookControllerInput()
@@ -274,27 +255,7 @@ void InputAxisBindingDialog::hookControllerInput()
   if (!controller_interface)
     return;
 
-  controller_interface->SetHook([this](const ControllerInterface::Hook& ei) {
-    if (ei.type == ControllerInterface::Hook::Type::Axis)
-    {
-      // wait until it's at least half pushed so we don't get confused between axises with small movement
-      if (std::abs(ei.value) < 0.5f)
-        return ControllerInterface::Hook::CallbackResult::ContinueMonitoring;
-
-      QMetaObject::invokeMethod(this, "bindToControllerAxis", Q_ARG(int, ei.controller_index),
-                                Q_ARG(int, ei.button_or_axis_number), Q_ARG(std::optional<bool>, std::nullopt));
-      return ControllerInterface::Hook::CallbackResult::StopMonitoring;
-    }
-    else if (ei.type == ControllerInterface::Hook::Type::Button && m_axis_type == Controller::AxisType::Half &&
-             ei.value > 0.0f)
-    {
-      QMetaObject::invokeMethod(this, "bindToControllerButton", Q_ARG(int, ei.controller_index),
-                                Q_ARG(int, ei.button_or_axis_number));
-      return ControllerInterface::Hook::CallbackResult::StopMonitoring;
-    }
-
-    return ControllerInterface::Hook::CallbackResult::ContinueMonitoring;
-  });
+  controller_interface->SetHook(InputAxisBindingMonitor(this, m_axis_type));
 }
 
 void InputAxisBindingDialog::unhookControllerInput()
diff --git a/src/duckstation-qt/inputbindingmonitor.cpp b/src/duckstation-qt/inputbindingmonitor.cpp
new file mode 100644
index 000000000..906ed236d
--- /dev/null
+++ b/src/duckstation-qt/inputbindingmonitor.cpp
@@ -0,0 +1,60 @@
+#include "inputbindingmonitor.h"
+
+ControllerInterface::Hook::CallbackResult
+InputButtonBindingMonitor::operator()(const ControllerInterface::Hook& ei) const
+{
+  if (ei.type == ControllerInterface::Hook::Type::Axis)
+  {
+    // wait until it's at least half pushed so we don't get confused between axises with small movement
+    if (std::abs(ei.value) < 0.5f)
+      return ControllerInterface::Hook::CallbackResult::ContinueMonitoring;
+
+    // TODO: this probably should consider the "last value"
+    QMetaObject::invokeMethod(m_parent, "bindToControllerAxis", Q_ARG(int, ei.controller_index),
+                              Q_ARG(int, ei.button_or_axis_number), Q_ARG(std::optional<bool>, ei.value > 0));
+    return ControllerInterface::Hook::CallbackResult::StopMonitoring;
+  }
+  else if (ei.type == ControllerInterface::Hook::Type::Button && ei.value > 0.0f)
+  {
+    QMetaObject::invokeMethod(m_parent, "bindToControllerButton", Q_ARG(int, ei.controller_index),
+                              Q_ARG(int, ei.button_or_axis_number));
+    return ControllerInterface::Hook::CallbackResult::StopMonitoring;
+  }
+
+  return ControllerInterface::Hook::CallbackResult::ContinueMonitoring;
+}
+
+ControllerInterface::Hook::CallbackResult InputAxisBindingMonitor::operator()(const ControllerInterface::Hook& ei) const
+{
+  if (ei.type == ControllerInterface::Hook::Type::Axis)
+  {
+    // wait until it's at least half pushed so we don't get confused between axises with small movement
+    if (std::abs(ei.value) < 0.5f)
+      return ControllerInterface::Hook::CallbackResult::ContinueMonitoring;
+
+    QMetaObject::invokeMethod(m_parent, "bindToControllerAxis", Q_ARG(int, ei.controller_index),
+                              Q_ARG(int, ei.button_or_axis_number), Q_ARG(std::optional<bool>, std::nullopt));
+    return ControllerInterface::Hook::CallbackResult::StopMonitoring;
+  }
+  else if (ei.type == ControllerInterface::Hook::Type::Button && m_axis_type == Controller::AxisType::Half &&
+           ei.value > 0.0f)
+  {
+    QMetaObject::invokeMethod(m_parent, "bindToControllerButton", Q_ARG(int, ei.controller_index),
+                              Q_ARG(int, ei.button_or_axis_number));
+    return ControllerInterface::Hook::CallbackResult::StopMonitoring;
+  }
+
+  return ControllerInterface::Hook::CallbackResult::ContinueMonitoring;
+}
+
+ControllerInterface::Hook::CallbackResult
+InputRumbleBindingMonitor::operator()(const ControllerInterface::Hook& ei) const
+{
+  if (ei.type == ControllerInterface::Hook::Type::Button && ei.value > 0.0f)
+  {
+    QMetaObject::invokeMethod(m_parent, "bindToControllerRumble", Q_ARG(int, ei.controller_index));
+    return ControllerInterface::Hook::CallbackResult::StopMonitoring;
+  }
+
+  return ControllerInterface::Hook::CallbackResult::ContinueMonitoring;
+}
\ No newline at end of file
diff --git a/src/duckstation-qt/inputbindingmonitor.h b/src/duckstation-qt/inputbindingmonitor.h
new file mode 100644
index 000000000..d22935535
--- /dev/null
+++ b/src/duckstation-qt/inputbindingmonitor.h
@@ -0,0 +1,45 @@
+#pragma once
+
+#include "frontend-common/controller_interface.h"
+#include <QtCore/QObject>
+
+// NOTE: Those Monitor classes must be copyable to meet the requirements of std::function, but at the same time we want
+// copies to be opaque to the caling code and share context. Therefore, all mutable context of the monitor (if required)
+// must be enclosed in a std::shared_ptr. m_parent/m_axis_type don't mutate so they don't need to be stored as such.
+
+class InputButtonBindingMonitor
+{
+public:
+  explicit InputButtonBindingMonitor(QObject* parent) : m_parent(parent) {}
+
+  ControllerInterface::Hook::CallbackResult operator()(const ControllerInterface::Hook& ei) const;
+
+private:
+  QObject* m_parent;
+};
+
+class InputAxisBindingMonitor
+{
+public:
+  explicit InputAxisBindingMonitor(QObject* parent, Controller::AxisType axis_type)
+    : m_parent(parent), m_axis_type(axis_type)
+  {
+  }
+
+  ControllerInterface::Hook::CallbackResult operator()(const ControllerInterface::Hook& ei) const;
+
+private:
+  QObject* m_parent;
+  Controller::AxisType m_axis_type;
+};
+
+class InputRumbleBindingMonitor
+{
+public:
+  explicit InputRumbleBindingMonitor(QObject* parent) : m_parent(parent) {}
+
+  ControllerInterface::Hook::CallbackResult operator()(const ControllerInterface::Hook& ei) const;
+
+private:
+  QObject* m_parent;
+};
\ No newline at end of file
diff --git a/src/duckstation-qt/inputbindingwidgets.cpp b/src/duckstation-qt/inputbindingwidgets.cpp
index 9d55dc16e..4c9050962 100644
--- a/src/duckstation-qt/inputbindingwidgets.cpp
+++ b/src/duckstation-qt/inputbindingwidgets.cpp
@@ -4,6 +4,7 @@
 #include "core/settings.h"
 #include "frontend-common/controller_interface.h"
 #include "inputbindingdialog.h"
+#include "inputbindingmonitor.h"
 #include "qthostinterface.h"
 #include "qtutils.h"
 #include <QtCore/QTimer>
@@ -236,27 +237,7 @@ void InputButtonBindingWidget::hookControllerInput()
   if (!controller_interface)
     return;
 
-  controller_interface->SetHook([this](const ControllerInterface::Hook& ei) {
-    if (ei.type == ControllerInterface::Hook::Type::Axis)
-    {
-      // wait until it's at least half pushed so we don't get confused between axises with small movement
-      if (std::abs(ei.value) < 0.5f)
-        return ControllerInterface::Hook::CallbackResult::ContinueMonitoring;
-
-      // TODO: this probably should consider the "last value"
-      QMetaObject::invokeMethod(this, "bindToControllerAxis", Q_ARG(int, ei.controller_index),
-                                Q_ARG(int, ei.button_or_axis_number), Q_ARG(std::optional<bool>, ei.value > 0));
-      return ControllerInterface::Hook::CallbackResult::StopMonitoring;
-    }
-    else if (ei.type == ControllerInterface::Hook::Type::Button && ei.value > 0.0f)
-    {
-      QMetaObject::invokeMethod(this, "bindToControllerButton", Q_ARG(int, ei.controller_index),
-                                Q_ARG(int, ei.button_or_axis_number));
-      return ControllerInterface::Hook::CallbackResult::StopMonitoring;
-    }
-
-    return ControllerInterface::Hook::CallbackResult::ContinueMonitoring;
-  });
+  controller_interface->SetHook(InputButtonBindingMonitor(this));
 }
 
 void InputButtonBindingWidget::unhookControllerInput()
@@ -306,27 +287,7 @@ void InputAxisBindingWidget::hookControllerInput()
   if (!controller_interface)
     return;
 
-  controller_interface->SetHook([this](const ControllerInterface::Hook& ei) {
-    if (ei.type == ControllerInterface::Hook::Type::Axis)
-    {
-      // wait until it's at least half pushed so we don't get confused between axises with small movement
-      if (std::abs(ei.value) < 0.5f)
-        return ControllerInterface::Hook::CallbackResult::ContinueMonitoring;
-
-      QMetaObject::invokeMethod(this, "bindToControllerAxis", Q_ARG(int, ei.controller_index),
-                                Q_ARG(int, ei.button_or_axis_number), Q_ARG(std::optional<bool>, std::nullopt));
-      return ControllerInterface::Hook::CallbackResult::StopMonitoring;
-    }
-    else if (ei.type == ControllerInterface::Hook::Type::Button && m_axis_type == Controller::AxisType::Half &&
-             ei.value > 0.0f)
-    {
-      QMetaObject::invokeMethod(this, "bindToControllerButton", Q_ARG(int, ei.controller_index),
-                                Q_ARG(int, ei.button_or_axis_number));
-      return ControllerInterface::Hook::CallbackResult::StopMonitoring;
-    }
-
-    return ControllerInterface::Hook::CallbackResult::ContinueMonitoring;
-  });
+  controller_interface->SetHook(InputAxisBindingMonitor(this, m_axis_type));
 }
 
 void InputAxisBindingWidget::unhookControllerInput()
@@ -391,15 +352,7 @@ void InputRumbleBindingWidget::hookControllerInput()
   if (!controller_interface)
     return;
 
-  controller_interface->SetHook([this](const ControllerInterface::Hook& ei) {
-    if (ei.type == ControllerInterface::Hook::Type::Button && ei.value > 0.0f)
-    {
-      QMetaObject::invokeMethod(this, "bindToControllerRumble", Q_ARG(int, ei.controller_index));
-      return ControllerInterface::Hook::CallbackResult::StopMonitoring;
-    }
-
-    return ControllerInterface::Hook::CallbackResult::ContinueMonitoring;
-  });
+  controller_interface->SetHook(InputRumbleBindingMonitor(this));
 }
 
 void InputRumbleBindingWidget::unhookControllerInput()

From 2880b71b485adbe33657d88d49ea104c51df144b Mon Sep 17 00:00:00 2001
From: Silent <zdanio95@gmail.com>
Date: Sun, 15 Nov 2020 14:56:52 +0100
Subject: [PATCH 2/3] Support for SDL Joysticks

This enables use of non-controller peripherals,
such as DirectInput steering wheels or flight sticks
---
 src/core/negcon.cpp                           |   5 +-
 src/duckstation-qt/inputbindingdialog.cpp     |  18 +-
 src/duckstation-qt/inputbindingdialog.h       |   4 +-
 src/duckstation-qt/inputbindingmonitor.cpp    | 104 +++++-
 src/duckstation-qt/inputbindingmonitor.h      |  19 +
 src/duckstation-qt/inputbindingwidgets.cpp    |  18 +-
 src/duckstation-qt/inputbindingwidgets.h      |   4 +-
 src/frontend-common/common_host_interface.cpp |  67 +++-
 src/frontend-common/controller_interface.cpp  |   8 +-
 src/frontend-common/controller_interface.h    |  21 +-
 .../sdl_controller_interface.cpp              | 330 +++++++++++++++++-
 .../sdl_controller_interface.h                |  17 +-
 .../xinput_controller_interface.cpp           |  22 +-
 .../xinput_controller_interface.h             |   6 +-
 14 files changed, 570 insertions(+), 73 deletions(-)

diff --git a/src/core/negcon.cpp b/src/core/negcon.cpp
index a12367633..031e85f50 100644
--- a/src/core/negcon.cpp
+++ b/src/core/negcon.cpp
@@ -63,9 +63,8 @@ void NeGcon::SetAxisState(s32 axis_code, float value)
     return;
   }
 
-  // I, II, L: 0..1 -> 0..255 or -1..0 -> 0..255 to support negative axis ranges,
-  // e.g. if bound to analog stick instead of trigger
-  const u8 u8_value = static_cast<u8>(std::clamp(std::abs(value) * 255.0f, 0.0f, 255.0f));
+  // I, II, L: -1..1 -> 0..255
+  const u8 u8_value = static_cast<u8>(std::clamp(((value + 1.0f) / 2.0f) * 255.0f, 0.0f, 255.0f));
 
   SetAxisState(static_cast<Axis>(axis_code), u8_value);
 }
diff --git a/src/duckstation-qt/inputbindingdialog.cpp b/src/duckstation-qt/inputbindingdialog.cpp
index 294a0af19..053245848 100644
--- a/src/duckstation-qt/inputbindingdialog.cpp
+++ b/src/duckstation-qt/inputbindingdialog.cpp
@@ -128,16 +128,18 @@ void InputBindingDialog::addNewBinding(std::string new_binding)
   saveListToSettings();
 }
 
-void InputBindingDialog::bindToControllerAxis(int controller_index, int axis_index, std::optional<bool> positive)
+void InputBindingDialog::bindToControllerAxis(int controller_index, int axis_index, bool inverted,
+                                              std::optional<bool> half_axis_positive)
 {
+  const char* invert_char = inverted ? "-" : "";
   const char* sign_char = "";
-  if (positive)
+  if (half_axis_positive)
   {
-    sign_char = *positive ? "+" : "-";
+    sign_char = *half_axis_positive ? "+" : "-";
   }
 
   std::string binding =
-    StringUtil::StdStringFromFormat("Controller%d/%sAxis%d", controller_index, sign_char, axis_index);
+    StringUtil::StdStringFromFormat("Controller%d/%sAxis%d%s", controller_index, sign_char, axis_index, invert_char);
   addNewBinding(std::move(binding));
   stopListeningForInput();
 }
@@ -149,6 +151,14 @@ void InputBindingDialog::bindToControllerButton(int controller_index, int button
   stopListeningForInput();
 }
 
+void InputBindingDialog::bindToControllerHat(int controller_index, int hat_index, const QString& hat_direction)
+{
+  std::string binding = StringUtil::StdStringFromFormat("Controller%d/Hat%d %s", controller_index, hat_index,
+                                                        hat_direction.toLatin1().constData());
+  addNewBinding(std::move(binding));
+  stopListeningForInput();
+}
+
 void InputBindingDialog::onAddBindingButtonClicked()
 {
   if (isListeningForInput())
diff --git a/src/duckstation-qt/inputbindingdialog.h b/src/duckstation-qt/inputbindingdialog.h
index ff26b3776..5e6b64982 100644
--- a/src/duckstation-qt/inputbindingdialog.h
+++ b/src/duckstation-qt/inputbindingdialog.h
@@ -19,8 +19,10 @@ public:
   ~InputBindingDialog();
 
 protected Q_SLOTS:
-  void bindToControllerAxis(int controller_index, int axis_index, std::optional<bool> positive);
+  void bindToControllerAxis(int controller_index, int axis_index, bool inverted,
+                            std::optional<bool> half_axis_positive);
   void bindToControllerButton(int controller_index, int button_index);
+  void bindToControllerHat(int controller_index, int hat_index, const QString& hat_direction);
   void onAddBindingButtonClicked();
   void onRemoveBindingButtonClicked();
   void onClearBindingsButtonClicked();
diff --git a/src/duckstation-qt/inputbindingmonitor.cpp b/src/duckstation-qt/inputbindingmonitor.cpp
index 906ed236d..e0e0ed7f1 100644
--- a/src/duckstation-qt/inputbindingmonitor.cpp
+++ b/src/duckstation-qt/inputbindingmonitor.cpp
@@ -1,4 +1,5 @@
 #include "inputbindingmonitor.h"
+#include <cmath>
 
 ControllerInterface::Hook::CallbackResult
 InputButtonBindingMonitor::operator()(const ControllerInterface::Hook& ei) const
@@ -6,20 +7,32 @@ InputButtonBindingMonitor::operator()(const ControllerInterface::Hook& ei) const
   if (ei.type == ControllerInterface::Hook::Type::Axis)
   {
     // wait until it's at least half pushed so we don't get confused between axises with small movement
-    if (std::abs(ei.value) < 0.5f)
+    if (std::abs(std::get<float>(ei.value)) < 0.5f)
       return ControllerInterface::Hook::CallbackResult::ContinueMonitoring;
 
     // TODO: this probably should consider the "last value"
     QMetaObject::invokeMethod(m_parent, "bindToControllerAxis", Q_ARG(int, ei.controller_index),
-                              Q_ARG(int, ei.button_or_axis_number), Q_ARG(std::optional<bool>, ei.value > 0));
+                              Q_ARG(int, ei.button_or_axis_number), Q_ARG(bool, false),
+                              Q_ARG(std::optional<bool>, std::get<float>(ei.value) > 0));
     return ControllerInterface::Hook::CallbackResult::StopMonitoring;
   }
-  else if (ei.type == ControllerInterface::Hook::Type::Button && ei.value > 0.0f)
+  else if (ei.type == ControllerInterface::Hook::Type::Button && std::get<float>(ei.value) > 0.0f)
   {
     QMetaObject::invokeMethod(m_parent, "bindToControllerButton", Q_ARG(int, ei.controller_index),
                               Q_ARG(int, ei.button_or_axis_number));
     return ControllerInterface::Hook::CallbackResult::StopMonitoring;
   }
+  else if (ei.type == ControllerInterface::Hook::Type::Hat)
+  {
+    const std::string_view hat_position = std::get<std::string_view>(ei.value);
+    if (!hat_position.empty())
+    {
+      QString str = QString::fromLatin1(hat_position.data(), static_cast<int>(hat_position.size()));
+      QMetaObject::invokeMethod(m_parent, "bindToControllerHat", Q_ARG(int, ei.controller_index),
+                                Q_ARG(int, ei.button_or_axis_number), Q_ARG(QString, std::move(str)));
+      return ControllerInterface::Hook::CallbackResult::StopMonitoring;
+    }
+  }
 
   return ControllerInterface::Hook::CallbackResult::ContinueMonitoring;
 }
@@ -28,16 +41,17 @@ ControllerInterface::Hook::CallbackResult InputAxisBindingMonitor::operator()(co
 {
   if (ei.type == ControllerInterface::Hook::Type::Axis)
   {
-    // wait until it's at least half pushed so we don't get confused between axises with small movement
-    if (std::abs(ei.value) < 0.5f)
+    std::optional<bool> half_axis_positive, inverted;
+    if (!ProcessAxisInput(ei, half_axis_positive, inverted))
       return ControllerInterface::Hook::CallbackResult::ContinueMonitoring;
 
     QMetaObject::invokeMethod(m_parent, "bindToControllerAxis", Q_ARG(int, ei.controller_index),
-                              Q_ARG(int, ei.button_or_axis_number), Q_ARG(std::optional<bool>, std::nullopt));
+                              Q_ARG(int, ei.button_or_axis_number), Q_ARG(bool, inverted.value_or(false)),
+                              Q_ARG(std::optional<bool>, half_axis_positive));
     return ControllerInterface::Hook::CallbackResult::StopMonitoring;
   }
   else if (ei.type == ControllerInterface::Hook::Type::Button && m_axis_type == Controller::AxisType::Half &&
-           ei.value > 0.0f)
+           std::get<float>(ei.value) > 0.0f)
   {
     QMetaObject::invokeMethod(m_parent, "bindToControllerButton", Q_ARG(int, ei.controller_index),
                               Q_ARG(int, ei.button_or_axis_number));
@@ -47,10 +61,84 @@ ControllerInterface::Hook::CallbackResult InputAxisBindingMonitor::operator()(co
   return ControllerInterface::Hook::CallbackResult::ContinueMonitoring;
 }
 
+bool InputAxisBindingMonitor::ProcessAxisInput(const ControllerInterface::Hook& ei,
+                                               std::optional<bool>& half_axis_positive,
+                                               std::optional<bool>& inverted) const
+{
+  const float value = std::get<float>(ei.value);
+
+  if (!ei.track_history) // Keyboard, mouse, game controller
+  {
+    // wait until it's at least half pushed so we don't get confused between axises with small movement
+    if (std::abs(value) < 0.5f)
+      return false;
+
+    return true;
+  }
+  else // Joystick
+  {
+    auto& history = m_context->m_inputs_history;
+    // Reject inputs coming from multiple sources
+    if (!history.empty())
+    {
+      const auto& item = history.front();
+      if (ei.controller_index != item.controller_index || ei.button_or_axis_number != item.axis_number)
+        return false;
+    }
+    history.push_back({ei.controller_index, ei.button_or_axis_number, value});
+    return AnalyzeInputHistory(half_axis_positive, inverted);
+  }
+}
+
+bool InputAxisBindingMonitor::AnalyzeInputHistory(std::optional<bool>& half_axis_positive,
+                                                  std::optional<bool>& inverted) const
+{
+  const auto& history = m_context->m_inputs_history;
+  const auto [min, max] = std::minmax_element(
+    history.begin(), history.end(), [](const auto& left, const auto& right) { return left.value < right.value; });
+
+  // Ignore small input magnitudes
+  if (std::abs(max->value - min->value) < 0.5f)
+    return false;
+
+  // Used heuristics:
+  // * If history contains inputs with both - and + sign (ignoring 0), bind a full axis
+  // * If history contains only 0 and inputs of the same sign AND maxes out at 1.0/-1.0, bind a half axis
+  // * Use the direction of input changes to determine whether the axis is inverted or not
+  if (std::signbit(min->value) != std::signbit(max->value))
+  {
+    if (min->value != 0.0f && max->value != 0.0f)
+    {
+      // If max value comes before the min value, invert the half axis
+      if (m_axis_type == Controller::AxisType::Half)
+      {
+        inverted = std::distance(min, max) < 0;
+      }
+
+      return true;
+    }
+  }
+  else
+  {
+    if ((std::abs(min->value) > 0.99f || std::abs(max->value) > 0.99f) &&
+        (std::abs(min->value) < 0.01f || std::abs(max->value) < 0.01f))
+    {
+
+      if (m_axis_type == Controller::AxisType::Half)
+      {
+        half_axis_positive = max->value > 0.0f;
+      }
+      return true;
+    }
+  }
+
+  return false;
+}
+
 ControllerInterface::Hook::CallbackResult
 InputRumbleBindingMonitor::operator()(const ControllerInterface::Hook& ei) const
 {
-  if (ei.type == ControllerInterface::Hook::Type::Button && ei.value > 0.0f)
+  if (ei.type == ControllerInterface::Hook::Type::Button && std::get<float>(ei.value) > 0.0f)
   {
     QMetaObject::invokeMethod(m_parent, "bindToControllerRumble", Q_ARG(int, ei.controller_index));
     return ControllerInterface::Hook::CallbackResult::StopMonitoring;
diff --git a/src/duckstation-qt/inputbindingmonitor.h b/src/duckstation-qt/inputbindingmonitor.h
index d22935535..89e90c54c 100644
--- a/src/duckstation-qt/inputbindingmonitor.h
+++ b/src/duckstation-qt/inputbindingmonitor.h
@@ -2,6 +2,8 @@
 
 #include "frontend-common/controller_interface.h"
 #include <QtCore/QObject>
+#include <memory>
+#include <vector>
 
 // NOTE: Those Monitor classes must be copyable to meet the requirements of std::function, but at the same time we want
 // copies to be opaque to the caling code and share context. Therefore, all mutable context of the monitor (if required)
@@ -29,8 +31,25 @@ public:
   ControllerInterface::Hook::CallbackResult operator()(const ControllerInterface::Hook& ei) const;
 
 private:
+  bool ProcessAxisInput(const ControllerInterface::Hook& ei, std::optional<bool>& half_axis_positive,
+                        std::optional<bool>& inverted) const;
+  bool AnalyzeInputHistory(std::optional<bool>& half_axis_positive, std::optional<bool>& inverted) const;
+
+  struct Context
+  {
+    struct History
+    {
+      int controller_index;
+      int axis_number;
+      float value;
+    };
+
+    std::vector<History> m_inputs_history;
+  };
+
   QObject* m_parent;
   Controller::AxisType m_axis_type;
+  std::shared_ptr<Context> m_context = std::make_shared<Context>();
 };
 
 class InputRumbleBindingMonitor
diff --git a/src/duckstation-qt/inputbindingwidgets.cpp b/src/duckstation-qt/inputbindingwidgets.cpp
index 4c9050962..98a84470e 100644
--- a/src/duckstation-qt/inputbindingwidgets.cpp
+++ b/src/duckstation-qt/inputbindingwidgets.cpp
@@ -41,16 +41,18 @@ void InputBindingWidget::updateText()
     setText(QString::fromStdString(m_bindings[0]));
 }
 
-void InputBindingWidget::bindToControllerAxis(int controller_index, int axis_index, std::optional<bool> positive)
+void InputBindingWidget::bindToControllerAxis(int controller_index, int axis_index, bool inverted,
+                                              std::optional<bool> half_axis_positive)
 {
+  const char* invert_char = inverted ? "-" : "";
   const char* sign_char = "";
-  if (positive)
+  if (half_axis_positive)
   {
-    sign_char = *positive ? "+" : "-";
+    sign_char = *half_axis_positive ? "+" : "-";
   }
 
   m_new_binding_value =
-    StringUtil::StdStringFromFormat("Controller%d/%sAxis%d", controller_index, sign_char, axis_index);
+    StringUtil::StdStringFromFormat("Controller%d/%sAxis%d%s", controller_index, sign_char, axis_index, invert_char);
   setNewBinding();
   stopListeningForInput();
 }
@@ -62,6 +64,14 @@ void InputBindingWidget::bindToControllerButton(int controller_index, int button
   stopListeningForInput();
 }
 
+void InputBindingWidget::bindToControllerHat(int controller_index, int hat_index, const QString& hat_direction)
+{
+  m_new_binding_value = StringUtil::StdStringFromFormat("Controller%d/Hat%d %s", controller_index, hat_index,
+                                                        hat_direction.toLatin1().constData());
+  setNewBinding();
+  stopListeningForInput();
+}
+
 void InputBindingWidget::beginRebindAll()
 {
   m_is_binding_all = true;
diff --git a/src/duckstation-qt/inputbindingwidgets.h b/src/duckstation-qt/inputbindingwidgets.h
index e34f44d8d..34113dfcd 100644
--- a/src/duckstation-qt/inputbindingwidgets.h
+++ b/src/duckstation-qt/inputbindingwidgets.h
@@ -20,8 +20,10 @@ public:
   ALWAYS_INLINE void setNextWidget(InputBindingWidget* widget) { m_next_widget = widget; }
 
 public Q_SLOTS:
-  void bindToControllerAxis(int controller_index, int axis_index, std::optional<bool> positive);
+  void bindToControllerAxis(int controller_index, int axis_index, bool inverted,
+                            std::optional<bool> half_axis_positive);
   void bindToControllerButton(int controller_index, int button_index);
+  void bindToControllerHat(int controller_index, int hat_index, const QString& hat_direction);
   void beginRebindAll();
   void clearBinding();
   void reloadBinding();
diff --git a/src/frontend-common/common_host_interface.cpp b/src/frontend-common/common_host_interface.cpp
index 75c130c40..8dc7ef0fa 100644
--- a/src/frontend-common/common_host_interface.cpp
+++ b/src/frontend-common/common_host_interface.cpp
@@ -1242,6 +1242,30 @@ bool CommonHostInterface::AddButtonToInputMap(const std::string& binding, const
 
       return true;
     }
+    else if (StringUtil::StartsWith(button, "Hat"))
+    {
+      const std::optional<int> hat_index = StringUtil::FromChars<int>(button.substr(3));
+      const std::optional<std::string_view> hat_direction = [](const auto& button) {
+        std::optional<std::string_view> result;
+
+        const size_t pos = button.find(' ');
+        if (pos != button.npos)
+        {
+          result = button.substr(pos + 1);
+        }
+        return result;
+      }(button);
+
+      if (!hat_index || !hat_direction ||
+          !m_controller_interface->BindControllerHatToButton(*controller_index, *hat_index, *hat_direction,
+                                                             std::move(handler)))
+      {
+        Log_WarningPrintf("Failed to bind controller hat '%s' to button", binding.c_str());
+        return false;
+      }
+
+      return true;
+    }
 
     Log_WarningPrintf("Malformed controller binding '%s' in button", binding.c_str());
     return false;
@@ -1266,7 +1290,8 @@ bool CommonHostInterface::AddAxisToInputMap(const std::string& binding, const st
         return false;
       }
 
-      m_keyboard_input_handlers.emplace(key_id.value(), std::move(handler));
+      m_keyboard_input_handlers.emplace(key_id.value(),
+                                        [cb = std::move(handler)](bool pressed) { cb(pressed ? 1.0f : -1.0f); });
       return true;
     }
 
@@ -1281,7 +1306,8 @@ bool CommonHostInterface::AddAxisToInputMap(const std::string& binding, const st
           return false;
         }
 
-        m_mouse_input_handlers.emplace(static_cast<HostMouseButton>(button_index.value()), std::move(handler));
+        m_mouse_input_handlers.emplace(static_cast<HostMouseButton>(button_index.value()),
+                                       [cb = std::move(handler)](bool pressed) { cb(pressed ? 1.0f : -1.0f); });
         return true;
       }
 
@@ -1305,17 +1331,38 @@ bool CommonHostInterface::AddAxisToInputMap(const std::string& binding, const st
       return false;
     }
 
-    if (StringUtil::StartsWith(axis, "Axis"))
+    if (StringUtil::StartsWith(axis, "Axis") || StringUtil::StartsWith(axis, "+Axis") ||
+        StringUtil::StartsWith(axis, "-Axis"))
     {
-      const std::optional<int> axis_index = StringUtil::FromChars<int>(axis.substr(4));
-      if (!axis_index ||
-          !m_controller_interface->BindControllerAxis(*controller_index, *axis_index, std::move(handler)))
+      const std::optional<int> axis_index =
+        StringUtil::FromChars<int>(axis.substr(axis[0] == '+' || axis[0] == '-' ? 5 : 4));
+      if (axis_index)
       {
-        Log_WarningPrintf("Failed to bind controller axis '%s' to axis", binding.c_str());
-        return false;
-      }
+        ControllerInterface::AxisSide axis_side = ControllerInterface::AxisSide::Full;
+        if (axis[0] == '+')
+          axis_side = ControllerInterface::AxisSide::Positive;
+        else if (axis[0] == '-')
+          axis_side = ControllerInterface::AxisSide::Negative;
 
-      return true;
+        const bool inverted = StringUtil::EndsWith(axis, "-");
+        if (!inverted)
+        {
+          if (m_controller_interface->BindControllerAxis(*controller_index, *axis_index, axis_side, std::move(handler)))
+          {
+            return true;
+          }
+        }
+        else
+        {
+          if (m_controller_interface->BindControllerAxis(*controller_index, *axis_index, axis_side,
+                                                         [cb = std::move(handler)](float value) { cb(-value); }))
+          {
+            return true;
+          }
+        }
+      }
+      Log_WarningPrintf("Failed to bind controller axis '%s' to axis", binding.c_str());
+      return false;
     }
     else if (StringUtil::StartsWith(axis, "Button") && axis_type == Controller::AxisType::Half)
     {
diff --git a/src/frontend-common/controller_interface.cpp b/src/frontend-common/controller_interface.cpp
index 0cfc58e8f..c27f093fc 100644
--- a/src/frontend-common/controller_interface.cpp
+++ b/src/frontend-common/controller_interface.cpp
@@ -36,13 +36,14 @@ void ControllerInterface::ClearHook()
     m_event_intercept_callback = {};
 }
 
-bool ControllerInterface::DoEventHook(Hook::Type type, int controller_index, int button_or_axis_number, float value)
+bool ControllerInterface::DoEventHook(Hook::Type type, int controller_index, int button_or_axis_number,
+                                      std::variant<float, std::string_view> value, bool track_history)
 {
   std::unique_lock<std::mutex> lock(m_event_intercept_mutex);
   if (!m_event_intercept_callback)
     return false;
 
-  const Hook ei{type, controller_index, button_or_axis_number, value};
+  const Hook ei{type, controller_index, button_or_axis_number, std::move(value), track_history};
   const Hook::CallbackResult action = m_event_intercept_callback(ei);
   if (action == Hook::CallbackResult::StopMonitoring)
     m_event_intercept_callback = {};
@@ -64,7 +65,8 @@ void ControllerInterface::OnControllerDisconnected(int host_id)
 
 void ControllerInterface::ClearBindings() {}
 
-bool ControllerInterface::BindControllerAxis(int controller_index, int axis_number, AxisCallback callback)
+bool ControllerInterface::BindControllerAxis(int controller_index, int axis_number, AxisSide axis_side,
+                                             AxisCallback callback)
 {
   return false;
 }
diff --git a/src/frontend-common/controller_interface.h b/src/frontend-common/controller_interface.h
index 952f6a2b6..d40c167e5 100644
--- a/src/frontend-common/controller_interface.h
+++ b/src/frontend-common/controller_interface.h
@@ -6,6 +6,7 @@
 #include <map>
 #include <mutex>
 #include <optional>
+#include <variant>
 
 class HostInterface;
 class Controller;
@@ -31,6 +32,13 @@ public:
     MAX_NUM_BUTTONS = 15
   };
 
+  enum AxisSide
+  {
+    Full,
+    Positive,
+    Negative
+  };
+
   using AxisCallback = CommonHostInterface::InputAxisHandler;
   using ButtonCallback = CommonHostInterface::InputButtonHandler;
 
@@ -50,10 +58,12 @@ public:
   virtual void ClearBindings() = 0;
 
   // Binding to events. If a binding for this axis/button already exists, returns false.
-  virtual bool BindControllerAxis(int controller_index, int axis_number, AxisCallback callback) = 0;
+  virtual bool BindControllerAxis(int controller_index, int axis_number, AxisSide axis_side, AxisCallback callback) = 0;
   virtual bool BindControllerButton(int controller_index, int button_number, ButtonCallback callback) = 0;
   virtual bool BindControllerAxisToButton(int controller_index, int axis_number, bool direction,
                                           ButtonCallback callback) = 0;
+  virtual bool BindControllerHatToButton(int controller_index, int hat_number, std::string_view hat_position,
+                                         ButtonCallback callback) = 0;
   virtual bool BindControllerButtonToAxis(int controller_index, int button_number, AxisCallback callback) = 0;
 
   virtual void PollEvents() = 0;
@@ -71,7 +81,8 @@ public:
     enum class Type
     {
       Axis,
-      Button
+      Button,
+      Hat // Only for joysticks
     };
 
     enum class CallbackResult
@@ -85,13 +96,15 @@ public:
     Type type;
     int controller_index;
     int button_or_axis_number;
-    float value; // 0/1 for buttons, -1..1 for axises
+    std::variant<float, std::string_view> value; // 0/1 for buttons, -1..1 for axes, hat direction name for hats
+    bool track_history;                          // Track axis movement to spot inversion/half axes
   };
   void SetHook(Hook::Callback callback);
   void ClearHook();
 
 protected:
-  bool DoEventHook(Hook::Type type, int controller_index, int button_or_axis_number, float value);
+  bool DoEventHook(Hook::Type type, int controller_index, int button_or_axis_number,
+                   std::variant<float, std::string_view> value, bool track_history = false);
 
   void OnControllerConnected(int host_id);
   void OnControllerDisconnected(int host_id);
diff --git a/src/frontend-common/sdl_controller_interface.cpp b/src/frontend-common/sdl_controller_interface.cpp
index d9852841c..10d299446 100644
--- a/src/frontend-common/sdl_controller_interface.cpp
+++ b/src/frontend-common/sdl_controller_interface.cpp
@@ -106,18 +106,29 @@ bool SDLControllerInterface::ProcessSDLEvent(const SDL_Event* event)
     case SDL_CONTROLLERBUTTONUP:
       return HandleControllerButtonEvent(event);
 
+    case SDL_JOYDEVICEADDED:
+      if (SDL_IsGameController(event->jdevice.which))
+        return true;
+
+      Log_InfoPrintf("Joystick %d inserted", event->jdevice.which);
+      OpenJoystick(event->jdevice.which);
+      return true;
+
+    case SDL_JOYAXISMOTION:
+      return HandleJoystickAxisEvent(&event->jaxis);
+
+    case SDL_JOYHATMOTION:
+      return HandleJoystickHatEvent(&event->jhat);
+
+    case SDL_JOYBUTTONDOWN:
+    case SDL_JOYBUTTONUP:
+      return HandleJoystickButtonEvent(&event->jbutton);
+
     default:
       return false;
   }
 }
 
-SDLControllerInterface::ControllerDataVector::iterator
-SDLControllerInterface::GetControllerDataForController(void* controller)
-{
-  return std::find_if(m_controllers.begin(), m_controllers.end(),
-                      [controller](const ControllerData& cd) { return cd.controller == controller; });
-}
-
 SDLControllerInterface::ControllerDataVector::iterator SDLControllerInterface::GetControllerDataForJoystickId(int id)
 {
   return std::find_if(m_controllers.begin(), m_controllers.end(),
@@ -179,10 +190,10 @@ bool SDLControllerInterface::OpenGameController(int index)
                  SDL_GameControllerName(gcontroller));
 
   ControllerData cd = {};
-  cd.controller = gcontroller;
   cd.player_id = player_id;
   cd.joystick_id = joystick_id;
   cd.haptic_left_right_effect = -1;
+  cd.is_game_controller = true;
 
   SDL_Haptic* haptic = SDL_HapticOpenFromJoystick(joystick);
   if (haptic)
@@ -233,7 +244,7 @@ bool SDLControllerInterface::CloseGameController(int joystick_index, bool notify
   if (it->haptic)
     SDL_HapticClose(static_cast<SDL_Haptic*>(it->haptic));
 
-  SDL_GameControllerClose(static_cast<SDL_GameController*>(it->controller));
+  SDL_GameControllerClose(SDL_GameControllerFromInstanceID(joystick_index));
   m_controllers.erase(it);
 
   if (notify)
@@ -241,18 +252,264 @@ bool SDLControllerInterface::CloseGameController(int joystick_index, bool notify
   return true;
 }
 
+bool SDLControllerInterface::OpenJoystick(int index)
+{
+  SDL_Joystick* joystick = SDL_JoystickOpen(index);
+  if (!joystick)
+  {
+    Log_WarningPrintf("Failed to open joystick %d", index);
+
+    return false;
+  }
+
+  int joystick_id = SDL_JoystickInstanceID(joystick);
+#if SDL_VERSION_ATLEAST(2, 0, 9)
+  int player_id = SDL_JoystickGetDevicePlayerIndex(index);
+#else
+  int player_id = -1;
+#endif
+  if (player_id < 0 || GetControllerDataForPlayerId(player_id) != m_controllers.end())
+  {
+    const int free_player_id = GetFreePlayerId();
+    Log_WarningPrintf(
+      "Controller %d (joystick %d) returned player ID %d, which is invalid or in use. Using ID %d instead.", index,
+      joystick_id, player_id, free_player_id);
+    player_id = free_player_id;
+  }
+
+  const char* name = SDL_JoystickName(joystick);
+
+  Log_InfoPrintf("Opened controller %d (instance id %d, player id %d): %s", 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.is_game_controller = false;
+
+  SDL_Haptic* haptic = SDL_HapticOpenFromJoystick(joystick);
+  if (haptic)
+  {
+    SDL_HapticEffect ef = {};
+    ef.leftright.type = SDL_HAPTIC_LEFTRIGHT;
+    ef.leftright.length = 1000;
+
+    int ef_id = SDL_HapticNewEffect(haptic, &ef);
+    if (ef_id >= 0)
+    {
+      cd.haptic = haptic;
+      cd.haptic_left_right_effect = ef_id;
+    }
+    else
+    {
+      Log_ErrorPrintf("Failed to create haptic left/right effect: %s", SDL_GetError());
+      if (SDL_HapticRumbleSupported(haptic) && SDL_HapticRumbleInit(haptic) != 0)
+      {
+        cd.haptic = haptic;
+      }
+      else
+      {
+        Log_ErrorPrintf("No haptic rumble supported: %s", SDL_GetError());
+        SDL_HapticClose(haptic);
+      }
+    }
+  }
+
+  if (cd.haptic)
+    Log_InfoPrintf("Rumble is supported on '%s'", name);
+  else
+    Log_WarningPrintf("Rumble is not supported on '%s'", name);
+
+  m_controllers.push_back(std::move(cd));
+  OnControllerConnected(player_id);
+  return true;
+}
+
+bool SDLControllerInterface::HandleJoystickAxisEvent(const SDL_JoyAxisEvent* event)
+{
+  const float value = static_cast<float>(event->value) / (event->value < 0 ? 32768.0f : 32767.0f);
+  Log_DebugPrintf("controller %d axis %d %d %f", event->which, event->axis, event->value, value);
+
+  auto it = GetControllerDataForJoystickId(event->which);
+  if (it == m_controllers.end() || it->is_game_controller)
+    return false;
+
+  if (DoEventHook(Hook::Type::Axis, it->player_id, event->axis, value, true))
+    return true;
+
+  bool processed = false;
+
+  const AxisCallback& cb = it->axis_mapping[event->axis][AxisSide::Full];
+  if (cb)
+  {
+    cb(value);
+    processed = true;
+  }
+
+  if (value > 0.0f)
+  {
+    const AxisCallback& cb = it->axis_mapping[event->axis][AxisSide::Positive];
+    if (cb)
+    {
+      // Expand 0..1 - -1..1
+      cb(value * 2.0f - 1.0f);
+      processed = true;
+    }
+  }
+  else if (value < 0.0f)
+  {
+    const AxisCallback& cb = it->axis_mapping[event->axis][AxisSide::Negative];
+    if (cb)
+    {
+      // Expand 0..-1 - -1..1
+      cb(value * -2.0f - 1.0f);
+      processed = true;
+    }
+  }
+
+  if (processed)
+    return true;
+
+  // set the other direction to false so large movements don't leave the opposite on
+  const bool outside_deadzone = (std::abs(value) >= it->deadzone);
+  const bool positive = (value >= 0.0f);
+  const ButtonCallback& other_button_cb = it->axis_button_mapping[event->axis][BoolToUInt8(!positive)];
+  const ButtonCallback& button_cb = it->axis_button_mapping[event->axis][BoolToUInt8(positive)];
+  if (button_cb)
+  {
+    button_cb(outside_deadzone);
+    if (other_button_cb)
+      other_button_cb(false);
+    return true;
+  }
+  else if (other_button_cb)
+  {
+    other_button_cb(false);
+    return true;
+  }
+  else
+  {
+    return false;
+  }
+}
+
+bool SDLControllerInterface::HandleJoystickButtonEvent(const SDL_JoyButtonEvent* event)
+{
+  Log_DebugPrintf("controller %d button %d %s", event->which, event->button,
+                  event->state == SDL_PRESSED ? "pressed" : "released");
+
+  auto it = GetControllerDataForJoystickId(event->which);
+  if (it == m_controllers.end() || it->is_game_controller)
+    return false;
+
+  const bool pressed = (event->state == SDL_PRESSED);
+  if (DoEventHook(Hook::Type::Button, it->player_id, event->button, pressed ? 1.0f : 0.0f))
+    return true;
+
+  const ButtonCallback& cb = it->button_mapping[event->button];
+  if (cb)
+  {
+    cb(pressed);
+    return true;
+  }
+
+  const AxisCallback& axis_cb = it->button_axis_mapping[event->button];
+  if (axis_cb)
+  {
+    axis_cb(pressed ? 1.0f : -1.0f);
+    return true;
+  }
+
+  return false;
+}
+
+bool SDLControllerInterface::HandleJoystickHatEvent(const SDL_JoyHatEvent* event)
+{
+  Log_DebugPrintf("controller %d hat %d %d", event->which, event->hat, event->value);
+
+  auto it = GetControllerDataForJoystickId(event->which);
+  if (it == m_controllers.end() || it->is_game_controller)
+    return false;
+
+  auto HatEventHook = [hat = event->hat, value = event->value, player_id = it->player_id, this](int hat_position) {
+    if ((value & hat_position) == 0)
+      return false;
+
+    std::string_view position_str;
+    switch (value)
+    {
+      case SDL_HAT_UP:
+        position_str = "Up";
+        break;
+      case SDL_HAT_RIGHT:
+        position_str = "Right";
+        break;
+      case SDL_HAT_DOWN:
+        position_str = "Down";
+        break;
+      case SDL_HAT_LEFT:
+        position_str = "Left";
+        break;
+      default:
+        return false;
+    }
+
+    return DoEventHook(Hook::Type::Hat, player_id, hat, position_str);
+  };
+
+  if (event->value == SDL_HAT_CENTERED)
+  {
+    if (HatEventHook(SDL_HAT_CENTERED))
+      return true;
+  }
+  else
+  {
+    // event->value can be a bitmask of multiple direction, so probe them all
+    if (HatEventHook(SDL_HAT_UP) || HatEventHook(SDL_HAT_RIGHT) || HatEventHook(SDL_HAT_DOWN) ||
+        HatEventHook(SDL_HAT_LEFT))
+      return true;
+  }
+
+  bool processed = false;
+
+  if (const ButtonCallback& cb = it->hat_button_mapping[event->hat][0]; cb)
+  {
+    cb(event->value & SDL_HAT_UP);
+    processed = true;
+  }
+  if (const ButtonCallback& cb = it->hat_button_mapping[event->hat][1]; cb)
+  {
+    cb(event->value & SDL_HAT_RIGHT);
+    processed = true;
+  }
+  if (const ButtonCallback& cb = it->hat_button_mapping[event->hat][2]; cb)
+  {
+    cb(event->value & SDL_HAT_DOWN);
+    processed = true;
+  }
+  if (const ButtonCallback& cb = it->hat_button_mapping[event->hat][3]; cb)
+  {
+    cb(event->value & SDL_HAT_LEFT);
+    processed = true;
+  }
+
+  return processed;
+}
+
 void SDLControllerInterface::ClearBindings()
 {
   for (auto& it : m_controllers)
   {
-    for (AxisCallback& ac : it.axis_mapping)
-      ac = {};
-    for (ButtonCallback& bc : it.button_mapping)
-      bc = {};
+    it.axis_mapping.fill({});
+    it.button_mapping.fill({});
+    it.axis_button_mapping.fill({});
+    it.button_axis_mapping.fill({});
+    it.hat_button_mapping.clear();
   }
 }
 
-bool SDLControllerInterface::BindControllerAxis(int controller_index, int axis_number, AxisCallback callback)
+bool SDLControllerInterface::BindControllerAxis(int controller_index, int axis_number, AxisSide axis_side,
+                                                AxisCallback callback)
 {
   auto it = GetControllerDataForPlayerId(controller_index);
   if (it == m_controllers.end())
@@ -261,7 +518,7 @@ bool SDLControllerInterface::BindControllerAxis(int controller_index, int axis_n
   if (axis_number < 0 || axis_number >= MAX_NUM_AXISES)
     return false;
 
-  it->axis_mapping[axis_number] = std::move(callback);
+  it->axis_mapping[axis_number][axis_side] = std::move(callback);
   return true;
 }
 
@@ -292,6 +549,33 @@ bool SDLControllerInterface::BindControllerAxisToButton(int controller_index, in
   return true;
 }
 
+bool SDLControllerInterface::BindControllerHatToButton(int controller_index, int hat_number,
+                                                       std::string_view hat_position, ButtonCallback callback)
+{
+  auto it = GetControllerDataForPlayerId(controller_index);
+  if (it == m_controllers.end())
+    return false;
+
+  size_t index;
+  if (hat_position == "Up")
+    index = 0;
+  else if (hat_position == "Right")
+    index = 1;
+  else if (hat_position == "Down")
+    index = 2;
+  else if (hat_position == "Left")
+    index = 3;
+  else
+    return false;
+
+  // We need 4 entries per hat_number
+  if (it->hat_button_mapping.size() < hat_number + 1)
+    it->hat_button_mapping.resize(hat_number + 1);
+
+  it->hat_button_mapping[hat_number][index] = std::move(callback);
+  return true;
+}
+
 bool SDLControllerInterface::BindControllerButtonToAxis(int controller_index, int button_number, AxisCallback callback)
 {
   auto it = GetControllerDataForPlayerId(controller_index);
@@ -317,10 +601,18 @@ bool SDLControllerInterface::HandleControllerAxisEvent(const SDL_Event* ev)
   if (DoEventHook(Hook::Type::Axis, it->player_id, ev->caxis.axis, value))
     return true;
 
-  const AxisCallback& cb = it->axis_mapping[ev->caxis.axis];
+  const AxisCallback& cb = it->axis_mapping[ev->caxis.axis][AxisSide::Full];
   if (cb)
   {
-    cb(value);
+    // Extend triggers from a 0 - 1 range to a -1 - 1 range for consistency with other inputs
+    if (ev->caxis.axis == SDL_CONTROLLER_AXIS_TRIGGERLEFT || ev->caxis.axis == SDL_CONTROLLER_AXIS_TRIGGERRIGHT)
+    {
+      cb((value * 2.0f) - 1.0f);
+    }
+    else
+    {
+      cb(value);
+    }
     return true;
   }
 
@@ -367,11 +659,11 @@ bool SDLControllerInterface::HandleControllerButtonEvent(const SDL_Event* ev)
     return true;
   }
 
-  // Assume a half-axis, i.e. in 0..1 range
   const AxisCallback& axis_cb = it->button_axis_mapping[ev->cbutton.button];
   if (axis_cb)
   {
-    axis_cb(pressed ? 1.0f : 0.0f);
+    axis_cb(pressed ? 1.0f : -1.0f);
+    return true;
   }
 
   return false;
diff --git a/src/frontend-common/sdl_controller_interface.h b/src/frontend-common/sdl_controller_interface.h
index 8f5dbc212..fed97c021 100644
--- a/src/frontend-common/sdl_controller_interface.h
+++ b/src/frontend-common/sdl_controller_interface.h
@@ -25,10 +25,12 @@ public:
   void ClearBindings() override;
 
   // Binding to events. If a binding for this axis/button already exists, returns false.
-  bool BindControllerAxis(int controller_index, int axis_number, AxisCallback callback) override;
+  bool BindControllerAxis(int controller_index, int axis_number, AxisSide axis_side, AxisCallback callback) override;
   bool BindControllerButton(int controller_index, int button_number, ButtonCallback callback) override;
   bool BindControllerAxisToButton(int controller_index, int axis_number, bool direction,
                                   ButtonCallback callback) override;
+  bool BindControllerHatToButton(int controller_index, int hat_number, std::string_view hat_position,
+                                 ButtonCallback callback) override;
   bool BindControllerButtonToAxis(int controller_index, int button_number, AxisCallback callback) override;
 
   // Changing rumble strength.
@@ -45,23 +47,25 @@ public:
 private:
   struct ControllerData
   {
-    void* controller;
     void* haptic;
     int haptic_left_right_effect;
     int joystick_id;
     int player_id;
+    bool is_game_controller;
 
     float deadzone = 0.25f;
 
-    std::array<AxisCallback, MAX_NUM_AXISES> axis_mapping;
+    // TODO: Turn to vectors to support arbitrary amounts of buttons and axes (for Joysticks)
+    // Preferably implement a simple "flat map", an ordered view over a vector
+    std::array<std::array<AxisCallback, 3>, MAX_NUM_AXISES> axis_mapping;
     std::array<ButtonCallback, MAX_NUM_BUTTONS> button_mapping;
     std::array<std::array<ButtonCallback, 2>, MAX_NUM_AXISES> axis_button_mapping;
     std::array<AxisCallback, MAX_NUM_BUTTONS> button_axis_mapping;
+    std::vector<std::array<ButtonCallback, 4>> hat_button_mapping;
   };
 
   using ControllerDataVector = std::vector<ControllerData>;
 
-  ControllerDataVector::iterator GetControllerDataForController(void* controller);
   ControllerDataVector::iterator GetControllerDataForJoystickId(int id);
   ControllerDataVector::iterator GetControllerDataForPlayerId(int id);
   int GetFreePlayerId() const;
@@ -71,6 +75,11 @@ private:
   bool HandleControllerAxisEvent(const SDL_Event* event);
   bool HandleControllerButtonEvent(const SDL_Event* event);
 
+  bool OpenJoystick(int index);
+  bool HandleJoystickAxisEvent(const struct SDL_JoyAxisEvent* event);
+  bool HandleJoystickButtonEvent(const struct SDL_JoyButtonEvent* event);
+  bool HandleJoystickHatEvent(const struct SDL_JoyHatEvent* event);
+
   ControllerDataVector m_controllers;
 
   std::mutex m_event_intercept_mutex;
diff --git a/src/frontend-common/xinput_controller_interface.cpp b/src/frontend-common/xinput_controller_interface.cpp
index 575e18361..90f7b6f95 100644
--- a/src/frontend-common/xinput_controller_interface.cpp
+++ b/src/frontend-common/xinput_controller_interface.cpp
@@ -158,16 +158,11 @@ void XInputControllerInterface::CheckForStateChanges(u32 index, const XINPUT_STA
 
 void XInputControllerInterface::ClearBindings()
 {
-  for (auto& it : m_controllers)
-  {
-    for (AxisCallback& ac : it.axis_mapping)
-      ac = {};
-    for (ButtonCallback& bc : it.button_mapping)
-      bc = {};
-  }
+  m_controllers.fill({});
 }
 
-bool XInputControllerInterface::BindControllerAxis(int controller_index, int axis_number, AxisCallback callback)
+bool XInputControllerInterface::BindControllerAxis(int controller_index, int axis_number, AxisSide axis_side,
+                                                   AxisCallback callback)
 {
   if (static_cast<u32>(controller_index) >= m_controllers.size() || !m_controllers[controller_index].connected)
     return false;
@@ -175,7 +170,7 @@ bool XInputControllerInterface::BindControllerAxis(int controller_index, int axi
   if (axis_number < 0 || axis_number >= NUM_AXISES)
     return false;
 
-  m_controllers[controller_index].axis_mapping[axis_number] = std::move(callback);
+  m_controllers[controller_index].axis_mapping[axis_number][axis_side] = std::move(callback);
   return true;
 }
 
@@ -204,6 +199,13 @@ bool XInputControllerInterface::BindControllerAxisToButton(int controller_index,
   return true;
 }
 
+bool XInputControllerInterface::BindControllerHatToButton(int controller_index, int hat_number,
+                                                          std::string_view hat_position, ButtonCallback callback)
+{
+  // Hats don't exist in XInput
+  return false;
+}
+
 bool XInputControllerInterface::BindControllerButtonToAxis(int controller_index, int button_number,
                                                            AxisCallback callback)
 {
@@ -226,7 +228,7 @@ bool XInputControllerInterface::HandleAxisEvent(u32 index, Axis axis, s32 value)
   if (DoEventHook(Hook::Type::Axis, index, static_cast<u32>(axis), f_value))
     return true;
 
-  const AxisCallback& cb = m_controllers[index].axis_mapping[static_cast<u32>(axis)];
+  const AxisCallback& cb = m_controllers[index].axis_mapping[static_cast<u32>(axis)][AxisSide::Full];
   if (cb)
   {
     cb(f_value);
diff --git a/src/frontend-common/xinput_controller_interface.h b/src/frontend-common/xinput_controller_interface.h
index 3c1996f84..8927ca3bf 100644
--- a/src/frontend-common/xinput_controller_interface.h
+++ b/src/frontend-common/xinput_controller_interface.h
@@ -22,10 +22,12 @@ public:
   void ClearBindings() override;
 
   // Binding to events. If a binding for this axis/button already exists, returns false.
-  bool BindControllerAxis(int controller_index, int axis_number, AxisCallback callback) override;
+  bool BindControllerAxis(int controller_index, int axis_number, AxisSide axis_side, AxisCallback callback) override;
   bool BindControllerButton(int controller_index, int button_number, ButtonCallback callback) override;
   bool BindControllerAxisToButton(int controller_index, int axis_number, bool direction,
                                   ButtonCallback callback) override;
+  bool BindControllerHatToButton(int controller_index, int hat_number, std::string_view hat_position,
+                                 ButtonCallback callback) override;
   bool BindControllerButtonToAxis(int controller_index, int button_number, AxisCallback callback) override;
 
   // Changing rumble strength.
@@ -61,7 +63,7 @@ private:
 
     float deadzone = 0.25f;
 
-    std::array<AxisCallback, MAX_NUM_AXISES> axis_mapping;
+    std::array<std::array<AxisCallback, 3>, MAX_NUM_AXISES> axis_mapping;
     std::array<ButtonCallback, MAX_NUM_BUTTONS> button_mapping;
     std::array<std::array<ButtonCallback, 2>, MAX_NUM_AXISES> axis_button_mapping;
     std::array<AxisCallback, MAX_NUM_BUTTONS> button_axis_mapping;

From 086f251370a6a0ccf6062395d0b98df88f04ec20 Mon Sep 17 00:00:00 2001
From: Silent <zdanio95@gmail.com>
Date: Sun, 15 Nov 2020 15:21:39 +0100
Subject: [PATCH 3/3] Tidy up SDL controller methods interface

---
 .../sdl_controller_interface.cpp              | 38 +++++++++----------
 .../sdl_controller_interface.h                |  8 ++--
 2 files changed, 22 insertions(+), 24 deletions(-)

diff --git a/src/frontend-common/sdl_controller_interface.cpp b/src/frontend-common/sdl_controller_interface.cpp
index 10d299446..034ef3633 100644
--- a/src/frontend-common/sdl_controller_interface.cpp
+++ b/src/frontend-common/sdl_controller_interface.cpp
@@ -100,11 +100,11 @@ bool SDLControllerInterface::ProcessSDLEvent(const SDL_Event* event)
     }
 
     case SDL_CONTROLLERAXISMOTION:
-      return HandleControllerAxisEvent(event);
+      return HandleControllerAxisEvent(&event->caxis);
 
     case SDL_CONTROLLERBUTTONDOWN:
     case SDL_CONTROLLERBUTTONUP:
-      return HandleControllerButtonEvent(event);
+      return HandleControllerButtonEvent(&event->cbutton);
 
     case SDL_JOYDEVICEADDED:
       if (SDL_IsGameController(event->jdevice.which))
@@ -589,23 +589,23 @@ bool SDLControllerInterface::BindControllerButtonToAxis(int controller_index, in
   return true;
 }
 
-bool SDLControllerInterface::HandleControllerAxisEvent(const SDL_Event* ev)
+bool SDLControllerInterface::HandleControllerAxisEvent(const SDL_ControllerAxisEvent* ev)
 {
-  const float value = static_cast<float>(ev->caxis.value) / (ev->caxis.value < 0 ? 32768.0f : 32767.0f);
-  Log_DebugPrintf("controller %d axis %d %d %f", ev->caxis.which, ev->caxis.axis, ev->caxis.value, value);
+  const float value = static_cast<float>(ev->value) / (ev->value < 0 ? 32768.0f : 32767.0f);
+  Log_DebugPrintf("controller %d axis %d %d %f", ev->which, ev->axis, ev->value, value);
 
-  auto it = GetControllerDataForJoystickId(ev->caxis.which);
+  auto it = GetControllerDataForJoystickId(ev->which);
   if (it == m_controllers.end())
     return false;
 
-  if (DoEventHook(Hook::Type::Axis, it->player_id, ev->caxis.axis, value))
+  if (DoEventHook(Hook::Type::Axis, it->player_id, ev->axis, value))
     return true;
 
-  const AxisCallback& cb = it->axis_mapping[ev->caxis.axis][AxisSide::Full];
+  const AxisCallback& cb = it->axis_mapping[ev->axis][AxisSide::Full];
   if (cb)
   {
     // Extend triggers from a 0 - 1 range to a -1 - 1 range for consistency with other inputs
-    if (ev->caxis.axis == SDL_CONTROLLER_AXIS_TRIGGERLEFT || ev->caxis.axis == SDL_CONTROLLER_AXIS_TRIGGERRIGHT)
+    if (ev->axis == SDL_CONTROLLER_AXIS_TRIGGERLEFT || ev->axis == SDL_CONTROLLER_AXIS_TRIGGERRIGHT)
     {
       cb((value * 2.0f) - 1.0f);
     }
@@ -619,8 +619,8 @@ bool SDLControllerInterface::HandleControllerAxisEvent(const SDL_Event* ev)
   // set the other direction to false so large movements don't leave the opposite on
   const bool outside_deadzone = (std::abs(value) >= it->deadzone);
   const bool positive = (value >= 0.0f);
-  const ButtonCallback& other_button_cb = it->axis_button_mapping[ev->caxis.axis][BoolToUInt8(!positive)];
-  const ButtonCallback& button_cb = it->axis_button_mapping[ev->caxis.axis][BoolToUInt8(positive)];
+  const ButtonCallback& other_button_cb = it->axis_button_mapping[ev->axis][BoolToUInt8(!positive)];
+  const ButtonCallback& button_cb = it->axis_button_mapping[ev->axis][BoolToUInt8(positive)];
   if (button_cb)
   {
     button_cb(outside_deadzone);
@@ -639,27 +639,27 @@ bool SDLControllerInterface::HandleControllerAxisEvent(const SDL_Event* ev)
   }
 }
 
-bool SDLControllerInterface::HandleControllerButtonEvent(const SDL_Event* ev)
+bool SDLControllerInterface::HandleControllerButtonEvent(const SDL_ControllerButtonEvent* ev)
 {
-  Log_DebugPrintf("controller %d button %d %s", ev->cbutton.which, ev->cbutton.button,
-                  ev->cbutton.state == SDL_PRESSED ? "pressed" : "released");
+  Log_DebugPrintf("controller %d button %d %s", ev->which, ev->button,
+                  ev->state == SDL_PRESSED ? "pressed" : "released");
 
-  auto it = GetControllerDataForJoystickId(ev->cbutton.which);
+  auto it = GetControllerDataForJoystickId(ev->which);
   if (it == m_controllers.end())
     return false;
 
-  const bool pressed = (ev->cbutton.state == SDL_PRESSED);
-  if (DoEventHook(Hook::Type::Button, it->player_id, ev->cbutton.button, pressed ? 1.0f : 0.0f))
+  const bool pressed = (ev->state == SDL_PRESSED);
+  if (DoEventHook(Hook::Type::Button, it->player_id, ev->button, pressed ? 1.0f : 0.0f))
     return true;
 
-  const ButtonCallback& cb = it->button_mapping[ev->cbutton.button];
+  const ButtonCallback& cb = it->button_mapping[ev->button];
   if (cb)
   {
     cb(pressed);
     return true;
   }
 
-  const AxisCallback& axis_cb = it->button_axis_mapping[ev->cbutton.button];
+  const AxisCallback& axis_cb = it->button_axis_mapping[ev->button];
   if (axis_cb)
   {
     axis_cb(pressed ? 1.0f : -1.0f);
diff --git a/src/frontend-common/sdl_controller_interface.h b/src/frontend-common/sdl_controller_interface.h
index fed97c021..0e82c90b5 100644
--- a/src/frontend-common/sdl_controller_interface.h
+++ b/src/frontend-common/sdl_controller_interface.h
@@ -6,8 +6,6 @@
 #include <mutex>
 #include <vector>
 
-union SDL_Event;
-
 class SDLControllerInterface final : public ControllerInterface
 {
 public:
@@ -42,7 +40,7 @@ public:
 
   void PollEvents() override;
 
-  bool ProcessSDLEvent(const SDL_Event* event);
+  bool ProcessSDLEvent(const union SDL_Event* event);
 
 private:
   struct ControllerData
@@ -72,8 +70,8 @@ private:
 
   bool OpenGameController(int index);
   bool CloseGameController(int joystick_index, bool notify);
-  bool HandleControllerAxisEvent(const SDL_Event* event);
-  bool HandleControllerButtonEvent(const SDL_Event* event);
+  bool HandleControllerAxisEvent(const struct SDL_ControllerAxisEvent* event);
+  bool HandleControllerButtonEvent(const struct SDL_ControllerButtonEvent* event);
 
   bool OpenJoystick(int index);
   bool HandleJoystickAxisEvent(const struct SDL_JoyAxisEvent* event);