diff --git a/src/input_common/main.cpp b/src/input_common/main.cpp
index 8e67a7437..57e7a25fe 100644
--- a/src/input_common/main.cpp
+++ b/src/input_common/main.cpp
@@ -18,66 +18,166 @@
 
 namespace InputCommon {
 
-static std::shared_ptr<Keyboard> keyboard;
-static std::shared_ptr<MotionEmu> motion_emu;
-#ifdef HAVE_SDL2
-static std::unique_ptr<SDL::State> sdl;
-#endif
-static std::unique_ptr<CemuhookUDP::State> udp;
-static std::shared_ptr<GCButtonFactory> gcbuttons;
-static std::shared_ptr<GCAnalogFactory> gcanalog;
+struct InputSubsystem::Impl {
+    void Initialize() {
+        auto gcadapter = std::make_shared<GCAdapter::Adapter>();
+        gcbuttons = std::make_shared<GCButtonFactory>(gcadapter);
+        Input::RegisterFactory<Input::ButtonDevice>("gcpad", gcbuttons);
+        gcanalog = std::make_shared<GCAnalogFactory>(gcadapter);
+        Input::RegisterFactory<Input::AnalogDevice>("gcpad", gcanalog);
 
-void Init() {
-    auto gcadapter = std::make_shared<GCAdapter::Adapter>();
-    gcbuttons = std::make_shared<GCButtonFactory>(gcadapter);
-    Input::RegisterFactory<Input::ButtonDevice>("gcpad", gcbuttons);
-    gcanalog = std::make_shared<GCAnalogFactory>(gcadapter);
-    Input::RegisterFactory<Input::AnalogDevice>("gcpad", gcanalog);
-
-    keyboard = std::make_shared<Keyboard>();
-    Input::RegisterFactory<Input::ButtonDevice>("keyboard", keyboard);
-    Input::RegisterFactory<Input::AnalogDevice>("analog_from_button",
-                                                std::make_shared<AnalogFromButton>());
-    motion_emu = std::make_shared<MotionEmu>();
-    Input::RegisterFactory<Input::MotionDevice>("motion_emu", motion_emu);
+        keyboard = std::make_shared<Keyboard>();
+        Input::RegisterFactory<Input::ButtonDevice>("keyboard", keyboard);
+        Input::RegisterFactory<Input::AnalogDevice>("analog_from_button",
+                                                    std::make_shared<AnalogFromButton>());
+        motion_emu = std::make_shared<MotionEmu>();
+        Input::RegisterFactory<Input::MotionDevice>("motion_emu", motion_emu);
 
 #ifdef HAVE_SDL2
-    sdl = SDL::Init();
+        sdl = SDL::Init();
 #endif
-    udp = CemuhookUDP::Init();
-}
 
-void Shutdown() {
-    Input::UnregisterFactory<Input::ButtonDevice>("keyboard");
-    keyboard.reset();
-    Input::UnregisterFactory<Input::AnalogDevice>("analog_from_button");
-    Input::UnregisterFactory<Input::MotionDevice>("motion_emu");
-    motion_emu.reset();
+        udp = CemuhookUDP::Init();
+    }
+
+    void Shutdown() {
+        Input::UnregisterFactory<Input::ButtonDevice>("keyboard");
+        keyboard.reset();
+        Input::UnregisterFactory<Input::AnalogDevice>("analog_from_button");
+        Input::UnregisterFactory<Input::MotionDevice>("motion_emu");
+        motion_emu.reset();
 #ifdef HAVE_SDL2
-    sdl.reset();
+        sdl.reset();
 #endif
-    udp.reset();
-    Input::UnregisterFactory<Input::ButtonDevice>("gcpad");
-    Input::UnregisterFactory<Input::AnalogDevice>("gcpad");
+        udp.reset();
+        Input::UnregisterFactory<Input::ButtonDevice>("gcpad");
+        Input::UnregisterFactory<Input::AnalogDevice>("gcpad");
 
-    gcbuttons.reset();
-    gcanalog.reset();
+        gcbuttons.reset();
+        gcanalog.reset();
+    }
+
+    [[nodiscard]] std::vector<Common::ParamPackage> GetInputDevices() const {
+        std::vector<Common::ParamPackage> devices = {
+            Common::ParamPackage{{"display", "Any"}, {"class", "any"}},
+            Common::ParamPackage{{"display", "Keyboard/Mouse"}, {"class", "key"}},
+        };
+#ifdef HAVE_SDL2
+        auto sdl_devices = sdl->GetInputDevices();
+        devices.insert(devices.end(), sdl_devices.begin(), sdl_devices.end());
+#endif
+        auto udp_devices = udp->GetInputDevices();
+        devices.insert(devices.end(), udp_devices.begin(), udp_devices.end());
+        return devices;
+    }
+
+    [[nodiscard]] AnalogMapping GetAnalogMappingForDevice(
+        const Common::ParamPackage& params) const {
+        if (!params.Has("class") || params.Get("class", "") == "any") {
+            return {};
+        }
+        if (params.Get("class", "") == "key") {
+            // TODO consider returning the SDL key codes for the default keybindings
+            return {};
+        }
+#ifdef HAVE_SDL2
+        if (params.Get("class", "") == "sdl") {
+            return sdl->GetAnalogMappingForDevice(params);
+        }
+#endif
+        return {};
+    }
+
+    [[nodiscard]] ButtonMapping GetButtonMappingForDevice(
+        const Common::ParamPackage& params) const {
+        if (!params.Has("class") || params.Get("class", "") == "any") {
+            return {};
+        }
+        if (params.Get("class", "") == "key") {
+            // TODO consider returning the SDL key codes for the default keybindings
+            return {};
+        }
+#ifdef HAVE_SDL2
+        if (params.Get("class", "") == "sdl") {
+            return sdl->GetButtonMappingForDevice(params);
+        }
+#endif
+        return {};
+    }
+
+    std::shared_ptr<Keyboard> keyboard;
+    std::shared_ptr<MotionEmu> motion_emu;
+#ifdef HAVE_SDL2
+    std::unique_ptr<SDL::State> sdl;
+#endif
+    std::unique_ptr<CemuhookUDP::State> udp;
+    std::shared_ptr<GCButtonFactory> gcbuttons;
+    std::shared_ptr<GCAnalogFactory> gcanalog;
+};
+
+InputSubsystem::InputSubsystem() : impl{std::make_unique<Impl>()} {}
+
+InputSubsystem::~InputSubsystem() = default;
+
+void InputSubsystem::Initialize() {
+    impl->Initialize();
 }
 
-Keyboard* GetKeyboard() {
-    return keyboard.get();
+void InputSubsystem::Shutdown() {
+    impl->Shutdown();
 }
 
-MotionEmu* GetMotionEmu() {
-    return motion_emu.get();
+Keyboard* InputSubsystem::GetKeyboard() {
+    return impl->keyboard.get();
 }
 
-GCButtonFactory* GetGCButtons() {
-    return gcbuttons.get();
+const Keyboard* InputSubsystem::GetKeyboard() const {
+    return impl->keyboard.get();
 }
 
-GCAnalogFactory* GetGCAnalogs() {
-    return gcanalog.get();
+MotionEmu* InputSubsystem::GetMotionEmu() {
+    return impl->motion_emu.get();
+}
+
+const MotionEmu* InputSubsystem::GetMotionEmu() const {
+    return impl->motion_emu.get();
+}
+
+std::vector<Common::ParamPackage> InputSubsystem::GetInputDevices() const {
+    return impl->GetInputDevices();
+}
+
+AnalogMapping InputSubsystem::GetAnalogMappingForDevice(const Common::ParamPackage& device) const {
+    return impl->GetAnalogMappingForDevice(device);
+}
+
+ButtonMapping InputSubsystem::GetButtonMappingForDevice(const Common::ParamPackage& device) const {
+    return impl->GetButtonMappingForDevice(device);
+}
+
+GCAnalogFactory* InputSubsystem::GetGCAnalogs() {
+    return impl->gcanalog.get();
+}
+
+const GCAnalogFactory* InputSubsystem::GetGCAnalogs() const {
+    return impl->gcanalog.get();
+}
+
+GCButtonFactory* InputSubsystem::GetGCButtons() {
+    return impl->gcbuttons.get();
+}
+
+const GCButtonFactory* InputSubsystem::GetGCButtons() const {
+    return impl->gcbuttons.get();
+}
+
+std::vector<std::unique_ptr<Polling::DevicePoller>> InputSubsystem::GetPollers(
+    Polling::DeviceType type) const {
+#ifdef HAVE_SDL2
+    return impl->sdl->GetPollers(type);
+#else
+    return {};
+#endif
 }
 
 std::string GenerateKeyboardParam(int key_code) {
@@ -101,68 +201,4 @@ std::string GenerateAnalogParamFromKeys(int key_up, int key_down, int key_left,
     };
     return circle_pad_param.Serialize();
 }
-
-std::vector<Common::ParamPackage> GetInputDevices() {
-    std::vector<Common::ParamPackage> devices = {
-        Common::ParamPackage{{"display", "Any"}, {"class", "any"}},
-        Common::ParamPackage{{"display", "Keyboard/Mouse"}, {"class", "key"}},
-    };
-#ifdef HAVE_SDL2
-    auto sdl_devices = sdl->GetInputDevices();
-    devices.insert(devices.end(), sdl_devices.begin(), sdl_devices.end());
-#endif
-    auto udp_devices = udp->GetInputDevices();
-    devices.insert(devices.end(), udp_devices.begin(), udp_devices.end());
-    return devices;
-}
-
-std::unordered_map<Settings::NativeButton::Values, Common::ParamPackage> GetButtonMappingForDevice(
-    const Common::ParamPackage& params) {
-    std::unordered_map<Settings::NativeButton::Values, Common::ParamPackage> mappings;
-    if (!params.Has("class") || params.Get("class", "") == "any") {
-        return {};
-    }
-    if (params.Get("class", "") == "key") {
-        // TODO consider returning the SDL key codes for the default keybindings
-        return {};
-    }
-#ifdef HAVE_SDL2
-    if (params.Get("class", "") == "sdl") {
-        return sdl->GetButtonMappingForDevice(params);
-    }
-#endif
-    return {};
-}
-
-std::unordered_map<Settings::NativeAnalog::Values, Common::ParamPackage> GetAnalogMappingForDevice(
-    const Common::ParamPackage& params) {
-    std::unordered_map<Settings::NativeAnalog::Values, Common::ParamPackage> mappings;
-    if (!params.Has("class") || params.Get("class", "") == "any") {
-        return {};
-    }
-    if (params.Get("class", "") == "key") {
-        // TODO consider returning the SDL key codes for the default keybindings
-        return {};
-    }
-#ifdef HAVE_SDL2
-    if (params.Get("class", "") == "sdl") {
-        return sdl->GetAnalogMappingForDevice(params);
-    }
-#endif
-    return {};
-}
-
-namespace Polling {
-
-std::vector<std::unique_ptr<DevicePoller>> GetPollers(DeviceType type) {
-    std::vector<std::unique_ptr<DevicePoller>> pollers;
-
-#ifdef HAVE_SDL2
-    pollers = sdl->GetPollers(type);
-#endif
-
-    return pollers;
-}
-
-} // namespace Polling
 } // namespace InputCommon
diff --git a/src/input_common/main.h b/src/input_common/main.h
index e706c3750..f66308163 100644
--- a/src/input_common/main.h
+++ b/src/input_common/main.h
@@ -16,52 +16,6 @@ class ParamPackage;
 }
 
 namespace InputCommon {
-
-/// Initializes and registers all built-in input device factories.
-void Init();
-
-/// Deregisters all built-in input device factories and shuts them down.
-void Shutdown();
-
-class Keyboard;
-
-/// Gets the keyboard button device factory.
-Keyboard* GetKeyboard();
-
-class MotionEmu;
-
-/// Gets the motion emulation factory.
-MotionEmu* GetMotionEmu();
-
-GCButtonFactory* GetGCButtons();
-
-GCAnalogFactory* GetGCAnalogs();
-
-/// Generates a serialized param package for creating a keyboard button device
-std::string GenerateKeyboardParam(int key_code);
-
-/// Generates a serialized param package for creating an analog device taking input from keyboard
-std::string GenerateAnalogParamFromKeys(int key_up, int key_down, int key_left, int key_right,
-                                        int key_modifier, float modifier_scale);
-
-/**
- * Return a list of available input devices that this Factory can create a new device with.
- * Each returned Parampackage should have a `display` field used for display, a class field for
- * backends to determine if this backend is meant to service the request and any other information
- * needed to identify this in the backend later.
- */
-std::vector<Common::ParamPackage> GetInputDevices();
-
-/**
- * Given a ParamPackage for a Device returned from `GetInputDevices`, attempt to get the default
- * mapping for the device. This is currently only implemented for the sdl backend devices.
- */
-using ButtonMapping = std::unordered_map<Settings::NativeButton::Values, Common::ParamPackage>;
-using AnalogMapping = std::unordered_map<Settings::NativeAnalog::Values, Common::ParamPackage>;
-
-ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage&);
-AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage&);
-
 namespace Polling {
 
 enum class DeviceType { Button, AnalogPreferred };
@@ -90,4 +44,88 @@ public:
 // Get all DevicePoller from all backends for a specific device type
 std::vector<std::unique_ptr<DevicePoller>> GetPollers(DeviceType type);
 } // namespace Polling
+
+class GCAnalogFactory;
+class GCButtonFactory;
+class Keyboard;
+class MotionEmu;
+
+/**
+ * Given a ParamPackage for a Device returned from `GetInputDevices`, attempt to get the default
+ * mapping for the device. This is currently only implemented for the SDL backend devices.
+ */
+using AnalogMapping = std::unordered_map<Settings::NativeAnalog::Values, Common::ParamPackage>;
+using ButtonMapping = std::unordered_map<Settings::NativeButton::Values, Common::ParamPackage>;
+
+class InputSubsystem {
+public:
+    explicit InputSubsystem();
+    ~InputSubsystem();
+
+    InputSubsystem(const InputSubsystem&) = delete;
+    InputSubsystem& operator=(const InputSubsystem&) = delete;
+
+    InputSubsystem(InputSubsystem&&) = delete;
+    InputSubsystem& operator=(InputSubsystem&&) = delete;
+
+    /// Initializes and registers all built-in input device factories.
+    void Initialize();
+
+    /// Unregisters all built-in input device factories and shuts them down.
+    void Shutdown();
+
+    /// Retrieves the underlying keyboard device.
+    [[nodiscard]] Keyboard* GetKeyboard();
+
+    /// Retrieves the underlying keyboard device.
+    [[nodiscard]] const Keyboard* GetKeyboard() const;
+
+    /// Retrieves the underlying motion emulation factory.
+    [[nodiscard]] MotionEmu* GetMotionEmu();
+
+    /// Retrieves the underlying motion emulation factory.
+    [[nodiscard]] const MotionEmu* GetMotionEmu() const;
+
+    /**
+     * Returns all available input devices that this Factory can create a new device with.
+     * Each returned ParamPackage should have a `display` field used for display, a class field for
+     * backends to determine if this backend is meant to service the request and any other
+     * information needed to identify this in the backend later.
+     */
+    [[nodiscard]] std::vector<Common::ParamPackage> GetInputDevices() const;
+
+    /// Retrieves the analog mappings for the given device.
+    [[nodiscard]] AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& device) const;
+
+    /// Retrieves the button mappings for the given device.
+    [[nodiscard]] ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage& device) const;
+
+    /// Retrieves the underlying GameCube analog handler.
+    [[nodiscard]] GCAnalogFactory* GetGCAnalogs();
+
+    /// Retrieves the underlying GameCube analog handler.
+    [[nodiscard]] const GCAnalogFactory* GetGCAnalogs() const;
+
+    /// Retrieves the underlying GameCube button handler.
+    [[nodiscard]] GCButtonFactory* GetGCButtons();
+
+    /// Retrieves the underlying GameCube button handler.
+    [[nodiscard]] const GCButtonFactory* GetGCButtons() const;
+
+    /// Get all DevicePoller from all backends for a specific device type
+    [[nodiscard]] std::vector<std::unique_ptr<Polling::DevicePoller>> GetPollers(
+        Polling::DeviceType type) const;
+
+private:
+    struct Impl;
+    std::unique_ptr<Impl> impl;
+};
+
+/// Generates a serialized param package for creating a keyboard button device
+std::string GenerateKeyboardParam(int key_code);
+
+/// Generates a serialized param package for creating an analog device taking input from keyboard
+std::string GenerateAnalogParamFromKeys(int key_up, int key_down, int key_left, int key_right,
+                                        int key_modifier, float modifier_scale);
+
 } // namespace InputCommon
