summaryrefslogtreecommitdiffstats
path: root/src/hid_core/frontend
diff options
context:
space:
mode:
Diffstat (limited to 'src/hid_core/frontend')
-rw-r--r--src/hid_core/frontend/emulated_console.cpp324
-rw-r--r--src/hid_core/frontend/emulated_console.h192
-rw-r--r--src/hid_core/frontend/emulated_controller.cpp1972
-rw-r--r--src/hid_core/frontend/emulated_controller.h619
-rw-r--r--src/hid_core/frontend/emulated_devices.cpp483
-rw-r--r--src/hid_core/frontend/emulated_devices.h212
-rw-r--r--src/hid_core/frontend/input_converter.cpp436
-rw-r--r--src/hid_core/frontend/input_converter.h119
-rw-r--r--src/hid_core/frontend/input_interpreter.cpp64
-rw-r--r--src/hid_core/frontend/input_interpreter.h111
-rw-r--r--src/hid_core/frontend/motion_input.cpp357
-rw-r--r--src/hid_core/frontend/motion_input.h119
12 files changed, 5008 insertions, 0 deletions
diff --git a/src/hid_core/frontend/emulated_console.cpp b/src/hid_core/frontend/emulated_console.cpp
new file mode 100644
index 000000000..114c22fb7
--- /dev/null
+++ b/src/hid_core/frontend/emulated_console.cpp
@@ -0,0 +1,324 @@
+// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "common/settings.h"
+#include "hid_core/frontend/emulated_console.h"
+#include "hid_core/frontend/input_converter.h"
+
+namespace Core::HID {
+EmulatedConsole::EmulatedConsole() = default;
+
+EmulatedConsole::~EmulatedConsole() = default;
+
+void EmulatedConsole::ReloadFromSettings() {
+ // Using first motion device from player 1. No need to assign any unique config at the moment
+ const auto& player = Settings::values.players.GetValue()[0];
+ motion_params[0] = Common::ParamPackage(player.motions[0]);
+
+ ReloadInput();
+}
+
+void EmulatedConsole::SetTouchParams() {
+ std::size_t index = 0;
+
+ // We can't use mouse as touch if native mouse is enabled
+ if (!Settings::values.mouse_enabled) {
+ touch_params[index++] =
+ Common::ParamPackage{"engine:mouse,axis_x:0,axis_y:1,button:0,port:2"};
+ }
+
+ touch_params[index++] =
+ Common::ParamPackage{"engine:cemuhookudp,axis_x:17,axis_y:18,button:65536"};
+ touch_params[index++] =
+ Common::ParamPackage{"engine:cemuhookudp,axis_x:19,axis_y:20,button:131072"};
+
+ for (int i = 0; i < static_cast<int>(MaxActiveTouchInputs); i++) {
+ Common::ParamPackage touchscreen_param{};
+ touchscreen_param.Set("engine", "touch");
+ touchscreen_param.Set("axis_x", i * 2);
+ touchscreen_param.Set("axis_y", (i * 2) + 1);
+ touchscreen_param.Set("button", i);
+ touch_params[index++] = std::move(touchscreen_param);
+ }
+
+ if (Settings::values.touch_from_button_maps.empty()) {
+ LOG_WARNING(Input, "touch_from_button_maps is unset by frontend config");
+ return;
+ }
+
+ const auto button_index =
+ static_cast<u64>(Settings::values.touch_from_button_map_index.GetValue());
+ const auto& touch_buttons = Settings::values.touch_from_button_maps[button_index].buttons;
+
+ // Map the rest of the fingers from touch from button configuration
+ for (const auto& config_entry : touch_buttons) {
+ if (index >= MaxTouchDevices) {
+ continue;
+ }
+ Common::ParamPackage params{config_entry};
+ Common::ParamPackage touch_button_params;
+ const int x = params.Get("x", 0);
+ const int y = params.Get("y", 0);
+ params.Erase("x");
+ params.Erase("y");
+ touch_button_params.Set("engine", "touch_from_button");
+ touch_button_params.Set("button", params.Serialize());
+ touch_button_params.Set("x", x);
+ touch_button_params.Set("y", y);
+ touch_params[index] = std::move(touch_button_params);
+ index++;
+ }
+}
+
+void EmulatedConsole::ReloadInput() {
+ // If you load any device here add the equivalent to the UnloadInput() function
+ SetTouchParams();
+
+ motion_params[1] = Common::ParamPackage{"engine:virtual_gamepad,port:8,motion:0"};
+
+ for (std::size_t index = 0; index < motion_devices.size(); ++index) {
+ motion_devices[index] = Common::Input::CreateInputDevice(motion_params[index]);
+ if (!motion_devices[index]) {
+ continue;
+ }
+ motion_devices[index]->SetCallback({
+ .on_change =
+ [this](const Common::Input::CallbackStatus& callback) { SetMotion(callback); },
+ });
+ }
+
+ // Restore motion state
+ auto& emulated_motion = console.motion_values.emulated;
+ auto& motion = console.motion_state;
+ emulated_motion.ResetRotations();
+ emulated_motion.ResetQuaternion();
+ motion.accel = emulated_motion.GetAcceleration();
+ motion.gyro = emulated_motion.GetGyroscope();
+ motion.rotation = emulated_motion.GetRotations();
+ motion.orientation = emulated_motion.GetOrientation();
+ motion.is_at_rest = !emulated_motion.IsMoving(motion_sensitivity);
+
+ // Unique index for identifying touch device source
+ std::size_t index = 0;
+ for (auto& touch_device : touch_devices) {
+ touch_device = Common::Input::CreateInputDevice(touch_params[index]);
+ if (!touch_device) {
+ continue;
+ }
+ touch_device->SetCallback({
+ .on_change =
+ [this, index](const Common::Input::CallbackStatus& callback) {
+ SetTouch(callback, index);
+ },
+ });
+ index++;
+ }
+}
+
+void EmulatedConsole::UnloadInput() {
+ for (auto& motion : motion_devices) {
+ motion.reset();
+ }
+ for (auto& touch : touch_devices) {
+ touch.reset();
+ }
+}
+
+void EmulatedConsole::EnableConfiguration() {
+ is_configuring = true;
+ SaveCurrentConfig();
+}
+
+void EmulatedConsole::DisableConfiguration() {
+ is_configuring = false;
+}
+
+bool EmulatedConsole::IsConfiguring() const {
+ return is_configuring;
+}
+
+void EmulatedConsole::SaveCurrentConfig() {
+ if (!is_configuring) {
+ return;
+ }
+}
+
+void EmulatedConsole::RestoreConfig() {
+ if (!is_configuring) {
+ return;
+ }
+ ReloadFromSettings();
+}
+
+Common::ParamPackage EmulatedConsole::GetMotionParam() const {
+ return motion_params[0];
+}
+
+void EmulatedConsole::SetMotionParam(Common::ParamPackage param) {
+ motion_params[0] = std::move(param);
+ ReloadInput();
+}
+
+void EmulatedConsole::SetMotion(const Common::Input::CallbackStatus& callback) {
+ std::unique_lock lock{mutex};
+ auto& raw_status = console.motion_values.raw_status;
+ auto& emulated = console.motion_values.emulated;
+
+ raw_status = TransformToMotion(callback);
+ emulated.SetAcceleration(Common::Vec3f{
+ raw_status.accel.x.value,
+ raw_status.accel.y.value,
+ raw_status.accel.z.value,
+ });
+ emulated.SetGyroscope(Common::Vec3f{
+ raw_status.gyro.x.value,
+ raw_status.gyro.y.value,
+ raw_status.gyro.z.value,
+ });
+ emulated.UpdateRotation(raw_status.delta_timestamp);
+ emulated.UpdateOrientation(raw_status.delta_timestamp);
+
+ if (is_configuring) {
+ lock.unlock();
+ TriggerOnChange(ConsoleTriggerType::Motion);
+ return;
+ }
+
+ auto& motion = console.motion_state;
+ motion.accel = emulated.GetAcceleration();
+ motion.gyro = emulated.GetGyroscope();
+ motion.rotation = emulated.GetRotations();
+ motion.orientation = emulated.GetOrientation();
+ motion.quaternion = emulated.GetQuaternion();
+ motion.gyro_bias = emulated.GetGyroBias();
+ motion.is_at_rest = !emulated.IsMoving(motion_sensitivity);
+ // Find what is this value
+ motion.verticalization_error = 0.0f;
+
+ lock.unlock();
+ TriggerOnChange(ConsoleTriggerType::Motion);
+}
+
+void EmulatedConsole::SetTouch(const Common::Input::CallbackStatus& callback, std::size_t index) {
+ if (index >= MaxTouchDevices) {
+ return;
+ }
+ std::unique_lock lock{mutex};
+
+ const auto touch_input = TransformToTouch(callback);
+ auto touch_index = GetIndexFromFingerId(index);
+ bool is_new_input = false;
+
+ if (!touch_index.has_value() && touch_input.pressed.value) {
+ touch_index = GetNextFreeIndex();
+ is_new_input = true;
+ }
+
+ // No free entries or invalid state. Ignore input
+ if (!touch_index.has_value()) {
+ return;
+ }
+
+ auto& touch_value = console.touch_values[touch_index.value()];
+
+ if (is_new_input) {
+ touch_value.pressed.value = true;
+ touch_value.id = static_cast<int>(index);
+ }
+
+ touch_value.x = touch_input.x;
+ touch_value.y = touch_input.y;
+
+ if (!touch_input.pressed.value) {
+ touch_value.pressed.value = false;
+ }
+
+ if (is_configuring) {
+ lock.unlock();
+ TriggerOnChange(ConsoleTriggerType::Touch);
+ return;
+ }
+
+ // Touch outside allowed range. Ignore input
+ if (touch_index.value() >= MaxActiveTouchInputs) {
+ return;
+ }
+
+ console.touch_state[touch_index.value()] = {
+ .position = {touch_value.x.value, touch_value.y.value},
+ .id = static_cast<u32>(touch_index.value()),
+ .pressed = touch_input.pressed.value,
+ };
+
+ lock.unlock();
+ TriggerOnChange(ConsoleTriggerType::Touch);
+}
+
+ConsoleMotionValues EmulatedConsole::GetMotionValues() const {
+ std::scoped_lock lock{mutex};
+ return console.motion_values;
+}
+
+TouchValues EmulatedConsole::GetTouchValues() const {
+ std::scoped_lock lock{mutex};
+ return console.touch_values;
+}
+
+ConsoleMotion EmulatedConsole::GetMotion() const {
+ std::scoped_lock lock{mutex};
+ return console.motion_state;
+}
+
+TouchFingerState EmulatedConsole::GetTouch() const {
+ std::scoped_lock lock{mutex};
+ return console.touch_state;
+}
+
+std::optional<std::size_t> EmulatedConsole::GetIndexFromFingerId(std::size_t finger_id) const {
+ for (std::size_t index = 0; index < MaxTouchDevices; ++index) {
+ const auto& finger = console.touch_values[index];
+ if (!finger.pressed.value) {
+ continue;
+ }
+ if (finger.id == static_cast<int>(finger_id)) {
+ return index;
+ }
+ }
+ return std::nullopt;
+}
+
+std::optional<std::size_t> EmulatedConsole::GetNextFreeIndex() const {
+ for (std::size_t index = 0; index < MaxTouchDevices; ++index) {
+ if (!console.touch_values[index].pressed.value) {
+ return index;
+ }
+ }
+ return std::nullopt;
+}
+
+void EmulatedConsole::TriggerOnChange(ConsoleTriggerType type) {
+ std::scoped_lock lock{callback_mutex};
+ for (const auto& poller_pair : callback_list) {
+ const ConsoleUpdateCallback& poller = poller_pair.second;
+ if (poller.on_change) {
+ poller.on_change(type);
+ }
+ }
+}
+
+int EmulatedConsole::SetCallback(ConsoleUpdateCallback update_callback) {
+ std::scoped_lock lock{callback_mutex};
+ callback_list.insert_or_assign(last_callback_key, std::move(update_callback));
+ return last_callback_key++;
+}
+
+void EmulatedConsole::DeleteCallback(int key) {
+ std::scoped_lock lock{callback_mutex};
+ const auto& iterator = callback_list.find(key);
+ if (iterator == callback_list.end()) {
+ LOG_ERROR(Input, "Tried to delete non-existent callback {}", key);
+ return;
+ }
+ callback_list.erase(iterator);
+}
+} // namespace Core::HID
diff --git a/src/hid_core/frontend/emulated_console.h b/src/hid_core/frontend/emulated_console.h
new file mode 100644
index 000000000..847551395
--- /dev/null
+++ b/src/hid_core/frontend/emulated_console.h
@@ -0,0 +1,192 @@
+// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <array>
+#include <functional>
+#include <memory>
+#include <mutex>
+#include <optional>
+#include <unordered_map>
+
+#include "common/common_funcs.h"
+#include "common/common_types.h"
+#include "common/input.h"
+#include "common/param_package.h"
+#include "common/point.h"
+#include "common/quaternion.h"
+#include "common/vector_math.h"
+#include "hid_core/frontend/motion_input.h"
+#include "hid_core/hid_types.h"
+
+namespace Core::HID {
+static constexpr std::size_t MaxTouchDevices = 32;
+static constexpr std::size_t MaxActiveTouchInputs = 16;
+
+struct ConsoleMotionInfo {
+ Common::Input::MotionStatus raw_status{};
+ MotionInput emulated{};
+};
+
+using ConsoleMotionDevices = std::array<std::unique_ptr<Common::Input::InputDevice>, 2>;
+using TouchDevices = std::array<std::unique_ptr<Common::Input::InputDevice>, MaxTouchDevices>;
+
+using ConsoleMotionParams = std::array<Common::ParamPackage, 2>;
+using TouchParams = std::array<Common::ParamPackage, MaxTouchDevices>;
+
+using ConsoleMotionValues = ConsoleMotionInfo;
+using TouchValues = std::array<Common::Input::TouchStatus, MaxTouchDevices>;
+
+// Contains all motion related data that is used on the services
+struct ConsoleMotion {
+ Common::Vec3f accel{};
+ Common::Vec3f gyro{};
+ Common::Vec3f rotation{};
+ std::array<Common::Vec3f, 3> orientation{};
+ Common::Quaternion<f32> quaternion{};
+ Common::Vec3f gyro_bias{};
+ f32 verticalization_error{};
+ bool is_at_rest{};
+};
+
+using TouchFingerState = std::array<TouchFinger, MaxActiveTouchInputs>;
+
+struct ConsoleStatus {
+ // Data from input_common
+ ConsoleMotionValues motion_values{};
+ TouchValues touch_values{};
+
+ // Data for HID services
+ ConsoleMotion motion_state{};
+ TouchFingerState touch_state{};
+};
+
+enum class ConsoleTriggerType {
+ Motion,
+ Touch,
+ All,
+};
+
+struct ConsoleUpdateCallback {
+ std::function<void(ConsoleTriggerType)> on_change;
+};
+
+class EmulatedConsole {
+public:
+ /**
+ * Contains all input data within the emulated switch console tablet such as touch and motion
+ */
+ explicit EmulatedConsole();
+ ~EmulatedConsole();
+
+ YUZU_NON_COPYABLE(EmulatedConsole);
+ YUZU_NON_MOVEABLE(EmulatedConsole);
+
+ /// Removes all callbacks created from input devices
+ void UnloadInput();
+
+ /**
+ * Sets the emulated console into configuring mode
+ * This prevents the modification of the HID state of the emulated console by input commands
+ */
+ void EnableConfiguration();
+
+ /// Returns the emulated console into normal mode, allowing the modification of the HID state
+ void DisableConfiguration();
+
+ /// Returns true if the emulated console is in configuring mode
+ bool IsConfiguring() const;
+
+ /// Reload all input devices
+ void ReloadInput();
+
+ /// Overrides current mapped devices with the stored configuration and reloads all input devices
+ void ReloadFromSettings();
+
+ /// Saves the current mapped configuration
+ void SaveCurrentConfig();
+
+ /// Reverts any mapped changes made that weren't saved
+ void RestoreConfig();
+
+ // Returns the current mapped motion device
+ Common::ParamPackage GetMotionParam() const;
+
+ /**
+ * Updates the current mapped motion device
+ * @param param ParamPackage with controller data to be mapped
+ */
+ void SetMotionParam(Common::ParamPackage param);
+
+ /// Returns the latest status of motion input from the console with parameters
+ ConsoleMotionValues GetMotionValues() const;
+
+ /// Returns the latest status of touch input from the console with parameters
+ TouchValues GetTouchValues() const;
+
+ /// Returns the latest status of motion input from the console
+ ConsoleMotion GetMotion() const;
+
+ /// Returns the latest status of touch input from the console
+ TouchFingerState GetTouch() const;
+
+ /**
+ * Adds a callback to the list of events
+ * @param update_callback A ConsoleUpdateCallback that will be triggered
+ * @return an unique key corresponding to the callback index in the list
+ */
+ int SetCallback(ConsoleUpdateCallback update_callback);
+
+ /**
+ * Removes a callback from the list stopping any future events to this object
+ * @param key Key corresponding to the callback index in the list
+ */
+ void DeleteCallback(int key);
+
+private:
+ /// Creates and stores the touch params
+ void SetTouchParams();
+
+ /**
+ * Updates the motion status of the console
+ * @param callback A CallbackStatus containing gyro and accelerometer data
+ */
+ void SetMotion(const Common::Input::CallbackStatus& callback);
+
+ /**
+ * Updates the touch status of the console
+ * @param callback A CallbackStatus containing the touch position
+ * @param index Finger ID to be updated
+ */
+ void SetTouch(const Common::Input::CallbackStatus& callback, std::size_t index);
+
+ std::optional<std::size_t> GetIndexFromFingerId(std::size_t finger_id) const;
+
+ std::optional<std::size_t> GetNextFreeIndex() const;
+
+ /**
+ * Triggers a callback that something has changed on the console status
+ * @param type Input type of the event to trigger
+ */
+ void TriggerOnChange(ConsoleTriggerType type);
+
+ bool is_configuring{false};
+ f32 motion_sensitivity{0.01f};
+
+ ConsoleMotionParams motion_params;
+ TouchParams touch_params;
+
+ ConsoleMotionDevices motion_devices;
+ TouchDevices touch_devices;
+
+ mutable std::mutex mutex;
+ mutable std::mutex callback_mutex;
+ std::unordered_map<int, ConsoleUpdateCallback> callback_list;
+ int last_callback_key = 0;
+
+ // Stores the current status of all console input
+ ConsoleStatus console;
+};
+
+} // namespace Core::HID
diff --git a/src/hid_core/frontend/emulated_controller.cpp b/src/hid_core/frontend/emulated_controller.cpp
new file mode 100644
index 000000000..3d2d1e9f9
--- /dev/null
+++ b/src/hid_core/frontend/emulated_controller.cpp
@@ -0,0 +1,1972 @@
+// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <algorithm>
+#include <common/scope_exit.h>
+
+#include "common/polyfill_ranges.h"
+#include "common/thread.h"
+#include "hid_core/frontend/emulated_controller.h"
+#include "hid_core/frontend/input_converter.h"
+#include "hid_core/hid_util.h"
+
+namespace Core::HID {
+constexpr s32 HID_JOYSTICK_MAX = 0x7fff;
+constexpr s32 HID_TRIGGER_MAX = 0x7fff;
+constexpr u32 TURBO_BUTTON_DELAY = 4;
+// Use a common UUID for TAS and Virtual Gamepad
+constexpr Common::UUID TAS_UUID =
+ Common::UUID{{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7, 0xA5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}};
+constexpr Common::UUID VIRTUAL_UUID =
+ Common::UUID{{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7, 0xFF, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}};
+
+EmulatedController::EmulatedController(NpadIdType npad_id_type_) : npad_id_type(npad_id_type_) {}
+
+EmulatedController::~EmulatedController() = default;
+
+NpadStyleIndex EmulatedController::MapSettingsTypeToNPad(Settings::ControllerType type) {
+ switch (type) {
+ case Settings::ControllerType::ProController:
+ return NpadStyleIndex::ProController;
+ case Settings::ControllerType::DualJoyconDetached:
+ return NpadStyleIndex::JoyconDual;
+ case Settings::ControllerType::LeftJoycon:
+ return NpadStyleIndex::JoyconLeft;
+ case Settings::ControllerType::RightJoycon:
+ return NpadStyleIndex::JoyconRight;
+ case Settings::ControllerType::Handheld:
+ return NpadStyleIndex::Handheld;
+ case Settings::ControllerType::GameCube:
+ return NpadStyleIndex::GameCube;
+ case Settings::ControllerType::Pokeball:
+ return NpadStyleIndex::Pokeball;
+ case Settings::ControllerType::NES:
+ return NpadStyleIndex::NES;
+ case Settings::ControllerType::SNES:
+ return NpadStyleIndex::SNES;
+ case Settings::ControllerType::N64:
+ return NpadStyleIndex::N64;
+ case Settings::ControllerType::SegaGenesis:
+ return NpadStyleIndex::SegaGenesis;
+ default:
+ return NpadStyleIndex::ProController;
+ }
+}
+
+Settings::ControllerType EmulatedController::MapNPadToSettingsType(NpadStyleIndex type) {
+ switch (type) {
+ case NpadStyleIndex::ProController:
+ return Settings::ControllerType::ProController;
+ case NpadStyleIndex::JoyconDual:
+ return Settings::ControllerType::DualJoyconDetached;
+ case NpadStyleIndex::JoyconLeft:
+ return Settings::ControllerType::LeftJoycon;
+ case NpadStyleIndex::JoyconRight:
+ return Settings::ControllerType::RightJoycon;
+ case NpadStyleIndex::Handheld:
+ return Settings::ControllerType::Handheld;
+ case NpadStyleIndex::GameCube:
+ return Settings::ControllerType::GameCube;
+ case NpadStyleIndex::Pokeball:
+ return Settings::ControllerType::Pokeball;
+ case NpadStyleIndex::NES:
+ return Settings::ControllerType::NES;
+ case NpadStyleIndex::SNES:
+ return Settings::ControllerType::SNES;
+ case NpadStyleIndex::N64:
+ return Settings::ControllerType::N64;
+ case NpadStyleIndex::SegaGenesis:
+ return Settings::ControllerType::SegaGenesis;
+ default:
+ return Settings::ControllerType::ProController;
+ }
+}
+
+void EmulatedController::ReloadFromSettings() {
+ const auto player_index = Service::HID::NpadIdTypeToIndex(npad_id_type);
+ const auto& player = Settings::values.players.GetValue()[player_index];
+
+ for (std::size_t index = 0; index < player.buttons.size(); ++index) {
+ button_params[index] = Common::ParamPackage(player.buttons[index]);
+ }
+ for (std::size_t index = 0; index < player.analogs.size(); ++index) {
+ stick_params[index] = Common::ParamPackage(player.analogs[index]);
+ }
+ for (std::size_t index = 0; index < player.motions.size(); ++index) {
+ motion_params[index] = Common::ParamPackage(player.motions[index]);
+ }
+
+ controller.color_values = {};
+ ReloadColorsFromSettings();
+
+ ring_params[0] = Common::ParamPackage(Settings::values.ringcon_analogs);
+
+ // Other or debug controller should always be a pro controller
+ if (npad_id_type != NpadIdType::Other) {
+ SetNpadStyleIndex(MapSettingsTypeToNPad(player.controller_type));
+ original_npad_type = npad_type;
+ } else {
+ SetNpadStyleIndex(NpadStyleIndex::ProController);
+ original_npad_type = npad_type;
+ }
+
+ Disconnect();
+ if (player.connected) {
+ Connect();
+ }
+
+ ReloadInput();
+}
+
+void EmulatedController::ReloadColorsFromSettings() {
+ const auto player_index = Service::HID::NpadIdTypeToIndex(npad_id_type);
+ const auto& player = Settings::values.players.GetValue()[player_index];
+
+ // Avoid updating colors if overridden by physical controller
+ if (controller.color_values[LeftIndex].body != 0 &&
+ controller.color_values[RightIndex].body != 0) {
+ return;
+ }
+
+ controller.colors_state.fullkey = {
+ .body = GetNpadColor(player.body_color_left),
+ .button = GetNpadColor(player.button_color_left),
+ };
+ controller.colors_state.left = {
+ .body = GetNpadColor(player.body_color_left),
+ .button = GetNpadColor(player.button_color_left),
+ };
+ controller.colors_state.right = {
+ .body = GetNpadColor(player.body_color_right),
+ .button = GetNpadColor(player.button_color_right),
+ };
+}
+
+void EmulatedController::LoadDevices() {
+ // TODO(german77): Use more buttons to detect the correct device
+ const auto left_joycon = button_params[Settings::NativeButton::DRight];
+ const auto right_joycon = button_params[Settings::NativeButton::A];
+
+ // Triggers for GC controllers
+ trigger_params[LeftIndex] = button_params[Settings::NativeButton::ZL];
+ trigger_params[RightIndex] = button_params[Settings::NativeButton::ZR];
+
+ color_params[LeftIndex] = left_joycon;
+ color_params[RightIndex] = right_joycon;
+ color_params[LeftIndex].Set("color", true);
+ color_params[RightIndex].Set("color", true);
+
+ battery_params[LeftIndex] = left_joycon;
+ battery_params[RightIndex] = right_joycon;
+ battery_params[LeftIndex].Set("battery", true);
+ battery_params[RightIndex].Set("battery", true);
+
+ camera_params[0] = right_joycon;
+ camera_params[0].Set("camera", true);
+ nfc_params[1] = right_joycon;
+ nfc_params[1].Set("nfc", true);
+
+ // Only map virtual devices to the first controller
+ if (npad_id_type == NpadIdType::Player1 || npad_id_type == NpadIdType::Handheld) {
+ camera_params[1] = Common::ParamPackage{"engine:camera,camera:1"};
+ ring_params[1] = Common::ParamPackage{"engine:joycon,axis_x:100,axis_y:101"};
+ nfc_params[0] = Common::ParamPackage{"engine:virtual_amiibo,nfc:1"};
+ }
+
+ output_params[LeftIndex] = left_joycon;
+ output_params[RightIndex] = right_joycon;
+ output_params[2] = camera_params[1];
+ output_params[3] = nfc_params[0];
+ output_params[LeftIndex].Set("output", true);
+ output_params[RightIndex].Set("output", true);
+ output_params[2].Set("output", true);
+ 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);
+ std::ranges::transform(motion_params, motion_devices.begin(), Common::Input::CreateInputDevice);
+ std::ranges::transform(trigger_params, trigger_devices.begin(),
+ Common::Input::CreateInputDevice);
+ std::ranges::transform(battery_params, battery_devices.begin(),
+ Common::Input::CreateInputDevice);
+ std::ranges::transform(color_params, color_devices.begin(), Common::Input::CreateInputDevice);
+ std::ranges::transform(camera_params, camera_devices.begin(), Common::Input::CreateInputDevice);
+ std::ranges::transform(ring_params, ring_analog_devices.begin(),
+ Common::Input::CreateInputDevice);
+ std::ranges::transform(nfc_params, nfc_devices.begin(), Common::Input::CreateInputDevice);
+ std::ranges::transform(output_params, output_devices.begin(),
+ Common::Input::CreateOutputDevice);
+
+ // Initialize TAS devices
+ std::ranges::transform(tas_button_params, tas_button_devices.begin(),
+ 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);
+ std::ranges::transform(virtual_motion_params, virtual_motion_devices.begin(),
+ Common::Input::CreateInputDevice);
+}
+
+void EmulatedController::LoadTASParams() {
+ const auto player_index = Service::HID::NpadIdTypeToIndex(npad_id_type);
+ Common::ParamPackage common_params{};
+ common_params.Set("engine", "tas");
+ common_params.Set("port", static_cast<int>(player_index));
+ for (auto& param : tas_button_params) {
+ param = common_params;
+ }
+ for (auto& param : tas_stick_params) {
+ param = common_params;
+ }
+
+ // TODO(german77): Replace this with an input profile or something better
+ tas_button_params[Settings::NativeButton::A].Set("button", 0);
+ tas_button_params[Settings::NativeButton::B].Set("button", 1);
+ tas_button_params[Settings::NativeButton::X].Set("button", 2);
+ tas_button_params[Settings::NativeButton::Y].Set("button", 3);
+ tas_button_params[Settings::NativeButton::LStick].Set("button", 4);
+ tas_button_params[Settings::NativeButton::RStick].Set("button", 5);
+ tas_button_params[Settings::NativeButton::L].Set("button", 6);
+ tas_button_params[Settings::NativeButton::R].Set("button", 7);
+ tas_button_params[Settings::NativeButton::ZL].Set("button", 8);
+ tas_button_params[Settings::NativeButton::ZR].Set("button", 9);
+ tas_button_params[Settings::NativeButton::Plus].Set("button", 10);
+ tas_button_params[Settings::NativeButton::Minus].Set("button", 11);
+ tas_button_params[Settings::NativeButton::DLeft].Set("button", 12);
+ tas_button_params[Settings::NativeButton::DUp].Set("button", 13);
+ tas_button_params[Settings::NativeButton::DRight].Set("button", 14);
+ tas_button_params[Settings::NativeButton::DDown].Set("button", 15);
+ tas_button_params[Settings::NativeButton::SLLeft].Set("button", 16);
+ tas_button_params[Settings::NativeButton::SRLeft].Set("button", 17);
+ tas_button_params[Settings::NativeButton::Home].Set("button", 18);
+ tas_button_params[Settings::NativeButton::Screenshot].Set("button", 19);
+ tas_button_params[Settings::NativeButton::SLRight].Set("button", 20);
+ tas_button_params[Settings::NativeButton::SRRight].Set("button", 21);
+
+ tas_stick_params[Settings::NativeAnalog::LStick].Set("axis_x", 0);
+ tas_stick_params[Settings::NativeAnalog::LStick].Set("axis_y", 1);
+ tas_stick_params[Settings::NativeAnalog::RStick].Set("axis_x", 2);
+ tas_stick_params[Settings::NativeAnalog::RStick].Set("axis_y", 3);
+
+ // set to optimal stick to avoid sanitizing the stick and tweaking the coordinates
+ // making sure they play back in the game as originally written down in the script file
+ tas_stick_params[Settings::NativeAnalog::LStick].Set("deadzone", 0.0f);
+ tas_stick_params[Settings::NativeAnalog::LStick].Set("range", 1.0f);
+ tas_stick_params[Settings::NativeAnalog::RStick].Set("deadzone", 0.0f);
+ tas_stick_params[Settings::NativeAnalog::RStick].Set("range", 1.0f);
+}
+
+void EmulatedController::LoadVirtualGamepadParams() {
+ const auto player_index = Service::HID::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;
+ }
+ for (auto& param : virtual_stick_params) {
+ param = common_params;
+ }
+ for (auto& param : virtual_motion_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::SLLeft].Set("button", 16);
+ virtual_button_params[Settings::NativeButton::SRLeft].Set("button", 17);
+ virtual_button_params[Settings::NativeButton::Home].Set("button", 18);
+ virtual_button_params[Settings::NativeButton::Screenshot].Set("button", 19);
+ virtual_button_params[Settings::NativeButton::SLRight].Set("button", 20);
+ virtual_button_params[Settings::NativeButton::SRRight].Set("button", 21);
+
+ 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);
+ virtual_stick_params[Settings::NativeAnalog::LStick].Set("deadzone", 0.0f);
+ virtual_stick_params[Settings::NativeAnalog::LStick].Set("range", 1.0f);
+ virtual_stick_params[Settings::NativeAnalog::RStick].Set("deadzone", 0.0f);
+ virtual_stick_params[Settings::NativeAnalog::RStick].Set("range", 1.0f);
+
+ virtual_motion_params[Settings::NativeMotion::MotionLeft].Set("motion", 0);
+ virtual_motion_params[Settings::NativeMotion::MotionRight].Set("motion", 0);
+}
+
+void EmulatedController::ReloadInput() {
+ // If you load any device here add the equivalent to the UnloadInput() function
+ LoadDevices();
+ for (std::size_t index = 0; index < button_devices.size(); ++index) {
+ if (!button_devices[index]) {
+ continue;
+ }
+ const auto uuid = Common::UUID{button_params[index].Get("guid", "")};
+ button_devices[index]->SetCallback({
+ .on_change =
+ [this, index, uuid](const Common::Input::CallbackStatus& callback) {
+ SetButton(callback, index, uuid);
+ },
+ });
+ button_devices[index]->ForceUpdate();
+ }
+
+ for (std::size_t index = 0; index < stick_devices.size(); ++index) {
+ if (!stick_devices[index]) {
+ continue;
+ }
+ const auto uuid = Common::UUID{stick_params[index].Get("guid", "")};
+ stick_devices[index]->SetCallback({
+ .on_change =
+ [this, index, uuid](const Common::Input::CallbackStatus& callback) {
+ SetStick(callback, index, uuid);
+ },
+ });
+ stick_devices[index]->ForceUpdate();
+ }
+
+ for (std::size_t index = 0; index < trigger_devices.size(); ++index) {
+ if (!trigger_devices[index]) {
+ continue;
+ }
+ const auto uuid = Common::UUID{trigger_params[index].Get("guid", "")};
+ trigger_devices[index]->SetCallback({
+ .on_change =
+ [this, index, uuid](const Common::Input::CallbackStatus& callback) {
+ SetTrigger(callback, index, uuid);
+ },
+ });
+ trigger_devices[index]->ForceUpdate();
+ }
+
+ for (std::size_t index = 0; index < battery_devices.size(); ++index) {
+ if (!battery_devices[index]) {
+ continue;
+ }
+ battery_devices[index]->SetCallback({
+ .on_change =
+ [this, index](const Common::Input::CallbackStatus& callback) {
+ SetBattery(callback, index);
+ },
+ });
+ battery_devices[index]->ForceUpdate();
+ }
+
+ for (std::size_t index = 0; index < color_devices.size(); ++index) {
+ if (!color_devices[index]) {
+ continue;
+ }
+ color_devices[index]->SetCallback({
+ .on_change =
+ [this, index](const Common::Input::CallbackStatus& callback) {
+ SetColors(callback, index);
+ },
+ });
+ color_devices[index]->ForceUpdate();
+ }
+
+ for (std::size_t index = 0; index < motion_devices.size(); ++index) {
+ if (!motion_devices[index]) {
+ continue;
+ }
+ motion_devices[index]->SetCallback({
+ .on_change =
+ [this, index](const Common::Input::CallbackStatus& callback) {
+ SetMotion(callback, index);
+ },
+ });
+
+ // Restore motion state
+ auto& emulated_motion = controller.motion_values[index].emulated;
+ auto& motion = controller.motion_state[index];
+ emulated_motion.ResetRotations();
+ emulated_motion.ResetQuaternion();
+ motion.accel = emulated_motion.GetAcceleration();
+ motion.gyro = emulated_motion.GetGyroscope();
+ motion.rotation = emulated_motion.GetRotations();
+ motion.euler = emulated_motion.GetEulerAngles();
+ motion.orientation = emulated_motion.GetOrientation();
+ motion.is_at_rest = !emulated_motion.IsMoving(motion_sensitivity);
+ }
+
+ for (std::size_t index = 0; index < camera_devices.size(); ++index) {
+ if (!camera_devices[index]) {
+ continue;
+ }
+ camera_devices[index]->SetCallback({
+ .on_change =
+ [this](const Common::Input::CallbackStatus& callback) { SetCamera(callback); },
+ });
+ camera_devices[index]->ForceUpdate();
+ }
+
+ for (std::size_t index = 0; index < ring_analog_devices.size(); ++index) {
+ if (!ring_analog_devices[index]) {
+ continue;
+ }
+ ring_analog_devices[index]->SetCallback({
+ .on_change =
+ [this](const Common::Input::CallbackStatus& callback) { SetRingAnalog(callback); },
+ });
+ ring_analog_devices[index]->ForceUpdate();
+ }
+
+ for (std::size_t index = 0; index < nfc_devices.size(); ++index) {
+ if (!nfc_devices[index]) {
+ continue;
+ }
+ nfc_devices[index]->SetCallback({
+ .on_change =
+ [this](const Common::Input::CallbackStatus& callback) { SetNfc(callback); },
+ });
+ nfc_devices[index]->ForceUpdate();
+ }
+
+ // Register TAS devices. No need to force update
+ for (std::size_t index = 0; index < tas_button_devices.size(); ++index) {
+ if (!tas_button_devices[index]) {
+ continue;
+ }
+ tas_button_devices[index]->SetCallback({
+ .on_change =
+ [this, index](const Common::Input::CallbackStatus& callback) {
+ SetButton(callback, index, TAS_UUID);
+ },
+ });
+ }
+
+ for (std::size_t index = 0; index < tas_stick_devices.size(); ++index) {
+ if (!tas_stick_devices[index]) {
+ continue;
+ }
+ tas_stick_devices[index]->SetCallback({
+ .on_change =
+ [this, index](const Common::Input::CallbackStatus& callback) {
+ SetStick(callback, index, TAS_UUID);
+ },
+ });
+ }
+
+ // 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);
+ },
+ });
+ }
+
+ for (std::size_t index = 0; index < virtual_motion_devices.size(); ++index) {
+ if (!virtual_motion_devices[index]) {
+ continue;
+ }
+ virtual_motion_devices[index]->SetCallback({
+ .on_change =
+ [this, index](const Common::Input::CallbackStatus& callback) {
+ SetMotion(callback, index);
+ },
+ });
+ }
+ turbo_button_state = 0;
+ is_initalized = true;
+}
+
+void EmulatedController::UnloadInput() {
+ is_initalized = false;
+ for (auto& button : button_devices) {
+ button.reset();
+ }
+ for (auto& stick : stick_devices) {
+ stick.reset();
+ }
+ for (auto& motion : motion_devices) {
+ motion.reset();
+ }
+ for (auto& trigger : trigger_devices) {
+ trigger.reset();
+ }
+ for (auto& battery : battery_devices) {
+ battery.reset();
+ }
+ for (auto& color : color_devices) {
+ color.reset();
+ }
+ for (auto& output : output_devices) {
+ output.reset();
+ }
+ for (auto& button : tas_button_devices) {
+ button.reset();
+ }
+ for (auto& stick : tas_stick_devices) {
+ stick.reset();
+ }
+ for (auto& button : virtual_button_devices) {
+ button.reset();
+ }
+ for (auto& stick : virtual_stick_devices) {
+ stick.reset();
+ }
+ for (auto& motion : virtual_motion_devices) {
+ motion.reset();
+ }
+ for (auto& camera : camera_devices) {
+ camera.reset();
+ }
+ for (auto& ring : ring_analog_devices) {
+ ring.reset();
+ }
+ for (auto& nfc : nfc_devices) {
+ nfc.reset();
+ }
+}
+
+void EmulatedController::EnableConfiguration() {
+ std::scoped_lock lock{connect_mutex, npad_mutex};
+ is_configuring = true;
+ tmp_is_connected = is_connected;
+ tmp_npad_type = npad_type;
+}
+
+void EmulatedController::DisableConfiguration() {
+ is_configuring = false;
+
+ // Get Joycon colors before turning on the controller
+ for (const auto& color_device : color_devices) {
+ color_device->ForceUpdate();
+ }
+
+ // Apply temporary npad type to the real controller
+ if (tmp_npad_type != npad_type) {
+ if (is_connected) {
+ Disconnect();
+ }
+ SetNpadStyleIndex(tmp_npad_type);
+ original_npad_type = tmp_npad_type;
+ }
+
+ // Apply temporary connected status to the real controller
+ if (tmp_is_connected != is_connected) {
+ if (tmp_is_connected) {
+ Connect();
+ return;
+ }
+ Disconnect();
+ }
+}
+
+void EmulatedController::EnableSystemButtons() {
+ std::scoped_lock lock{mutex};
+ system_buttons_enabled = true;
+}
+
+void EmulatedController::DisableSystemButtons() {
+ std::scoped_lock lock{mutex};
+ system_buttons_enabled = false;
+ controller.home_button_state.raw = 0;
+ controller.capture_button_state.raw = 0;
+}
+
+void EmulatedController::ResetSystemButtons() {
+ std::scoped_lock lock{mutex};
+ controller.home_button_state.home.Assign(false);
+ controller.capture_button_state.capture.Assign(false);
+}
+
+bool EmulatedController::IsConfiguring() const {
+ return is_configuring;
+}
+
+void EmulatedController::SaveCurrentConfig() {
+ const auto player_index = Service::HID::NpadIdTypeToIndex(npad_id_type);
+ auto& player = Settings::values.players.GetValue()[player_index];
+ player.connected = is_connected;
+ player.controller_type = MapNPadToSettingsType(npad_type);
+ for (std::size_t index = 0; index < player.buttons.size(); ++index) {
+ player.buttons[index] = button_params[index].Serialize();
+ }
+ for (std::size_t index = 0; index < player.analogs.size(); ++index) {
+ player.analogs[index] = stick_params[index].Serialize();
+ }
+ for (std::size_t index = 0; index < player.motions.size(); ++index) {
+ player.motions[index] = motion_params[index].Serialize();
+ }
+ if (npad_id_type == NpadIdType::Player1) {
+ Settings::values.ringcon_analogs = ring_params[0].Serialize();
+ }
+}
+
+void EmulatedController::RestoreConfig() {
+ if (!is_configuring) {
+ return;
+ }
+ ReloadFromSettings();
+}
+
+std::vector<Common::ParamPackage> EmulatedController::GetMappedDevices() const {
+ std::vector<Common::ParamPackage> devices;
+ for (const auto& param : button_params) {
+ if (!param.Has("engine")) {
+ continue;
+ }
+ const auto devices_it = std::find_if(
+ devices.begin(), devices.end(), [&param](const Common::ParamPackage& param_) {
+ return param.Get("engine", "") == param_.Get("engine", "") &&
+ param.Get("guid", "") == param_.Get("guid", "") &&
+ param.Get("port", 0) == param_.Get("port", 0) &&
+ param.Get("pad", 0) == param_.Get("pad", 0);
+ });
+ if (devices_it != devices.end()) {
+ continue;
+ }
+
+ auto& device = devices.emplace_back();
+ device.Set("engine", param.Get("engine", ""));
+ device.Set("guid", param.Get("guid", ""));
+ device.Set("port", param.Get("port", 0));
+ device.Set("pad", param.Get("pad", 0));
+ }
+
+ for (const auto& param : stick_params) {
+ if (!param.Has("engine")) {
+ continue;
+ }
+ if (param.Get("engine", "") == "analog_from_button") {
+ continue;
+ }
+ const auto devices_it = std::find_if(
+ devices.begin(), devices.end(), [&param](const Common::ParamPackage& param_) {
+ return param.Get("engine", "") == param_.Get("engine", "") &&
+ param.Get("guid", "") == param_.Get("guid", "") &&
+ param.Get("port", 0) == param_.Get("port", 0) &&
+ param.Get("pad", 0) == param_.Get("pad", 0);
+ });
+ if (devices_it != devices.end()) {
+ continue;
+ }
+
+ auto& device = devices.emplace_back();
+ device.Set("engine", param.Get("engine", ""));
+ device.Set("guid", param.Get("guid", ""));
+ device.Set("port", param.Get("port", 0));
+ device.Set("pad", param.Get("pad", 0));
+ }
+ return devices;
+}
+
+Common::ParamPackage EmulatedController::GetButtonParam(std::size_t index) const {
+ if (index >= button_params.size()) {
+ return {};
+ }
+ return button_params[index];
+}
+
+Common::ParamPackage EmulatedController::GetStickParam(std::size_t index) const {
+ if (index >= stick_params.size()) {
+ return {};
+ }
+ return stick_params[index];
+}
+
+Common::ParamPackage EmulatedController::GetMotionParam(std::size_t index) const {
+ if (index >= motion_params.size()) {
+ return {};
+ }
+ return motion_params[index];
+}
+
+void EmulatedController::SetButtonParam(std::size_t index, Common::ParamPackage param) {
+ if (index >= button_params.size()) {
+ return;
+ }
+ button_params[index] = std::move(param);
+ ReloadInput();
+}
+
+void EmulatedController::SetStickParam(std::size_t index, Common::ParamPackage param) {
+ if (index >= stick_params.size()) {
+ return;
+ }
+ stick_params[index] = std::move(param);
+ ReloadInput();
+}
+
+void EmulatedController::SetMotionParam(std::size_t index, Common::ParamPackage param) {
+ if (index >= motion_params.size()) {
+ return;
+ }
+ motion_params[index] = std::move(param);
+ ReloadInput();
+}
+
+void EmulatedController::StartMotionCalibration() {
+ for (ControllerMotionInfo& motion : controller.motion_values) {
+ motion.emulated.Calibrate();
+ }
+}
+
+void EmulatedController::SetButton(const Common::Input::CallbackStatus& callback, std::size_t index,
+ Common::UUID uuid) {
+ if (index >= controller.button_values.size()) {
+ return;
+ }
+ std::unique_lock lock{mutex};
+ bool value_changed = false;
+ const auto new_status = TransformToButton(callback);
+ auto& current_status = controller.button_values[index];
+
+ // Only read button values that have the same uuid or are pressed once
+ if (current_status.uuid != uuid) {
+ if (!new_status.value) {
+ return;
+ }
+ }
+
+ current_status.toggle = new_status.toggle;
+ current_status.turbo = new_status.turbo;
+ current_status.uuid = uuid;
+
+ // Update button status with current
+ if (!current_status.toggle) {
+ current_status.locked = false;
+ if (current_status.value != new_status.value) {
+ current_status.value = new_status.value;
+ value_changed = true;
+ }
+ } else {
+ // Toggle button and lock status
+ if (new_status.value && !current_status.locked) {
+ current_status.locked = true;
+ current_status.value = !current_status.value;
+ value_changed = true;
+ }
+
+ // Unlock button ready for next press
+ if (!new_status.value && current_status.locked) {
+ current_status.locked = false;
+ }
+ }
+
+ if (!value_changed) {
+ return;
+ }
+
+ if (is_configuring) {
+ controller.npad_button_state.raw = NpadButton::None;
+ controller.debug_pad_button_state.raw = 0;
+ controller.home_button_state.raw = 0;
+ controller.capture_button_state.raw = 0;
+ lock.unlock();
+ TriggerOnChange(ControllerTriggerType::Button, false);
+ return;
+ }
+
+ // GC controllers have triggers not buttons
+ if (npad_type == NpadStyleIndex::GameCube) {
+ if (index == Settings::NativeButton::ZR) {
+ return;
+ }
+ if (index == Settings::NativeButton::ZL) {
+ return;
+ }
+ }
+
+ switch (index) {
+ case Settings::NativeButton::A:
+ controller.npad_button_state.a.Assign(current_status.value);
+ controller.debug_pad_button_state.a.Assign(current_status.value);
+ break;
+ case Settings::NativeButton::B:
+ controller.npad_button_state.b.Assign(current_status.value);
+ controller.debug_pad_button_state.b.Assign(current_status.value);
+ break;
+ case Settings::NativeButton::X:
+ controller.npad_button_state.x.Assign(current_status.value);
+ controller.debug_pad_button_state.x.Assign(current_status.value);
+ break;
+ case Settings::NativeButton::Y:
+ controller.npad_button_state.y.Assign(current_status.value);
+ controller.debug_pad_button_state.y.Assign(current_status.value);
+ break;
+ case Settings::NativeButton::LStick:
+ controller.npad_button_state.stick_l.Assign(current_status.value);
+ break;
+ case Settings::NativeButton::RStick:
+ controller.npad_button_state.stick_r.Assign(current_status.value);
+ break;
+ case Settings::NativeButton::L:
+ controller.npad_button_state.l.Assign(current_status.value);
+ controller.debug_pad_button_state.l.Assign(current_status.value);
+ break;
+ case Settings::NativeButton::R:
+ controller.npad_button_state.r.Assign(current_status.value);
+ controller.debug_pad_button_state.r.Assign(current_status.value);
+ break;
+ case Settings::NativeButton::ZL:
+ controller.npad_button_state.zl.Assign(current_status.value);
+ controller.debug_pad_button_state.zl.Assign(current_status.value);
+ break;
+ case Settings::NativeButton::ZR:
+ controller.npad_button_state.zr.Assign(current_status.value);
+ controller.debug_pad_button_state.zr.Assign(current_status.value);
+ break;
+ case Settings::NativeButton::Plus:
+ controller.npad_button_state.plus.Assign(current_status.value);
+ controller.debug_pad_button_state.plus.Assign(current_status.value);
+ break;
+ case Settings::NativeButton::Minus:
+ controller.npad_button_state.minus.Assign(current_status.value);
+ controller.debug_pad_button_state.minus.Assign(current_status.value);
+ break;
+ case Settings::NativeButton::DLeft:
+ controller.npad_button_state.left.Assign(current_status.value);
+ controller.debug_pad_button_state.d_left.Assign(current_status.value);
+ break;
+ case Settings::NativeButton::DUp:
+ controller.npad_button_state.up.Assign(current_status.value);
+ controller.debug_pad_button_state.d_up.Assign(current_status.value);
+ break;
+ case Settings::NativeButton::DRight:
+ controller.npad_button_state.right.Assign(current_status.value);
+ controller.debug_pad_button_state.d_right.Assign(current_status.value);
+ break;
+ case Settings::NativeButton::DDown:
+ controller.npad_button_state.down.Assign(current_status.value);
+ controller.debug_pad_button_state.d_down.Assign(current_status.value);
+ break;
+ case Settings::NativeButton::SLLeft:
+ controller.npad_button_state.left_sl.Assign(current_status.value);
+ break;
+ case Settings::NativeButton::SLRight:
+ controller.npad_button_state.right_sl.Assign(current_status.value);
+ break;
+ case Settings::NativeButton::SRLeft:
+ controller.npad_button_state.left_sr.Assign(current_status.value);
+ break;
+ case Settings::NativeButton::SRRight:
+ controller.npad_button_state.right_sr.Assign(current_status.value);
+ break;
+ case Settings::NativeButton::Home:
+ if (!system_buttons_enabled) {
+ break;
+ }
+ controller.home_button_state.home.Assign(current_status.value);
+ break;
+ case Settings::NativeButton::Screenshot:
+ if (!system_buttons_enabled) {
+ break;
+ }
+ controller.capture_button_state.capture.Assign(current_status.value);
+ break;
+ }
+
+ lock.unlock();
+
+ if (!is_connected) {
+ if (npad_id_type == NpadIdType::Player1 && npad_type != NpadStyleIndex::Handheld) {
+ Connect();
+ }
+ if (npad_id_type == NpadIdType::Handheld && npad_type == NpadStyleIndex::Handheld) {
+ Connect();
+ }
+ }
+ TriggerOnChange(ControllerTriggerType::Button, true);
+}
+
+void EmulatedController::SetStick(const Common::Input::CallbackStatus& callback, std::size_t index,
+ Common::UUID uuid) {
+ if (index >= controller.stick_values.size()) {
+ return;
+ }
+ auto trigger_guard =
+ SCOPE_GUARD({ TriggerOnChange(ControllerTriggerType::Stick, !is_configuring); });
+ std::scoped_lock lock{mutex};
+ const auto stick_value = TransformToStick(callback);
+
+ // Only read stick values that have the same uuid or are over the threshold to avoid flapping
+ if (controller.stick_values[index].uuid != uuid) {
+ const bool is_tas = uuid == TAS_UUID;
+ if (is_tas && stick_value.x.value == 0 && stick_value.y.value == 0) {
+ trigger_guard.Cancel();
+ return;
+ }
+ if (!is_tas && !stick_value.down && !stick_value.up && !stick_value.left &&
+ !stick_value.right) {
+ trigger_guard.Cancel();
+ return;
+ }
+ }
+
+ controller.stick_values[index] = stick_value;
+ controller.stick_values[index].uuid = uuid;
+
+ if (is_configuring) {
+ controller.analog_stick_state.left = {};
+ controller.analog_stick_state.right = {};
+ return;
+ }
+
+ const AnalogStickState stick{
+ .x = static_cast<s32>(controller.stick_values[index].x.value * HID_JOYSTICK_MAX),
+ .y = static_cast<s32>(controller.stick_values[index].y.value * HID_JOYSTICK_MAX),
+ };
+
+ switch (index) {
+ case Settings::NativeAnalog::LStick:
+ controller.analog_stick_state.left = stick;
+ controller.npad_button_state.stick_l_left.Assign(controller.stick_values[index].left);
+ controller.npad_button_state.stick_l_up.Assign(controller.stick_values[index].up);
+ controller.npad_button_state.stick_l_right.Assign(controller.stick_values[index].right);
+ controller.npad_button_state.stick_l_down.Assign(controller.stick_values[index].down);
+ break;
+ case Settings::NativeAnalog::RStick:
+ controller.analog_stick_state.right = stick;
+ controller.npad_button_state.stick_r_left.Assign(controller.stick_values[index].left);
+ controller.npad_button_state.stick_r_up.Assign(controller.stick_values[index].up);
+ controller.npad_button_state.stick_r_right.Assign(controller.stick_values[index].right);
+ controller.npad_button_state.stick_r_down.Assign(controller.stick_values[index].down);
+ break;
+ }
+}
+
+void EmulatedController::SetTrigger(const Common::Input::CallbackStatus& callback,
+ std::size_t index, Common::UUID uuid) {
+ if (index >= controller.trigger_values.size()) {
+ return;
+ }
+ auto trigger_guard =
+ SCOPE_GUARD({ TriggerOnChange(ControllerTriggerType::Trigger, !is_configuring); });
+ std::scoped_lock lock{mutex};
+ const auto trigger_value = TransformToTrigger(callback);
+
+ // Only read trigger values that have the same uuid or are pressed once
+ if (controller.trigger_values[index].uuid != uuid) {
+ if (!trigger_value.pressed.value) {
+ return;
+ }
+ }
+
+ controller.trigger_values[index] = trigger_value;
+ controller.trigger_values[index].uuid = uuid;
+
+ if (is_configuring) {
+ controller.gc_trigger_state.left = 0;
+ controller.gc_trigger_state.right = 0;
+ return;
+ }
+
+ // Only GC controllers have analog triggers
+ if (npad_type != NpadStyleIndex::GameCube) {
+ trigger_guard.Cancel();
+ return;
+ }
+
+ const auto& trigger = controller.trigger_values[index];
+
+ switch (index) {
+ case Settings::NativeTrigger::LTrigger:
+ controller.gc_trigger_state.left = static_cast<s32>(trigger.analog.value * HID_TRIGGER_MAX);
+ controller.npad_button_state.zl.Assign(trigger.pressed.value);
+ break;
+ case Settings::NativeTrigger::RTrigger:
+ controller.gc_trigger_state.right =
+ static_cast<s32>(trigger.analog.value * HID_TRIGGER_MAX);
+ controller.npad_button_state.zr.Assign(trigger.pressed.value);
+ break;
+ }
+}
+
+void EmulatedController::SetMotion(const Common::Input::CallbackStatus& callback,
+ std::size_t index) {
+ if (index >= controller.motion_values.size()) {
+ return;
+ }
+ SCOPE_EXIT({ TriggerOnChange(ControllerTriggerType::Motion, !is_configuring); });
+ std::scoped_lock lock{mutex};
+ auto& raw_status = controller.motion_values[index].raw_status;
+ auto& emulated = controller.motion_values[index].emulated;
+
+ raw_status = TransformToMotion(callback);
+ emulated.SetAcceleration(Common::Vec3f{
+ raw_status.accel.x.value,
+ raw_status.accel.y.value,
+ raw_status.accel.z.value,
+ });
+ emulated.SetGyroscope(Common::Vec3f{
+ raw_status.gyro.x.value,
+ raw_status.gyro.y.value,
+ raw_status.gyro.z.value,
+ });
+ emulated.SetUserGyroThreshold(raw_status.gyro.x.properties.threshold);
+ emulated.UpdateRotation(raw_status.delta_timestamp);
+ emulated.UpdateOrientation(raw_status.delta_timestamp);
+
+ auto& motion = controller.motion_state[index];
+ motion.accel = emulated.GetAcceleration();
+ motion.gyro = emulated.GetGyroscope();
+ motion.rotation = emulated.GetRotations();
+ motion.euler = emulated.GetEulerAngles();
+ motion.orientation = emulated.GetOrientation();
+ motion.is_at_rest = !emulated.IsMoving(motion_sensitivity);
+}
+
+void EmulatedController::SetColors(const Common::Input::CallbackStatus& callback,
+ std::size_t index) {
+ if (index >= controller.color_values.size()) {
+ return;
+ }
+ auto trigger_guard =
+ SCOPE_GUARD({ TriggerOnChange(ControllerTriggerType::Color, !is_configuring); });
+ std::scoped_lock lock{mutex};
+ controller.color_values[index] = TransformToColor(callback);
+
+ if (is_configuring) {
+ return;
+ }
+
+ if (controller.color_values[index].body == 0) {
+ trigger_guard.Cancel();
+ return;
+ }
+
+ controller.colors_state.fullkey = {
+ .body = GetNpadColor(controller.color_values[index].body),
+ .button = GetNpadColor(controller.color_values[index].buttons),
+ };
+ if (npad_type == NpadStyleIndex::ProController) {
+ controller.colors_state.left = {
+ .body = GetNpadColor(controller.color_values[index].left_grip),
+ .button = GetNpadColor(controller.color_values[index].buttons),
+ };
+ controller.colors_state.right = {
+ .body = GetNpadColor(controller.color_values[index].right_grip),
+ .button = GetNpadColor(controller.color_values[index].buttons),
+ };
+ } else {
+ switch (index) {
+ case LeftIndex:
+ controller.colors_state.left = {
+ .body = GetNpadColor(controller.color_values[index].body),
+ .button = GetNpadColor(controller.color_values[index].buttons),
+ };
+ break;
+ case RightIndex:
+ controller.colors_state.right = {
+ .body = GetNpadColor(controller.color_values[index].body),
+ .button = GetNpadColor(controller.color_values[index].buttons),
+ };
+ break;
+ }
+ }
+}
+
+void EmulatedController::SetBattery(const Common::Input::CallbackStatus& callback,
+ std::size_t index) {
+ if (index >= controller.battery_values.size()) {
+ return;
+ }
+ SCOPE_EXIT({ TriggerOnChange(ControllerTriggerType::Battery, !is_configuring); });
+ std::scoped_lock lock{mutex};
+ controller.battery_values[index] = TransformToBattery(callback);
+
+ if (is_configuring) {
+ return;
+ }
+
+ bool is_charging = false;
+ bool is_powered = false;
+ NpadBatteryLevel battery_level = NpadBatteryLevel::Empty;
+ switch (controller.battery_values[index]) {
+ case Common::Input::BatteryLevel::Charging:
+ is_charging = true;
+ is_powered = true;
+ battery_level = NpadBatteryLevel::Full;
+ break;
+ case Common::Input::BatteryLevel::Medium:
+ battery_level = NpadBatteryLevel::High;
+ break;
+ case Common::Input::BatteryLevel::Low:
+ battery_level = NpadBatteryLevel::Low;
+ break;
+ case Common::Input::BatteryLevel::Critical:
+ battery_level = NpadBatteryLevel::Critical;
+ break;
+ case Common::Input::BatteryLevel::Empty:
+ battery_level = NpadBatteryLevel::Empty;
+ break;
+ case Common::Input::BatteryLevel::None:
+ case Common::Input::BatteryLevel::Full:
+ default:
+ is_powered = true;
+ battery_level = NpadBatteryLevel::Full;
+ break;
+ }
+
+ switch (index) {
+ case LeftIndex:
+ controller.battery_state.left = {
+ .is_powered = is_powered,
+ .is_charging = is_charging,
+ .battery_level = battery_level,
+ };
+ break;
+ case RightIndex:
+ controller.battery_state.right = {
+ .is_powered = is_powered,
+ .is_charging = is_charging,
+ .battery_level = battery_level,
+ };
+ break;
+ case DualIndex:
+ controller.battery_state.dual = {
+ .is_powered = is_powered,
+ .is_charging = is_charging,
+ .battery_level = battery_level,
+ };
+ break;
+ }
+}
+
+void EmulatedController::SetCamera(const Common::Input::CallbackStatus& callback) {
+ SCOPE_EXIT({ TriggerOnChange(ControllerTriggerType::IrSensor, !is_configuring); });
+ std::scoped_lock lock{mutex};
+ controller.camera_values = TransformToCamera(callback);
+
+ if (is_configuring) {
+ return;
+ }
+
+ controller.camera_state.sample++;
+ controller.camera_state.format =
+ static_cast<Core::IrSensor::ImageTransferProcessorFormat>(controller.camera_values.format);
+ controller.camera_state.data = controller.camera_values.data;
+}
+
+void EmulatedController::SetRingAnalog(const Common::Input::CallbackStatus& callback) {
+ SCOPE_EXIT({ TriggerOnChange(ControllerTriggerType::RingController, !is_configuring); });
+ std::scoped_lock lock{mutex};
+ const auto force_value = TransformToStick(callback);
+
+ controller.ring_analog_value = force_value.x;
+
+ if (is_configuring) {
+ return;
+ }
+
+ controller.ring_analog_state.force = force_value.x.value;
+}
+
+void EmulatedController::SetNfc(const Common::Input::CallbackStatus& callback) {
+ SCOPE_EXIT({ TriggerOnChange(ControllerTriggerType::Nfc, !is_configuring); });
+ std::scoped_lock lock{mutex};
+ controller.nfc_values = TransformToNfc(callback);
+
+ if (is_configuring) {
+ return;
+ }
+
+ controller.nfc_state = controller.nfc_values;
+}
+
+bool EmulatedController::SetVibration(std::size_t device_index, VibrationValue vibration) {
+ if (!is_initalized) {
+ return false;
+ }
+ if (device_index >= output_devices.size()) {
+ return false;
+ }
+ if (!output_devices[device_index]) {
+ return false;
+ }
+ const auto player_index = Service::HID::NpadIdTypeToIndex(npad_id_type);
+ const auto& player = Settings::values.players.GetValue()[player_index];
+ const f32 strength = static_cast<f32>(player.vibration_strength) / 100.0f;
+
+ if (!player.vibration_enabled) {
+ return false;
+ }
+
+ // Exponential amplification is too strong at low amplitudes. Switch to a linear
+ // amplification if strength is set below 0.7f
+ const Common::Input::VibrationAmplificationType type =
+ strength > 0.7f ? Common::Input::VibrationAmplificationType::Exponential
+ : Common::Input::VibrationAmplificationType::Linear;
+
+ const Common::Input::VibrationStatus status = {
+ .low_amplitude = std::min(vibration.low_amplitude * strength, 1.0f),
+ .low_frequency = vibration.low_frequency,
+ .high_amplitude = std::min(vibration.high_amplitude * strength, 1.0f),
+ .high_frequency = vibration.high_frequency,
+ .type = type,
+ };
+ return output_devices[device_index]->SetVibration(status) ==
+ Common::Input::DriverResult::Success;
+}
+
+bool EmulatedController::IsVibrationEnabled(std::size_t device_index) {
+ const auto player_index = Service::HID::NpadIdTypeToIndex(npad_id_type);
+ const auto& player = Settings::values.players.GetValue()[player_index];
+
+ if (!is_initalized) {
+ return false;
+ }
+
+ if (!player.vibration_enabled) {
+ return false;
+ }
+
+ if (device_index >= output_devices.size()) {
+ return false;
+ }
+
+ if (!output_devices[device_index]) {
+ return false;
+ }
+
+ return output_devices[device_index]->IsVibrationEnabled();
+}
+
+Common::Input::DriverResult EmulatedController::SetPollingMode(
+ EmulatedDeviceIndex device_index, Common::Input::PollingMode polling_mode) {
+ LOG_INFO(Service_HID, "Set polling mode {}, device_index={}", polling_mode, device_index);
+
+ if (!is_initalized) {
+ return Common::Input::DriverResult::InvalidHandle;
+ }
+
+ auto& left_output_device = output_devices[static_cast<std::size_t>(DeviceIndex::Left)];
+ auto& right_output_device = output_devices[static_cast<std::size_t>(DeviceIndex::Right)];
+ auto& nfc_output_device = output_devices[3];
+
+ if (device_index == EmulatedDeviceIndex::LeftIndex) {
+ controller.left_polling_mode = polling_mode;
+ return left_output_device->SetPollingMode(polling_mode);
+ }
+
+ if (device_index == EmulatedDeviceIndex::RightIndex) {
+ controller.right_polling_mode = polling_mode;
+ const auto virtual_nfc_result = nfc_output_device->SetPollingMode(polling_mode);
+ const auto mapped_nfc_result = right_output_device->SetPollingMode(polling_mode);
+
+ // Restore previous state
+ if (mapped_nfc_result != Common::Input::DriverResult::Success) {
+ right_output_device->SetPollingMode(Common::Input::PollingMode::Active);
+ }
+
+ if (virtual_nfc_result == Common::Input::DriverResult::Success) {
+ return virtual_nfc_result;
+ }
+ return mapped_nfc_result;
+ }
+
+ controller.left_polling_mode = polling_mode;
+ controller.right_polling_mode = polling_mode;
+ left_output_device->SetPollingMode(polling_mode);
+ right_output_device->SetPollingMode(polling_mode);
+ nfc_output_device->SetPollingMode(polling_mode);
+ return Common::Input::DriverResult::Success;
+}
+
+Common::Input::PollingMode EmulatedController::GetPollingMode(
+ EmulatedDeviceIndex device_index) const {
+ if (device_index == EmulatedDeviceIndex::LeftIndex) {
+ return controller.left_polling_mode;
+ }
+ return controller.right_polling_mode;
+}
+
+bool EmulatedController::SetCameraFormat(
+ Core::IrSensor::ImageTransferProcessorFormat camera_format) {
+ LOG_INFO(Service_HID, "Set camera format {}", camera_format);
+
+ if (!is_initalized) {
+ return false;
+ }
+
+ auto& right_output_device = output_devices[static_cast<std::size_t>(DeviceIndex::Right)];
+ auto& camera_output_device = output_devices[2];
+
+ if (right_output_device->SetCameraFormat(static_cast<Common::Input::CameraFormat>(
+ camera_format)) == Common::Input::DriverResult::Success) {
+ return true;
+ }
+
+ // Fallback to Qt camera if native device doesn't have support
+ return camera_output_device->SetCameraFormat(static_cast<Common::Input::CameraFormat>(
+ camera_format)) == Common::Input::DriverResult::Success;
+}
+
+Common::ParamPackage EmulatedController::GetRingParam() const {
+ return ring_params[0];
+}
+
+void EmulatedController::SetRingParam(Common::ParamPackage param) {
+ ring_params[0] = std::move(param);
+ ReloadInput();
+}
+
+bool EmulatedController::HasNfc() const {
+
+ if (!is_initalized) {
+ return false;
+ }
+
+ const auto& nfc_output_device = output_devices[3];
+
+ switch (npad_type) {
+ case NpadStyleIndex::JoyconRight:
+ case NpadStyleIndex::JoyconDual:
+ case NpadStyleIndex::ProController:
+ case NpadStyleIndex::Handheld:
+ break;
+ default:
+ return false;
+ }
+
+ const bool has_virtual_nfc =
+ npad_id_type == NpadIdType::Player1 || npad_id_type == NpadIdType::Handheld;
+ const bool is_virtual_nfc_supported =
+ nfc_output_device->SupportsNfc() != Common::Input::NfcState::NotSupported;
+
+ return is_connected && (has_virtual_nfc && is_virtual_nfc_supported);
+}
+
+bool EmulatedController::AddNfcHandle() {
+ nfc_handles++;
+ return SetPollingMode(EmulatedDeviceIndex::RightIndex, Common::Input::PollingMode::NFC) ==
+ Common::Input::DriverResult::Success;
+}
+
+bool EmulatedController::RemoveNfcHandle() {
+ nfc_handles--;
+ if (nfc_handles <= 0) {
+ return SetPollingMode(EmulatedDeviceIndex::RightIndex,
+ Common::Input::PollingMode::Active) ==
+ Common::Input::DriverResult::Success;
+ }
+ return true;
+}
+
+bool EmulatedController::StartNfcPolling() {
+ if (!is_initalized) {
+ return false;
+ }
+
+ auto& nfc_output_device = output_devices[static_cast<std::size_t>(DeviceIndex::Right)];
+ auto& nfc_virtual_output_device = output_devices[3];
+
+ const auto device_result = nfc_output_device->StartNfcPolling();
+ const auto virtual_device_result = nfc_virtual_output_device->StartNfcPolling();
+
+ return device_result == Common::Input::NfcState::Success ||
+ virtual_device_result == Common::Input::NfcState::Success;
+}
+
+bool EmulatedController::StopNfcPolling() {
+ if (!is_initalized) {
+ return false;
+ }
+
+ auto& nfc_output_device = output_devices[static_cast<std::size_t>(DeviceIndex::Right)];
+ auto& nfc_virtual_output_device = output_devices[3];
+
+ const auto device_result = nfc_output_device->StopNfcPolling();
+ const auto virtual_device_result = nfc_virtual_output_device->StopNfcPolling();
+
+ return device_result == Common::Input::NfcState::Success ||
+ virtual_device_result == Common::Input::NfcState::Success;
+}
+
+bool EmulatedController::ReadAmiiboData(std::vector<u8>& data) {
+ if (!is_initalized) {
+ return false;
+ }
+
+ auto& nfc_output_device = output_devices[static_cast<std::size_t>(DeviceIndex::Right)];
+ auto& nfc_virtual_output_device = output_devices[3];
+
+ if (nfc_output_device->ReadAmiiboData(data) == Common::Input::NfcState::Success) {
+ return true;
+ }
+
+ return nfc_virtual_output_device->ReadAmiiboData(data) == Common::Input::NfcState::Success;
+}
+
+bool EmulatedController::ReadMifareData(const Common::Input::MifareRequest& request,
+ Common::Input::MifareRequest& out_data) {
+ if (!is_initalized) {
+ return false;
+ }
+
+ auto& nfc_output_device = output_devices[static_cast<std::size_t>(DeviceIndex::Right)];
+ auto& nfc_virtual_output_device = output_devices[3];
+
+ if (nfc_output_device->ReadMifareData(request, out_data) == Common::Input::NfcState::Success) {
+ return true;
+ }
+
+ return nfc_virtual_output_device->ReadMifareData(request, out_data) ==
+ Common::Input::NfcState::Success;
+}
+
+bool EmulatedController::WriteMifareData(const Common::Input::MifareRequest& request) {
+ if (!is_initalized) {
+ return false;
+ }
+
+ auto& nfc_output_device = output_devices[static_cast<std::size_t>(DeviceIndex::Right)];
+ auto& nfc_virtual_output_device = output_devices[3];
+
+ if (nfc_output_device->WriteMifareData(request) == Common::Input::NfcState::Success) {
+ return true;
+ }
+
+ return nfc_virtual_output_device->WriteMifareData(request) == Common::Input::NfcState::Success;
+}
+
+bool EmulatedController::WriteNfc(const std::vector<u8>& data) {
+ if (!is_initalized) {
+ return false;
+ }
+
+ auto& nfc_output_device = output_devices[static_cast<std::size_t>(DeviceIndex::Right)];
+ auto& nfc_virtual_output_device = output_devices[3];
+
+ if (nfc_output_device->SupportsNfc() != Common::Input::NfcState::NotSupported) {
+ return nfc_output_device->WriteNfcData(data) == Common::Input::NfcState::Success;
+ }
+
+ return nfc_virtual_output_device->WriteNfcData(data) == Common::Input::NfcState::Success;
+}
+
+void EmulatedController::SetLedPattern() {
+ if (!is_initalized) {
+ return;
+ }
+
+ for (auto& device : output_devices) {
+ if (!device) {
+ continue;
+ }
+
+ const LedPattern pattern = GetLedPattern();
+ const Common::Input::LedStatus status = {
+ .led_1 = pattern.position1 != 0,
+ .led_2 = pattern.position2 != 0,
+ .led_3 = pattern.position3 != 0,
+ .led_4 = pattern.position4 != 0,
+ };
+ device->SetLED(status);
+ }
+}
+
+void EmulatedController::SetGyroscopeZeroDriftMode(GyroscopeZeroDriftMode mode) {
+ for (auto& motion : controller.motion_values) {
+ switch (mode) {
+ case GyroscopeZeroDriftMode::Loose:
+ motion_sensitivity = motion.emulated.IsAtRestLoose;
+ motion.emulated.SetGyroThreshold(motion.emulated.ThresholdLoose);
+ break;
+ case GyroscopeZeroDriftMode::Tight:
+ motion_sensitivity = motion.emulated.IsAtRestThight;
+ motion.emulated.SetGyroThreshold(motion.emulated.ThresholdThight);
+ break;
+ case GyroscopeZeroDriftMode::Standard:
+ default:
+ motion_sensitivity = motion.emulated.IsAtRestStandard;
+ motion.emulated.SetGyroThreshold(motion.emulated.ThresholdStandard);
+ break;
+ }
+ }
+}
+
+void EmulatedController::SetSupportedNpadStyleTag(NpadStyleTag supported_styles) {
+ supported_style_tag = supported_styles;
+ if (!is_connected) {
+ return;
+ }
+
+ // Attempt to reconnect with the original type
+ if (npad_type != original_npad_type) {
+ Disconnect();
+ const auto current_npad_type = npad_type;
+ SetNpadStyleIndex(original_npad_type);
+ if (IsControllerSupported()) {
+ Connect();
+ return;
+ }
+ SetNpadStyleIndex(current_npad_type);
+ Connect();
+ }
+
+ if (IsControllerSupported()) {
+ return;
+ }
+
+ Disconnect();
+
+ // Fallback Fullkey controllers to Pro controllers
+ if (IsControllerFullkey() && supported_style_tag.fullkey) {
+ LOG_WARNING(Service_HID, "Reconnecting controller type {} as Pro controller", npad_type);
+ SetNpadStyleIndex(NpadStyleIndex::ProController);
+ Connect();
+ return;
+ }
+
+ // Fallback Dual joycon controllers to Pro controllers
+ if (npad_type == NpadStyleIndex::JoyconDual && supported_style_tag.fullkey) {
+ LOG_WARNING(Service_HID, "Reconnecting controller type {} as Pro controller", npad_type);
+ SetNpadStyleIndex(NpadStyleIndex::ProController);
+ Connect();
+ return;
+ }
+
+ // Fallback Pro controllers to Dual joycon
+ if (npad_type == NpadStyleIndex::ProController && supported_style_tag.joycon_dual) {
+ LOG_WARNING(Service_HID, "Reconnecting controller type {} as Dual Joycons", npad_type);
+ SetNpadStyleIndex(NpadStyleIndex::JoyconDual);
+ Connect();
+ return;
+ }
+
+ LOG_ERROR(Service_HID, "Controller type {} is not supported. Disconnecting controller",
+ npad_type);
+}
+
+bool EmulatedController::IsControllerFullkey(bool use_temporary_value) const {
+ std::scoped_lock lock{mutex};
+ const auto type = is_configuring && use_temporary_value ? tmp_npad_type : npad_type;
+ switch (type) {
+ case NpadStyleIndex::ProController:
+ case NpadStyleIndex::GameCube:
+ case NpadStyleIndex::NES:
+ case NpadStyleIndex::SNES:
+ case NpadStyleIndex::N64:
+ case NpadStyleIndex::SegaGenesis:
+ return true;
+ default:
+ return false;
+ }
+}
+
+bool EmulatedController::IsControllerSupported(bool use_temporary_value) const {
+ std::scoped_lock lock{mutex};
+ const auto type = is_configuring && use_temporary_value ? tmp_npad_type : npad_type;
+ switch (type) {
+ case NpadStyleIndex::ProController:
+ return supported_style_tag.fullkey.As<bool>();
+ case NpadStyleIndex::Handheld:
+ return supported_style_tag.handheld.As<bool>();
+ case NpadStyleIndex::JoyconDual:
+ return supported_style_tag.joycon_dual.As<bool>();
+ case NpadStyleIndex::JoyconLeft:
+ return supported_style_tag.joycon_left.As<bool>();
+ case NpadStyleIndex::JoyconRight:
+ return supported_style_tag.joycon_right.As<bool>();
+ case NpadStyleIndex::GameCube:
+ return supported_style_tag.gamecube.As<bool>();
+ case NpadStyleIndex::Pokeball:
+ return supported_style_tag.palma.As<bool>();
+ case NpadStyleIndex::NES:
+ return supported_style_tag.lark.As<bool>();
+ case NpadStyleIndex::SNES:
+ return supported_style_tag.lucia.As<bool>();
+ case NpadStyleIndex::N64:
+ return supported_style_tag.lagoon.As<bool>();
+ case NpadStyleIndex::SegaGenesis:
+ return supported_style_tag.lager.As<bool>();
+ default:
+ return false;
+ }
+}
+
+void EmulatedController::Connect(bool use_temporary_value) {
+ if (!IsControllerSupported(use_temporary_value)) {
+ const auto type = is_configuring && use_temporary_value ? tmp_npad_type : npad_type;
+ LOG_ERROR(Service_HID, "Controller type {} is not supported", type);
+ return;
+ }
+
+ auto trigger_guard =
+ SCOPE_GUARD({ TriggerOnChange(ControllerTriggerType::Connected, !is_configuring); });
+ std::scoped_lock lock{connect_mutex, mutex};
+ if (is_configuring) {
+ tmp_is_connected = true;
+ return;
+ }
+
+ if (is_connected) {
+ trigger_guard.Cancel();
+ return;
+ }
+ is_connected = true;
+}
+
+void EmulatedController::Disconnect() {
+ auto trigger_guard =
+ SCOPE_GUARD({ TriggerOnChange(ControllerTriggerType::Disconnected, !is_configuring); });
+ std::scoped_lock lock{connect_mutex, mutex};
+ if (is_configuring) {
+ tmp_is_connected = false;
+ return;
+ }
+
+ if (!is_connected) {
+ trigger_guard.Cancel();
+ return;
+ }
+ is_connected = false;
+}
+
+bool EmulatedController::IsConnected(bool get_temporary_value) const {
+ std::scoped_lock lock{connect_mutex};
+ if (get_temporary_value && is_configuring) {
+ return tmp_is_connected;
+ }
+ return is_connected;
+}
+
+NpadIdType EmulatedController::GetNpadIdType() const {
+ std::scoped_lock lock{mutex};
+ return npad_id_type;
+}
+
+NpadStyleIndex EmulatedController::GetNpadStyleIndex(bool get_temporary_value) const {
+ std::scoped_lock lock{npad_mutex};
+ if (get_temporary_value && is_configuring) {
+ return tmp_npad_type;
+ }
+ return npad_type;
+}
+
+void EmulatedController::SetNpadStyleIndex(NpadStyleIndex npad_type_) {
+ auto trigger_guard =
+ SCOPE_GUARD({ TriggerOnChange(ControllerTriggerType::Type, !is_configuring); });
+ std::scoped_lock lock{mutex, npad_mutex};
+
+ if (is_configuring) {
+ if (tmp_npad_type == npad_type_) {
+ trigger_guard.Cancel();
+ return;
+ }
+ tmp_npad_type = npad_type_;
+ return;
+ }
+
+ if (npad_type == npad_type_) {
+ trigger_guard.Cancel();
+ return;
+ }
+ if (is_connected) {
+ LOG_WARNING(Service_HID, "Controller {} type changed while it's connected",
+ Service::HID::NpadIdTypeToIndex(npad_id_type));
+ }
+ npad_type = npad_type_;
+}
+
+LedPattern EmulatedController::GetLedPattern() const {
+ switch (npad_id_type) {
+ case NpadIdType::Player1:
+ return LedPattern{1, 0, 0, 0};
+ case NpadIdType::Player2:
+ return LedPattern{1, 1, 0, 0};
+ case NpadIdType::Player3:
+ return LedPattern{1, 1, 1, 0};
+ case NpadIdType::Player4:
+ return LedPattern{1, 1, 1, 1};
+ case NpadIdType::Player5:
+ return LedPattern{1, 0, 0, 1};
+ case NpadIdType::Player6:
+ return LedPattern{1, 0, 1, 0};
+ case NpadIdType::Player7:
+ return LedPattern{1, 0, 1, 1};
+ case NpadIdType::Player8:
+ return LedPattern{0, 1, 1, 0};
+ default:
+ return LedPattern{0, 0, 0, 0};
+ }
+}
+
+ButtonValues EmulatedController::GetButtonsValues() const {
+ std::scoped_lock lock{mutex};
+ return controller.button_values;
+}
+
+SticksValues EmulatedController::GetSticksValues() const {
+ std::scoped_lock lock{mutex};
+ return controller.stick_values;
+}
+
+TriggerValues EmulatedController::GetTriggersValues() const {
+ std::scoped_lock lock{mutex};
+ return controller.trigger_values;
+}
+
+ControllerMotionValues EmulatedController::GetMotionValues() const {
+ std::scoped_lock lock{mutex};
+ return controller.motion_values;
+}
+
+ColorValues EmulatedController::GetColorsValues() const {
+ std::scoped_lock lock{mutex};
+ return controller.color_values;
+}
+
+BatteryValues EmulatedController::GetBatteryValues() const {
+ std::scoped_lock lock{mutex};
+ return controller.battery_values;
+}
+
+CameraValues EmulatedController::GetCameraValues() const {
+ std::scoped_lock lock{mutex};
+ return controller.camera_values;
+}
+
+RingAnalogValue EmulatedController::GetRingSensorValues() const {
+ return controller.ring_analog_value;
+}
+
+HomeButtonState EmulatedController::GetHomeButtons() const {
+ std::scoped_lock lock{mutex};
+ if (is_configuring) {
+ return {};
+ }
+ return controller.home_button_state;
+}
+
+CaptureButtonState EmulatedController::GetCaptureButtons() const {
+ std::scoped_lock lock{mutex};
+ if (is_configuring) {
+ return {};
+ }
+ return controller.capture_button_state;
+}
+
+NpadButtonState EmulatedController::GetNpadButtons() const {
+ std::scoped_lock lock{mutex};
+ if (is_configuring) {
+ return {};
+ }
+ return {controller.npad_button_state.raw & GetTurboButtonMask()};
+}
+
+DebugPadButton EmulatedController::GetDebugPadButtons() const {
+ std::scoped_lock lock{mutex};
+ if (is_configuring) {
+ return {};
+ }
+ return controller.debug_pad_button_state;
+}
+
+AnalogSticks EmulatedController::GetSticks() const {
+ std::scoped_lock lock{mutex};
+
+ if (is_configuring) {
+ return {};
+ }
+
+ return controller.analog_stick_state;
+}
+
+NpadGcTriggerState EmulatedController::GetTriggers() const {
+ std::scoped_lock lock{mutex};
+ if (is_configuring) {
+ return {};
+ }
+ return controller.gc_trigger_state;
+}
+
+MotionState EmulatedController::GetMotions() const {
+ std::unique_lock lock{mutex};
+ return controller.motion_state;
+}
+
+ControllerColors EmulatedController::GetColors() const {
+ std::scoped_lock lock{mutex};
+ return controller.colors_state;
+}
+
+BatteryLevelState EmulatedController::GetBattery() const {
+ std::scoped_lock lock{mutex};
+ return controller.battery_state;
+}
+
+const CameraState& EmulatedController::GetCamera() const {
+ std::scoped_lock lock{mutex};
+ return controller.camera_state;
+}
+
+RingSensorForce EmulatedController::GetRingSensorForce() const {
+ return controller.ring_analog_state;
+}
+
+const NfcState& EmulatedController::GetNfc() const {
+ std::scoped_lock lock{mutex};
+ return controller.nfc_state;
+}
+
+NpadColor EmulatedController::GetNpadColor(u32 color) {
+ return {
+ .r = static_cast<u8>((color >> 16) & 0xFF),
+ .g = static_cast<u8>((color >> 8) & 0xFF),
+ .b = static_cast<u8>(color & 0xFF),
+ .a = 0xff,
+ };
+}
+
+void EmulatedController::TriggerOnChange(ControllerTriggerType type, bool is_npad_service_update) {
+ std::scoped_lock lock{callback_mutex};
+ for (const auto& poller_pair : callback_list) {
+ const ControllerUpdateCallback& poller = poller_pair.second;
+ if (!is_npad_service_update && poller.is_npad_service) {
+ continue;
+ }
+ if (poller.on_change) {
+ poller.on_change(type);
+ }
+ }
+}
+
+int EmulatedController::SetCallback(ControllerUpdateCallback update_callback) {
+ std::scoped_lock lock{callback_mutex};
+ callback_list.insert_or_assign(last_callback_key, std::move(update_callback));
+ return last_callback_key++;
+}
+
+void EmulatedController::DeleteCallback(int key) {
+ std::scoped_lock lock{callback_mutex};
+ const auto& iterator = callback_list.find(key);
+ if (iterator == callback_list.end()) {
+ LOG_ERROR(Input, "Tried to delete non-existent callback {}", key);
+ return;
+ }
+ callback_list.erase(iterator);
+}
+
+void EmulatedController::StatusUpdate() {
+ turbo_button_state = (turbo_button_state + 1) % (TURBO_BUTTON_DELAY * 2);
+
+ // Some drivers like key motion need constant refreshing
+ for (std::size_t index = 0; index < motion_devices.size(); ++index) {
+ const auto& raw_status = controller.motion_values[index].raw_status;
+ auto& device = motion_devices[index];
+ if (!raw_status.force_update) {
+ continue;
+ }
+ if (!device) {
+ continue;
+ }
+ device->ForceUpdate();
+ }
+}
+
+NpadButton EmulatedController::GetTurboButtonMask() const {
+ // Apply no mask when disabled
+ if (turbo_button_state < TURBO_BUTTON_DELAY) {
+ return {NpadButton::All};
+ }
+
+ NpadButtonState button_mask{};
+ for (std::size_t index = 0; index < controller.button_values.size(); ++index) {
+ if (!controller.button_values[index].turbo) {
+ continue;
+ }
+
+ switch (index) {
+ case Settings::NativeButton::A:
+ button_mask.a.Assign(1);
+ break;
+ case Settings::NativeButton::B:
+ button_mask.b.Assign(1);
+ break;
+ case Settings::NativeButton::X:
+ button_mask.x.Assign(1);
+ break;
+ case Settings::NativeButton::Y:
+ button_mask.y.Assign(1);
+ break;
+ case Settings::NativeButton::L:
+ button_mask.l.Assign(1);
+ break;
+ case Settings::NativeButton::R:
+ button_mask.r.Assign(1);
+ break;
+ case Settings::NativeButton::ZL:
+ button_mask.zl.Assign(1);
+ break;
+ case Settings::NativeButton::ZR:
+ button_mask.zr.Assign(1);
+ break;
+ case Settings::NativeButton::DLeft:
+ button_mask.left.Assign(1);
+ break;
+ case Settings::NativeButton::DUp:
+ button_mask.up.Assign(1);
+ break;
+ case Settings::NativeButton::DRight:
+ button_mask.right.Assign(1);
+ break;
+ case Settings::NativeButton::DDown:
+ button_mask.down.Assign(1);
+ break;
+ case Settings::NativeButton::SLLeft:
+ button_mask.left_sl.Assign(1);
+ break;
+ case Settings::NativeButton::SLRight:
+ button_mask.right_sl.Assign(1);
+ break;
+ case Settings::NativeButton::SRLeft:
+ button_mask.left_sr.Assign(1);
+ break;
+ case Settings::NativeButton::SRRight:
+ button_mask.right_sr.Assign(1);
+ break;
+ default:
+ break;
+ }
+ }
+
+ return static_cast<NpadButton>(~button_mask.raw);
+}
+
+} // namespace Core::HID
diff --git a/src/hid_core/frontend/emulated_controller.h b/src/hid_core/frontend/emulated_controller.h
new file mode 100644
index 000000000..94798164d
--- /dev/null
+++ b/src/hid_core/frontend/emulated_controller.h
@@ -0,0 +1,619 @@
+// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <array>
+#include <functional>
+#include <memory>
+#include <mutex>
+#include <unordered_map>
+#include <vector>
+
+#include "common/common_types.h"
+#include "common/input.h"
+#include "common/param_package.h"
+#include "common/settings.h"
+#include "common/vector_math.h"
+#include "hid_core/frontend/motion_input.h"
+#include "hid_core/hid_types.h"
+#include "hid_core/irsensor/irs_types.h"
+
+namespace Core::HID {
+const std::size_t max_emulated_controllers = 2;
+const std::size_t output_devices_size = 4;
+struct ControllerMotionInfo {
+ Common::Input::MotionStatus raw_status{};
+ MotionInput emulated{};
+};
+
+using ButtonDevices =
+ std::array<std::unique_ptr<Common::Input::InputDevice>, Settings::NativeButton::NumButtons>;
+using StickDevices =
+ std::array<std::unique_ptr<Common::Input::InputDevice>, Settings::NativeAnalog::NumAnalogs>;
+using ControllerMotionDevices =
+ std::array<std::unique_ptr<Common::Input::InputDevice>, Settings::NativeMotion::NumMotions>;
+using TriggerDevices =
+ std::array<std::unique_ptr<Common::Input::InputDevice>, Settings::NativeTrigger::NumTriggers>;
+using ColorDevices =
+ std::array<std::unique_ptr<Common::Input::InputDevice>, max_emulated_controllers>;
+using BatteryDevices =
+ std::array<std::unique_ptr<Common::Input::InputDevice>, max_emulated_controllers>;
+using CameraDevices =
+ std::array<std::unique_ptr<Common::Input::InputDevice>, max_emulated_controllers>;
+using RingAnalogDevices =
+ std::array<std::unique_ptr<Common::Input::InputDevice>, max_emulated_controllers>;
+using NfcDevices =
+ std::array<std::unique_ptr<Common::Input::InputDevice>, max_emulated_controllers>;
+using OutputDevices = std::array<std::unique_ptr<Common::Input::OutputDevice>, output_devices_size>;
+
+using ButtonParams = std::array<Common::ParamPackage, Settings::NativeButton::NumButtons>;
+using StickParams = std::array<Common::ParamPackage, Settings::NativeAnalog::NumAnalogs>;
+using ControllerMotionParams = std::array<Common::ParamPackage, Settings::NativeMotion::NumMotions>;
+using TriggerParams = std::array<Common::ParamPackage, Settings::NativeTrigger::NumTriggers>;
+using ColorParams = std::array<Common::ParamPackage, max_emulated_controllers>;
+using BatteryParams = std::array<Common::ParamPackage, max_emulated_controllers>;
+using CameraParams = std::array<Common::ParamPackage, max_emulated_controllers>;
+using RingAnalogParams = std::array<Common::ParamPackage, max_emulated_controllers>;
+using NfcParams = std::array<Common::ParamPackage, max_emulated_controllers>;
+using OutputParams = std::array<Common::ParamPackage, output_devices_size>;
+
+using ButtonValues = std::array<Common::Input::ButtonStatus, Settings::NativeButton::NumButtons>;
+using SticksValues = std::array<Common::Input::StickStatus, Settings::NativeAnalog::NumAnalogs>;
+using TriggerValues =
+ std::array<Common::Input::TriggerStatus, Settings::NativeTrigger::NumTriggers>;
+using ControllerMotionValues = std::array<ControllerMotionInfo, Settings::NativeMotion::NumMotions>;
+using ColorValues = std::array<Common::Input::BodyColorStatus, max_emulated_controllers>;
+using BatteryValues = std::array<Common::Input::BatteryStatus, max_emulated_controllers>;
+using CameraValues = Common::Input::CameraStatus;
+using RingAnalogValue = Common::Input::AnalogStatus;
+using NfcValues = Common::Input::NfcStatus;
+using VibrationValues = std::array<Common::Input::VibrationStatus, max_emulated_controllers>;
+
+struct AnalogSticks {
+ AnalogStickState left{};
+ AnalogStickState right{};
+};
+
+struct ControllerColors {
+ NpadControllerColor fullkey{};
+ NpadControllerColor left{};
+ NpadControllerColor right{};
+};
+
+struct BatteryLevelState {
+ NpadPowerInfo dual{};
+ NpadPowerInfo left{};
+ NpadPowerInfo right{};
+};
+
+struct CameraState {
+ Core::IrSensor::ImageTransferProcessorFormat format{};
+ std::vector<u8> data{};
+ std::size_t sample{};
+};
+
+struct RingSensorForce {
+ f32 force;
+};
+
+using NfcState = Common::Input::NfcStatus;
+
+struct ControllerMotion {
+ Common::Vec3f accel{};
+ Common::Vec3f gyro{};
+ Common::Vec3f rotation{};
+ Common::Vec3f euler{};
+ std::array<Common::Vec3f, 3> orientation{};
+ bool is_at_rest{};
+};
+
+enum EmulatedDeviceIndex : u8 {
+ LeftIndex,
+ RightIndex,
+ DualIndex,
+ AllDevices,
+};
+
+using MotionState = std::array<ControllerMotion, 2>;
+
+struct ControllerStatus {
+ // Data from input_common
+ ButtonValues button_values{};
+ SticksValues stick_values{};
+ ControllerMotionValues motion_values{};
+ TriggerValues trigger_values{};
+ ColorValues color_values{};
+ BatteryValues battery_values{};
+ VibrationValues vibration_values{};
+ CameraValues camera_values{};
+ RingAnalogValue ring_analog_value{};
+ NfcValues nfc_values{};
+
+ // Data for HID services
+ HomeButtonState home_button_state{};
+ CaptureButtonState capture_button_state{};
+ NpadButtonState npad_button_state{};
+ DebugPadButton debug_pad_button_state{};
+ AnalogSticks analog_stick_state{};
+ MotionState motion_state{};
+ NpadGcTriggerState gc_trigger_state{};
+ ControllerColors colors_state{};
+ BatteryLevelState battery_state{};
+ CameraState camera_state{};
+ RingSensorForce ring_analog_state{};
+ NfcState nfc_state{};
+ Common::Input::PollingMode left_polling_mode{};
+ Common::Input::PollingMode right_polling_mode{};
+};
+
+enum class ControllerTriggerType {
+ Button,
+ Stick,
+ Trigger,
+ Motion,
+ Color,
+ Battery,
+ Vibration,
+ IrSensor,
+ RingController,
+ Nfc,
+ Connected,
+ Disconnected,
+ Type,
+ All,
+};
+
+struct ControllerUpdateCallback {
+ std::function<void(ControllerTriggerType)> on_change;
+ bool is_npad_service;
+};
+
+class EmulatedController {
+public:
+ /**
+ * Contains all input data (buttons, joysticks, vibration, and motion) within this controller.
+ * @param npad_id_type npad id type for this specific controller
+ */
+ explicit EmulatedController(NpadIdType npad_id_type_);
+ ~EmulatedController();
+
+ YUZU_NON_COPYABLE(EmulatedController);
+ YUZU_NON_MOVEABLE(EmulatedController);
+
+ /// Converts the controller type from settings to npad type
+ static NpadStyleIndex MapSettingsTypeToNPad(Settings::ControllerType type);
+
+ /// Converts npad type to the equivalent of controller type from settings
+ static Settings::ControllerType MapNPadToSettingsType(NpadStyleIndex type);
+
+ /// Gets the NpadIdType for this controller
+ NpadIdType GetNpadIdType() const;
+
+ /// Sets the NpadStyleIndex for this controller
+ void SetNpadStyleIndex(NpadStyleIndex npad_type_);
+
+ /**
+ * Gets the NpadStyleIndex for this controller
+ * @param get_temporary_value If true tmp_npad_type will be returned
+ * @return NpadStyleIndex set on the controller
+ */
+ NpadStyleIndex GetNpadStyleIndex(bool get_temporary_value = false) const;
+
+ /**
+ * Sets the supported controller types. Disconnects the controller if current type is not
+ * supported
+ * @param supported_styles bitflag with supported types
+ */
+ void SetSupportedNpadStyleTag(NpadStyleTag supported_styles);
+
+ /**
+ * Sets the connected status to true
+ * @param use_temporary_value If true tmp_npad_type will be used
+ */
+ void Connect(bool use_temporary_value = false);
+
+ /// Sets the connected status to false
+ void Disconnect();
+
+ /**
+ * Is the emulated connected
+ * @param get_temporary_value If true tmp_is_connected will be returned
+ * @return true if the controller has the connected status
+ */
+ bool IsConnected(bool get_temporary_value = false) const;
+
+ /// Removes all callbacks created from input devices
+ void UnloadInput();
+
+ /**
+ * Sets the emulated controller into configuring mode
+ * This prevents the modification of the HID state of the emulated controller by input commands
+ */
+ void EnableConfiguration();
+
+ /// Returns the emulated controller into normal mode, allowing the modification of the HID state
+ void DisableConfiguration();
+
+ /// Enables Home and Screenshot buttons
+ void EnableSystemButtons();
+
+ /// Disables Home and Screenshot buttons
+ void DisableSystemButtons();
+
+ /// Sets Home and Screenshot buttons to false
+ void ResetSystemButtons();
+
+ /// Returns true if the emulated controller is in configuring mode
+ bool IsConfiguring() const;
+
+ /// Reload all input devices
+ void ReloadInput();
+
+ /// Overrides current mapped devices with the stored configuration and reloads all input devices
+ void ReloadFromSettings();
+
+ /// Updates current colors with the ones stored in the configuration
+ void ReloadColorsFromSettings();
+
+ /// Saves the current mapped configuration
+ void SaveCurrentConfig();
+
+ /// Reverts any mapped changes made that weren't saved
+ void RestoreConfig();
+
+ /// Returns a vector of mapped devices from the mapped button and stick parameters
+ std::vector<Common::ParamPackage> GetMappedDevices() const;
+
+ // Returns the current mapped button device
+ Common::ParamPackage GetButtonParam(std::size_t index) const;
+
+ // Returns the current mapped stick device
+ Common::ParamPackage GetStickParam(std::size_t index) const;
+
+ // Returns the current mapped motion device
+ Common::ParamPackage GetMotionParam(std::size_t index) const;
+
+ /**
+ * Updates the current mapped button device
+ * @param param ParamPackage with controller data to be mapped
+ */
+ void SetButtonParam(std::size_t index, Common::ParamPackage param);
+
+ /**
+ * Updates the current mapped stick device
+ * @param param ParamPackage with controller data to be mapped
+ */
+ void SetStickParam(std::size_t index, Common::ParamPackage param);
+
+ /**
+ * Updates the current mapped motion device
+ * @param param ParamPackage with controller data to be mapped
+ */
+ void SetMotionParam(std::size_t index, Common::ParamPackage param);
+
+ /// Auto calibrates the current motion devices
+ void StartMotionCalibration();
+
+ /// Returns the latest button status from the controller with parameters
+ ButtonValues GetButtonsValues() const;
+
+ /// Returns the latest analog stick status from the controller with parameters
+ SticksValues GetSticksValues() const;
+
+ /// Returns the latest trigger status from the controller with parameters
+ TriggerValues GetTriggersValues() const;
+
+ /// Returns the latest motion status from the controller with parameters
+ ControllerMotionValues GetMotionValues() const;
+
+ /// Returns the latest color status from the controller with parameters
+ ColorValues GetColorsValues() const;
+
+ /// Returns the latest battery status from the controller with parameters
+ BatteryValues GetBatteryValues() const;
+
+ /// Returns the latest camera status from the controller with parameters
+ CameraValues GetCameraValues() const;
+
+ /// Returns the latest status of analog input from the ring sensor with parameters
+ RingAnalogValue GetRingSensorValues() const;
+
+ /// Returns the latest status of button input for the hid::HomeButton service
+ HomeButtonState GetHomeButtons() const;
+
+ /// Returns the latest status of button input for the hid::CaptureButton service
+ CaptureButtonState GetCaptureButtons() const;
+
+ /// Returns the latest status of button input for the hid::Npad service
+ NpadButtonState GetNpadButtons() const;
+
+ /// Returns the latest status of button input for the debug pad service
+ DebugPadButton GetDebugPadButtons() const;
+
+ /// Returns the latest status of stick input from the mouse
+ AnalogSticks GetSticks() const;
+
+ /// Returns the latest status of trigger input from the mouse
+ NpadGcTriggerState GetTriggers() const;
+
+ /// Returns the latest status of motion input from the mouse
+ MotionState GetMotions() const;
+
+ /// Returns the latest color value from the controller
+ ControllerColors GetColors() const;
+
+ /// Returns the latest battery status from the controller
+ BatteryLevelState GetBattery() const;
+
+ /// Returns the latest camera status from the controller
+ const CameraState& GetCamera() const;
+
+ /// Returns the latest ringcon force sensor value
+ RingSensorForce GetRingSensorForce() const;
+
+ /// Returns the latest ntag status from the controller
+ const NfcState& GetNfc() const;
+
+ /**
+ * Sends a specific vibration to the output device
+ * @return true if vibration had no errors
+ */
+ bool SetVibration(std::size_t device_index, VibrationValue vibration);
+
+ /**
+ * Sends a small vibration to the output device
+ * @return true if SetVibration was successful
+ */
+ bool IsVibrationEnabled(std::size_t device_index);
+
+ /**
+ * Sets the desired data to be polled from a controller
+ * @param device_index index of the controller to set the polling mode
+ * @param polling_mode type of input desired buttons, gyro, nfc, ir, etc.
+ * @return driver result from this command
+ */
+ Common::Input::DriverResult SetPollingMode(EmulatedDeviceIndex device_index,
+ Common::Input::PollingMode polling_mode);
+ /**
+ * Get the current polling mode from a controller
+ * @param device_index index of the controller to set the polling mode
+ * @return current polling mode
+ */
+ Common::Input::PollingMode GetPollingMode(EmulatedDeviceIndex device_index) const;
+
+ /**
+ * Sets the desired camera format to be polled from a controller
+ * @param camera_format size of each frame
+ * @return true if SetCameraFormat was successful
+ */
+ bool SetCameraFormat(Core::IrSensor::ImageTransferProcessorFormat camera_format);
+
+ // Returns the current mapped ring device
+ Common::ParamPackage GetRingParam() const;
+
+ /**
+ * Updates the current mapped ring device
+ * @param param ParamPackage with ring sensor data to be mapped
+ */
+ void SetRingParam(Common::ParamPackage param);
+
+ /// Returns true if the device has nfc support
+ bool HasNfc() const;
+
+ /// Sets the joycon in nfc mode and increments the handle count
+ bool AddNfcHandle();
+
+ /// Decrements the handle count if zero sets the joycon in active mode
+ bool RemoveNfcHandle();
+
+ /// Start searching for nfc tags
+ bool StartNfcPolling();
+
+ /// Stop searching for nfc tags
+ bool StopNfcPolling();
+
+ /// Returns true if the nfc tag was readable
+ bool ReadAmiiboData(std::vector<u8>& data);
+
+ /// Returns true if the nfc tag was written
+ bool WriteNfc(const std::vector<u8>& data);
+
+ /// Returns true if the nfc tag was readable
+ bool ReadMifareData(const Common::Input::MifareRequest& request,
+ Common::Input::MifareRequest& out_data);
+
+ /// Returns true if the nfc tag was written
+ bool WriteMifareData(const Common::Input::MifareRequest& request);
+
+ /// Returns the led pattern corresponding to this emulated controller
+ LedPattern GetLedPattern() const;
+
+ /// Asks the output device to change the player led pattern
+ void SetLedPattern();
+
+ /// Changes sensitivity of the motion sensor
+ void SetGyroscopeZeroDriftMode(GyroscopeZeroDriftMode mode);
+
+ /**
+ * Adds a callback to the list of events
+ * @param update_callback A ConsoleUpdateCallback that will be triggered
+ * @return an unique key corresponding to the callback index in the list
+ */
+ int SetCallback(ControllerUpdateCallback update_callback);
+
+ /**
+ * Removes a callback from the list stopping any future events to this object
+ * @param key Key corresponding to the callback index in the list
+ */
+ void DeleteCallback(int key);
+
+ /// Swaps the state of the turbo buttons and updates motion input
+ void StatusUpdate();
+
+private:
+ /// creates input devices from params
+ void LoadDevices();
+
+ /// 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
+ */
+ bool IsControllerFullkey(bool use_temporary_value = false) const;
+
+ /**
+ * Checks the current controller type against the supported_style_tag
+ * @param use_temporary_value If true tmp_npad_type will be used
+ * @return true if the controller is supported
+ */
+ bool IsControllerSupported(bool use_temporary_value = false) const;
+
+ /**
+ * Updates the button status of the controller
+ * @param callback A CallbackStatus containing the button status
+ * @param index Button ID of the to be updated
+ */
+ void SetButton(const Common::Input::CallbackStatus& callback, std::size_t index,
+ Common::UUID uuid);
+
+ /**
+ * Updates the analog stick status of the controller
+ * @param callback A CallbackStatus containing the analog stick status
+ * @param index stick ID of the to be updated
+ */
+ void SetStick(const Common::Input::CallbackStatus& callback, std::size_t index,
+ Common::UUID uuid);
+
+ /**
+ * Updates the trigger status of the controller
+ * @param callback A CallbackStatus containing the trigger status
+ * @param index trigger ID of the to be updated
+ */
+ void SetTrigger(const Common::Input::CallbackStatus& callback, std::size_t index,
+ Common::UUID uuid);
+
+ /**
+ * Updates the motion status of the controller
+ * @param callback A CallbackStatus containing gyro and accelerometer data
+ * @param index motion ID of the to be updated
+ */
+ void SetMotion(const Common::Input::CallbackStatus& callback, std::size_t index);
+
+ /**
+ * Updates the color status of the controller
+ * @param callback A CallbackStatus containing the color status
+ * @param index color ID of the to be updated
+ */
+ void SetColors(const Common::Input::CallbackStatus& callback, std::size_t index);
+
+ /**
+ * Updates the battery status of the controller
+ * @param callback A CallbackStatus containing the battery status
+ * @param index battery ID of the to be updated
+ */
+ void SetBattery(const Common::Input::CallbackStatus& callback, std::size_t index);
+
+ /**
+ * Updates the camera status of the controller
+ * @param callback A CallbackStatus containing the camera status
+ */
+ void SetCamera(const Common::Input::CallbackStatus& callback);
+
+ /**
+ * Updates the ring analog sensor status of the ring controller
+ * @param callback A CallbackStatus containing the force status
+ */
+ void SetRingAnalog(const Common::Input::CallbackStatus& callback);
+
+ /**
+ * Updates the nfc status of the controller
+ * @param callback A CallbackStatus containing the nfc status
+ */
+ void SetNfc(const Common::Input::CallbackStatus& callback);
+
+ /**
+ * Converts a color format from bgra to rgba
+ * @param color in bgra format
+ * @return NpadColor in rgba format
+ */
+ NpadColor GetNpadColor(u32 color);
+
+ /**
+ * Triggers a callback that something has changed on the controller status
+ * @param type Input type of the event to trigger
+ * @param is_service_update indicates if this event should only be sent to HID services
+ */
+ void TriggerOnChange(ControllerTriggerType type, bool is_service_update);
+
+ NpadButton GetTurboButtonMask() const;
+
+ const NpadIdType npad_id_type;
+ NpadStyleIndex npad_type{NpadStyleIndex::None};
+ NpadStyleIndex original_npad_type{NpadStyleIndex::None};
+ NpadStyleTag supported_style_tag{NpadStyleSet::All};
+ bool is_connected{false};
+ bool is_configuring{false};
+ bool is_initalized{false};
+ bool system_buttons_enabled{true};
+ f32 motion_sensitivity{Core::HID::MotionInput::IsAtRestStandard};
+ u32 turbo_button_state{0};
+ std::size_t nfc_handles{0};
+
+ // Temporary values to avoid doing changes while the controller is in configuring mode
+ NpadStyleIndex tmp_npad_type{NpadStyleIndex::None};
+ bool tmp_is_connected{false};
+
+ ButtonParams button_params;
+ StickParams stick_params;
+ ControllerMotionParams motion_params;
+ TriggerParams trigger_params;
+ BatteryParams battery_params;
+ ColorParams color_params;
+ CameraParams camera_params;
+ RingAnalogParams ring_params;
+ NfcParams nfc_params;
+ OutputParams output_params;
+
+ ButtonDevices button_devices;
+ StickDevices stick_devices;
+ ControllerMotionDevices motion_devices;
+ TriggerDevices trigger_devices;
+ BatteryDevices battery_devices;
+ ColorDevices color_devices;
+ CameraDevices camera_devices;
+ RingAnalogDevices ring_analog_devices;
+ NfcDevices nfc_devices;
+ OutputDevices output_devices;
+
+ // TAS related variables
+ ButtonParams tas_button_params;
+ StickParams tas_stick_params;
+ ButtonDevices tas_button_devices;
+ StickDevices tas_stick_devices;
+
+ // Virtual gamepad related variables
+ ButtonParams virtual_button_params;
+ StickParams virtual_stick_params;
+ ControllerMotionParams virtual_motion_params;
+ ButtonDevices virtual_button_devices;
+ StickDevices virtual_stick_devices;
+ ControllerMotionDevices virtual_motion_devices;
+
+ mutable std::mutex mutex;
+ mutable std::mutex callback_mutex;
+ mutable std::mutex npad_mutex;
+ mutable std::mutex connect_mutex;
+ std::unordered_map<int, ControllerUpdateCallback> callback_list;
+ int last_callback_key = 0;
+
+ // Stores the current status of all controller input
+ ControllerStatus controller;
+};
+
+} // namespace Core::HID
diff --git a/src/hid_core/frontend/emulated_devices.cpp b/src/hid_core/frontend/emulated_devices.cpp
new file mode 100644
index 000000000..a827aa9b7
--- /dev/null
+++ b/src/hid_core/frontend/emulated_devices.cpp
@@ -0,0 +1,483 @@
+// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <algorithm>
+#include <fmt/format.h>
+
+#include "hid_core/frontend/emulated_devices.h"
+#include "hid_core/frontend/input_converter.h"
+
+namespace Core::HID {
+
+EmulatedDevices::EmulatedDevices() = default;
+
+EmulatedDevices::~EmulatedDevices() = default;
+
+void EmulatedDevices::ReloadFromSettings() {
+ ReloadInput();
+}
+
+void EmulatedDevices::ReloadInput() {
+ // If you load any device here add the equivalent to the UnloadInput() function
+
+ // Native Mouse is mapped on port 1, pad 0
+ const Common::ParamPackage mouse_params{"engine:mouse,port:1,pad:0"};
+
+ // Keyboard keys is mapped on port 1, pad 0 for normal keys, pad 1 for moddifier keys
+ const Common::ParamPackage keyboard_params{"engine:keyboard,port:1"};
+
+ std::size_t key_index = 0;
+ for (auto& mouse_device : mouse_button_devices) {
+ Common::ParamPackage mouse_button_params = mouse_params;
+ mouse_button_params.Set("button", static_cast<int>(key_index));
+ mouse_device = Common::Input::CreateInputDevice(mouse_button_params);
+ key_index++;
+ }
+
+ Common::ParamPackage mouse_position_params = mouse_params;
+ mouse_position_params.Set("axis_x", 0);
+ mouse_position_params.Set("axis_y", 1);
+ mouse_position_params.Set("deadzone", 0.0f);
+ mouse_position_params.Set("range", 1.0f);
+ mouse_position_params.Set("threshold", 0.0f);
+ mouse_stick_device = Common::Input::CreateInputDevice(mouse_position_params);
+
+ // First two axis are reserved for mouse position
+ key_index = 2;
+ for (auto& mouse_device : mouse_wheel_devices) {
+ Common::ParamPackage mouse_wheel_params = mouse_params;
+ mouse_wheel_params.Set("axis", static_cast<int>(key_index));
+ mouse_device = Common::Input::CreateInputDevice(mouse_wheel_params);
+ key_index++;
+ }
+
+ key_index = 0;
+ for (auto& keyboard_device : keyboard_devices) {
+ Common::ParamPackage keyboard_key_params = keyboard_params;
+ keyboard_key_params.Set("button", static_cast<int>(key_index));
+ keyboard_key_params.Set("pad", 0);
+ keyboard_device = Common::Input::CreateInputDevice(keyboard_key_params);
+ key_index++;
+ }
+
+ key_index = 0;
+ for (auto& keyboard_device : keyboard_modifier_devices) {
+ Common::ParamPackage keyboard_moddifier_params = keyboard_params;
+ keyboard_moddifier_params.Set("button", static_cast<int>(key_index));
+ keyboard_moddifier_params.Set("pad", 1);
+ keyboard_device = Common::Input::CreateInputDevice(keyboard_moddifier_params);
+ key_index++;
+ }
+
+ for (std::size_t index = 0; index < mouse_button_devices.size(); ++index) {
+ if (!mouse_button_devices[index]) {
+ continue;
+ }
+ mouse_button_devices[index]->SetCallback({
+ .on_change =
+ [this, index](const Common::Input::CallbackStatus& callback) {
+ SetMouseButton(callback, index);
+ },
+ });
+ }
+
+ for (std::size_t index = 0; index < mouse_wheel_devices.size(); ++index) {
+ if (!mouse_wheel_devices[index]) {
+ continue;
+ }
+ mouse_wheel_devices[index]->SetCallback({
+ .on_change =
+ [this, index](const Common::Input::CallbackStatus& callback) {
+ SetMouseWheel(callback, index);
+ },
+ });
+ }
+
+ if (mouse_stick_device) {
+ mouse_stick_device->SetCallback({
+ .on_change =
+ [this](const Common::Input::CallbackStatus& callback) {
+ SetMousePosition(callback);
+ },
+ });
+ }
+
+ for (std::size_t index = 0; index < keyboard_devices.size(); ++index) {
+ if (!keyboard_devices[index]) {
+ continue;
+ }
+ keyboard_devices[index]->SetCallback({
+ .on_change =
+ [this, index](const Common::Input::CallbackStatus& callback) {
+ SetKeyboardButton(callback, index);
+ },
+ });
+ }
+
+ for (std::size_t index = 0; index < keyboard_modifier_devices.size(); ++index) {
+ if (!keyboard_modifier_devices[index]) {
+ continue;
+ }
+ keyboard_modifier_devices[index]->SetCallback({
+ .on_change =
+ [this, index](const Common::Input::CallbackStatus& callback) {
+ SetKeyboardModifier(callback, index);
+ },
+ });
+ }
+}
+
+void EmulatedDevices::UnloadInput() {
+ for (auto& button : mouse_button_devices) {
+ button.reset();
+ }
+ for (auto& analog : mouse_wheel_devices) {
+ analog.reset();
+ }
+ mouse_stick_device.reset();
+ for (auto& button : keyboard_devices) {
+ button.reset();
+ }
+ for (auto& button : keyboard_modifier_devices) {
+ button.reset();
+ }
+}
+
+void EmulatedDevices::EnableConfiguration() {
+ is_configuring = true;
+ SaveCurrentConfig();
+}
+
+void EmulatedDevices::DisableConfiguration() {
+ is_configuring = false;
+}
+
+bool EmulatedDevices::IsConfiguring() const {
+ return is_configuring;
+}
+
+void EmulatedDevices::SaveCurrentConfig() {
+ if (!is_configuring) {
+ return;
+ }
+}
+
+void EmulatedDevices::RestoreConfig() {
+ if (!is_configuring) {
+ return;
+ }
+ ReloadFromSettings();
+}
+
+void EmulatedDevices::SetKeyboardButton(const Common::Input::CallbackStatus& callback,
+ std::size_t index) {
+ if (index >= device_status.keyboard_values.size()) {
+ return;
+ }
+ std::unique_lock lock{mutex};
+ bool value_changed = false;
+ const auto new_status = TransformToButton(callback);
+ auto& current_status = device_status.keyboard_values[index];
+ current_status.toggle = new_status.toggle;
+
+ // Update button status with current status
+ if (!current_status.toggle) {
+ current_status.locked = false;
+ if (current_status.value != new_status.value) {
+ current_status.value = new_status.value;
+ value_changed = true;
+ }
+ } else {
+ // Toggle button and lock status
+ if (new_status.value && !current_status.locked) {
+ current_status.locked = true;
+ current_status.value = !current_status.value;
+ value_changed = true;
+ }
+
+ // Unlock button, ready for next press
+ if (!new_status.value && current_status.locked) {
+ current_status.locked = false;
+ }
+ }
+
+ if (!value_changed) {
+ return;
+ }
+
+ if (is_configuring) {
+ lock.unlock();
+ TriggerOnChange(DeviceTriggerType::Keyboard);
+ return;
+ }
+
+ // Index should be converted from NativeKeyboard to KeyboardKeyIndex
+ UpdateKey(index, current_status.value);
+
+ lock.unlock();
+ TriggerOnChange(DeviceTriggerType::Keyboard);
+}
+
+void EmulatedDevices::UpdateKey(std::size_t key_index, bool status) {
+ constexpr std::size_t KEYS_PER_BYTE = 8;
+ auto& entry = device_status.keyboard_state.key[key_index / KEYS_PER_BYTE];
+ const u8 mask = static_cast<u8>(1 << (key_index % KEYS_PER_BYTE));
+ if (status) {
+ entry = entry | mask;
+ } else {
+ entry = static_cast<u8>(entry & ~mask);
+ }
+}
+
+void EmulatedDevices::SetKeyboardModifier(const Common::Input::CallbackStatus& callback,
+ std::size_t index) {
+ if (index >= device_status.keyboard_moddifier_values.size()) {
+ return;
+ }
+ std::unique_lock lock{mutex};
+ bool value_changed = false;
+ const auto new_status = TransformToButton(callback);
+ auto& current_status = device_status.keyboard_moddifier_values[index];
+ current_status.toggle = new_status.toggle;
+
+ // Update button status with current
+ if (!current_status.toggle) {
+ current_status.locked = false;
+ if (current_status.value != new_status.value) {
+ current_status.value = new_status.value;
+ value_changed = true;
+ }
+ } else {
+ // Toggle button and lock status
+ if (new_status.value && !current_status.locked) {
+ current_status.locked = true;
+ current_status.value = !current_status.value;
+ value_changed = true;
+ }
+
+ // Unlock button ready for next press
+ if (!new_status.value && current_status.locked) {
+ current_status.locked = false;
+ }
+ }
+
+ if (!value_changed) {
+ return;
+ }
+
+ if (is_configuring) {
+ lock.unlock();
+ TriggerOnChange(DeviceTriggerType::KeyboardModdifier);
+ return;
+ }
+
+ switch (index) {
+ case Settings::NativeKeyboard::LeftControl:
+ case Settings::NativeKeyboard::RightControl:
+ device_status.keyboard_moddifier_state.control.Assign(current_status.value);
+ break;
+ case Settings::NativeKeyboard::LeftShift:
+ case Settings::NativeKeyboard::RightShift:
+ device_status.keyboard_moddifier_state.shift.Assign(current_status.value);
+ break;
+ case Settings::NativeKeyboard::LeftAlt:
+ device_status.keyboard_moddifier_state.left_alt.Assign(current_status.value);
+ break;
+ case Settings::NativeKeyboard::RightAlt:
+ device_status.keyboard_moddifier_state.right_alt.Assign(current_status.value);
+ break;
+ case Settings::NativeKeyboard::CapsLock:
+ device_status.keyboard_moddifier_state.caps_lock.Assign(current_status.value);
+ break;
+ case Settings::NativeKeyboard::ScrollLock:
+ device_status.keyboard_moddifier_state.scroll_lock.Assign(current_status.value);
+ break;
+ case Settings::NativeKeyboard::NumLock:
+ device_status.keyboard_moddifier_state.num_lock.Assign(current_status.value);
+ break;
+ }
+
+ lock.unlock();
+ TriggerOnChange(DeviceTriggerType::KeyboardModdifier);
+}
+
+void EmulatedDevices::SetMouseButton(const Common::Input::CallbackStatus& callback,
+ std::size_t index) {
+ if (index >= device_status.mouse_button_values.size()) {
+ return;
+ }
+ std::unique_lock lock{mutex};
+ bool value_changed = false;
+ const auto new_status = TransformToButton(callback);
+ auto& current_status = device_status.mouse_button_values[index];
+ current_status.toggle = new_status.toggle;
+
+ // Update button status with current
+ if (!current_status.toggle) {
+ current_status.locked = false;
+ if (current_status.value != new_status.value) {
+ current_status.value = new_status.value;
+ value_changed = true;
+ }
+ } else {
+ // Toggle button and lock status
+ if (new_status.value && !current_status.locked) {
+ current_status.locked = true;
+ current_status.value = !current_status.value;
+ value_changed = true;
+ }
+
+ // Unlock button ready for next press
+ if (!new_status.value && current_status.locked) {
+ current_status.locked = false;
+ }
+ }
+
+ if (!value_changed) {
+ return;
+ }
+
+ if (is_configuring) {
+ lock.unlock();
+ TriggerOnChange(DeviceTriggerType::Mouse);
+ return;
+ }
+
+ switch (index) {
+ case Settings::NativeMouseButton::Left:
+ device_status.mouse_button_state.left.Assign(current_status.value);
+ break;
+ case Settings::NativeMouseButton::Right:
+ device_status.mouse_button_state.right.Assign(current_status.value);
+ break;
+ case Settings::NativeMouseButton::Middle:
+ device_status.mouse_button_state.middle.Assign(current_status.value);
+ break;
+ case Settings::NativeMouseButton::Forward:
+ device_status.mouse_button_state.forward.Assign(current_status.value);
+ break;
+ case Settings::NativeMouseButton::Back:
+ device_status.mouse_button_state.back.Assign(current_status.value);
+ break;
+ }
+
+ lock.unlock();
+ TriggerOnChange(DeviceTriggerType::Mouse);
+}
+
+void EmulatedDevices::SetMouseWheel(const Common::Input::CallbackStatus& callback,
+ std::size_t index) {
+ if (index >= device_status.mouse_wheel_values.size()) {
+ return;
+ }
+ std::unique_lock lock{mutex};
+ const auto analog_value = TransformToAnalog(callback);
+
+ device_status.mouse_wheel_values[index] = analog_value;
+
+ if (is_configuring) {
+ device_status.mouse_wheel_state = {};
+ lock.unlock();
+ TriggerOnChange(DeviceTriggerType::Mouse);
+ return;
+ }
+
+ switch (index) {
+ case Settings::NativeMouseWheel::X:
+ device_status.mouse_wheel_state.x = static_cast<s32>(analog_value.value);
+ break;
+ case Settings::NativeMouseWheel::Y:
+ device_status.mouse_wheel_state.y = static_cast<s32>(analog_value.value);
+ break;
+ }
+
+ lock.unlock();
+ TriggerOnChange(DeviceTriggerType::Mouse);
+}
+
+void EmulatedDevices::SetMousePosition(const Common::Input::CallbackStatus& callback) {
+ std::unique_lock lock{mutex};
+ const auto touch_value = TransformToTouch(callback);
+
+ device_status.mouse_stick_value = touch_value;
+
+ if (is_configuring) {
+ device_status.mouse_position_state = {};
+ lock.unlock();
+ TriggerOnChange(DeviceTriggerType::Mouse);
+ return;
+ }
+
+ device_status.mouse_position_state.x = touch_value.x.value;
+ device_status.mouse_position_state.y = touch_value.y.value;
+
+ lock.unlock();
+ TriggerOnChange(DeviceTriggerType::Mouse);
+}
+
+KeyboardValues EmulatedDevices::GetKeyboardValues() const {
+ std::scoped_lock lock{mutex};
+ return device_status.keyboard_values;
+}
+
+KeyboardModifierValues EmulatedDevices::GetKeyboardModdifierValues() const {
+ std::scoped_lock lock{mutex};
+ return device_status.keyboard_moddifier_values;
+}
+
+MouseButtonValues EmulatedDevices::GetMouseButtonsValues() const {
+ std::scoped_lock lock{mutex};
+ return device_status.mouse_button_values;
+}
+
+KeyboardKey EmulatedDevices::GetKeyboard() const {
+ std::scoped_lock lock{mutex};
+ return device_status.keyboard_state;
+}
+
+KeyboardModifier EmulatedDevices::GetKeyboardModifier() const {
+ std::scoped_lock lock{mutex};
+ return device_status.keyboard_moddifier_state;
+}
+
+MouseButton EmulatedDevices::GetMouseButtons() const {
+ std::scoped_lock lock{mutex};
+ return device_status.mouse_button_state;
+}
+
+MousePosition EmulatedDevices::GetMousePosition() const {
+ std::scoped_lock lock{mutex};
+ return device_status.mouse_position_state;
+}
+
+AnalogStickState EmulatedDevices::GetMouseWheel() const {
+ std::scoped_lock lock{mutex};
+ return device_status.mouse_wheel_state;
+}
+
+void EmulatedDevices::TriggerOnChange(DeviceTriggerType type) {
+ std::scoped_lock lock{callback_mutex};
+ for (const auto& poller_pair : callback_list) {
+ const InterfaceUpdateCallback& poller = poller_pair.second;
+ if (poller.on_change) {
+ poller.on_change(type);
+ }
+ }
+}
+
+int EmulatedDevices::SetCallback(InterfaceUpdateCallback update_callback) {
+ std::scoped_lock lock{callback_mutex};
+ callback_list.insert_or_assign(last_callback_key, std::move(update_callback));
+ return last_callback_key++;
+}
+
+void EmulatedDevices::DeleteCallback(int key) {
+ std::scoped_lock lock{callback_mutex};
+ const auto& iterator = callback_list.find(key);
+ if (iterator == callback_list.end()) {
+ LOG_ERROR(Input, "Tried to delete non-existent callback {}", key);
+ return;
+ }
+ callback_list.erase(iterator);
+}
+} // namespace Core::HID
diff --git a/src/hid_core/frontend/emulated_devices.h b/src/hid_core/frontend/emulated_devices.h
new file mode 100644
index 000000000..b2e57318c
--- /dev/null
+++ b/src/hid_core/frontend/emulated_devices.h
@@ -0,0 +1,212 @@
+// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <array>
+#include <functional>
+#include <memory>
+#include <mutex>
+#include <unordered_map>
+#include <vector>
+
+#include "common/common_types.h"
+#include "common/input.h"
+#include "common/param_package.h"
+#include "common/settings.h"
+#include "hid_core/hid_types.h"
+
+namespace Core::HID {
+using KeyboardDevices = std::array<std::unique_ptr<Common::Input::InputDevice>,
+ Settings::NativeKeyboard::NumKeyboardKeys>;
+using KeyboardModifierDevices = std::array<std::unique_ptr<Common::Input::InputDevice>,
+ Settings::NativeKeyboard::NumKeyboardMods>;
+using MouseButtonDevices = std::array<std::unique_ptr<Common::Input::InputDevice>,
+ Settings::NativeMouseButton::NumMouseButtons>;
+using MouseWheelDevices = std::array<std::unique_ptr<Common::Input::InputDevice>,
+ Settings::NativeMouseWheel::NumMouseWheels>;
+using MouseStickDevice = std::unique_ptr<Common::Input::InputDevice>;
+
+using MouseButtonParams =
+ std::array<Common::ParamPackage, Settings::NativeMouseButton::NumMouseButtons>;
+
+using KeyboardValues =
+ std::array<Common::Input::ButtonStatus, Settings::NativeKeyboard::NumKeyboardKeys>;
+using KeyboardModifierValues =
+ std::array<Common::Input::ButtonStatus, Settings::NativeKeyboard::NumKeyboardMods>;
+using MouseButtonValues =
+ std::array<Common::Input::ButtonStatus, Settings::NativeMouseButton::NumMouseButtons>;
+using MouseWheelValues =
+ std::array<Common::Input::AnalogStatus, Settings::NativeMouseWheel::NumMouseWheels>;
+using MouseStickValue = Common::Input::TouchStatus;
+
+struct MousePosition {
+ f32 x;
+ f32 y;
+};
+
+struct DeviceStatus {
+ // Data from input_common
+ KeyboardValues keyboard_values{};
+ KeyboardModifierValues keyboard_moddifier_values{};
+ MouseButtonValues mouse_button_values{};
+ MouseWheelValues mouse_wheel_values{};
+ MouseStickValue mouse_stick_value{};
+
+ // Data for HID services
+ KeyboardKey keyboard_state{};
+ KeyboardModifier keyboard_moddifier_state{};
+ MouseButton mouse_button_state{};
+ MousePosition mouse_position_state{};
+ AnalogStickState mouse_wheel_state{};
+};
+
+enum class DeviceTriggerType {
+ Keyboard,
+ KeyboardModdifier,
+ Mouse,
+ RingController,
+};
+
+struct InterfaceUpdateCallback {
+ std::function<void(DeviceTriggerType)> on_change;
+};
+
+class EmulatedDevices {
+public:
+ /**
+ * Contains all input data related to external devices that aren't necessarily a controller
+ * This includes devices such as the keyboard or mouse
+ */
+ explicit EmulatedDevices();
+ ~EmulatedDevices();
+
+ YUZU_NON_COPYABLE(EmulatedDevices);
+ YUZU_NON_MOVEABLE(EmulatedDevices);
+
+ /// Removes all callbacks created from input devices
+ void UnloadInput();
+
+ /**
+ * Sets the emulated devices into configuring mode
+ * This prevents the modification of the HID state of the emulated devices by input commands
+ */
+ void EnableConfiguration();
+
+ /// Returns the emulated devices into normal mode, allowing the modification of the HID state
+ void DisableConfiguration();
+
+ /// Returns true if the emulated device is in configuring mode
+ bool IsConfiguring() const;
+
+ /// Reload all input devices
+ void ReloadInput();
+
+ /// Overrides current mapped devices with the stored configuration and reloads all input devices
+ void ReloadFromSettings();
+
+ /// Saves the current mapped configuration
+ void SaveCurrentConfig();
+
+ /// Reverts any mapped changes made that weren't saved
+ void RestoreConfig();
+
+ /// Returns the latest status of button input from the keyboard with parameters
+ KeyboardValues GetKeyboardValues() const;
+
+ /// Returns the latest status of button input from the keyboard modifiers with parameters
+ KeyboardModifierValues GetKeyboardModdifierValues() const;
+
+ /// Returns the latest status of button input from the mouse with parameters
+ MouseButtonValues GetMouseButtonsValues() const;
+
+ /// Returns the latest status of button input from the keyboard
+ KeyboardKey GetKeyboard() const;
+
+ /// Returns the latest status of button input from the keyboard modifiers
+ KeyboardModifier GetKeyboardModifier() const;
+
+ /// Returns the latest status of button input from the mouse
+ MouseButton GetMouseButtons() const;
+
+ /// Returns the latest mouse coordinates
+ MousePosition GetMousePosition() const;
+
+ /// Returns the latest mouse wheel change
+ AnalogStickState GetMouseWheel() const;
+
+ /**
+ * Adds a callback to the list of events
+ * @param update_callback InterfaceUpdateCallback that will be triggered
+ * @return an unique key corresponding to the callback index in the list
+ */
+ int SetCallback(InterfaceUpdateCallback update_callback);
+
+ /**
+ * Removes a callback from the list stopping any future events to this object
+ * @param key Key corresponding to the callback index in the list
+ */
+ void DeleteCallback(int key);
+
+private:
+ /// Helps assigning a value to keyboard_state
+ void UpdateKey(std::size_t key_index, bool status);
+
+ /**
+ * Updates the touch status of the keyboard device
+ * @param callback A CallbackStatus containing the key status
+ * @param index key ID to be updated
+ */
+ void SetKeyboardButton(const Common::Input::CallbackStatus& callback, std::size_t index);
+
+ /**
+ * Updates the keyboard status of the keyboard device
+ * @param callback A CallbackStatus containing the modifier key status
+ * @param index modifier key ID to be updated
+ */
+ void SetKeyboardModifier(const Common::Input::CallbackStatus& callback, std::size_t index);
+
+ /**
+ * Updates the mouse button status of the mouse device
+ * @param callback A CallbackStatus containing the button status
+ * @param index Button ID to be updated
+ */
+ void SetMouseButton(const Common::Input::CallbackStatus& callback, std::size_t index);
+
+ /**
+ * Updates the mouse wheel status of the mouse device
+ * @param callback A CallbackStatus containing the wheel status
+ * @param index wheel ID to be updated
+ */
+ void SetMouseWheel(const Common::Input::CallbackStatus& callback, std::size_t index);
+
+ /**
+ * Updates the mouse position status of the mouse device
+ * @param callback A CallbackStatus containing the position status
+ */
+ void SetMousePosition(const Common::Input::CallbackStatus& callback);
+
+ /**
+ * Triggers a callback that something has changed on the device status
+ * @param type Input type of the event to trigger
+ */
+ void TriggerOnChange(DeviceTriggerType type);
+
+ bool is_configuring{false};
+
+ KeyboardDevices keyboard_devices;
+ KeyboardModifierDevices keyboard_modifier_devices;
+ MouseButtonDevices mouse_button_devices;
+ MouseWheelDevices mouse_wheel_devices;
+ MouseStickDevice mouse_stick_device;
+
+ mutable std::mutex mutex;
+ mutable std::mutex callback_mutex;
+ std::unordered_map<int, InterfaceUpdateCallback> callback_list;
+ int last_callback_key = 0;
+
+ // Stores the current status of all external device input
+ DeviceStatus device_status;
+};
+
+} // namespace Core::HID
diff --git a/src/hid_core/frontend/input_converter.cpp b/src/hid_core/frontend/input_converter.cpp
new file mode 100644
index 000000000..f245a3f76
--- /dev/null
+++ b/src/hid_core/frontend/input_converter.cpp
@@ -0,0 +1,436 @@
+// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <algorithm>
+#include <random>
+
+#include "common/input.h"
+#include "hid_core/frontend/input_converter.h"
+
+namespace Core::HID {
+
+Common::Input::BatteryStatus TransformToBattery(const Common::Input::CallbackStatus& callback) {
+ Common::Input::BatteryStatus battery{Common::Input::BatteryStatus::None};
+ switch (callback.type) {
+ case Common::Input::InputType::Analog:
+ case Common::Input::InputType::Trigger: {
+ const auto value = TransformToTrigger(callback).analog.value;
+ battery = Common::Input::BatteryLevel::Empty;
+ if (value > 0.2f) {
+ battery = Common::Input::BatteryLevel::Critical;
+ }
+ if (value > 0.4f) {
+ battery = Common::Input::BatteryLevel::Low;
+ }
+ if (value > 0.6f) {
+ battery = Common::Input::BatteryLevel::Medium;
+ }
+ if (value > 0.8f) {
+ battery = Common::Input::BatteryLevel::Full;
+ }
+ if (value >= 0.95f) {
+ battery = Common::Input::BatteryLevel::Charging;
+ }
+ break;
+ }
+ case Common::Input::InputType::Button:
+ battery = callback.button_status.value ? Common::Input::BatteryLevel::Charging
+ : Common::Input::BatteryLevel::Critical;
+ break;
+ case Common::Input::InputType::Battery:
+ battery = callback.battery_status;
+ break;
+ default:
+ LOG_ERROR(Input, "Conversion from type {} to battery not implemented", callback.type);
+ break;
+ }
+
+ return battery;
+}
+
+Common::Input::ButtonStatus TransformToButton(const Common::Input::CallbackStatus& callback) {
+ Common::Input::ButtonStatus status{};
+ switch (callback.type) {
+ case Common::Input::InputType::Analog:
+ status.value = TransformToTrigger(callback).pressed.value;
+ status.toggle = callback.analog_status.properties.toggle;
+ status.inverted = callback.analog_status.properties.inverted_button;
+ break;
+ case Common::Input::InputType::Trigger:
+ status.value = TransformToTrigger(callback).pressed.value;
+ break;
+ case Common::Input::InputType::Button:
+ status = callback.button_status;
+ break;
+ case Common::Input::InputType::Motion:
+ status.value = std::abs(callback.motion_status.gyro.x.raw_value) > 1.0f;
+ break;
+ default:
+ LOG_ERROR(Input, "Conversion from type {} to button not implemented", callback.type);
+ break;
+ }
+
+ if (status.inverted) {
+ status.value = !status.value;
+ }
+
+ return status;
+}
+
+Common::Input::MotionStatus TransformToMotion(const Common::Input::CallbackStatus& callback) {
+ Common::Input::MotionStatus status{};
+ switch (callback.type) {
+ case Common::Input::InputType::Button: {
+ Common::Input::AnalogProperties properties{
+ .deadzone = 0.0f,
+ .range = 1.0f,
+ .offset = 0.0f,
+ };
+ status.delta_timestamp = 1000;
+ status.force_update = true;
+ status.accel.x = {
+ .value = 0.0f,
+ .raw_value = 0.0f,
+ .properties = properties,
+ };
+ status.accel.y = {
+ .value = 0.0f,
+ .raw_value = 0.0f,
+ .properties = properties,
+ };
+ status.accel.z = {
+ .value = 0.0f,
+ .raw_value = -1.0f,
+ .properties = properties,
+ };
+ status.gyro.x = {
+ .value = 0.0f,
+ .raw_value = 0.0f,
+ .properties = properties,
+ };
+ status.gyro.y = {
+ .value = 0.0f,
+ .raw_value = 0.0f,
+ .properties = properties,
+ };
+ status.gyro.z = {
+ .value = 0.0f,
+ .raw_value = 0.0f,
+ .properties = properties,
+ };
+ if (TransformToButton(callback).value) {
+ std::random_device device;
+ std::mt19937 gen(device());
+ std::uniform_int_distribution<s16> distribution(-5000, 5000);
+ status.accel.x.raw_value = static_cast<f32>(distribution(gen)) * 0.001f;
+ status.accel.y.raw_value = static_cast<f32>(distribution(gen)) * 0.001f;
+ status.accel.z.raw_value = static_cast<f32>(distribution(gen)) * 0.001f;
+ status.gyro.x.raw_value = static_cast<f32>(distribution(gen)) * 0.001f;
+ status.gyro.y.raw_value = static_cast<f32>(distribution(gen)) * 0.001f;
+ status.gyro.z.raw_value = static_cast<f32>(distribution(gen)) * 0.001f;
+ }
+ break;
+ }
+ case Common::Input::InputType::Motion:
+ status = callback.motion_status;
+ break;
+ default:
+ LOG_ERROR(Input, "Conversion from type {} to motion not implemented", callback.type);
+ break;
+ }
+ SanitizeAnalog(status.accel.x, false);
+ SanitizeAnalog(status.accel.y, false);
+ SanitizeAnalog(status.accel.z, false);
+ SanitizeAnalog(status.gyro.x, false);
+ SanitizeAnalog(status.gyro.y, false);
+ SanitizeAnalog(status.gyro.z, false);
+
+ return status;
+}
+
+Common::Input::StickStatus TransformToStick(const Common::Input::CallbackStatus& callback) {
+ Common::Input::StickStatus status{};
+
+ switch (callback.type) {
+ case Common::Input::InputType::Stick:
+ status = callback.stick_status;
+ break;
+ default:
+ LOG_ERROR(Input, "Conversion from type {} to stick not implemented", callback.type);
+ break;
+ }
+
+ SanitizeStick(status.x, status.y, true);
+ const auto& properties_x = status.x.properties;
+ const auto& properties_y = status.y.properties;
+ const float x = status.x.value;
+ const float y = status.y.value;
+
+ // Set directional buttons
+ status.right = x > properties_x.threshold;
+ status.left = x < -properties_x.threshold;
+ status.up = y > properties_y.threshold;
+ status.down = y < -properties_y.threshold;
+
+ return status;
+}
+
+Common::Input::TouchStatus TransformToTouch(const Common::Input::CallbackStatus& callback) {
+ Common::Input::TouchStatus status{};
+
+ switch (callback.type) {
+ case Common::Input::InputType::Touch:
+ status = callback.touch_status;
+ break;
+ case Common::Input::InputType::Stick:
+ status.x = callback.stick_status.x;
+ status.y = callback.stick_status.y;
+ break;
+ default:
+ LOG_ERROR(Input, "Conversion from type {} to touch not implemented", callback.type);
+ break;
+ }
+
+ SanitizeAnalog(status.x, true);
+ SanitizeAnalog(status.y, true);
+ float& x = status.x.value;
+ float& y = status.y.value;
+
+ // Adjust if value is inverted
+ x = status.x.properties.inverted ? 1.0f + x : x;
+ y = status.y.properties.inverted ? 1.0f + y : y;
+
+ // clamp value
+ x = std::clamp(x, 0.0f, 1.0f);
+ y = std::clamp(y, 0.0f, 1.0f);
+
+ if (status.pressed.inverted) {
+ status.pressed.value = !status.pressed.value;
+ }
+
+ return status;
+}
+
+Common::Input::TriggerStatus TransformToTrigger(const Common::Input::CallbackStatus& callback) {
+ Common::Input::TriggerStatus status{};
+ float& raw_value = status.analog.raw_value;
+ bool calculate_button_value = true;
+
+ switch (callback.type) {
+ case Common::Input::InputType::Analog:
+ status.analog.properties = callback.analog_status.properties;
+ raw_value = callback.analog_status.raw_value;
+ break;
+ case Common::Input::InputType::Button:
+ status.analog.properties.range = 1.0f;
+ status.analog.properties.inverted = callback.button_status.inverted;
+ raw_value = callback.button_status.value ? 1.0f : 0.0f;
+ break;
+ case Common::Input::InputType::Trigger:
+ status = callback.trigger_status;
+ calculate_button_value = false;
+ break;
+ case Common::Input::InputType::Motion:
+ status.analog.properties.range = 1.0f;
+ raw_value = callback.motion_status.accel.x.raw_value;
+ break;
+ default:
+ LOG_ERROR(Input, "Conversion from type {} to trigger not implemented", callback.type);
+ break;
+ }
+
+ SanitizeAnalog(status.analog, true);
+ const auto& properties = status.analog.properties;
+ float& value = status.analog.value;
+
+ // Set button status
+ if (calculate_button_value) {
+ status.pressed.value = value > properties.threshold;
+ }
+
+ // Adjust if value is inverted
+ value = properties.inverted ? 1.0f + value : value;
+
+ // clamp value
+ value = std::clamp(value, 0.0f, 1.0f);
+
+ return status;
+}
+
+Common::Input::AnalogStatus TransformToAnalog(const Common::Input::CallbackStatus& callback) {
+ Common::Input::AnalogStatus status{};
+
+ switch (callback.type) {
+ case Common::Input::InputType::Analog:
+ status.properties = callback.analog_status.properties;
+ status.raw_value = callback.analog_status.raw_value;
+ break;
+ default:
+ LOG_ERROR(Input, "Conversion from type {} to analog not implemented", callback.type);
+ break;
+ }
+
+ SanitizeAnalog(status, false);
+
+ // Adjust if value is inverted
+ status.value = status.properties.inverted ? -status.value : status.value;
+
+ return status;
+}
+
+Common::Input::CameraStatus TransformToCamera(const Common::Input::CallbackStatus& callback) {
+ Common::Input::CameraStatus camera{};
+ switch (callback.type) {
+ case Common::Input::InputType::IrSensor:
+ camera = {
+ .format = callback.camera_status,
+ .data = callback.raw_data,
+ };
+ break;
+ default:
+ LOG_ERROR(Input, "Conversion from type {} to camera not implemented", callback.type);
+ break;
+ }
+
+ return camera;
+}
+
+Common::Input::NfcStatus TransformToNfc(const Common::Input::CallbackStatus& callback) {
+ Common::Input::NfcStatus nfc{};
+ switch (callback.type) {
+ case Common::Input::InputType::Nfc:
+ return callback.nfc_status;
+ default:
+ LOG_ERROR(Input, "Conversion from type {} to NFC not implemented", callback.type);
+ break;
+ }
+
+ return nfc;
+}
+
+Common::Input::BodyColorStatus TransformToColor(const Common::Input::CallbackStatus& callback) {
+ switch (callback.type) {
+ case Common::Input::InputType::Color:
+ return callback.color_status;
+ break;
+ default:
+ LOG_ERROR(Input, "Conversion from type {} to color not implemented", callback.type);
+ return {};
+ break;
+ }
+}
+
+void SanitizeAnalog(Common::Input::AnalogStatus& analog, bool clamp_value) {
+ const auto& properties = analog.properties;
+ float& raw_value = analog.raw_value;
+ float& value = analog.value;
+
+ if (!std::isnormal(raw_value)) {
+ raw_value = 0;
+ }
+
+ // Apply center offset
+ raw_value -= properties.offset;
+
+ // Set initial values to be formatted
+ value = raw_value;
+
+ // Calculate vector size
+ const float r = std::abs(value);
+
+ // Return zero if value is smaller than the deadzone
+ if (r <= properties.deadzone || properties.deadzone == 1.0f) {
+ analog.value = 0;
+ return;
+ }
+
+ // Adjust range of value
+ const float deadzone_factor =
+ 1.0f / r * (r - properties.deadzone) / (1.0f - properties.deadzone);
+ value = value * deadzone_factor / properties.range;
+
+ // Invert direction if needed
+ if (properties.inverted) {
+ value = -value;
+ }
+
+ // Clamp value
+ if (clamp_value) {
+ value = std::clamp(value, -1.0f, 1.0f);
+ }
+}
+
+void SanitizeStick(Common::Input::AnalogStatus& analog_x, Common::Input::AnalogStatus& analog_y,
+ bool clamp_value) {
+ const auto& properties_x = analog_x.properties;
+ const auto& properties_y = analog_y.properties;
+ float& raw_x = analog_x.raw_value;
+ float& raw_y = analog_y.raw_value;
+ float& x = analog_x.value;
+ float& y = analog_y.value;
+
+ if (!std::isnormal(raw_x)) {
+ raw_x = 0;
+ }
+ if (!std::isnormal(raw_y)) {
+ raw_y = 0;
+ }
+
+ // Apply center offset
+ raw_x += properties_x.offset;
+ raw_y += properties_y.offset;
+
+ // Apply X scale correction from offset
+ if (std::abs(properties_x.offset) < 0.75f) {
+ if (raw_x > 0) {
+ raw_x /= 1 + properties_x.offset;
+ } else {
+ raw_x /= 1 - properties_x.offset;
+ }
+ }
+
+ // Apply Y scale correction from offset
+ if (std::abs(properties_y.offset) < 0.75f) {
+ if (raw_y > 0) {
+ raw_y /= 1 + properties_y.offset;
+ } else {
+ raw_y /= 1 - properties_y.offset;
+ }
+ }
+
+ // Invert direction if needed
+ raw_x = properties_x.inverted ? -raw_x : raw_x;
+ raw_y = properties_y.inverted ? -raw_y : raw_y;
+
+ // Set initial values to be formatted
+ x = raw_x;
+ y = raw_y;
+
+ // Calculate vector size
+ float r = x * x + y * y;
+ r = std::sqrt(r);
+
+ // TODO(German77): Use deadzone and range of both axis
+
+ // Return zero if values are smaller than the deadzone
+ if (r <= properties_x.deadzone || properties_x.deadzone >= 1.0f) {
+ x = 0;
+ y = 0;
+ return;
+ }
+
+ // Adjust range of joystick
+ const float deadzone_factor =
+ 1.0f / r * (r - properties_x.deadzone) / (1.0f - properties_x.deadzone);
+ x = x * deadzone_factor / properties_x.range;
+ y = y * deadzone_factor / properties_x.range;
+ r = r * deadzone_factor / properties_x.range;
+
+ // Normalize joystick
+ if (clamp_value && r > 1.0f) {
+ x /= r;
+ y /= r;
+ }
+}
+
+} // namespace Core::HID
diff --git a/src/hid_core/frontend/input_converter.h b/src/hid_core/frontend/input_converter.h
new file mode 100644
index 000000000..c51c03e57
--- /dev/null
+++ b/src/hid_core/frontend/input_converter.h
@@ -0,0 +1,119 @@
+// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+namespace Common::Input {
+struct CallbackStatus;
+enum class BatteryLevel : u32;
+using BatteryStatus = BatteryLevel;
+struct AnalogStatus;
+struct ButtonStatus;
+struct MotionStatus;
+struct StickStatus;
+struct TouchStatus;
+struct TriggerStatus;
+}; // namespace Common::Input
+
+namespace Core::HID {
+
+/**
+ * Converts raw input data into a valid battery status.
+ *
+ * @param callback Supported callbacks: Analog, Battery, Trigger.
+ * @return A valid BatteryStatus object.
+ */
+Common::Input::BatteryStatus TransformToBattery(const Common::Input::CallbackStatus& callback);
+
+/**
+ * Converts raw input data into a valid button status. Applies invert properties to the output.
+ *
+ * @param callback Supported callbacks: Analog, Button, Trigger.
+ * @return A valid TouchStatus object.
+ */
+Common::Input::ButtonStatus TransformToButton(const Common::Input::CallbackStatus& callback);
+
+/**
+ * Converts raw input data into a valid motion status.
+ *
+ * @param callback Supported callbacks: Motion.
+ * @return A valid TouchStatus object.
+ */
+Common::Input::MotionStatus TransformToMotion(const Common::Input::CallbackStatus& callback);
+
+/**
+ * Converts raw input data into a valid stick status. Applies offset, deadzone, range and invert
+ * properties to the output.
+ *
+ * @param callback Supported callbacks: Stick.
+ * @return A valid StickStatus object.
+ */
+Common::Input::StickStatus TransformToStick(const Common::Input::CallbackStatus& callback);
+
+/**
+ * Converts raw input data into a valid touch status.
+ *
+ * @param callback Supported callbacks: Touch.
+ * @return A valid TouchStatus object.
+ */
+Common::Input::TouchStatus TransformToTouch(const Common::Input::CallbackStatus& callback);
+
+/**
+ * Converts raw input data into a valid trigger status. Applies offset, deadzone, range and
+ * invert properties to the output. Button status uses the threshold property if necessary.
+ *
+ * @param callback Supported callbacks: Analog, Button, Trigger.
+ * @return A valid TriggerStatus object.
+ */
+Common::Input::TriggerStatus TransformToTrigger(const Common::Input::CallbackStatus& callback);
+
+/**
+ * Converts raw input data into a valid analog status. Applies offset, deadzone, range and
+ * invert properties to the output.
+ *
+ * @param callback Supported callbacks: Analog.
+ * @return A valid AnalogStatus object.
+ */
+Common::Input::AnalogStatus TransformToAnalog(const Common::Input::CallbackStatus& callback);
+
+/**
+ * Converts raw input data into a valid camera status.
+ *
+ * @param callback Supported callbacks: Camera.
+ * @return A valid CameraObject object.
+ */
+Common::Input::CameraStatus TransformToCamera(const Common::Input::CallbackStatus& callback);
+
+/**
+ * Converts raw input data into a valid nfc status.
+ *
+ * @param callback Supported callbacks: Nfc.
+ * @return A valid data tag vector.
+ */
+Common::Input::NfcStatus TransformToNfc(const Common::Input::CallbackStatus& callback);
+
+/**
+ * Converts raw input data into a valid color status.
+ *
+ * @param callback Supported callbacks: Color.
+ * @return A valid Color object.
+ */
+Common::Input::BodyColorStatus TransformToColor(const Common::Input::CallbackStatus& callback);
+
+/**
+ * Converts raw analog data into a valid analog value
+ * @param analog An analog object containing raw data and properties
+ * @param clamp_value determines if the value needs to be clamped between -1.0f and 1.0f.
+ */
+void SanitizeAnalog(Common::Input::AnalogStatus& analog, bool clamp_value);
+
+/**
+ * Converts raw stick data into a valid stick value
+ * @param analog_x raw analog data and properties for the x-axis
+ * @param analog_y raw analog data and properties for the y-axis
+ * @param clamp_value bool that determines if the value needs to be clamped into the unit circle.
+ */
+void SanitizeStick(Common::Input::AnalogStatus& analog_x, Common::Input::AnalogStatus& analog_y,
+ bool clamp_value);
+
+} // namespace Core::HID
diff --git a/src/hid_core/frontend/input_interpreter.cpp b/src/hid_core/frontend/input_interpreter.cpp
new file mode 100644
index 000000000..b6c8d8c5d
--- /dev/null
+++ b/src/hid_core/frontend/input_interpreter.cpp
@@ -0,0 +1,64 @@
+// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "core/core.h"
+#include "core/hle/service/hid/hid_server.h"
+#include "core/hle/service/sm/sm.h"
+#include "hid_core/frontend/input_interpreter.h"
+#include "hid_core/hid_types.h"
+#include "hid_core/resource_manager.h"
+#include "hid_core/resources/npad/npad.h"
+
+InputInterpreter::InputInterpreter(Core::System& system)
+ : npad{system.ServiceManager()
+ .GetService<Service::HID::IHidServer>("hid")
+ ->GetResourceManager()
+ ->GetNpad()} {
+ ResetButtonStates();
+}
+
+InputInterpreter::~InputInterpreter() = default;
+
+void InputInterpreter::PollInput() {
+ if (npad == nullptr) {
+ return;
+ }
+ const auto button_state = npad->GetAndResetPressState();
+
+ previous_index = current_index;
+ current_index = (current_index + 1) % button_states.size();
+
+ button_states[current_index] = button_state;
+}
+
+void InputInterpreter::ResetButtonStates() {
+ previous_index = 0;
+ current_index = 0;
+
+ button_states[0] = Core::HID::NpadButton::All;
+
+ for (std::size_t i = 1; i < button_states.size(); ++i) {
+ button_states[i] = Core::HID::NpadButton::None;
+ }
+}
+
+bool InputInterpreter::IsButtonPressed(Core::HID::NpadButton button) const {
+ return True(button_states[current_index] & button);
+}
+
+bool InputInterpreter::IsButtonPressedOnce(Core::HID::NpadButton button) const {
+ const bool current_press = True(button_states[current_index] & button);
+ const bool previous_press = True(button_states[previous_index] & button);
+
+ return current_press && !previous_press;
+}
+
+bool InputInterpreter::IsButtonHeld(Core::HID::NpadButton button) const {
+ Core::HID::NpadButton held_buttons{button_states[0]};
+
+ for (std::size_t i = 1; i < button_states.size(); ++i) {
+ held_buttons &= button_states[i];
+ }
+
+ return True(held_buttons & button);
+}
diff --git a/src/hid_core/frontend/input_interpreter.h b/src/hid_core/frontend/input_interpreter.h
new file mode 100644
index 000000000..3569aac93
--- /dev/null
+++ b/src/hid_core/frontend/input_interpreter.h
@@ -0,0 +1,111 @@
+// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <array>
+
+#include "common/common_types.h"
+
+namespace Core {
+class System;
+}
+
+namespace Core::HID {
+enum class NpadButton : u64;
+}
+
+namespace Service::HID {
+class NPad;
+}
+
+/**
+ * The InputInterpreter class interfaces with HID to retrieve button press states.
+ * Input is intended to be polled every 50ms so that a button is considered to be
+ * held down after 400ms has elapsed since the initial button press and subsequent
+ * repeated presses occur every 50ms.
+ */
+class InputInterpreter {
+public:
+ explicit InputInterpreter(Core::System& system);
+ virtual ~InputInterpreter();
+
+ /// Gets a button state from HID and inserts it into the array of button states.
+ void PollInput();
+
+ /// Resets all the button states to their defaults.
+ void ResetButtonStates();
+
+ /**
+ * Checks whether the button is pressed.
+ *
+ * @param button The button to check.
+ *
+ * @returns True when the button is pressed.
+ */
+ [[nodiscard]] bool IsButtonPressed(Core::HID::NpadButton button) const;
+
+ /**
+ * Checks whether any of the buttons in the parameter list is pressed.
+ *
+ * @tparam HIDButton The buttons to check.
+ *
+ * @returns True when at least one of the buttons is pressed.
+ */
+ template <Core::HID::NpadButton... T>
+ [[nodiscard]] bool IsAnyButtonPressed() {
+ return (IsButtonPressed(T) || ...);
+ }
+
+ /**
+ * The specified button is considered to be pressed once
+ * if it is currently pressed and not pressed previously.
+ *
+ * @param button The button to check.
+ *
+ * @returns True when the button is pressed once.
+ */
+ [[nodiscard]] bool IsButtonPressedOnce(Core::HID::NpadButton button) const;
+
+ /**
+ * Checks whether any of the buttons in the parameter list is pressed once.
+ *
+ * @tparam T The buttons to check.
+ *
+ * @returns True when at least one of the buttons is pressed once.
+ */
+ template <Core::HID::NpadButton... T>
+ [[nodiscard]] bool IsAnyButtonPressedOnce() const {
+ return (IsButtonPressedOnce(T) || ...);
+ }
+
+ /**
+ * The specified button is considered to be held down if it is pressed in all 9 button states.
+ *
+ * @param button The button to check.
+ *
+ * @returns True when the button is held down.
+ */
+ [[nodiscard]] bool IsButtonHeld(Core::HID::NpadButton button) const;
+
+ /**
+ * Checks whether any of the buttons in the parameter list is held down.
+ *
+ * @tparam T The buttons to check.
+ *
+ * @returns True when at least one of the buttons is held down.
+ */
+ template <Core::HID::NpadButton... T>
+ [[nodiscard]] bool IsAnyButtonHeld() const {
+ return (IsButtonHeld(T) || ...);
+ }
+
+private:
+ std::shared_ptr<Service::HID::NPad> npad;
+
+ /// Stores 9 consecutive button states polled from HID.
+ std::array<Core::HID::NpadButton, 9> button_states{};
+
+ std::size_t previous_index{};
+ std::size_t current_index{};
+};
diff --git a/src/hid_core/frontend/motion_input.cpp b/src/hid_core/frontend/motion_input.cpp
new file mode 100644
index 000000000..417cd03f9
--- /dev/null
+++ b/src/hid_core/frontend/motion_input.cpp
@@ -0,0 +1,357 @@
+// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <cmath>
+
+#include "common/math_util.h"
+#include "hid_core/frontend/motion_input.h"
+
+namespace Core::HID {
+
+MotionInput::MotionInput() {
+ // Initialize PID constants with default values
+ SetPID(0.3f, 0.005f, 0.0f);
+ SetGyroThreshold(ThresholdStandard);
+ ResetQuaternion();
+ ResetRotations();
+}
+
+void MotionInput::SetPID(f32 new_kp, f32 new_ki, f32 new_kd) {
+ kp = new_kp;
+ ki = new_ki;
+ kd = new_kd;
+}
+
+void MotionInput::SetAcceleration(const Common::Vec3f& acceleration) {
+ accel = acceleration;
+
+ accel.x = std::clamp(accel.x, -AccelMaxValue, AccelMaxValue);
+ accel.y = std::clamp(accel.y, -AccelMaxValue, AccelMaxValue);
+ accel.z = std::clamp(accel.z, -AccelMaxValue, AccelMaxValue);
+}
+
+void MotionInput::SetGyroscope(const Common::Vec3f& gyroscope) {
+ gyro = gyroscope - gyro_bias;
+
+ gyro.x = std::clamp(gyro.x, -GyroMaxValue, GyroMaxValue);
+ gyro.y = std::clamp(gyro.y, -GyroMaxValue, GyroMaxValue);
+ gyro.z = std::clamp(gyro.z, -GyroMaxValue, GyroMaxValue);
+
+ // Auto adjust gyro_bias to minimize drift
+ if (!IsMoving(IsAtRestRelaxed)) {
+ gyro_bias = (gyro_bias * 0.9999f) + (gyroscope * 0.0001f);
+ }
+
+ // Adjust drift when calibration mode is enabled
+ if (calibration_mode) {
+ gyro_bias = (gyro_bias * 0.99f) + (gyroscope * 0.01f);
+ StopCalibration();
+ }
+
+ if (gyro.Length() < gyro_threshold * user_gyro_threshold) {
+ gyro = {};
+ } else {
+ only_accelerometer = false;
+ }
+}
+
+void MotionInput::SetQuaternion(const Common::Quaternion<f32>& quaternion) {
+ quat = quaternion;
+}
+
+void MotionInput::SetEulerAngles(const Common::Vec3f& euler_angles) {
+ const float cr = std::cos(euler_angles.x * 0.5f);
+ const float sr = std::sin(euler_angles.x * 0.5f);
+ const float cp = std::cos(euler_angles.y * 0.5f);
+ const float sp = std::sin(euler_angles.y * 0.5f);
+ const float cy = std::cos(euler_angles.z * 0.5f);
+ const float sy = std::sin(euler_angles.z * 0.5f);
+
+ quat.w = cr * cp * cy + sr * sp * sy;
+ quat.xyz.x = sr * cp * cy - cr * sp * sy;
+ quat.xyz.y = cr * sp * cy + sr * cp * sy;
+ quat.xyz.z = cr * cp * sy - sr * sp * cy;
+}
+
+void MotionInput::SetGyroBias(const Common::Vec3f& bias) {
+ gyro_bias = bias;
+}
+
+void MotionInput::SetGyroThreshold(f32 threshold) {
+ gyro_threshold = threshold;
+}
+
+void MotionInput::SetUserGyroThreshold(f32 threshold) {
+ user_gyro_threshold = threshold / ThresholdStandard;
+}
+
+void MotionInput::EnableReset(bool reset) {
+ reset_enabled = reset;
+}
+
+void MotionInput::ResetRotations() {
+ rotations = {};
+}
+
+void MotionInput::ResetQuaternion() {
+ quat = {{0.0f, 0.0f, -1.0f}, 0.0f};
+}
+
+bool MotionInput::IsMoving(f32 sensitivity) const {
+ return gyro.Length() >= sensitivity || accel.Length() <= 0.9f || accel.Length() >= 1.1f;
+}
+
+bool MotionInput::IsCalibrated(f32 sensitivity) const {
+ return real_error.Length() < sensitivity;
+}
+
+void MotionInput::UpdateRotation(u64 elapsed_time) {
+ const auto sample_period = static_cast<f32>(elapsed_time) / 1000000.0f;
+ if (sample_period > 0.1f) {
+ return;
+ }
+ rotations += gyro * sample_period;
+}
+
+void MotionInput::Calibrate() {
+ calibration_mode = true;
+ calibration_counter = 0;
+}
+
+void MotionInput::StopCalibration() {
+ if (calibration_counter++ > CalibrationSamples) {
+ calibration_mode = false;
+ ResetQuaternion();
+ ResetRotations();
+ }
+}
+
+// Based on Madgwick's implementation of Mayhony's AHRS algorithm.
+// https://github.com/xioTechnologies/Open-Source-AHRS-With-x-IMU/blob/master/x-IMU%20IMU%20and%20AHRS%20Algorithms/x-IMU%20IMU%20and%20AHRS%20Algorithms/AHRS/MahonyAHRS.cs
+void MotionInput::UpdateOrientation(u64 elapsed_time) {
+ if (!IsCalibrated(0.1f)) {
+ ResetOrientation();
+ }
+ // Short name local variable for readability
+ f32 q1 = quat.w;
+ f32 q2 = quat.xyz[0];
+ f32 q3 = quat.xyz[1];
+ f32 q4 = quat.xyz[2];
+ const auto sample_period = static_cast<f32>(elapsed_time) / 1000000.0f;
+
+ // Ignore invalid elapsed time
+ if (sample_period > 0.1f) {
+ return;
+ }
+
+ const auto normal_accel = accel.Normalized();
+ auto rad_gyro = gyro * Common::PI * 2;
+ const f32 swap = rad_gyro.x;
+ rad_gyro.x = rad_gyro.y;
+ rad_gyro.y = -swap;
+ rad_gyro.z = -rad_gyro.z;
+
+ // Clear gyro values if there is no gyro present
+ if (only_accelerometer) {
+ rad_gyro.x = 0;
+ rad_gyro.y = 0;
+ rad_gyro.z = 0;
+ }
+
+ // Ignore drift correction if acceleration is not reliable
+ if (accel.Length() >= 0.75f && accel.Length() <= 1.25f) {
+ const f32 ax = -normal_accel.x;
+ const f32 ay = normal_accel.y;
+ const f32 az = -normal_accel.z;
+
+ // Estimated direction of gravity
+ const f32 vx = 2.0f * (q2 * q4 - q1 * q3);
+ const f32 vy = 2.0f * (q1 * q2 + q3 * q4);
+ const f32 vz = q1 * q1 - q2 * q2 - q3 * q3 + q4 * q4;
+
+ // Error is cross product between estimated direction and measured direction of gravity
+ const Common::Vec3f new_real_error = {
+ az * vx - ax * vz,
+ ay * vz - az * vy,
+ ax * vy - ay * vx,
+ };
+
+ derivative_error = new_real_error - real_error;
+ real_error = new_real_error;
+
+ // Prevent integral windup
+ if (ki != 0.0f && !IsCalibrated(0.05f)) {
+ integral_error += real_error;
+ } else {
+ integral_error = {};
+ }
+
+ // Apply feedback terms
+ if (!only_accelerometer) {
+ rad_gyro += kp * real_error;
+ rad_gyro += ki * integral_error;
+ rad_gyro += kd * derivative_error;
+ } else {
+ // Give more weight to accelerometer values to compensate for the lack of gyro
+ rad_gyro += 35.0f * kp * real_error;
+ rad_gyro += 10.0f * ki * integral_error;
+ rad_gyro += 10.0f * kd * derivative_error;
+
+ // Emulate gyro values for games that need them
+ gyro.x = -rad_gyro.y;
+ gyro.y = rad_gyro.x;
+ gyro.z = -rad_gyro.z;
+ UpdateRotation(elapsed_time);
+ }
+ }
+
+ const f32 gx = rad_gyro.y;
+ const f32 gy = rad_gyro.x;
+ const f32 gz = rad_gyro.z;
+
+ // Integrate rate of change of quaternion
+ const f32 pa = q2;
+ const f32 pb = q3;
+ const f32 pc = q4;
+ q1 = q1 + (-q2 * gx - q3 * gy - q4 * gz) * (0.5f * sample_period);
+ q2 = pa + (q1 * gx + pb * gz - pc * gy) * (0.5f * sample_period);
+ q3 = pb + (q1 * gy - pa * gz + pc * gx) * (0.5f * sample_period);
+ q4 = pc + (q1 * gz + pa * gy - pb * gx) * (0.5f * sample_period);
+
+ quat.w = q1;
+ quat.xyz[0] = q2;
+ quat.xyz[1] = q3;
+ quat.xyz[2] = q4;
+ quat = quat.Normalized();
+}
+
+std::array<Common::Vec3f, 3> MotionInput::GetOrientation() const {
+ const Common::Quaternion<float> quad{
+ .xyz = {-quat.xyz[1], -quat.xyz[0], -quat.w},
+ .w = -quat.xyz[2],
+ };
+ const std::array<float, 16> matrix4x4 = quad.ToMatrix();
+
+ return {Common::Vec3f(matrix4x4[0], matrix4x4[1], -matrix4x4[2]),
+ Common::Vec3f(matrix4x4[4], matrix4x4[5], -matrix4x4[6]),
+ Common::Vec3f(-matrix4x4[8], -matrix4x4[9], matrix4x4[10])};
+}
+
+Common::Vec3f MotionInput::GetAcceleration() const {
+ return accel;
+}
+
+Common::Vec3f MotionInput::GetGyroscope() const {
+ return gyro;
+}
+
+Common::Vec3f MotionInput::GetGyroBias() const {
+ return gyro_bias;
+}
+
+Common::Quaternion<f32> MotionInput::GetQuaternion() const {
+ return quat;
+}
+
+Common::Vec3f MotionInput::GetRotations() const {
+ return rotations;
+}
+
+Common::Vec3f MotionInput::GetEulerAngles() const {
+ // roll (x-axis rotation)
+ const float sinr_cosp = 2 * (quat.w * quat.xyz.x + quat.xyz.y * quat.xyz.z);
+ const float cosr_cosp = 1 - 2 * (quat.xyz.x * quat.xyz.x + quat.xyz.y * quat.xyz.y);
+
+ // pitch (y-axis rotation)
+ const float sinp = std::sqrt(1 + 2 * (quat.w * quat.xyz.y - quat.xyz.x * quat.xyz.z));
+ const float cosp = std::sqrt(1 - 2 * (quat.w * quat.xyz.y - quat.xyz.x * quat.xyz.z));
+
+ // yaw (z-axis rotation)
+ const float siny_cosp = 2 * (quat.w * quat.xyz.z + quat.xyz.x * quat.xyz.y);
+ const float cosy_cosp = 1 - 2 * (quat.xyz.y * quat.xyz.y + quat.xyz.z * quat.xyz.z);
+
+ return {
+ std::atan2(sinr_cosp, cosr_cosp),
+ 2 * std::atan2(sinp, cosp) - Common::PI / 2,
+ std::atan2(siny_cosp, cosy_cosp),
+ };
+}
+
+void MotionInput::ResetOrientation() {
+ if (!reset_enabled || only_accelerometer) {
+ return;
+ }
+ if (!IsMoving(IsAtRestRelaxed) && accel.z <= -0.9f) {
+ ++reset_counter;
+ if (reset_counter > 900) {
+ quat.w = 0;
+ quat.xyz[0] = 0;
+ quat.xyz[1] = 0;
+ quat.xyz[2] = -1;
+ SetOrientationFromAccelerometer();
+ integral_error = {};
+ reset_counter = 0;
+ }
+ } else {
+ reset_counter = 0;
+ }
+}
+
+void MotionInput::SetOrientationFromAccelerometer() {
+ int iterations = 0;
+ const f32 sample_period = 0.015f;
+
+ const auto normal_accel = accel.Normalized();
+
+ while (!IsCalibrated(0.01f) && ++iterations < 100) {
+ // Short name local variable for readability
+ f32 q1 = quat.w;
+ f32 q2 = quat.xyz[0];
+ f32 q3 = quat.xyz[1];
+ f32 q4 = quat.xyz[2];
+
+ Common::Vec3f rad_gyro;
+ const f32 ax = -normal_accel.x;
+ const f32 ay = normal_accel.y;
+ const f32 az = -normal_accel.z;
+
+ // Estimated direction of gravity
+ const f32 vx = 2.0f * (q2 * q4 - q1 * q3);
+ const f32 vy = 2.0f * (q1 * q2 + q3 * q4);
+ const f32 vz = q1 * q1 - q2 * q2 - q3 * q3 + q4 * q4;
+
+ // Error is cross product between estimated direction and measured direction of gravity
+ const Common::Vec3f new_real_error = {
+ az * vx - ax * vz,
+ ay * vz - az * vy,
+ ax * vy - ay * vx,
+ };
+
+ derivative_error = new_real_error - real_error;
+ real_error = new_real_error;
+
+ rad_gyro += 10.0f * kp * real_error;
+ rad_gyro += 5.0f * ki * integral_error;
+ rad_gyro += 10.0f * kd * derivative_error;
+
+ const f32 gx = rad_gyro.y;
+ const f32 gy = rad_gyro.x;
+ const f32 gz = rad_gyro.z;
+
+ // Integrate rate of change of quaternion
+ const f32 pa = q2;
+ const f32 pb = q3;
+ const f32 pc = q4;
+ q1 = q1 + (-q2 * gx - q3 * gy - q4 * gz) * (0.5f * sample_period);
+ q2 = pa + (q1 * gx + pb * gz - pc * gy) * (0.5f * sample_period);
+ q3 = pb + (q1 * gy - pa * gz + pc * gx) * (0.5f * sample_period);
+ q4 = pc + (q1 * gz + pa * gy - pb * gx) * (0.5f * sample_period);
+
+ quat.w = q1;
+ quat.xyz[0] = q2;
+ quat.xyz[1] = q3;
+ quat.xyz[2] = q4;
+ quat = quat.Normalized();
+ }
+}
+} // namespace Core::HID
diff --git a/src/hid_core/frontend/motion_input.h b/src/hid_core/frontend/motion_input.h
new file mode 100644
index 000000000..11678983d
--- /dev/null
+++ b/src/hid_core/frontend/motion_input.h
@@ -0,0 +1,119 @@
+// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include "common/common_types.h"
+#include "common/quaternion.h"
+#include "common/vector_math.h"
+
+namespace Core::HID {
+
+class MotionInput {
+public:
+ static constexpr float ThresholdLoose = 0.01f;
+ static constexpr float ThresholdStandard = 0.007f;
+ static constexpr float ThresholdThight = 0.002f;
+
+ static constexpr float IsAtRestRelaxed = 0.05f;
+ static constexpr float IsAtRestLoose = 0.02f;
+ static constexpr float IsAtRestStandard = 0.01f;
+ static constexpr float IsAtRestThight = 0.005f;
+
+ static constexpr float GyroMaxValue = 5.0f;
+ static constexpr float AccelMaxValue = 7.0f;
+
+ static constexpr std::size_t CalibrationSamples = 300;
+
+ explicit MotionInput();
+
+ MotionInput(const MotionInput&) = default;
+ MotionInput& operator=(const MotionInput&) = default;
+
+ MotionInput(MotionInput&&) = default;
+ MotionInput& operator=(MotionInput&&) = default;
+
+ void SetPID(f32 new_kp, f32 new_ki, f32 new_kd);
+ void SetAcceleration(const Common::Vec3f& acceleration);
+ void SetGyroscope(const Common::Vec3f& gyroscope);
+ void SetQuaternion(const Common::Quaternion<f32>& quaternion);
+ void SetEulerAngles(const Common::Vec3f& euler_angles);
+ void SetGyroBias(const Common::Vec3f& bias);
+ void SetGyroThreshold(f32 threshold);
+
+ /// Applies a modifier on top of the normal gyro threshold
+ void SetUserGyroThreshold(f32 threshold);
+
+ void EnableReset(bool reset);
+ void ResetRotations();
+ void ResetQuaternion();
+
+ void UpdateRotation(u64 elapsed_time);
+ void UpdateOrientation(u64 elapsed_time);
+
+ void Calibrate();
+
+ [[nodiscard]] std::array<Common::Vec3f, 3> GetOrientation() const;
+ [[nodiscard]] Common::Vec3f GetAcceleration() const;
+ [[nodiscard]] Common::Vec3f GetGyroscope() const;
+ [[nodiscard]] Common::Vec3f GetGyroBias() const;
+ [[nodiscard]] Common::Vec3f GetRotations() const;
+ [[nodiscard]] Common::Quaternion<f32> GetQuaternion() const;
+ [[nodiscard]] Common::Vec3f GetEulerAngles() const;
+
+ [[nodiscard]] bool IsMoving(f32 sensitivity) const;
+ [[nodiscard]] bool IsCalibrated(f32 sensitivity) const;
+
+private:
+ void StopCalibration();
+ void ResetOrientation();
+ void SetOrientationFromAccelerometer();
+
+ // PID constants
+ f32 kp;
+ f32 ki;
+ f32 kd;
+
+ // PID errors
+ Common::Vec3f real_error;
+ Common::Vec3f integral_error;
+ Common::Vec3f derivative_error;
+
+ // Quaternion containing the device orientation
+ Common::Quaternion<f32> quat;
+
+ // Number of full rotations in each axis
+ Common::Vec3f rotations;
+
+ // Acceleration vector measurement in G force
+ Common::Vec3f accel;
+
+ // Gyroscope vector measurement in radians/s.
+ Common::Vec3f gyro;
+
+ // Vector to be subtracted from gyro measurements
+ Common::Vec3f gyro_bias;
+
+ // Minimum gyro amplitude to detect if the device is moving
+ f32 gyro_threshold = 0.0f;
+
+ // Multiplies gyro_threshold by this value
+ f32 user_gyro_threshold = 0.0f;
+
+ // Number of invalid sequential data
+ u32 reset_counter = 0;
+
+ // If the provided data is invalid the device will be autocalibrated
+ bool reset_enabled = true;
+
+ // Use accelerometer values to calculate position
+ bool only_accelerometer = true;
+
+ // When enabled it will aggressively adjust for gyro drift
+ bool calibration_mode = false;
+
+ // Used to auto disable calibration mode
+ std::size_t calibration_counter = 0;
+};
+
+} // namespace Core::HID