diff --git a/src/core/hid/emulated_controller.cpp b/src/core/hid/emulated_controller.cpp
index 67969e938..f238d6ccd 100644
--- a/src/core/hid/emulated_controller.cpp
+++ b/src/core/hid/emulated_controller.cpp
@@ -145,6 +145,7 @@ void EmulatedController::LoadDevices() {
     output_params[3].Set("output", true);
 
     LoadTASParams();
+    LoadVirtualGamepadParams();
 
     std::ranges::transform(button_params, button_devices.begin(), Common::Input::CreateInputDevice);
     std::ranges::transform(stick_params, stick_devices.begin(), Common::Input::CreateInputDevice);
@@ -163,6 +164,12 @@ void EmulatedController::LoadDevices() {
                            Common::Input::CreateInputDevice);
     std::ranges::transform(tas_stick_params, tas_stick_devices.begin(),
                            Common::Input::CreateInputDevice);
+
+    // Initialize virtual gamepad devices
+    std::ranges::transform(virtual_button_params, virtual_button_devices.begin(),
+                           Common::Input::CreateInputDevice);
+    std::ranges::transform(virtual_stick_params, virtual_stick_devices.begin(),
+                           Common::Input::CreateInputDevice);
 }
 
 void EmulatedController::LoadTASParams() {
@@ -205,6 +212,46 @@ void EmulatedController::LoadTASParams() {
     tas_stick_params[Settings::NativeAnalog::RStick].Set("axis_y", 3);
 }
 
+void EmulatedController::LoadVirtualGamepadParams() {
+    const auto player_index = NpadIdTypeToIndex(npad_id_type);
+    Common::ParamPackage common_params{};
+    common_params.Set("engine", "virtual_gamepad");
+    common_params.Set("port", static_cast<int>(player_index));
+    for (auto& param : virtual_button_params) {
+        param = common_params;
+    }
+    for (auto& param : virtual_stick_params) {
+        param = common_params;
+    }
+
+    // TODO(german77): Replace this with an input profile or something better
+    virtual_button_params[Settings::NativeButton::A].Set("button", 0);
+    virtual_button_params[Settings::NativeButton::B].Set("button", 1);
+    virtual_button_params[Settings::NativeButton::X].Set("button", 2);
+    virtual_button_params[Settings::NativeButton::Y].Set("button", 3);
+    virtual_button_params[Settings::NativeButton::LStick].Set("button", 4);
+    virtual_button_params[Settings::NativeButton::RStick].Set("button", 5);
+    virtual_button_params[Settings::NativeButton::L].Set("button", 6);
+    virtual_button_params[Settings::NativeButton::R].Set("button", 7);
+    virtual_button_params[Settings::NativeButton::ZL].Set("button", 8);
+    virtual_button_params[Settings::NativeButton::ZR].Set("button", 9);
+    virtual_button_params[Settings::NativeButton::Plus].Set("button", 10);
+    virtual_button_params[Settings::NativeButton::Minus].Set("button", 11);
+    virtual_button_params[Settings::NativeButton::DLeft].Set("button", 12);
+    virtual_button_params[Settings::NativeButton::DUp].Set("button", 13);
+    virtual_button_params[Settings::NativeButton::DRight].Set("button", 14);
+    virtual_button_params[Settings::NativeButton::DDown].Set("button", 15);
+    virtual_button_params[Settings::NativeButton::SL].Set("button", 16);
+    virtual_button_params[Settings::NativeButton::SR].Set("button", 17);
+    virtual_button_params[Settings::NativeButton::Home].Set("button", 18);
+    virtual_button_params[Settings::NativeButton::Screenshot].Set("button", 19);
+
+    virtual_stick_params[Settings::NativeAnalog::LStick].Set("axis_x", 0);
+    virtual_stick_params[Settings::NativeAnalog::LStick].Set("axis_y", 1);
+    virtual_stick_params[Settings::NativeAnalog::RStick].Set("axis_x", 2);
+    virtual_stick_params[Settings::NativeAnalog::RStick].Set("axis_y", 3);
+}
+
 void EmulatedController::ReloadInput() {
     // If you load any device here add the equivalent to the UnloadInput() function
     LoadDevices();
@@ -322,6 +369,35 @@ void EmulatedController::ReloadInput() {
                 },
         });
     }
