diff --git a/src/citra_qt/configuration/configure_input.cpp b/src/citra_qt/configuration/configure_input.cpp
index b5fd9bda2..85eeb5f61 100644
--- a/src/citra_qt/configuration/configure_input.cpp
+++ b/src/citra_qt/configuration/configure_input.cpp
@@ -276,6 +276,7 @@ ConfigureInput::ConfigureInput(QWidget* parent)
 
     ui->buttonDelete->setEnabled(ui->profile->count() > 1);
 
+    connect(ui->buttonAutoMap, &QPushButton::clicked, this, &ConfigureInput::AutoMap);
     connect(ui->buttonClearAll, &QPushButton::clicked, this, &ConfigureInput::ClearAll);
     connect(ui->buttonRestoreDefaults, &QPushButton::clicked, this,
             &ConfigureInput::RestoreDefaults);
@@ -440,6 +441,52 @@ void ConfigureInput::UpdateButtonLabels() {
     EmitInputKeysChanged();
 }
 
+void ConfigureInput::MapFromButton(const Common::ParamPackage& params) {
+    Common::ParamPackage aux_param;
+    bool mapped = false;
+    for (int button_id = 0; button_id < Settings::NativeButton::NumButtons; button_id++) {
+        aux_param = InputCommon::GetSDLControllerButtonBindByGUID(params.Get("guid", "0"),
+                                                                  params.Get("port", 0), button_id);
+        if (aux_param.Has("engine")) {
+            buttons_param[button_id] = aux_param;
+            mapped = true;
+        }
+    }
+    for (int analog_id = 0; analog_id < Settings::NativeAnalog::NumAnalogs; analog_id++) {
+        aux_param = InputCommon::GetSDLControllerAnalogBindByGUID(params.Get("guid", "0"),
+                                                                  params.Get("port", 0), analog_id);
+        if (aux_param.Has("engine")) {
+            analogs_param[analog_id] = aux_param;
+            mapped = true;
+        }
+    }
+    if (!mapped) {
+        QMessageBox::warning(
+            this, tr("Warning"),
+            tr("Auto mapping failed. Your controller may not have a corresponding mapping"));
+    }
+}
+
+void ConfigureInput::AutoMap() {
+    if (QMessageBox::information(this, tr("Information"),
+                                 tr("After pressing OK, press any button on your joystick"),
+                                 QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Cancel) {
+        return;
+    }
+    input_setter = [=](const Common::ParamPackage& params) {
+        MapFromButton(params);
+        ApplyConfiguration();
+        Settings::SaveProfile(ui->profile->currentIndex());
+    };
+    device_pollers = InputCommon::Polling::GetPollers(InputCommon::Polling::DeviceType::Button);
+    want_keyboard_keys = false;
+    for (auto& poller : device_pollers) {
+        poller->Start();
+    }
+    timeout_timer->start(5000); // Cancel after 5 seconds
+    poll_timer->start(200);     // Check for new inputs every 200ms
+}
+
 void ConfigureInput::HandleClick(QPushButton* button,
                                  std::function<void(const Common::ParamPackage&)> new_input_setter,
                                  InputCommon::Polling::DeviceType type) {
diff --git a/src/citra_qt/configuration/configure_input.h b/src/citra_qt/configuration/configure_input.h
index fa9555563..a08510fb1 100644
--- a/src/citra_qt/configuration/configure_input.h
+++ b/src/citra_qt/configuration/configure_input.h
@@ -98,6 +98,9 @@ private:
     /// Generates list of all used keys
     QList<QKeySequence> GetUsedKeyboardKeys();
 
+    void MapFromButton(const Common::ParamPackage& params);
+    void AutoMap();
+
     /// Restore all buttons to their default values.
     void RestoreDefaults();
     /// Clear all input configuration
diff --git a/src/citra_qt/configuration/configure_input.ui b/src/citra_qt/configuration/configure_input.ui
index 3a76730ad..63e5d5538 100644
--- a/src/citra_qt/configuration/configure_input.ui
+++ b/src/citra_qt/configuration/configure_input.ui
@@ -727,17 +727,32 @@
         </widget>
        </item>
        <item>
-        <spacer name="horizontalSpacer">
-         <property name="orientation">
-          <enum>Qt::Horizontal</enum>
+        <widget class="QPushButton" name="buttonAutoMap">
+         <property name="sizePolicy">
+          <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
+           <horstretch>0</horstretch>
+           <verstretch>0</verstretch>
+          </sizepolicy>
          </property>
-         <property name="sizeHint" stdset="0">
+         <property name="sizeIncrement">
           <size>
-           <width>40</width>
-           <height>20</height>
+           <width>0</width>
+           <height>0</height>
           </size>
          </property>
-        </spacer>
+         <property name="baseSize">
+          <size>
+           <width>0</width>
+           <height>0</height>
+          </size>
+         </property>
+         <property name="layoutDirection">
+          <enum>Qt::LeftToRight</enum>
+         </property>
+         <property name="text">
+          <string>Auto Map</string>
+         </property>
+        </widget>
        </item>
        <item>
         <widget class="QPushButton" name="buttonClearAll">
diff --git a/src/input_common/main.cpp b/src/input_common/main.cpp
index 7aa15a9b4..89e441ddb 100644
--- a/src/input_common/main.cpp
+++ b/src/input_common/main.cpp
@@ -10,6 +10,7 @@
 #include "input_common/main.h"
 #include "input_common/motion_emu.h"
 #include "input_common/sdl/sdl.h"
+#include "input_common/sdl/sdl_impl.h"
 #include "input_common/touch_from_button.h"
 #include "input_common/udp/udp.h"
 
@@ -76,6 +77,18 @@ std::string GenerateAnalogParamFromKeys(int key_up, int key_down, int key_left,
     return circle_pad_param.Serialize();
 }
 
+Common::ParamPackage GetSDLControllerButtonBindByGUID(const std::string& guid, int port,
+                                                      int button) {
+    return dynamic_cast<SDL::SDLState*>(sdl.get())->GetSDLControllerButtonBindByGUID(
+        guid, port, static_cast<Settings::NativeButton::Values>(button));
+}
+
+Common::ParamPackage GetSDLControllerAnalogBindByGUID(const std::string& guid, int port,
+                                                      int analog) {
+    return dynamic_cast<SDL::SDLState*>(sdl.get())->GetSDLControllerAnalogBindByGUID(
+        guid, port, static_cast<Settings::NativeAnalog::Values>(analog));
+}
+
 void ReloadInputDevices() {
     if (udp)
         udp->ReloadUDPClient();
diff --git a/src/input_common/main.h b/src/input_common/main.h
index d1229b207..606b198a8 100644
--- a/src/input_common/main.h
+++ b/src/input_common/main.h
@@ -37,6 +37,11 @@ std::string GenerateKeyboardParam(int key_code);
 std::string GenerateAnalogParamFromKeys(int key_up, int key_down, int key_left, int key_right,
                                         int key_modifier, float modifier_scale);
 
+Common::ParamPackage GetSDLControllerButtonBindByGUID(const std::string& guid, int port,
+                                                      int button);
+Common::ParamPackage GetSDLControllerAnalogBindByGUID(const std::string& guid, int port,
+                                                      int analog);
+
 /// Reloads the input devices
 void ReloadInputDevices();
 
diff --git a/src/input_common/sdl/sdl_impl.cpp b/src/input_common/sdl/sdl_impl.cpp
index eaa64151d..362a91bb9 100644
--- a/src/input_common/sdl/sdl_impl.cpp
+++ b/src/input_common/sdl/sdl_impl.cpp
@@ -23,6 +23,54 @@
 #include "core/frontend/input.h"
 #include "input_common/sdl/sdl_impl.h"
 
+// These structures are not actually defined in the headers, so we need to define them here to use
+// them.
+typedef struct {
+    SDL_GameControllerBindType inputType;
+    union {
+        int button;
+
+        struct {
+            int axis;
+            int axis_min;
+            int axis_max;
+        } axis;
+
+        struct {
+            int hat;
+            int hat_mask;
+        } hat;
+
+    } input;
+
+    SDL_GameControllerBindType outputType;
+    union {
+        SDL_GameControllerButton button;
+
+        struct {
+            SDL_GameControllerAxis axis;
+            int axis_min;
+            int axis_max;
+        } axis;
+
+    } output;
+
+} SDL_ExtendedGameControllerBind;
+
+struct _SDL_GameController {
+    SDL_Joystick* joystick; /* underlying joystick device */
+    int ref_count;
+
+    const char* name;
+    int num_bindings;
+    SDL_ExtendedGameControllerBind* bindings;
+    SDL_ExtendedGameControllerBind** last_match_axis;
+    Uint8* last_hat_mask;
+    Uint32 guide_button_down;
+
+    struct _SDL_GameController* next; /* pointer to next game controller we have allocated */
+};
+
 namespace InputCommon {
 
 namespace SDL {
@@ -48,11 +96,36 @@ static int SDLEventWatcher(void* userdata, SDL_Event* event) {
     return 0;
 }
 
+constexpr std::array<SDL_GameControllerButton, Settings::NativeButton::NumButtons>
+    xinput_to_3ds_mapping = {{
+        SDL_CONTROLLER_BUTTON_B,
+        SDL_CONTROLLER_BUTTON_A,
+        SDL_CONTROLLER_BUTTON_Y,
+        SDL_CONTROLLER_BUTTON_X,
+        SDL_CONTROLLER_BUTTON_DPAD_UP,
+        SDL_CONTROLLER_BUTTON_DPAD_DOWN,
+        SDL_CONTROLLER_BUTTON_DPAD_LEFT,
+        SDL_CONTROLLER_BUTTON_DPAD_RIGHT,
+        SDL_CONTROLLER_BUTTON_LEFTSHOULDER,
+        SDL_CONTROLLER_BUTTON_RIGHTSHOULDER,
+        SDL_CONTROLLER_BUTTON_START,
+        SDL_CONTROLLER_BUTTON_BACK,
+        SDL_CONTROLLER_BUTTON_INVALID,
+        SDL_CONTROLLER_BUTTON_INVALID,
+        SDL_CONTROLLER_BUTTON_INVALID,
+        SDL_CONTROLLER_BUTTON_INVALID,
+        SDL_CONTROLLER_BUTTON_GUIDE,
+    }};
+
+struct SDLJoystickDeleter {
+    void operator()(SDL_Joystick* object) {
+        SDL_JoystickClose(object);
+    }
+};
 class SDLJoystick {
 public:
-    SDLJoystick(std::string guid_, int port_, SDL_Joystick* joystick,
-                decltype(&SDL_JoystickClose) deleter = &SDL_JoystickClose)
-        : guid{std::move(guid_)}, port{port_}, sdl_joystick{joystick, deleter} {}
+    SDLJoystick(std::string guid_, int port_, SDL_Joystick* joystick)
+        : guid{std::move(guid_)}, port{port_}, sdl_joystick{joystick} {}
 
     void SetButton(int button, bool value) {
         std::lock_guard lock{mutex};
@@ -118,10 +191,12 @@ public:
         return sdl_joystick.get();
     }
 
-    void SetSDLJoystick(SDL_Joystick* joystick,
-                        decltype(&SDL_JoystickClose) deleter = &SDL_JoystickClose) {
-        sdl_joystick =
-            std::unique_ptr<SDL_Joystick, decltype(&SDL_JoystickClose)>(joystick, deleter);
+    void SetSDLJoystick(SDL_Joystick* joystick) {
+        sdl_joystick = std::unique_ptr<SDL_Joystick, SDLJoystickDeleter>(joystick);
+    }
+
+    SDL_GameController* GetGameController() const {
+        return SDL_GameControllerFromInstanceID(SDL_JoystickInstanceID(sdl_joystick.get()));
     }
 
 private:
@@ -132,10 +207,48 @@ private:
     } state;
     std::string guid;
     int port;
-    std::unique_ptr<SDL_Joystick, decltype(&SDL_JoystickClose)> sdl_joystick;
+    std::unique_ptr<SDL_Joystick, SDLJoystickDeleter> sdl_joystick;
     mutable std::mutex mutex;
 };
 
+struct SDLGameControllerDeleter {
+    void operator()(SDL_GameController* object) {
+        SDL_GameControllerClose(object);
+    }
+};
+class SDLGameController {
+public:
+    SDLGameController(std::string guid_, int port_, SDL_GameController* controller)
+        : guid{std::move(guid_)}, port{port_}, sdl_controller{controller} {}
+
+    /**
+     * The guid of the joystick/controller
+     */
+    const std::string& GetGUID() const {
+        return guid;
+    }
+
+    /**
+     * The number of joystick from the same type that were connected before this joystick
+     */
+    int GetPort() const {
+        return port;
+    }
+
+    SDL_GameController* GetSDLGameController() const {
+        return sdl_controller.get();
+    }
+
+    void SetSDLGameController(SDL_GameController* controller) {
+        sdl_controller = std::unique_ptr<SDL_GameController, SDLGameControllerDeleter>(controller);
+    }
+
+private:
+    std::string guid;
+    int port;
+    std::unique_ptr<SDL_GameController, SDLGameControllerDeleter> sdl_controller;
+};
+
 /**
  * Get the nth joystick with the corresponding GUID
  */
@@ -144,16 +257,32 @@ std::shared_ptr<SDLJoystick> SDLState::GetSDLJoystickByGUID(const std::string& g
     const auto it = joystick_map.find(guid);
     if (it != joystick_map.end()) {
         while (it->second.size() <= static_cast<std::size_t>(port)) {
-            auto joystick = std::make_shared<SDLJoystick>(guid, static_cast<int>(it->second.size()),
-                                                          nullptr, [](SDL_Joystick*) {});
+            auto joystick =
+                std::make_shared<SDLJoystick>(guid, static_cast<int>(it->second.size()), nullptr);
             it->second.emplace_back(std::move(joystick));
         }
         return it->second[port];
     }
-    auto joystick = std::make_shared<SDLJoystick>(guid, 0, nullptr, [](SDL_Joystick*) {});
+    auto joystick = std::make_shared<SDLJoystick>(guid, 0, nullptr);
     return joystick_map[guid].emplace_back(std::move(joystick));
 }
 
+std::shared_ptr<SDLGameController> SDLState::GetSDLGameControllerByGUID(const std::string& guid,
+                                                                        int port) {
+    std::lock_guard lock{controller_map_mutex};
+    const auto it = controller_map.find(guid);
+    if (it != controller_map.end()) {
+        while (it->second.size() <= static_cast<std::size_t>(port)) {
+            auto controller = std::make_shared<SDLGameController>(
+                guid, static_cast<int>(it->second.size()), nullptr);
+            it->second.emplace_back(std::move(controller));
+        }
+        return it->second[port];
+    }
+    auto controller = std::make_shared<SDLGameController>(guid, 0, nullptr);
+    return controller_map[guid].emplace_back(std::move(controller));
+}
+
 /**
  * Check how many identical joysticks (by guid) were connected before the one with sdl_id and so tie
  * it to a SDLJoystick with the same guid and that port
@@ -193,10 +322,129 @@ std::shared_ptr<SDLJoystick> SDLState::GetSDLJoystickBySDLID(SDL_JoystickID sdl_
     return joystick_map[guid].emplace_back(std::move(joystick));
 }
 
+Common::ParamPackage SDLState::GetSDLControllerButtonBindByGUID(
+    const std::string& guid, int port, Settings::NativeButton::Values button) {
+    Common::ParamPackage params({{"engine", "sdl"}});
+    params.Set("guid", guid);
+    params.Set("port", port);
+    SDL_GameController* controller = GetSDLGameControllerByGUID(guid, port)->GetSDLGameController();
+    SDL_GameControllerButtonBind button_bind;
+
+    if (!controller) {
+        LOG_WARNING(Input, "failed to open controller {}", guid);
+        return {{}};
+    }
+
+    auto mapped_button = xinput_to_3ds_mapping[static_cast<int>(button)];
+    if (mapped_button == SDL_CONTROLLER_BUTTON_INVALID) {
+        if (button == Settings::NativeButton::Values::ZL) {
+            button_bind =
+                SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_TRIGGERLEFT);
+        } else if (button == Settings::NativeButton::Values::ZR) {
+            button_bind =
+                SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_TRIGGERRIGHT);
+        } else {
+            return {{}};
+        }
+    } else {
+        button_bind = SDL_GameControllerGetBindForButton(controller, mapped_button);
+    }
+
+    switch (button_bind.bindType) {
+    case SDL_CONTROLLER_BINDTYPE_BUTTON:
+        params.Set("button", button_bind.value.button);
+        break;
+    case SDL_CONTROLLER_BINDTYPE_HAT:
+        params.Set("hat", button_bind.value.hat.hat);
+        switch (button_bind.value.hat.hat_mask) {
+        case SDL_HAT_UP:
+            params.Set("direction", "up");
+            break;
+        case SDL_HAT_DOWN:
+            params.Set("direction", "down");
+            break;
+        case SDL_HAT_LEFT:
+            params.Set("direction", "left");
+            break;
+        case SDL_HAT_RIGHT:
+            params.Set("direction", "right");
+            break;
+        default:
+            return {{}};
+        }
+        break;
+    case SDL_CONTROLLER_BINDTYPE_AXIS:
+        params.Set("axis", button_bind.value.axis);
+
+#if SDL_VERSION_ATLEAST(2, 0, 6)
+        {
+            const SDL_ExtendedGameControllerBind extended_bind =
+                controller->bindings[mapped_button];
+            if (extended_bind.input.axis.axis_max < extended_bind.input.axis.axis_min) {
+                params.Set("direction", "-");
+            } else {
+                params.Set("direction", "+");
+            }
+            params.Set(
+                "threshold",
+                (extended_bind.input.axis.axis_min +
+                 (extended_bind.input.axis.axis_max - extended_bind.input.axis.axis_min) / 2.0f) /
+                    SDL_JOYSTICK_AXIS_MAX);
+        }
+#else
+        params.Set("direction", "+"); // lacks extended_bind, so just a guess
+#endif
+        break;
+    case SDL_CONTROLLER_BINDTYPE_NONE:
+        LOG_WARNING(Input, "Button not bound: {}", Settings::NativeButton::mapping[button]);
+        return {{}};
+    default:
+        LOG_WARNING(Input, "unknown SDL bind type {}", button_bind.bindType);
+        return {{}};
+    }
+
+    return params;
+}
+
+Common::ParamPackage SDLState::GetSDLControllerAnalogBindByGUID(
+    const std::string& guid, int port, Settings::NativeAnalog::Values analog) {
+    Common::ParamPackage params({{"engine", "sdl"}});
+    params.Set("guid", guid);
+    params.Set("port", port);
+    SDL_GameController* controller = GetSDLGameControllerByGUID(guid, port)->GetSDLGameController();
+    SDL_GameControllerButtonBind button_bind_x;
+    SDL_GameControllerButtonBind button_bind_y;
+
+    if (!controller) {
+        LOG_WARNING(Input, "failed to open controller {}", guid);
+        return {{}};
+    }
+
+    if (analog == Settings::NativeAnalog::Values::CirclePad) {
+        button_bind_x = SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_LEFTX);
+        button_bind_y = SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_LEFTY);
+    } else if (analog == Settings::NativeAnalog::Values::CStick) {
+        button_bind_x = SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_RIGHTX);
+        button_bind_y = SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_RIGHTY);
+    } else {
+        LOG_WARNING(Input, "analog value out of range {}", analog);
+        return {{}};
+    }
+
+    if (button_bind_x.bindType != SDL_CONTROLLER_BINDTYPE_AXIS ||
+        button_bind_y.bindType != SDL_CONTROLLER_BINDTYPE_AXIS) {
+        return {{}};
+    }
+    params.Set("axis_x", button_bind_x.value.axis);
+    params.Set("axis_y", button_bind_y.value.axis);
+    return params;
+}
+
 void SDLState::InitJoystick(int joystick_index) {
     SDL_Joystick* sdl_joystick = SDL_JoystickOpen(joystick_index);
     if (!sdl_joystick) {
-        LOG_ERROR(Input, "failed to open joystick {}", joystick_index);
+        LOG_ERROR(Input, "failed to open joystick {}, with error: {}", joystick_index,
+                  SDL_GetError());
         return;
     }
     const std::string guid = GetGUID(sdl_joystick);
@@ -219,6 +467,35 @@ void SDLState::InitJoystick(int joystick_index) {
     joystick_guid_list.emplace_back(std::move(joystick));
 }
 
+void SDLState::InitGameController(int controller_index) {
+    SDL_GameController* sdl_controller = SDL_GameControllerOpen(controller_index);
+    if (!sdl_controller) {
+        LOG_WARNING(Input, "failed to open joystick {} as controller", controller_index);
+        return;
+    }
+    const std::string guid = GetGUID(SDL_GameControllerGetJoystick(sdl_controller));
+
+    LOG_INFO(Input, "opened joystick {} as controller", controller_index);
+    std::lock_guard lock{controller_map_mutex};
+    if (controller_map.find(guid) == controller_map.end()) {
+        auto controller = std::make_shared<SDLGameController>(guid, 0, sdl_controller);
+        controller_map[guid].emplace_back(std::move(controller));
+        return;
+    }
+    auto& controller_guid_list = controller_map[guid];
+    const auto it = std::find_if(controller_guid_list.begin(), controller_guid_list.end(),
+                                 [](const std::shared_ptr<SDLGameController>& controller) {
+                                     return !controller->GetSDLGameController();
+                                 });
+    if (it != controller_guid_list.end()) {
+        (*it)->SetSDLGameController(sdl_controller);
+        return;
+    }
+    auto controller =
+        std::make_shared<SDLGameController>(guid, controller_guid_list.size(), sdl_controller);
+    controller_guid_list.emplace_back(std::move(controller));
+}
+
 void SDLState::CloseJoystick(SDL_Joystick* sdl_joystick) {
     std::string guid = GetGUID(sdl_joystick);
     std::shared_ptr<SDLJoystick> joystick;
@@ -235,7 +512,23 @@ void SDLState::CloseJoystick(SDL_Joystick* sdl_joystick) {
     }
     // Destruct SDL_Joystick outside the lock guard because SDL can internally call event calback
     // which locks the mutex again
-    joystick->SetSDLJoystick(nullptr, [](SDL_Joystick*) {});
+    joystick->SetSDLJoystick(nullptr);
+}
+
+void SDLState::CloseGameController(SDL_GameController* sdl_controller) {
+    std::string guid = GetGUID(SDL_GameControllerGetJoystick(sdl_controller));
+    std::shared_ptr<SDLGameController> controller;
+    {
+        std::lock_guard lock{controller_map_mutex};
+        auto& controller_guid_list = controller_map[guid];
+        const auto controller_it =
+            std::find_if(controller_guid_list.begin(), controller_guid_list.end(),
+                         [&sdl_controller](const std::shared_ptr<SDLGameController>& controller) {
+                             return controller->GetSDLGameController() == sdl_controller;
+                         });
+        controller = *controller_it;
+    }
+    controller->SetSDLGameController(nullptr);
 }
 
 void SDLState::HandleGameControllerEvent(const SDL_Event& event) {
@@ -265,13 +558,21 @@ void SDLState::HandleGameControllerEvent(const SDL_Event& event) {
         break;
     }
     case SDL_JOYDEVICEREMOVED:
-        LOG_DEBUG(Input, "Controller removed with Instance_ID {}", event.jdevice.which);
+        LOG_DEBUG(Input, "Joystick removed with Instance_ID {}", event.jdevice.which);
         CloseJoystick(SDL_JoystickFromInstanceID(event.jdevice.which));
         break;
     case SDL_JOYDEVICEADDED:
-        LOG_DEBUG(Input, "Controller connected with device index {}", event.jdevice.which);
+        LOG_DEBUG(Input, "Joystick connected with device index {}", event.jdevice.which);
         InitJoystick(event.jdevice.which);
         break;
+    case SDL_CONTROLLERDEVICEREMOVED:
+        LOG_DEBUG(Input, "Controller removed with Instance_ID {}", event.cdevice.which);
+        CloseGameController(SDL_GameControllerFromInstanceID(event.cdevice.which));
+        break;
+    case SDL_CONTROLLERDEVICEADDED:
+        LOG_DEBUG(Input, "Controller connected with device index {}", event.cdevice.which);
+        InitGameController(event.cdevice.which);
+        break;
     }
 }
 
@@ -280,6 +581,11 @@ void SDLState::CloseJoysticks() {
     joystick_map.clear();
 }
 
+void SDLState::CloseGameControllers() {
+    std::lock_guard lock{controller_map_mutex};
+    controller_map.clear();
+}
+
 class SDLButton final : public Input::ButtonDevice {
 public:
     explicit SDLButton(std::shared_ptr<SDLJoystick> joystick_, int button_)
@@ -464,9 +770,9 @@ SDLState::SDLState() {
     RegisterFactory<AnalogDevice>("sdl", std::make_shared<SDLAnalogFactory>(*this));
 
     // If the frontend is going to manage the event loop, then we dont start one here
-    start_thread = !SDL_WasInit(SDL_INIT_JOYSTICK);
-    if (start_thread && SDL_Init(SDL_INIT_JOYSTICK) < 0) {
-        LOG_CRITICAL(Input, "SDL_Init(SDL_INIT_JOYSTICK) failed with: {}", SDL_GetError());
+    start_thread = !SDL_WasInit(SDL_INIT_GAMECONTROLLER);
+    if (start_thread && SDL_Init(SDL_INIT_GAMECONTROLLER) < 0) {
+        LOG_CRITICAL(Input, "SDL_Init(SDL_INIT_GAMECONTROLLER) failed with: {}", SDL_GetError());
         return;
     }
     if (SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1") == SDL_FALSE) {
@@ -495,6 +801,9 @@ SDLState::SDLState() {
     // Because the events for joystick connection happens before we have our event watcher added, we
     // can just open all the joysticks right here
     for (int i = 0; i < SDL_NumJoysticks(); ++i) {
+        if (SDL_IsGameController(i)) {
+            InitGameController(i);
+        }
         InitJoystick(i);
     }
 }
@@ -505,12 +814,13 @@ SDLState::~SDLState() {
     UnregisterFactory<AnalogDevice>("sdl");
 
     CloseJoysticks();
+    CloseGameControllers();
     SDL_DelEventWatch(&SDLEventWatcher, this);
 
     initialized = false;
     if (start_thread) {
         poll_thread.join();
-        SDL_QuitSubSystem(SDL_INIT_JOYSTICK);
+        SDL_QuitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER);
     }
 }
 
diff --git a/src/input_common/sdl/sdl_impl.h b/src/input_common/sdl/sdl_impl.h
index 2579741d6..05f627d0f 100644
--- a/src/input_common/sdl/sdl_impl.h
+++ b/src/input_common/sdl/sdl_impl.h
@@ -8,15 +8,18 @@
 #include <memory>
 #include <thread>
 #include "common/threadsafe_queue.h"
+#include "core/settings.h"
 #include "input_common/sdl/sdl.h"
 
 union SDL_Event;
 using SDL_Joystick = struct _SDL_Joystick;
 using SDL_JoystickID = s32;
+using SDL_GameController = struct _SDL_GameController;
 
 namespace InputCommon::SDL {
 
 class SDLJoystick;
+class SDLGameController;
 class SDLButtonFactory;
 class SDLAnalogFactory;
 
@@ -34,6 +37,14 @@ public:
     std::shared_ptr<SDLJoystick> GetSDLJoystickBySDLID(SDL_JoystickID sdl_id);
     std::shared_ptr<SDLJoystick> GetSDLJoystickByGUID(const std::string& guid, int port);
 
+    std::shared_ptr<SDLGameController> GetSDLGameControllerByGUID(const std::string& guid,
+                                                                  int port);
+
+    Common::ParamPackage GetSDLControllerButtonBindByGUID(const std::string& guid, int port,
+                                                          Settings::NativeButton::Values button);
+    Common::ParamPackage GetSDLControllerAnalogBindByGUID(const std::string& guid, int port,
+                                                          Settings::NativeAnalog::Values analog);
+
     /// Get all DevicePoller that use the SDL backend for a specific device type
     Pollers GetPollers(Polling::DeviceType type) override;
 
@@ -45,13 +56,21 @@ private:
     void InitJoystick(int joystick_index);
     void CloseJoystick(SDL_Joystick* sdl_joystick);
 
+    void InitGameController(int joystick_index);
+    void CloseGameController(SDL_GameController* sdl_controller);
+
     /// Needs to be called before SDL_QuitSubSystem.
     void CloseJoysticks();
+    void CloseGameControllers();
 
     /// Map of GUID of a list of corresponding virtual Joysticks
     std::unordered_map<std::string, std::vector<std::shared_ptr<SDLJoystick>>> joystick_map;
     std::mutex joystick_map_mutex;
 
+    /// Map of GUID of a list of corresponding virtual Controllers
+    std::unordered_map<std::string, std::vector<std::shared_ptr<SDLGameController>>> controller_map;
+    std::mutex controller_map_mutex;
+
     std::shared_ptr<SDLButtonFactory> button_factory;
     std::shared_ptr<SDLAnalogFactory> analog_factory;