diff --git a/src/yuzu/bootmanager.cpp b/src/yuzu/bootmanager.cpp
index 8fc322b30..f1b428bde 100644
--- a/src/yuzu/bootmanager.cpp
+++ b/src/yuzu/bootmanager.cpp
@@ -304,8 +304,9 @@ static Core::Frontend::EmuWindow::WindowSystemInfo GetWindowSystemInfo(QWindow*
     return wsi;
 }
 
-GRenderWindow::GRenderWindow(GMainWindow* parent_, EmuThread* emu_thread_)
-    : QWidget(parent_), emu_thread(emu_thread_) {
+GRenderWindow::GRenderWindow(GMainWindow* parent, EmuThread* emu_thread_,
+                             InputCommon::InputSubsystem* input_subsystem_)
+    : QWidget(parent), emu_thread(emu_thread_), input_subsystem{input_subsystem_} {
     setWindowTitle(QStringLiteral("yuzu %1 | %2-%3")
                        .arg(QString::fromUtf8(Common::g_build_name),
                             QString::fromUtf8(Common::g_scm_branch),
@@ -314,15 +315,15 @@ GRenderWindow::GRenderWindow(GMainWindow* parent_, EmuThread* emu_thread_)
     auto layout = new QHBoxLayout(this);
     layout->setMargin(0);
     setLayout(layout);
-    InputCommon::Init();
+    input_subsystem->Initialize();
 
     this->setMouseTracking(true);
 
-    connect(this, &GRenderWindow::FirstFrameDisplayed, parent_, &GMainWindow::OnLoadComplete);
+    connect(this, &GRenderWindow::FirstFrameDisplayed, parent, &GMainWindow::OnLoadComplete);
 }
 
 GRenderWindow::~GRenderWindow() {
-    InputCommon::Shutdown();
+    input_subsystem->Shutdown();
 }
 
 void GRenderWindow::PollEvents() {
@@ -391,11 +392,11 @@ void GRenderWindow::closeEvent(QCloseEvent* event) {
 }
 
 void GRenderWindow::keyPressEvent(QKeyEvent* event) {
-    InputCommon::GetKeyboard()->PressKey(event->key());
+    input_subsystem->GetKeyboard()->PressKey(event->key());
 }
 
 void GRenderWindow::keyReleaseEvent(QKeyEvent* event) {
-    InputCommon::GetKeyboard()->ReleaseKey(event->key());
+    input_subsystem->GetKeyboard()->ReleaseKey(event->key());
 }
 
 void GRenderWindow::mousePressEvent(QMouseEvent* event) {
@@ -409,7 +410,7 @@ void GRenderWindow::mousePressEvent(QMouseEvent* event) {
         const auto [x, y] = ScaleTouch(pos);
         this->TouchPressed(x, y);
     } else if (event->button() == Qt::RightButton) {
-        InputCommon::GetMotionEmu()->BeginTilt(pos.x(), pos.y());
+        input_subsystem->GetMotionEmu()->BeginTilt(pos.x(), pos.y());
     }
     QWidget::mousePressEvent(event);
 }
@@ -423,7 +424,7 @@ void GRenderWindow::mouseMoveEvent(QMouseEvent* event) {
     auto pos = event->pos();
     const auto [x, y] = ScaleTouch(pos);
     this->TouchMoved(x, y);
-    InputCommon::GetMotionEmu()->Tilt(pos.x(), pos.y());
+    input_subsystem->GetMotionEmu()->Tilt(pos.x(), pos.y());
     QWidget::mouseMoveEvent(event);
 }
 
@@ -436,7 +437,7 @@ void GRenderWindow::mouseReleaseEvent(QMouseEvent* event) {
     if (event->button() == Qt::LeftButton) {
         this->TouchReleased();
     } else if (event->button() == Qt::RightButton) {
-        InputCommon::GetMotionEmu()->EndTilt();
+        input_subsystem->GetMotionEmu()->EndTilt();
     }
 }
 
@@ -485,7 +486,7 @@ bool GRenderWindow::event(QEvent* event) {
 
 void GRenderWindow::focusOutEvent(QFocusEvent* event) {
     QWidget::focusOutEvent(event);
-    InputCommon::GetKeyboard()->ReleaseAllKeys();
+    input_subsystem->GetKeyboard()->ReleaseAllKeys();
 }
 
 void GRenderWindow::resizeEvent(QResizeEvent* event) {
diff --git a/src/yuzu/bootmanager.h b/src/yuzu/bootmanager.h
index 6c59b4d5c..ecb3b8135 100644
--- a/src/yuzu/bootmanager.h
+++ b/src/yuzu/bootmanager.h
@@ -23,6 +23,10 @@ class QKeyEvent;
 class QTouchEvent;
 class QStringList;
 
+namespace InputCommon {
+class InputSubsystem;
+}
+
 namespace VideoCore {
 enum class LoadCallbackStage;
 }
@@ -121,7 +125,8 @@ class GRenderWindow : public QWidget, public Core::Frontend::EmuWindow {
     Q_OBJECT
 
 public:
-    GRenderWindow(GMainWindow* parent, EmuThread* emu_thread);
+    explicit GRenderWindow(GMainWindow* parent, EmuThread* emu_thread_,
+                           InputCommon::InputSubsystem* input_subsystem_);
     ~GRenderWindow() override;
 
     // EmuWindow implementation.
@@ -183,6 +188,7 @@ private:
     QStringList GetUnsupportedGLExtensions() const;
 
     EmuThread* emu_thread;
+    InputCommon::InputSubsystem* input_subsystem;
 
     // Main context that will be shared with all other contexts that are requested.
     // If this is used in a shared context setting, then this should not be used directly, but
diff --git a/src/yuzu/configuration/configure_debug_controller.cpp b/src/yuzu/configuration/configure_debug_controller.cpp
index 72885b4b8..0097c9a29 100644
--- a/src/yuzu/configuration/configure_debug_controller.cpp
+++ b/src/yuzu/configuration/configure_debug_controller.cpp
@@ -5,9 +5,10 @@
 #include "ui_configure_debug_controller.h"
 #include "yuzu/configuration/configure_debug_controller.h"
 
-ConfigureDebugController::ConfigureDebugController(QWidget* parent)
+ConfigureDebugController::ConfigureDebugController(QWidget* parent,
+                                                   InputCommon::InputSubsystem* input_subsystem)
     : QDialog(parent), ui(std::make_unique<Ui::ConfigureDebugController>()),
-      debug_controller(new ConfigureInputPlayer(this, 9, nullptr, true)) {
+      debug_controller(new ConfigureInputPlayer(this, 9, nullptr, input_subsystem, true)) {
     ui->setupUi(this);
 
     ui->controllerLayout->addWidget(debug_controller);
diff --git a/src/yuzu/configuration/configure_debug_controller.h b/src/yuzu/configuration/configure_debug_controller.h
index 36475bbea..34dcf705f 100644
--- a/src/yuzu/configuration/configure_debug_controller.h
+++ b/src/yuzu/configuration/configure_debug_controller.h
@@ -10,6 +10,10 @@
 
 class QPushButton;
 
+namespace InputCommon {
+class InputSubsystem;
+}
+
 namespace Ui {
 class ConfigureDebugController;
 }
@@ -18,7 +22,8 @@ class ConfigureDebugController : public QDialog {
     Q_OBJECT
 
 public:
-    explicit ConfigureDebugController(QWidget* parent);
+    explicit ConfigureDebugController(QWidget* parent,
+                                      InputCommon::InputSubsystem* input_subsystem);
     ~ConfigureDebugController() override;
 
     void ApplyConfiguration();
diff --git a/src/yuzu/configuration/configure_dialog.cpp b/src/yuzu/configuration/configure_dialog.cpp
index 857577591..8186929a6 100644
--- a/src/yuzu/configuration/configure_dialog.cpp
+++ b/src/yuzu/configuration/configure_dialog.cpp
@@ -12,7 +12,8 @@
 #include "yuzu/configuration/configure_input_player.h"
 #include "yuzu/hotkeys.h"
 
-ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry)
+ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry,
+                                 InputCommon::InputSubsystem* input_subsystem)
     : QDialog(parent), ui(new Ui::ConfigureDialog), registry(registry) {
     Settings::configuring_global = true;
 
@@ -20,6 +21,8 @@ ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry)
     ui->hotkeysTab->Populate(registry);
     setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
 
+    ui->inputTab->Initialize(input_subsystem);
+
     SetConfiguration();
     PopulateSelectionList();
 
diff --git a/src/yuzu/configuration/configure_dialog.h b/src/yuzu/configuration/configure_dialog.h
index 4289bc225..570c3b941 100644
--- a/src/yuzu/configuration/configure_dialog.h
+++ b/src/yuzu/configuration/configure_dialog.h
@@ -9,6 +9,10 @@
 
 class HotkeyRegistry;
 
+namespace InputCommon {
+class InputSubsystem;
+}
+
 namespace Ui {
 class ConfigureDialog;
 }
@@ -17,7 +21,8 @@ class ConfigureDialog : public QDialog {
     Q_OBJECT
 
 public:
-    explicit ConfigureDialog(QWidget* parent, HotkeyRegistry& registry);
+    explicit ConfigureDialog(QWidget* parent, HotkeyRegistry& registry,
+                             InputCommon::InputSubsystem* input_subsystem);
     ~ConfigureDialog() override;
 
     void ApplyConfiguration();
diff --git a/src/yuzu/configuration/configure_input.cpp b/src/yuzu/configuration/configure_input.cpp
index 0d004c2f7..5223eed1d 100644
--- a/src/yuzu/configuration/configure_input.cpp
+++ b/src/yuzu/configuration/configure_input.cpp
@@ -65,16 +65,20 @@ void OnDockedModeChanged(bool last_state, bool new_state) {
 ConfigureInput::ConfigureInput(QWidget* parent)
     : QWidget(parent), ui(std::make_unique<Ui::ConfigureInput>()) {
     ui->setupUi(this);
+}
 
+ConfigureInput::~ConfigureInput() = default;
+
+void ConfigureInput::Initialize(InputCommon::InputSubsystem* input_subsystem) {
     player_controllers = {
-        new ConfigureInputPlayer(this, 0, ui->consoleInputSettings),
-        new ConfigureInputPlayer(this, 1, ui->consoleInputSettings),
-        new ConfigureInputPlayer(this, 2, ui->consoleInputSettings),
-        new ConfigureInputPlayer(this, 3, ui->consoleInputSettings),
-        new ConfigureInputPlayer(this, 4, ui->consoleInputSettings),
-        new ConfigureInputPlayer(this, 5, ui->consoleInputSettings),
-        new ConfigureInputPlayer(this, 6, ui->consoleInputSettings),
-        new ConfigureInputPlayer(this, 7, ui->consoleInputSettings),
+        new ConfigureInputPlayer(this, 0, ui->consoleInputSettings, input_subsystem),
+        new ConfigureInputPlayer(this, 1, ui->consoleInputSettings, input_subsystem),
+        new ConfigureInputPlayer(this, 2, ui->consoleInputSettings, input_subsystem),
+        new ConfigureInputPlayer(this, 3, ui->consoleInputSettings, input_subsystem),
+        new ConfigureInputPlayer(this, 4, ui->consoleInputSettings, input_subsystem),
+        new ConfigureInputPlayer(this, 5, ui->consoleInputSettings, input_subsystem),
+        new ConfigureInputPlayer(this, 6, ui->consoleInputSettings, input_subsystem),
+        new ConfigureInputPlayer(this, 7, ui->consoleInputSettings, input_subsystem),
     };
 
     player_tabs = {
@@ -115,10 +119,12 @@ ConfigureInput::ConfigureInput(QWidget* parent)
     advanced = new ConfigureInputAdvanced(this);
     ui->tabAdvanced->setLayout(new QHBoxLayout(ui->tabAdvanced));
     ui->tabAdvanced->layout()->addWidget(advanced);
-    connect(advanced, &ConfigureInputAdvanced::CallDebugControllerDialog,
-            [this] { CallConfigureDialog<ConfigureDebugController>(*this); });
-    connect(advanced, &ConfigureInputAdvanced::CallMouseConfigDialog,
-            [this] { CallConfigureDialog<ConfigureMouseAdvanced>(*this); });
+    connect(advanced, &ConfigureInputAdvanced::CallDebugControllerDialog, [this, input_subsystem] {
+        CallConfigureDialog<ConfigureDebugController>(*this, input_subsystem);
+    });
+    connect(advanced, &ConfigureInputAdvanced::CallMouseConfigDialog, [this, input_subsystem] {
+        CallConfigureDialog<ConfigureMouseAdvanced>(*this, input_subsystem);
+    });
     connect(advanced, &ConfigureInputAdvanced::CallTouchscreenConfigDialog,
             [this] { CallConfigureDialog<ConfigureTouchscreenAdvanced>(*this); });
 
@@ -129,8 +135,6 @@ ConfigureInput::ConfigureInput(QWidget* parent)
     LoadConfiguration();
 }
 
-ConfigureInput::~ConfigureInput() = default;
-
 QList<QWidget*> ConfigureInput::GetSubTabs() const {
     return {
         ui->tabPlayer1, ui->tabPlayer2, ui->tabPlayer3, ui->tabPlayer4,  ui->tabPlayer5,
diff --git a/src/yuzu/configuration/configure_input.h b/src/yuzu/configuration/configure_input.h
index 78ca659da..d08a24f96 100644
--- a/src/yuzu/configuration/configure_input.h
+++ b/src/yuzu/configuration/configure_input.h
@@ -7,8 +7,8 @@
 #include <array>
 #include <memory>
 
-#include <QDialog>
 #include <QKeyEvent>
+#include <QWidget>
 
 #include "yuzu/configuration/configure_input_advanced.h"
 #include "yuzu/configuration/configure_input_player.h"
@@ -19,6 +19,10 @@ class QCheckBox;
 class QString;
 class QTimer;
 
+namespace InputCommon {
+class InputSubsystem;
+}
+
 namespace Ui {
 class ConfigureInput;
 }
@@ -32,6 +36,9 @@ public:
     explicit ConfigureInput(QWidget* parent = nullptr);
     ~ConfigureInput() override;
 
+    /// Initializes the input dialog with the given input subsystem.
+    void Initialize(InputCommon::InputSubsystem* input_subsystem_);
+
     /// Save all button configurations to settings file.
     void ApplyConfiguration();
 
diff --git a/src/yuzu/configuration/configure_input_player.cpp b/src/yuzu/configuration/configure_input_player.cpp
index d3980eb49..80bf40acb 100644
--- a/src/yuzu/configuration/configure_input_player.cpp
+++ b/src/yuzu/configuration/configure_input_player.cpp
@@ -11,12 +11,12 @@
 #include <QMenu>
 #include <QMessageBox>
 #include <QTimer>
-#include "common/assert.h"
 #include "common/param_package.h"
 #include "core/core.h"
 #include "core/hle/service/hid/controllers/npad.h"
 #include "core/hle/service/hid/hid.h"
 #include "core/hle/service/sm/sm.h"
+#include "input_common/gcadapter/gc_poller.h"
 #include "input_common/main.h"
 #include "ui_configure_input_player.h"
 #include "yuzu/configuration/config.h"
@@ -229,9 +229,11 @@ QString AnalogToText(const Common::ParamPackage& param, const std::string& dir)
 } // namespace
 
 ConfigureInputPlayer::ConfigureInputPlayer(QWidget* parent, std::size_t player_index,
-                                           QWidget* bottom_row, bool debug)
+                                           QWidget* bottom_row,
+                                           InputCommon::InputSubsystem* input_subsystem_,
+                                           bool debug)
     : QWidget(parent), ui(std::make_unique<Ui::ConfigureInputPlayer>()), player_index(player_index),
-      debug(debug), timeout_timer(std::make_unique<QTimer>()),
+      debug(debug), input_subsystem{input_subsystem_}, timeout_timer(std::make_unique<QTimer>()),
       poll_timer(std::make_unique<QTimer>()), bottom_row(bottom_row) {
     ui->setupUi(this);
 
@@ -287,7 +289,7 @@ ConfigureInputPlayer::ConfigureInputPlayer(QWidget* parent, std::size_t player_i
                         params.Set("direction", "+");
                         params.Set("threshold", "0.5");
                     }
-                    (*param) = std::move(params);
+                    *param = std::move(params);
                 },
                 InputCommon::Polling::DeviceType::Button);
         });
@@ -401,15 +403,15 @@ ConfigureInputPlayer::ConfigureInputPlayer(QWidget* parent, std::size_t player_i
 
     connect(poll_timer.get(), &QTimer::timeout, [this] {
         Common::ParamPackage params;
-        if (InputCommon::GetGCButtons()->IsPolling()) {
-            params = InputCommon::GetGCButtons()->GetNextInput();
+        if (input_subsystem->GetGCButtons()->IsPolling()) {
+            params = input_subsystem->GetGCButtons()->GetNextInput();
             if (params.Has("engine")) {
                 SetPollingResult(params, false);
                 return;
             }
         }
-        if (InputCommon::GetGCAnalogs()->IsPolling()) {
-            params = InputCommon::GetGCAnalogs()->GetNextInput();
+        if (input_subsystem->GetGCAnalogs()->IsPolling()) {
+            params = input_subsystem->GetGCAnalogs()->GetNextInput();
             if (params.Has("engine")) {
                 SetPollingResult(params, false);
                 return;
@@ -514,7 +516,7 @@ void ConfigureInputPlayer::LoadConfiguration() {
 }
 
 void ConfigureInputPlayer::UpdateInputDevices() {
-    input_devices = InputCommon::GetInputDevices();
+    input_devices = input_subsystem->GetInputDevices();
     ui->comboDevices->clear();
     for (auto device : input_devices) {
         ui->comboDevices->addItem(QString::fromStdString(device.Get("display", "Unknown")), {});
@@ -642,8 +644,8 @@ void ConfigureInputPlayer::UpdateMappingWithDefaults() {
         return;
     }
     const auto& device = input_devices[ui->comboDevices->currentIndex()];
-    auto button_mapping = InputCommon::GetButtonMappingForDevice(device);
-    auto analog_mapping = InputCommon::GetAnalogMappingForDevice(device);
+    auto button_mapping = input_subsystem->GetButtonMappingForDevice(device);
+    auto analog_mapping = input_subsystem->GetAnalogMappingForDevice(device);
     for (int i = 0; i < buttons_param.size(); ++i) {
         buttons_param[i] = button_mapping[static_cast<Settings::NativeButton::Values>(i)];
     }
@@ -666,7 +668,7 @@ void ConfigureInputPlayer::HandleClick(
 
     input_setter = new_input_setter;
 
-    device_pollers = InputCommon::Polling::GetPollers(type);
+    device_pollers = input_subsystem->GetPollers(type);
 
     for (auto& poller : device_pollers) {
         poller->Start();
@@ -676,9 +678,9 @@ void ConfigureInputPlayer::HandleClick(
     QWidget::grabKeyboard();
 
     if (type == InputCommon::Polling::DeviceType::Button) {
-        InputCommon::GetGCButtons()->BeginConfiguration();
+        input_subsystem->GetGCButtons()->BeginConfiguration();
     } else {
-        InputCommon::GetGCAnalogs()->BeginConfiguration();
+        input_subsystem->GetGCAnalogs()->BeginConfiguration();
     }
 
     timeout_timer->start(2500); // Cancel after 2.5 seconds
@@ -695,8 +697,8 @@ void ConfigureInputPlayer::SetPollingResult(const Common::ParamPackage& params,
     QWidget::releaseMouse();
     QWidget::releaseKeyboard();
 
-    InputCommon::GetGCButtons()->EndConfiguration();
-    InputCommon::GetGCAnalogs()->EndConfiguration();
+    input_subsystem->GetGCButtons()->EndConfiguration();
+    input_subsystem->GetGCAnalogs()->EndConfiguration();
 
     if (!abort) {
         (*input_setter)(params);
diff --git a/src/yuzu/configuration/configure_input_player.h b/src/yuzu/configuration/configure_input_player.h
index 25d4cde5e..a25bc3bd9 100644
--- a/src/yuzu/configuration/configure_input_player.h
+++ b/src/yuzu/configuration/configure_input_player.h
@@ -10,12 +10,11 @@
 #include <optional>
 #include <string>
 
-#include <QDialog>
+#include <QWidget>
 
 #include "common/param_package.h"
 #include "core/settings.h"
 #include "ui_configure_input.h"
-#include "yuzu/uisettings.h"
 
 class QCheckBox;
 class QKeyEvent;
@@ -27,6 +26,10 @@ class QString;
 class QTimer;
 class QWidget;
 
+namespace InputCommon {
+class InputSubsystem;
+}
+
 namespace InputCommon::Polling {
 class DevicePoller;
 enum class DeviceType;
@@ -41,6 +44,7 @@ class ConfigureInputPlayer : public QWidget {
 
 public:
     explicit ConfigureInputPlayer(QWidget* parent, std::size_t player_index, QWidget* bottom_row,
+                                  InputCommon::InputSubsystem* input_subsystem_,
                                   bool debug = false);
     ~ConfigureInputPlayer() override;
 
@@ -111,6 +115,8 @@ private:
     std::size_t player_index;
     bool debug;
 
+    InputCommon::InputSubsystem* input_subsystem;
+
     std::unique_ptr<QTimer> timeout_timer;
     std::unique_ptr<QTimer> poll_timer;
 
diff --git a/src/yuzu/configuration/configure_mouse_advanced.cpp b/src/yuzu/configuration/configure_mouse_advanced.cpp
index dcda8ab14..2af3afda8 100644
--- a/src/yuzu/configuration/configure_mouse_advanced.cpp
+++ b/src/yuzu/configuration/configure_mouse_advanced.cpp
@@ -76,8 +76,10 @@ static QString ButtonToText(const Common::ParamPackage& param) {
     return QObject::tr("[unknown]");
 }
 
-ConfigureMouseAdvanced::ConfigureMouseAdvanced(QWidget* parent)
-    : QDialog(parent), ui(std::make_unique<Ui::ConfigureMouseAdvanced>()),
+ConfigureMouseAdvanced::ConfigureMouseAdvanced(QWidget* parent,
+                                               InputCommon::InputSubsystem* input_subsystem_)
+    : QDialog(parent),
+      ui(std::make_unique<Ui::ConfigureMouseAdvanced>()), input_subsystem{input_subsystem_},
       timeout_timer(std::make_unique<QTimer>()), poll_timer(std::make_unique<QTimer>()) {
     ui->setupUi(this);
     setFocusPolicy(Qt::ClickFocus);
@@ -209,7 +211,7 @@ void ConfigureMouseAdvanced::HandleClick(
 
     input_setter = new_input_setter;
 
-    device_pollers = InputCommon::Polling::GetPollers(type);
+    device_pollers = input_subsystem->GetPollers(type);
 
     for (auto& poller : device_pollers) {
         poller->Start();
diff --git a/src/yuzu/configuration/configure_mouse_advanced.h b/src/yuzu/configuration/configure_mouse_advanced.h
index e7d27dab7..65b6fca9a 100644
--- a/src/yuzu/configuration/configure_mouse_advanced.h
+++ b/src/yuzu/configuration/configure_mouse_advanced.h
@@ -8,12 +8,14 @@
 #include <optional>
 #include <QDialog>
 
-#include "core/settings.h"
-
 class QCheckBox;
 class QPushButton;
 class QTimer;
 
+namespace InputCommon {
+class InputSubsystem;
+}
+
 namespace Ui {
 class ConfigureMouseAdvanced;
 }
@@ -22,7 +24,7 @@ class ConfigureMouseAdvanced : public QDialog {
     Q_OBJECT
 
 public:
-    explicit ConfigureMouseAdvanced(QWidget* parent);
+    explicit ConfigureMouseAdvanced(QWidget* parent, InputCommon::InputSubsystem* input_subsystem_);
     ~ConfigureMouseAdvanced() override;
 
     void ApplyConfiguration();
@@ -57,6 +59,8 @@ private:
 
     std::unique_ptr<Ui::ConfigureMouseAdvanced> ui;
 
+    InputCommon::InputSubsystem* input_subsystem;
+
     /// This will be the the setting function when an input is awaiting configuration.
     std::optional<std::function<void(const Common::ParamPackage&)>> input_setter;
 
diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp
index cd7e78eb4..cab9d680a 100644
--- a/src/yuzu/main.cpp
+++ b/src/yuzu/main.cpp
@@ -94,6 +94,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
 #include "core/perf_stats.h"
 #include "core/settings.h"
 #include "core/telemetry_session.h"
+#include "input_common/main.h"
 #include "video_core/gpu.h"
 #include "video_core/shader_notify.h"
 #include "yuzu/about_dialog.h"
@@ -186,9 +187,9 @@ static void InitializeLogging() {
 }
 
 GMainWindow::GMainWindow()
-    : config(new Config()), emu_thread(nullptr),
-      vfs(std::make_shared<FileSys::RealVfsFilesystem>()),
-      provider(std::make_unique<FileSys::ManualContentProvider>()) {
+    : input_subsystem{std::make_unique<InputCommon::InputSubsystem>()},
+      config{std::make_unique<Config>()}, vfs{std::make_shared<FileSys::RealVfsFilesystem>()},
+      provider{std::make_unique<FileSys::ManualContentProvider>()} {
     InitializeLogging();
 
     LoadTranslation();
@@ -473,7 +474,7 @@ void GMainWindow::InitializeWidgets() {
 #ifdef YUZU_ENABLE_COMPATIBILITY_REPORTING
     ui.action_Report_Compatibility->setVisible(true);
 #endif
-    render_window = new GRenderWindow(this, emu_thread.get());
+    render_window = new GRenderWindow(this, emu_thread.get(), input_subsystem.get());
     render_window->hide();
 
     game_list = new GameList(vfs, provider.get(), this);
@@ -2213,7 +2214,7 @@ void GMainWindow::OnConfigure() {
     const auto old_theme = UISettings::values.theme;
     const bool old_discord_presence = UISettings::values.enable_discord_presence;
 
-    ConfigureDialog configure_dialog(this, hotkey_registry);
+    ConfigureDialog configure_dialog(this, hotkey_registry, input_subsystem.get());
     connect(&configure_dialog, &ConfigureDialog::LanguageChanged, this,
             &GMainWindow::OnLanguageChanged);
 
diff --git a/src/yuzu/main.h b/src/yuzu/main.h
index 01f9131e5..957f20fa8 100644
--- a/src/yuzu/main.h
+++ b/src/yuzu/main.h
@@ -40,12 +40,20 @@ namespace Core::Frontend {
 struct SoftwareKeyboardParameters;
 } // namespace Core::Frontend
 
+namespace DiscordRPC {
+class DiscordInterface;
+}
+
 namespace FileSys {
 class ContentProvider;
 class ManualContentProvider;
 class VfsFilesystem;
 } // namespace FileSys
 
+namespace InputCommon {
+class InputSubsystem;
+}
+
 enum class EmulatedDirectoryTarget {
     NAND,
     SDMC,
@@ -62,10 +70,6 @@ enum class ReinitializeKeyBehavior {
     Warning,
 };
 
-namespace DiscordRPC {
-class DiscordInterface;
-}
-
 class GMainWindow : public QMainWindow {
     Q_OBJECT
 
@@ -86,8 +90,6 @@ public:
     GMainWindow();
     ~GMainWindow() override;
 
-    std::unique_ptr<DiscordRPC::DiscordInterface> discord_rpc;
-
     bool DropAction(QDropEvent* event);
     void AcceptDropEvent(QDropEvent* event);
 
@@ -255,6 +257,9 @@ private:
 
     Ui::MainWindow ui;
 
+    std::unique_ptr<DiscordRPC::DiscordInterface> discord_rpc;
+    std::unique_ptr<InputCommon::InputSubsystem> input_subsystem;
+
     GRenderWindow* render_window;
     GameList* game_list;
     LoadingScreen* loading_screen;
diff --git a/src/yuzu_cmd/emu_window/emu_window_sdl2.cpp b/src/yuzu_cmd/emu_window/emu_window_sdl2.cpp
index e5e684206..a804d5185 100644
--- a/src/yuzu_cmd/emu_window/emu_window_sdl2.cpp
+++ b/src/yuzu_cmd/emu_window/emu_window_sdl2.cpp
@@ -13,23 +13,25 @@
 #include "input_common/sdl/sdl.h"
 #include "yuzu_cmd/emu_window/emu_window_sdl2.h"
 
-EmuWindow_SDL2::EmuWindow_SDL2(Core::System& system, bool fullscreen) : system{system} {
+EmuWindow_SDL2::EmuWindow_SDL2(Core::System& system, bool fullscreen,
+                               InputCommon::InputSubsystem* input_subsystem_)
+    : system{system}, input_subsystem{input_subsystem_} {
     if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK) < 0) {
         LOG_CRITICAL(Frontend, "Failed to initialize SDL2! Exiting...");
         exit(1);
     }
-    InputCommon::Init();
+    input_subsystem->Initialize();
     SDL_SetMainReady();
 }
 
 EmuWindow_SDL2::~EmuWindow_SDL2() {
-    InputCommon::Shutdown();
+    input_subsystem->Shutdown();
     SDL_Quit();
 }
 
 void EmuWindow_SDL2::OnMouseMotion(s32 x, s32 y) {
     TouchMoved((unsigned)std::max(x, 0), (unsigned)std::max(y, 0));
-    InputCommon::GetMotionEmu()->Tilt(x, y);
+    input_subsystem->GetMotionEmu()->Tilt(x, y);
 }
 
 void EmuWindow_SDL2::OnMouseButton(u32 button, u8 state, s32 x, s32 y) {
@@ -41,9 +43,9 @@ void EmuWindow_SDL2::OnMouseButton(u32 button, u8 state, s32 x, s32 y) {
         }
     } else if (button == SDL_BUTTON_RIGHT) {
         if (state == SDL_PRESSED) {
-            InputCommon::GetMotionEmu()->BeginTilt(x, y);
+            input_subsystem->GetMotionEmu()->BeginTilt(x, y);
         } else {
-            InputCommon::GetMotionEmu()->EndTilt();
+            input_subsystem->GetMotionEmu()->EndTilt();
         }
     }
 }
@@ -79,9 +81,9 @@ void EmuWindow_SDL2::OnFingerUp() {
 
 void EmuWindow_SDL2::OnKeyEvent(int key, u8 state) {
     if (state == SDL_PRESSED) {
-        InputCommon::GetKeyboard()->PressKey(key);
+        input_subsystem->GetKeyboard()->PressKey(key);
     } else if (state == SDL_RELEASED) {
-        InputCommon::GetKeyboard()->ReleaseKey(key);
+        input_subsystem->GetKeyboard()->ReleaseKey(key);
     }
 }
 
diff --git a/src/yuzu_cmd/emu_window/emu_window_sdl2.h b/src/yuzu_cmd/emu_window/emu_window_sdl2.h
index fffac4252..82750ffec 100644
--- a/src/yuzu_cmd/emu_window/emu_window_sdl2.h
+++ b/src/yuzu_cmd/emu_window/emu_window_sdl2.h
@@ -14,9 +14,14 @@ namespace Core {
 class System;
 }
 
+namespace InputCommon {
+class InputSubsystem;
+}
+
 class EmuWindow_SDL2 : public Core::Frontend::EmuWindow {
 public:
-    explicit EmuWindow_SDL2(Core::System& system, bool fullscreen);
+    explicit EmuWindow_SDL2(Core::System& system, bool fullscreen,
+                            InputCommon::InputSubsystem* input_subsystem);
     ~EmuWindow_SDL2();
 
     /// Polls window events
@@ -76,4 +81,7 @@ protected:
 
     /// Keeps track of how often to update the title bar during gameplay
     u32 last_time = 0;
+
+    /// Input subsystem to use with this window.
+    InputCommon::InputSubsystem* input_subsystem;
 };
diff --git a/src/yuzu_cmd/emu_window/emu_window_sdl2_gl.cpp b/src/yuzu_cmd/emu_window/emu_window_sdl2_gl.cpp
index e78025737..881b67a76 100644
--- a/src/yuzu_cmd/emu_window/emu_window_sdl2_gl.cpp
+++ b/src/yuzu_cmd/emu_window/emu_window_sdl2_gl.cpp
@@ -87,8 +87,9 @@ bool EmuWindow_SDL2_GL::SupportsRequiredGLExtensions() {
     return unsupported_ext.empty();
 }
 
-EmuWindow_SDL2_GL::EmuWindow_SDL2_GL(Core::System& system, bool fullscreen)
-    : EmuWindow_SDL2{system, fullscreen} {
+EmuWindow_SDL2_GL::EmuWindow_SDL2_GL(Core::System& system, bool fullscreen,
+                                     InputCommon::InputSubsystem* input_subsystem)
+    : EmuWindow_SDL2{system, fullscreen, input_subsystem} {
     SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 4);
     SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
     SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_COMPATIBILITY);
diff --git a/src/yuzu_cmd/emu_window/emu_window_sdl2_gl.h b/src/yuzu_cmd/emu_window/emu_window_sdl2_gl.h
index 48bb41683..732a64edd 100644
--- a/src/yuzu_cmd/emu_window/emu_window_sdl2_gl.h
+++ b/src/yuzu_cmd/emu_window/emu_window_sdl2_gl.h
@@ -8,9 +8,14 @@
 #include "core/frontend/emu_window.h"
 #include "yuzu_cmd/emu_window/emu_window_sdl2.h"
 
+namespace InputCommon {
+class InputSubsystem;
+}
+
 class EmuWindow_SDL2_GL final : public EmuWindow_SDL2 {
 public:
-    explicit EmuWindow_SDL2_GL(Core::System& system, bool fullscreen);
+    explicit EmuWindow_SDL2_GL(Core::System& system, bool fullscreen,
+                               InputCommon::InputSubsystem* input_subsystem);
     ~EmuWindow_SDL2_GL();
 
     void Present() override;
diff --git a/src/yuzu_cmd/emu_window/emu_window_sdl2_vk.cpp b/src/yuzu_cmd/emu_window/emu_window_sdl2_vk.cpp
index cb8e68a39..53491f86e 100644
--- a/src/yuzu_cmd/emu_window/emu_window_sdl2_vk.cpp
+++ b/src/yuzu_cmd/emu_window/emu_window_sdl2_vk.cpp
@@ -19,8 +19,9 @@
 #include <SDL.h>
 #include <SDL_syswm.h>
 
-EmuWindow_SDL2_VK::EmuWindow_SDL2_VK(Core::System& system, bool fullscreen)
-    : EmuWindow_SDL2{system, fullscreen} {
+EmuWindow_SDL2_VK::EmuWindow_SDL2_VK(Core::System& system, bool fullscreen,
+                                     InputCommon::InputSubsystem* input_subsystem)
+    : EmuWindow_SDL2{system, fullscreen, input_subsystem} {
     const std::string window_title = fmt::format("yuzu {} | {}-{} (Vulkan)", Common::g_build_name,
                                                  Common::g_scm_branch, Common::g_scm_desc);
     render_window =
diff --git a/src/yuzu_cmd/emu_window/emu_window_sdl2_vk.h b/src/yuzu_cmd/emu_window/emu_window_sdl2_vk.h
index 77a6ca72b..f99704d4c 100644
--- a/src/yuzu_cmd/emu_window/emu_window_sdl2_vk.h
+++ b/src/yuzu_cmd/emu_window/emu_window_sdl2_vk.h
@@ -13,9 +13,14 @@ namespace Core {
 class System;
 }
 
+namespace InputCommon {
+class InputSubsystem;
+}
+
 class EmuWindow_SDL2_VK final : public EmuWindow_SDL2 {
 public:
-    explicit EmuWindow_SDL2_VK(Core::System& system, bool fullscreen);
+    explicit EmuWindow_SDL2_VK(Core::System& system, bool fullscreen,
+                               InputCommon::InputSubsystem* input_subsystem);
     ~EmuWindow_SDL2_VK();
 
     void Present() override;
diff --git a/src/yuzu_cmd/yuzu.cpp b/src/yuzu_cmd/yuzu.cpp
index 8efe49390..4f00c804d 100644
--- a/src/yuzu_cmd/yuzu.cpp
+++ b/src/yuzu_cmd/yuzu.cpp
@@ -23,12 +23,14 @@
 #include "common/telemetry.h"
 #include "core/core.h"
 #include "core/crypto/key_manager.h"
+#include "core/file_sys/registered_cache.h"
 #include "core/file_sys/vfs_real.h"
 #include "core/gdbstub/gdbstub.h"
 #include "core/hle/service/filesystem/filesystem.h"
 #include "core/loader/loader.h"
 #include "core/settings.h"
 #include "core/telemetry_session.h"
+#include "input_common/main.h"
 #include "video_core/renderer_base.h"
 #include "yuzu_cmd/config.h"
 #include "yuzu_cmd/emu_window/emu_window_sdl2.h"
@@ -37,8 +39,6 @@
 #include "yuzu_cmd/emu_window/emu_window_sdl2_vk.h"
 #endif
 
-#include "core/file_sys/registered_cache.h"
-
 #ifdef _WIN32
 // windows.h needs to be included before shellapi.h
 #include <windows.h>
@@ -179,15 +179,16 @@ int main(int argc, char** argv) {
     Settings::Apply();
 
     Core::System& system{Core::System::GetInstance()};
+    InputCommon::InputSubsystem input_subsystem;
 
     std::unique_ptr<EmuWindow_SDL2> emu_window;
     switch (Settings::values.renderer_backend.GetValue()) {
     case Settings::RendererBackend::OpenGL:
-        emu_window = std::make_unique<EmuWindow_SDL2_GL>(system, fullscreen);
+        emu_window = std::make_unique<EmuWindow_SDL2_GL>(system, fullscreen, &input_subsystem);
         break;
     case Settings::RendererBackend::Vulkan:
 #ifdef HAS_VULKAN
-        emu_window = std::make_unique<EmuWindow_SDL2_VK>(system, fullscreen);
+        emu_window = std::make_unique<EmuWindow_SDL2_VK>(system, fullscreen, &input_subsystem);
         break;
 #else
         LOG_CRITICAL(Frontend, "Vulkan backend has not been compiled!");
diff --git a/src/yuzu_tester/emu_window/emu_window_sdl2_hide.cpp b/src/yuzu_tester/emu_window/emu_window_sdl2_hide.cpp
index 8584f6671..78f75fb38 100644
--- a/src/yuzu_tester/emu_window/emu_window_sdl2_hide.cpp
+++ b/src/yuzu_tester/emu_window/emu_window_sdl2_hide.cpp
@@ -13,7 +13,6 @@
 
 #include <glad/glad.h>
 
-#include "common/assert.h"
 #include "common/logging/log.h"
 #include "common/scm_rev.h"
 #include "core/settings.h"
@@ -53,7 +52,7 @@ EmuWindow_SDL2_Hide::EmuWindow_SDL2_Hide() {
         exit(1);
     }
 
-    InputCommon::Init();
+    input_subsystem->Initialize();
 
     SDL_SetMainReady();
 
@@ -105,7 +104,7 @@ EmuWindow_SDL2_Hide::EmuWindow_SDL2_Hide() {
 }
 
 EmuWindow_SDL2_Hide::~EmuWindow_SDL2_Hide() {
-    InputCommon::Shutdown();
+    input_subsystem->Shutdown();
     SDL_GL_DeleteContext(gl_context);
     SDL_Quit();
 }
diff --git a/src/yuzu_tester/emu_window/emu_window_sdl2_hide.h b/src/yuzu_tester/emu_window/emu_window_sdl2_hide.h
index c13a82df2..a553b4b95 100644
--- a/src/yuzu_tester/emu_window/emu_window_sdl2_hide.h
+++ b/src/yuzu_tester/emu_window/emu_window_sdl2_hide.h
@@ -8,6 +8,10 @@
 
 struct SDL_Window;
 
+namespace InputCommon {
+class InputSubsystem;
+}
+
 class EmuWindow_SDL2_Hide : public Core::Frontend::EmuWindow {
 public:
     explicit EmuWindow_SDL2_Hide();
@@ -25,6 +29,8 @@ private:
     /// Whether the GPU and driver supports the OpenGL extension required
     bool SupportsRequiredGLExtensions();
 
+    std::unique_ptr<InputCommon::InputSubsystem> input_subsystem;
+
     /// Internal SDL2 render window
     SDL_Window* render_window;