+
+    // Use a common UUID for Virtual Gamepad
+    static constexpr Common::UUID VIRTUAL_UUID = Common::UUID{
+        {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7, 0xFF, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}};
+
+    // Register virtual devices. No need to force update
+    for (std::size_t index = 0; index < virtual_button_devices.size(); ++index) {
+        if (!virtual_button_devices[index]) {
+            continue;
+        }
+        virtual_button_devices[index]->SetCallback({
+            .on_change =
+                [this, index](const Common::Input::CallbackStatus& callback) {
+                    SetButton(callback, index, VIRTUAL_UUID);
+                },
+        });
+    }
+
+    for (std::size_t index = 0; index < virtual_stick_devices.size(); ++index) {
+        if (!virtual_stick_devices[index]) {
+            continue;
+        }
+        virtual_stick_devices[index]->SetCallback({
+            .on_change =
+                [this, index](const Common::Input::CallbackStatus& callback) {
+                    SetStick(callback, index, VIRTUAL_UUID);
+                },
+        });
+    }
 }
 
 void EmulatedController::UnloadInput() {
@@ -349,6 +425,12 @@ void EmulatedController::UnloadInput() {
     for (auto& stick : tas_stick_devices) {
         stick.reset();
     }
+    for (auto& button : virtual_button_devices) {
+        button.reset();
+    }
+    for (auto& stick : virtual_stick_devices) {
+        stick.reset();
+    }
     camera_devices.reset();
     nfc_devices.reset();
 }
diff --git a/src/core/hid/emulated_controller.h b/src/core/hid/emulated_controller.h
index fa7a34278..a398543a6 100644
--- a/src/core/hid/emulated_controller.h
+++ b/src/core/hid/emulated_controller.h
@@ -385,6 +385,9 @@ private:
     /// Set the params for TAS devices
     void LoadTASParams();
 
+    /// Set the params for virtual pad devices
+    void LoadVirtualGamepadParams();
+
     /**
      * @param use_temporary_value If true tmp_npad_type will be used
      * @return true if the controller style is fullkey
@@ -500,6 +503,12 @@ private:
     ButtonDevices tas_button_devices;
     StickDevices tas_stick_devices;
 
+    // Virtual gamepad related variables
+    ButtonParams virtual_button_params;
+    StickParams virtual_stick_params;
+    ButtonDevices virtual_button_devices;
+    StickDevices virtual_stick_devices;
+
     mutable std::mutex mutex;
     mutable std::mutex callback_mutex;
     std::unordered_map<int, ControllerUpdateCallback> callback_list;
diff --git a/src/input_common/CMakeLists.txt b/src/input_common/CMakeLists.txt
index 7932aaab0..f24c89b04 100644
--- a/src/input_common/CMakeLists.txt
+++ b/src/input_common/CMakeLists.txt
@@ -20,6 +20,8 @@ add_library(input_common STATIC
     drivers/udp_client.h
     drivers/virtual_amiibo.cpp
     drivers/virtual_amiibo.h
+    drivers/virtual_gamepad.cpp
+    drivers/virtual_gamepad.h
     helpers/stick_from_buttons.cpp
     helpers/stick_from_buttons.h
     helpers/touch_from_buttons.cpp
diff --git a/src/input_common/drivers/virtual_gamepad.cpp b/src/input_common/drivers/virtual_gamepad.cpp
new file mode 100644
index 000000000..7db945aa6
--- /dev/null
+++ b/src/input_common/drivers/virtual_gamepad.cpp
@@ -0,0 +1,78 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "input_common/drivers/virtual_gamepad.h"
+
+namespace InputCommon {
+constexpr std::size_t PlayerIndexCount = 10;
+
+VirtualGamepad::VirtualGamepad(std::string input_engine_) : InputEngine(std::move(input_engine_)) {
+    for (std::size_t i = 0; i < PlayerIndexCount; i++) {
+        PreSetController(GetIdentifier(i));
+    }
+}
+
+void VirtualGamepad::SetButtonState(std::size_t player_index, int button_id, bool value) {
+    if (player_index > PlayerIndexCount) {
+        return;
+    }
+    const auto identifier = GetIdentifier(player_index);
+    SetButton(identifier, button_id, value);
+}
+
+void VirtualGamepad::SetButtonState(std::size_t player_index, VirtualButton button_id, bool value) {
+    SetButtonState(player_index, static_cast<int>(button_id), value);
+}
+
+void VirtualGamepad::SetStickPosition(std::size_t player_index, int axis_id, float x_value,
+                                      float y_value) {
+    if (player_index > PlayerIndexCount) {
+        return;
+    }
+    const auto identifier = GetIdentifier(player_index);
+    SetAxis(identifier, axis_id * 2, x_value);
+    SetAxis(identifier, (axis_id * 2) + 1, y_value);
+}
+
+void VirtualGamepad::SetStickPosition(std::size_t player_index, VirtualStick axis_id, float x_value,
+                                      float y_value) {
+    SetStickPosition(player_index, static_cast<int>(axis_id), x_value, y_value);
+}
+
+void VirtualGamepad::ResetControllers() {
+    for (std::size_t i = 0; i < PlayerIndexCount; i++) {
+        SetStickPosition(i, VirtualStick::Left, 0.0f, 0.0f);
+        SetStickPosition(i, VirtualStick::Right, 0.0f, 0.0f);
+
+        SetButtonState(i, VirtualButton::ButtonA, false);
+        SetButtonState(i, VirtualButton::ButtonB, false);
+        SetButtonState(i, VirtualButton::ButtonX, false);
+        SetButtonState(i, VirtualButton::ButtonY, false);
+        SetButtonState(i, VirtualButton::StickL, false);
+        SetButtonState(i, VirtualButton::StickR, false);
+        SetButtonState(i, VirtualButton::TriggerL, false);
+        SetButtonState(i, VirtualButton::TriggerR, false);
+        SetButtonState(i, VirtualButton::TriggerZL, false);
+        SetButtonState(i, VirtualButton::TriggerZR, false);
+        SetButtonState(i, VirtualButton::ButtonPlus, false);
+        SetButtonState(i, VirtualButton::ButtonMinus, false);
+        SetButtonState(i, VirtualButton::ButtonLeft, false);
+        SetButtonState(i, VirtualButton::ButtonUp, false);
+        SetButtonState(i, VirtualButton::ButtonRight, false);
+        SetButtonState(i, VirtualButton::ButtonDown, false);
+        SetButtonState(i, VirtualButton::ButtonSL, false);
+        SetButtonState(i, VirtualButton::ButtonSR, false);
+        SetButtonState(i, VirtualButton::ButtonHome, false);
+        SetButtonState(i, VirtualButton::ButtonCapture, false);
+    }
+}
+
+PadIdentifier VirtualGamepad::GetIdentifier(std::size_t player_index) const {
+    return {
+        .guid = Common::UUID{},
+        .port = player_index,
+        .pad = 0,
+    };
+}
+
+} // namespace InputCommon
diff --git a/src/input_common/drivers/virtual_gamepad.h b/src/input_common/drivers/virtual_gamepad.h
new file mode 100644
index 000000000..3df91cc6f
--- /dev/null
+++ b/src/input_common/drivers/virtual_gamepad.h
@@ -0,0 +1,73 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include "input_common/input_engine.h"
+
+namespace InputCommon {
+
+/**
+ * A virtual controller that is always assigned to the game input
+ */
+class VirtualGamepad final : public InputEngine {
+public:
+    enum class VirtualButton {
+        ButtonA,
+        ButtonB,
+        ButtonX,
+        ButtonY,
+        StickL,
+        StickR,
+        TriggerL,
+        TriggerR,
+        TriggerZL,
+        TriggerZR,
+        ButtonPlus,
+        ButtonMinus,
+        ButtonLeft,
+        ButtonUp,
+        ButtonRight,
+        ButtonDown,
+        ButtonSL,
+        ButtonSR,
+        ButtonHome,
+        ButtonCapture,
+    };
+
+    enum class VirtualStick {
+        Left = 0,
+        Right = 1,
+    };
+
+    explicit VirtualGamepad(std::string input_engine_);
+
+    /**
+     * Sets the status of all buttons bound with the key to pressed
+     * @param player_index the player number that will take this action
+     * @param button_id the id of the button
+     * @param value indicates if the button is pressed or not
+     */
+    void SetButtonState(std::size_t player_index, int button_id, bool value);
+    void SetButtonState(std::size_t player_index, VirtualButton button_id, bool value);
+
+    /**
+     * Sets the status of all buttons bound with the key to released
+     * @param player_index the player number that will take this action
+     * @param axis_id the id of the axis to move
+     * @param x_value the position of the stick in the x axis
+     * @param y_value the position of the stick in the y axis
+     */
+    void SetStickPosition(std::size_t player_index, int axis_id, float x_value, float y_value);
+    void SetStickPosition(std::size_t player_index, VirtualStick axis_id, float x_value,
+                          float y_value);
+
+    /// Restores all inputs into the neutral position
+    void ResetControllers();
+
+private:
+    /// Returns the correct identifier corresponding to the player index
+    PadIdentifier GetIdentifier(std::size_t player_index) const;
+};
+
+} // namespace InputCommon
diff --git a/src/input_common/main.cpp b/src/input_common/main.cpp
index 942a13535..75b856c95 100644
--- a/src/input_common/main.cpp
+++ b/src/input_common/main.cpp
@@ -12,6 +12,7 @@
 #include "input_common/drivers/touch_screen.h"
 #include "input_common/drivers/udp_client.h"
 #include "input_common/drivers/virtual_amiibo.h"
+#include "input_common/drivers/virtual_gamepad.h"
 #include "input_common/helpers/stick_from_buttons.h"
 #include "input_common/helpers/touch_from_buttons.h"
 #include "input_common/input_engine.h"
@@ -85,6 +86,12 @@ struct InputSubsystem::Impl {
         Common::Input::RegisterOutputFactory(virtual_amiibo->GetEngineName(),
                                              virtual_amiibo_output_factory);
 
+        virtual_gamepad = std::make_shared<VirtualGamepad>("virtual_gamepad");
+        virtual_gamepad->SetMappingCallback(mapping_callback);
+        virtual_gamepad_input_factory = std::make_shared<InputFactory>(virtual_gamepad);
+        Common::Input::RegisterInputFactory(virtual_gamepad->GetEngineName(),
+                                            virtual_gamepad_input_factory);
+
 #ifdef HAVE_SDL2
         sdl = std::make_shared<SDLDriver>("sdl");
         sdl->SetMappingCallback(mapping_callback);
@@ -132,6 +139,9 @@ struct InputSubsystem::Impl {
         Common::Input::UnregisterOutputFactory(virtual_amiibo->GetEngineName());
         virtual_amiibo.reset();
 
+        Common::Input::UnregisterInputFactory(virtual_gamepad->GetEngineName());
+        virtual_gamepad.reset();
+
 #ifdef HAVE_SDL2
         Common::Input::UnregisterInputFactory(sdl->GetEngineName());
         Common::Input::UnregisterOutputFactory(sdl->GetEngineName());
@@ -290,6 +300,9 @@ struct InputSubsystem::Impl {
         if (engine == tas_input->GetEngineName()) {
             return true;
         }
+        if (engine == virtual_gamepad->GetEngineName()) {
+            return true;
+        }
 #ifdef HAVE_SDL2
         if (engine == sdl->GetEngineName()) {
             return true;
@@ -338,6 +351,7 @@ struct InputSubsystem::Impl {
     std::shared_ptr<CemuhookUDP::UDPClient> udp_client;
     std::shared_ptr<Camera> camera;
     std::shared_ptr<VirtualAmiibo> virtual_amiibo;
+    std::shared_ptr<VirtualGamepad> virtual_gamepad;
 
     std::shared_ptr<InputFactory> keyboard_factory;
     std::shared_ptr<InputFactory> mouse_factory;
@@ -347,6 +361,7 @@ struct InputSubsystem::Impl {
     std::shared_ptr<InputFactory> tas_input_factory;
     std::shared_ptr<InputFactory> camera_input_factory;
     std::shared_ptr<InputFactory> virtual_amiibo_input_factory;
+    std::shared_ptr<InputFactory> virtual_gamepad_input_factory;
 
     std::shared_ptr<OutputFactory> keyboard_output_factory;
     std::shared_ptr<OutputFactory> mouse_output_factory;
@@ -423,6 +438,14 @@ const VirtualAmiibo* InputSubsystem::GetVirtualAmiibo() const {
     return impl->virtual_amiibo.get();
 }
 
+VirtualGamepad* InputSubsystem::GetVirtualGamepad() {
+    return impl->virtual_gamepad.get();
+}
+
+const VirtualGamepad* InputSubsystem::GetVirtualGamepad() const {
+    return impl->virtual_gamepad.get();
+}
+
 std::vector<Common::ParamPackage> InputSubsystem::GetInputDevices() const {
     return impl->GetInputDevices();
 }
diff --git a/src/input_common/main.h b/src/input_common/main.h
index 6218c37f6..1207d786c 100644
--- a/src/input_common/main.h
+++ b/src/input_common/main.h
@@ -34,6 +34,7 @@ class Keyboard;
 class Mouse;
 class TouchScreen;
 class VirtualAmiibo;
+class VirtualGamepad;
 struct MappingData;
 } // namespace InputCommon
 
@@ -108,6 +109,12 @@ public:
     /// Retrieves the underlying virtual amiibo input device.
     [[nodiscard]] const VirtualAmiibo* GetVirtualAmiibo() const;
 
+    /// Retrieves the underlying virtual gamepad input device.
+    [[nodiscard]] VirtualGamepad* GetVirtualGamepad();
+
+    /// Retrieves the underlying virtual gamepad input device.
+    [[nodiscard]] const VirtualGamepad* GetVirtualGamepad() 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 `engine` field